Это вторая часть статьи, посвященной автоматизации системного тестирования на основе виртуальных машин. Первую часть можно найти здесь.
В этой части статьи мы будем использовать навыки, полученные в первой части, для реальной автоматизации системных тестов. В конце статьи мы получим скрипт, который каждый желающий может запустить у себя на компьютере и совершенно с нуля получить развёрнутый стенд из трёх машин, установленным тестируемым приложением, а также пройденными настоящими системными тестами (мы напишем три теста).
Напоминаю, что статья не ставит себе целью предложить универсальное решение, подходящее под прогон большого количества тестов, и не ставит себе целью преуменьшить значение существующих решений по автоматизации тестирования. Статья лишь является "входным мостиком" в мир автоматизации системного тестирования и помогает понять основные принципы, которыми надо руководствоваться при этом. Выбор решений продиктован исключительно моим личным выбором с точки зрения удобства, доступности и низкого порога вхождения.
Итак, в прошлой части мы запаслись внушительным арсеналом из навыков работы с виртуальными машинами из командной строки: научились устанавливать виртуалки, раскатывать на них ОС (на примере Ubuntu Server 18.04), соединять виртуалку с хостом по сети и даже организовывать канал управления через SSH. Всё это нам пригодится в этой статье, но прежде чем перейти к практике, нужно обсудить несколько вопросов.
Что же мы хотим получить?
Самый главный вопрос, на который нужно дать ответ — это "Какой результат мы хотим получить?". Да, в прошлый раз мы много говорили об автоматизации установки, развертывания и настройки виртуалок, но в отрыве от конечной цели это всё не имеет большого смысла.
Лично для меня системные тесты "всё в одном" выглядят так: я скачиваю из VCS несколько небольших файликов (сам скрипт с запуском, плюс, возможно, несколько вспомогательных артефактов), подкладываю куда нужно тестируемую программу (в виде инсталлятора или пакета, например), нажимаю одну кнопку и иду пить кофе. Когда я возвращаюсь, я хочу либо увидеть что все тесты прошли, либо что такие-то тесты сломались. Я не хочу заниматься какой-либо настройкой стенда, не хочу разворачивать виртуалки или что-то там настраивать. Хочу скачать скрипт и воспользоваться им.
Более того, этот скрипт должен быть повторно используемым: если я подложу новую сборку программы, я хочу снова запустить скрипт и он должен без вопросов снова отработать. В идеале скрипт должен кешировать промежуточные результаты, чтобы мне не приходилось каждый раз ждать пока создадутся и настроятся виртуалки (ведь эти действия должны выполниться только один раз).
Также было бы неплохо получить ещё один скрипт, который бы позволял при необходимости очистить все созданные сущности и освободить место на диске.
Что будем тестировать
Для статьи в качестве подопытной программы я выбрал интересную кандидатуру. Мы будем тестировать мини-фаервол, написанный с ипользованием библиотеки Data Plane Development Kit (DPDK). Если вы не знакомы с DPDK — не пугайтесь, мы не собираемся его изучать, мы возьмём готовое приложение из тех примеров, которые поставляются вместе с DPDK. Приложение на DPDK идеально подходит к этой статье, потому что совершенно непонятно, как именно можно автоматизировать end-to-end тесты для таких приложений.
DPDK (Data Plane Development Kit) — под этим громким названием скрывается просто набор библиотек, написанных на языке C. Эти библиотеки упрощают создание приложений, которые занимаются обработкой сетевого трафика. Примечательной особенностью этой библиотеки является тот факт, что она взаимодействует с сетевыми адаптерами напрямую. Обычно оборудованием управляет операционная система, она же выполняет приём и обработку сетевых пакетов. Платформа DPDK позволяет отодвинуть операционную систему в сторону и взять всё управление в свои руки. Зачем это нужно? Такой подход позволяет добиться впечатляющих показателей производительности. Дело в том, что ядро операционной системы, например Linux, выполняет с сетевыми пакетами очень много манипуляций, которые нам, возможно, и не нужны. Оно и понятно, ведь Linux — он как швейцарский нож, подходит для решения практически любой задачи. Если же нужно решить всего одну относительно простую задачу и сделать это максимально эффективно, то DPDK будет хорошим выбором.
Само приложение имеет очень простой принцип работы:
- оно принимает сетевые пакеты с одного из сетевых интерфейсов;
- сопоставляет полученный пакет со списком правил фильтрации;
- если пакет попал под правило DROP — то пакет отбрасывается;
- если пакет попал под правило ACCEPT — то пакет выплёвывается из другого сетевого адаптера;
Соответственно, нам нужно проверить, что:
- приложение устанавливается на ОС и успешно запускается;
- правила фильтрации отрабатывают так, как и было задумано;
План работ
Как и в предыдущей части, давайте для начала представим, какие действия нам нужно было бы проделать вручную, если бы мы хотели протестировать такой базовый фаерволл с использованием виртуалок:
- Создать три виртуальных машины (client, middlebox, server), установить везде Ubuntu Server 18.04 (например);
- Создать две виртуальные сети: сеть между client и middlebox (назовём эту сеть для краткости
net_1
) и сеть между middlebox и server (назовём еёnet_2
); - Подключить машины к этим сетям;
- Настроить виртуальные машины, привести их в боевое состояние;
- Установить наше приложение-фаервол в машину middlebox;
- Прогнать сами тесты.
Вот все эти вещи мы и хотим автоматизировать в нашем скрипте. При этом добавим пару оговорок:
- Как мы помним из первой части статьи, для выполнения команд на виртуалках нам потребуется организовать SSH-канал управления между виртуалками и хостом, это будет один из пунктов, который нам нужно будет дополнительно сделать, хотя для ручного тестирования этот пункт явно необязателен;
- Хоть теоретически хост может связаться с виртуалкой по SSH используя виртуальные сети
net_1
иnet_2
, но лучше использовать для этого отдельную сеть (назовём еёnet_for_ssh
). Глобальных причин для этого две:
- Т.к. мы тестируем фаерволл, то не хотелось бы, чтобы управляющий трафик оказывал какое-либо влияние на ход проведения тестов;
- В случае, если что-то пойдет не так (например, фаерволл свалится), мы бы не хотели чтобы управление у виртуалки отваливалось.
За работу!
Для тестирования мини-фаерволла мы развернём такой стенд:
С учётом знаний, которые мы получили в предыдущей части статьи, автоматизировать развёртывание стенда совсем несложно:
#!/bin/bash
set -euo pipefail
# =======================================
# Подготовка сети net_for_ssh
# =======================================
virsh net-define net_for_ssh.xml
virsh net-start net_for_ssh
# =======================================
# Подготовка сети net_1
# =======================================
virsh net-define net_1.xml
virsh net-start net_1
# =======================================
# Подготовка сети net_2
# =======================================
virsh net-define net_2.xml
virsh net-start net_2
# =======================================
# Подготовка машины client
# =======================================
virt-builder ubuntu-18.04 --format qcow2 --output client.qcow2 --install wget --root-password password:1111 --run-command "ssh-keygen -A" --run-command "sed -i \"s/.*PermitRootLogin.*/PermitRootLogin yes/g\" /etc/ssh/sshd_config" --copy-in netcfg_client.yaml:/etc/netplan/
virt-install --import --name client --ram 1024 --disk client.qcow2 --network network=net_for_ssh --network network=net_1,mac=52:54:56:11:00:00 --noautoconsole
# =======================================
# Подготовка машины middlebox
# =======================================
virt-builder ubuntu-18.04 --format qcow2 --output middlebox.qcow2 --install python,daemon,libnuma1 --root-password password:1111 --run-command "ssh-keygen -A" --run-command "sed -i \"s/.*PermitRootLogin.*/PermitRootLogin yes/g\" /etc/ssh/sshd_config" --copy-in netcfg_middlebox.yaml:/etc/netplan/
virt-install --import --name middlebox --vcpus=2,sockets=1,cores=2,threads=1 --cpu host --ram 2048 --disk middlebox.qcow2 --network network=net_for_ssh --network network=net_1,model=e1000 --network network=net_2,model=e1000 --noautoconsole
# =======================================
# Подготовка машины server
# =======================================
virt-builder ubuntu-18.04 --format qcow2 --output server.qcow2 --install nginx --root-password password:1111 --run-command "ssh-keygen -A" --run-command "sed -i \"s/.*PermitRootLogin.*/PermitRootLogin yes/g\" /etc/ssh/sshd_config" --copy-in netcfg_server.yaml:/etc/netplan/
virt-install --import --name server --ram 1024 --disk server.qcow2 --network network=net_for_ssh --network network=net_2,mac=52:54:56:00:00:00 --noautoconsole
# =======================================
# Убедимся, что наши машины запустились
# и доступны для команд управления
# =======================================
SSH_CMD="sshpass -p 1111 ssh -o StrictHostKeyChecking=no"
while ! SSH_CMD root@192.168.100.2 "echo Hello world from client!" echo
do
echo "Waiting for client VM ..."
sleep 1
done
while ! SSH_CMD root@192.168.100.3 "echo Hello world from middlebox!" echo
do
echo "Waiting for middlebox VM ..."
sleep 1
done
while ! SSH_CMD root@192.168.100.4 "echo Hello world from server!" echo
do
echo "Waiting for server VM ..."
sleep 1
done
Для запуска этого скрипта потребуются следующие артефакты:
<network>
<name>net_for_ssh</name>
<bridge name='net_for_ssh'/>
<ip address='192.168.100.1' netmask='255.255.255.0'/>
</network>
<network>
<name>net_1</name>
<bridge name='net_1'/>
<ip address='192.168.101.1' netmask='255.255.255.0'/>
</network>
<network>
<name>net_2</name>
<bridge name='net_2'/>
<ip address='192.168.102.1' netmask='255.255.255.0'/>
</network>
network:
version: 2
renderer: networkd
ethernets:
ens3:
addresses:
- 192.168.100.2/24
ens4:
addresses:
- 192.168.101.2/24
gateway4: 192.168.101.3
network:
version: 2
renderer: networkd
ethernets:
ens3:
addresses:
- 192.168.100.3/24
network:
version: 2
renderer: networkd
ethernets:
ens3:
addresses:
- 192.168.100.4/24
ens4:
addresses:
- 192.168.102.2/24
gateway4: 192.168.102.3
Большая часть команд и приёмов Вам должна быть уже известна из первой части статьи, а мы лишь остановимся на некоторых интересных моментах:
- Мы используем параметр
--install
командыvirt-builder
, чтобы установить на виртуалки дополнительные пакеты. Это просто удобное сокращение для--run-command "apt install ..."
. Собственно, Вам даже не обязательно знать, какой пакетный менеджер работает на гостевой системе —virt-builder
сам разберётся. Для машиныclient
мы устанавливаем пакетыwget
, дляserver
—nginx
(чтобы тестировать фаерволл с помощью http-запросов на сервер). Дляmiddlebox
мы устанавливаем зависимости, необходимые для настройки и запуска DPDK-приложений; - Для машин
client
иserver
мы указываем, какие МАС-адреса нужно присвоить сетевым адаптерам, смотрящих в сторону фаерволла. Это пригодится нам при прогоне тестов; - Для машины
middlebox
мы указываем топологию виртуального процессора (параметр--vcpus
): нам требуется один CPU c двумя ядрами без поддержки технологии hyperthreading. Два ядра — это минимальное количество ядер, необходимое для запуска DPDK приложения. Кроме того мы указываем параметр--cpu host
, что означает, что процессор на вируалке должен иметь те же возможности, что и процессор на хостовой системе. Дело в том, что по-умолчанию QEMU создаёт виртуальный процессор, который не поддерживает даже SSE3 инструкции. А без этого, опять же, DPDK приложение не запустится. - Также для машины
middlebox
мы указываем модель сетевых адаптеров, участвующих в машрутизации и фильтрации трафика:e1000
. Та модель адаптера, которая создаётся по-умолчанию на данный момент не поддерживается библиотекой DPDK.
На текущий момент наш скрипт run_tests.sh
сможет корректно отработать только один раз (или даже вообще ни разу, если Вы проделывали у себя шаги из первой части статьи). При повторном запуске у Вас будут возникать ошибки, связанные с двумя моментами:
- Нельзя повторно создать уже созданную сеть;
- Нельзя повторно создать уже созданную виртуальную машину.
Для того, чтобы нам было проще дорабатывать скрипт run_tests.sh
, нам нужно создать скрипт, который будет удалять все наши наработки. Выглядеть этот скрипт будет так:
#!/bin/bash
set -euo pipefail
# =======================================
# Удаление машины client
# =======================================
if virsh list --all | grep -q " client "; then
if virsh domstate client | grep -q "running"; then
virsh destroy client
fi
virsh undefine client --snapshots-metadata --remove-all-storage
fi
# =======================================
# Удаление машины middlebox
# =======================================
if virsh list --all | grep -q " middlebox "; then
if virsh domstate middlebox | grep -q "running"; then
virsh destroy middlebox
fi
virsh undefine middlebox --snapshots-metadata --remove-all-storage
fi
# =======================================
# Удаление машины server
# =======================================
if virsh list --all | grep -q " server "; then
if virsh domstate server | grep -q "running"; then
virsh destroy server
fi
virsh undefine server --snapshots-metadata --remove-all-storage
fi
# =======================================
# Удаление сети net_for_ssh
# =======================================
if virsh net-list --all | grep -q " net_for_ssh "; then
if virsh net-list --all | grep " net_for_ssh " | grep -q " active "; then
virsh net-destroy net_for_ssh
fi
virsh net-undefine net_for_ssh
fi
# =======================================
# Удаление сети net_1
# =======================================
if virsh net-list --all | grep -q " net_1 "; then
if virsh net-list --all | grep " net_1 " | grep -q " active "; then
virsh net-destroy net_1
fi
virsh net-undefine net_1
fi
# =======================================
# Удаление сети net_2
# =======================================
if virsh net-list --all | grep -q " net_2 "; then
if virsh net-list --all | grep " net_2 " | grep -q " active "; then
virsh net-destroy net_2
fi
virsh net-undefine net_2
fi
Вот этот скрипт, в отличие от run_tests.sh
, будет отрабатывать всегда. Основные моменты в этом скрипте:
- Удаляет машины или сети только если они созданы (существование виртуалок проверяется с помощью команды
virsh list --all
, а существование сетей — с помощью командыvirsh net-list -all
); - Чтобы удалить машину/сеть, сначала нужно убедиться, что эта машина/сеть выключена, иначе удалить её не получится;
- Виртуалки удаляются вместе со снепшотами (параметр
--snapshots-metadata
) и подключенными дисками (параметр--remove-all-storage
).
Пока что для повторного запуска run_tests.sh
нужно запускать run_clean.sh
. В дальнейшем мы доработаем run_tests.sh
так, чтобы он отрабатывал и без помощи run_clean.sh
.
Скрипт run_clean.sh
явно напрашивается на рефакторинг. Можно было бы сделать цикл по именам виртуалок и сетей и таким образом избавиться от копипасты. Но я стараюсь делать скрипты максимально прямолинейными, чтобы скрипты были более читабельные, чтобы было легче разобраться, как это работает.
Здесь стоит упомянуть один неприятный момент. Когда мы запускаем SSH-команды — SSH добавляет публичный ключ виртуалки в файл ~/.ssh/known_hosts
. Если мы удалим виртуалки, создадим её заново и попробуем поключится к ней по SSH — он откажется подключаться, думая, что Вас кто-то хочет обмануть. Мы можем избежать этой ситуации подкорректировав нашу переменную SSH_CMD
:
SSH_CMD="sshpass -p 1111 ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR"
Я добавил параметр -o UserKnownHostsFile=/dev/null
, чтобы запуск скрипта тестового сценария никак не влиял на хостовую машину.
Немного об организации тестов
В прошлой части я упомянул, что идеальный вариант автоматизации системных тестов — это автоматизация действий человека при работе за компьютером. Но работу пользователя с Ubuntu Server (без GUI) можно представить как последовательность выполнения bash-команд, чем мы и воспользовались.
В рамках тестов мы собираемся запускать относительно много bash-команд, причём на разных виртуалках. Давайте продумаем пару моментов, прежде чем приступать к делу.
Во-первых, сделаем очень простую, но в тоже время очень полезную вещь:
EXEC_CLIENT="$SSH_CMD root@192.168.100.2"
EXEC_MIDDLEBOX="$SSH_CMD root@192.168.100.3"
EXEC_SERVER="$SSH_CMD root@192.168.100.4"
Мы всего лишь объявили три переменные. Но это уже позволяет писать более читабельный скрипт. Например:
$EXEC_CLIENT echo hello from client
$EXEC_SERVER echo hello from server
Во-вторых, что насчёт многострочных команд? Здесь нам поможет такая возможность bash-интерпретатора как Heredoc. Heredoc позволяет нам записывать тестовые сценарии в следующим виде:
$EXEC_CLIENT << EOF
echo hello from client
ls
pwd
EOF
$EXEC_MIDDLEBOX << EOF
echo hello from middlebox
some_another_command
EOF
Здесь EOF
— это всего лишь последовательность символов, которой мы обозначаем начало и конец строки. Вместо EOF
Вы можете использовать другую последовательность символов, главное, чтобы она была одинаковой в начале и в конце строки.
Когда вы используете мультистрочные команды, скорее всего вы захотите добавить в начало строчку set -xeuo pipefail
. Например:
$EXEC_MIDDLEBOX << EOF
set -xeuo pipefail
command1
command2 | command3
EOF
Напомню на всяких случай, что означает эта команда:
- параметр
-x
заставляет bash-интерпретатор печатать каждую команду перед тем как её выполнить; - параметр
-e
останавливает выполнение всего скрипта, если хотя бы одна из команд завершилась с ошибкой; - параметр
-u
останавливает выполнение всего скрипта, если Вы обратились в нём к несуществующей переменной; - парамерт
-o pipeline
останавливает конвейер, если какая-то команда в его составе завершилась с ошибкой.
Таким образом, если что-то пойдёт не так в Вашем тестовом сценарии — Вы сразу об этом узнаете.
И последний момент, на который я хотел бы обратить внимание. Иногда в таких bash-тестах Вам нужно проверить, что какая-то команда выполняется именно с ошибкой. Это можно сделать следующим образом:
$EXEC_MIDDLEBOX << EOF
set -xeuo pipefail
command1
! command2
EOF
Вышеприведенный скрипт выполнится успешно только в одном единственном случае: если command1 вернёт 0, а command2 вернёт значение, отличное от нуля.
Переходим к самим тестам
В рамках статьи мы напишем три системных теста:
- Проверим, что приложение успешно устанавливается и запускается;
- Проверим, что приложение в принципе машрутизирует трафик;
- Проверим, что приложение может блокировать определенный вид трафика.
Давайте взглянем на первый тест:
$SCP_CMD l3fwd-acl-1.0.0.deb root@192.168.100.3:~
$EXEC_MIDDLEBOX << EOF
set -xeuo pipefail
dpkg -i l3fwd-acl-1.0.0.deb
echo 256 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
mkdir -p /mnt/huge
mount -t hugetlbfs nodev /mnt/huge
modprobe uio_pci_generic
dpdk-devbind --bind=uio_pci_generic ens4 ens5
echo "R0.0.0.0/0 192.168.102.0/24 0 : 65535 0 : 65535 0x0/0x0 1" > /etc/rule_ipv4.db
echo "R0.0.0.0/0 192.168.101.0/24 0 : 65535 0 : 65535 0x0/0x0 0" >> /etc/rule_ipv4.db
echo "R0:0:0:0:0:0:0:0/0 0:0:0:0:0:0:0:0/0 0 : 65535 0 : 65535 0x0/0x0 0" > /etc/rule_ipv6.db
daemon --name l3fwd --unsafe --output /var/log/l3fwd -- l3fwd-acl -l 1 -n 4 -- -p 0x3 -P --config="(0,0,1),(1,0,1)" --rule_ipv4="/etc/rule_ipv4.db" --rule_ipv6="/etc/rule_ipv6.db"
EOF
Вот развёрнутый список того, что делает этот скрипт (для интересующихся):
- Устанавливает скопированный на виртуалку deb-пакет;
- Резервирует и монтирует 256 гигантских страниц по 2 мегабайта (DPDK-приложения по-умолчанию используют гигантские страницы для размещения своих данных);
- Подгружает poll-mode драйвер uio_pci_generic (он поставляется в составе Ubuntu Server). Этот драйвер необходим для того, чтобы DPDK приложение смогло через него получить прямой доступ к сетевым адаптерам;
- Отвязывает интерфейсы ens4 (в сторону клиента) и ens5 (в сторону сервера) от стандартного сетевого драйвера и привязывает их к драйверу uio_pci_generic;
- Создаёт файл rule_ipv4.db с правилами маршрутизации для пакетов IPv4 и кладёт туда два правила: все пакеты с адресом назначения 192.168.102.0/24 отправлять на порт 1 (то есть все пакеты от клиента к серверу надо отправлять на порт, который смотрит в сторону сервера), а все пакеты с адресом назначения 192.168.101.0/24 отправлять на порт 0 (то есть в сторону клиента);
- Также создаёт аналогичный файл для rule_ipv6.db, но туда кладёт одно правило по умолчанию "Все пакеты отправляй на порт 0". Реальных IPv6 пакетов генерироваться в рамках тестов не будет, но без этого файла DPDK приложение запускаться не будет;
- Запускает тестируемое l3fwd приложение и отправляет его работать в фон с помощью
daemon
. Подробно останавливаться на формате запуска не будем, но для интересующихся этот формат можно посмотреть на странице с документациейl3fwd
: https://doc.dpdk.org/guides/sample_app_ug/l3_forward.html
Посмотрите, какие манипуляция с окружением нам приходится делать чтобы проверить приложение DPDK с точки зрения конечного пользователя:
- Установить пакет
.deb
в систему; - Подправить параметры ядра;
- Смонтировать раздел к файловой системе;
- Загрузить модуль ядра;
- Привязать сетевые интерфейсы, участвующие в маршрутизации, к драйверу
uio_pci_generic
; - Запустить приложение в фоне.
В этом и состоит сущность системного тестирования: мы не просто тестируем программу "в вакууме", мы не делаем никаких заглушек и ухищрений чтобы она хоть как-то заработала. Вместо этого мы помещаем программу в реальную обстановку и производим все манипуляции, которые пришлось бы сделать на реальном компьютере реальному пользователю. Это даёт колоссальный уровень спокойствия за тестируемую программу.
И этот лейтмотив только усиливается во втором тесте: вместо того, чтобы проверять работу DPDK-приложения с помощью фиксированного набора пакетов (как это можно было бы сделать в unit-тестах, например), мы будем проверять приложение так, как это бы сделал реальный человек: попробуем передать что-нибудь по сети с помощью реальных утилит и посмотрим на результат:
$EXEC_CLIENT arp -s 192.168.101.3 52:54:56:00:00:00
$EXEC_SERVER arp -s 192.168.102.3 52:54:56:11:00:00
$EXEC_CLIENT << EOF
set -xeuo pipefail
ping -c 5 192.168.102.2
wget --timeout=5 --tries=1 http://192.168.102.2
EOF
Перед тем, как пускать трафик, мы добавили две статические ARP-записи.
Зачем это нужно? Дело в том, что тестовое приложение l3fwd
, взятое из примеров
библиотеки DPDK, настолько простое, что оно даже не обратывает протокол ARP.
Приложение l3fwd
просто фильтрует трафик в соответствии с правилами, заданными
в файлах rule_ipv4.db
и rule_ipv6.db
, и кроме этого не делает больше ровным
счётом ничего: ни проверки чексуммы, ни фрагментации/дефрагментации пакетов, ни-че-го.
Это как раз один из способов достижения максимальной производительности: просто
не делать того, что конкретно Вам не нужно конкретно в Вашей ситуации.
Это приводит к тому, что сетевые пакеты пролетают сквозь машину middlebox
вообще без никаких изменений, хотя у него должны поменяться MAC-адреса
в Ethernet-заголовке (иначе client
и server
будут просто отпрасывать такие пакеты).
Мы можем закостылить эту пролему следующим образом: будем отправлять пакеты
с уже заранее изменённым destination MAC-адресом. Для этого здесь и используются
статические ARP-записи.
Третий же тест довольно очевиден и вытекает из первых двух:
# =======================================
# Добавим правило, заврещающее tcp трафик
# =======================================
$EXEC_MIDDLEBOX << EOF
set -xeuo pipefail
daemon --name l3fwd --stop
# Это и есть запрещающее правило
echo "@0.0.0.0/0 0.0.0.0/0 0 : 65535 0 : 65535 0x06/0xff" > /etc/rule_ipv4.db
echo "R0.0.0.0/0 192.168.102.0/24 0 : 65535 0 : 65535 0x0/0x0 1" >> /etc/rule_ipv4.db
echo "R0.0.0.0/0 192.168.101.0/24 0 : 65535 0 : 65535 0x0/0x0 0" >> /etc/rule_ipv4.db
daemon --name l3fwd --unsafe --output /var/log/l3fwd -- l3fwd-acl -l 1 -n 4 -- -p 0x3 -P --config="(0,0,1),(1,0,1)" --rule_ipv4="/etc/rule_ipv4.db" --rule_ipv6="/etc/rule_ipv6.db"
EOF
# =======================================
# Проверяем, что ping продолжает ходить,
# а http трафик - перестал
# =======================================
$EXEC_CLIENT << EOF
set -xeuo pipefail
ping -c 5 192.168.102.2
! wget --timeout=5 --tries=1 http://192.168.102.2
EOF
Обратите внимание, что перед wget стоит восклицательный знак: это означает, что тест будет успешен, только если команда wget завершиться с ошибкой.
Дорабатываем скрипт run_tests.sh
С самими тестами мы разобрались, осталось немного подумать над нашим скриптом по запуску тестов, который сейчас может успешно отрабатывать только при предварительном прогоне скрипта run_clean.sh
.
Давайте поразмышляем вот над чем: весь скрипт можно условно разделить на две больших секции: до копирования сборки DPDK-приложения на middlebox
и после. В чём разница? Дело в том, что по отношению к стенду (системе из виртуальных машин) сборка DPDK-приложения является внешней переменной Х. Мы заранее знаем, как поведёт себя скрипт до появления этой внешней переменной, но мы не знаем насколько успешно пройдут тесты после внедрения внешнего неизвестного элемента. Вдруг сборка окажется неработоспособной и тесты свалятся?
Поэтому мы можем внести элемент кеширования в наш скрипт. Как насчёт фиксации результатов работы скрипта на момент ровно перед появлением внешней переменной, и отката к этому состоянию при повторном запуске скрипта? Ведь мы знаем, что участок скрипта до появления неопределённого элемента должен всегда выполняться одинаково. А фиксацию можно сделать очень просто: мы можем создать снепшот, например с названием init
, для каждой виртуальной машины после её первоначальной настройки.
Осталось лишь немного подправить скрипт чтобы он учитывал эту новую логику:
# =======================================
# Подготовка машины client
# =======================================
if ! virsh list --all | grep -q " client "
then
virt-builder ubuntu-18.04 --format qcow2 --output client.qcow2 --hostname client --install wget,net-tools --root-password password:1111 --run-command "ssh-keygen -A" --run-command "sed -i \"s/.*PermitRootLogin.*/PermitRootLogin yes/g\" /etc/ssh/sshd_config" --copy-in netcfg_client.yaml:/etc/netplan/
virt-install --import --name client --ram 1024 --disk client.qcow2 --network network=net_for_ssh --network network=net_1,mac=52:54:56:11:00:00 --noautoconsole
virsh snapshot-create-as client --name init
else
virsh snapshot-revert client --snapshotname init
fi
Теперь если машина уже существует, то скрипт вместо её создания будет откатывать её к снепшоту init
.
Конечно, никто не запрещает создавать и другие снепшоты уже во время самих тестов, чтобы при необходимости откатываться к ним: например, мы могли бы создать снепшот после установки .deb
-пакета и базовых настроек DPDK, после чего откатываться к нему перед началом второго теста. Здесь лучше всего действовать по ситуации.
Для сетей же достаточно просто добавить проверку, чтобы создавать их только в том случае, если они не существуют:
# =======================================
# Подготовка сети net_1
# =======================================
if ! virsh net-list --all | grep -q " net_1 "
then
virsh net-define net_1.xml
virsh net-start net_1
fi
Вот и всё, теперь наш скрипт мало того, что запускается каждый раз без особых проблем, так ещё и экономит нам значительное количество времени на повторных запусках за счёт кеширования процесса подготовки стенда. Ну а Вас остаётся только поздравить: ведь теперь Вы умеете создавать настоящие системные тесты с использованием виртуалок!
Итоги
Что ж, мы проделали довольно много работы, в том числе и муторной, и самое время оглянуться назад и посмотреть, чего же мы достигли. А достигли мы немалого: мы написали скрипт, который в автоматическом режиме полностью с нуля разворачивает у нас на компьютере стенд из трёх виртуальных машин, раскатывает на этом стенде сборку тестируемого приложения, а также прогоняет настоящие системные (end-to-end) тесты для этого приложения.
Заметьте, что эти тесты проверяют работу нашего приложения от начала и до конца именно в том виде, в котором это приложение будет в реальности работать: мы поместили DPDK-приложение в конкретную среду (стенд из трех машин Ubuntu Server 18.04) и с конкретным набором реальных пользовательских проверок (вызов утилиты ping и wget). В нашем стенде нет ни единой заглушки, мы можем взять наш .deb пакет и прямо сейчас выложить его на сайте для скачивания всеми желающими. И именно поэтому такие тесты дают такую четкую уверенность, что если программа работает внутри тестов, то она точно отработает в реальных условиях (по крайней мере, в ТАКИХ ЖЕ реальных условиях). И эта уверенность дорогого стоит.
Но это ещё не всё: все наши артефакты это два скрипта (run_tests.sh и run_clean.sh), три xml-файла и три yaml-файла. Всё это текстовые файлы и идеально хранятся в любой VCS. Эти скрипты можно легко переносить между компьютерами и всё равно прогонять тесты одной кнопкой.
Наконец, если привязать репозиторий с тестами к репозиторию нашего приложения, то мы получим согласованную связку "приложение — тесты". Теперь если нам понадобиться вернуться к старой версии нашего приложения, то тесты автоматически откатятся к этой же версии. Не получится такой ситуации, что тесты "ушли вперед", и теперь старую версию приложения на них не прогонишь.
Ну а если у Вас осталось некоторое ощущение "недоделанности", "костыльности" и "кривизны" подхода, который мы продемонстрировали в этой статье, то Вы, на самом деле, будете правы. Все эти наработки очень даже хороши как Proof Of Concept для идеи автоматизации системных тестов с использованием виртуальных машин. Но строить полноценную большую и промышленную систему тестов на основе баш-скриптов было бы… довольно оптимистично. Впрочем, есть и более элегантное, законченное и отработаное решение, которое может помочь Вам клепать системные тесты как горячие пирожки на любых операционных системах и даже не задумываться всерьез обо всей сложной внутренней кухне, которая при всём при этом происходит. Но это уже тема для нашей следующей статьи...
> Репо с итоговым скриптом можно найти здесь
amarao
Вы это, ничего слаще баша в жизни не ели? pytest/testinfra с одной стороны, Ансибл с другой. Оба отлично умеют по SSH, один даёт офигенную выразительность и точность, второй позволяет делать/проверять чёрти-что за счёт гигантской коллекции модулей.
testo-lang Автор
Пожалуйста, прочитайте ещё раз дисклеймер.
Статья ориентирована на начинающих, причём не на программистов, а на тестировщиков. Далеко не все тестировщики знают Python и, уж тем более, Ansible. А для понимания этой статьи достаточно базовых знаний Linux и Bash.
Главная цель статьи — внести понимание о системных тестах на виртуалках и об основных моментах, на которых нужно заострить внимание. Выбор инструментов, будь то bash, SSH или virt-builder — это лишь способ максимально упростить статью. Разумеется, я не рекомендую этот инструментарий для продакшена.
amarao
Я не верю в начинающего, способного писать на баше сколь-либо прилично. Я не верю в миддла, способного писать на баше сколь-либо прилично. Я не верю в сеньёра, способного писать на баше сколь-либо прилично, На баше умеют писать Гуру Баша (я не иронизирую), и это чертовски сложно.
Я за 20 лет своей карьеры так и не научился прилично писать на баше. Наговнякать я могу. Подмазать так, чтобы два куска кода с друг другом сошлись — могу.
Написать приличный тест я уже не смогу, мне надо будет неделю медитировать над тем, "как правильно писать на баше".
Сравните тесты, которые вы приводите, и тест на testinfra:
Заметим, это в себя включает автоматом и поддержку пачки серверов, и ssh до него и т.д.
testo-lang Автор
Тот же самый тест, который Вы привели, на баше будет выглядеть так:
amarao
Нет. Если вы выполните этот код
bash test.sh
, то он пропингует 5.5.5.5 с вашей локальной машины.Приведённый выше код:
А ещё он даст возможность ограничить группу хостов, вывести подробную ошибку (например, expected ' 0% packet loss', got '20% packet loss', да ещё и подчеркнёт то место, где различия), пропустить часть тестов из тестсьюта.
Короче, человек, который джуниора тащит писать на баше тесты создаёт боль джуниору и проблему компании.
testo-lang Автор
Я согласен с тем, что писать системные тесты для продакшена на баше — это плохая, плохая идея.
Но я также считаю, что использование баша в этой статье в познавательных целях вполне оправдано. Вы что-нибудь слышали о принципе «от простого к сложному?». Сейчас я объясняю базовые принципы. А в одной из следующих статей я могу написать «Ребятки, а помните сколько манипуляций мы делали, чтобы автоматизировать системные тесты на баше? Давайте теперь посмотрим, как теперь за нас это сделает инструмент Х».
amarao
Я не могу понять, почему из всех эзотерических языков программирования именно баш? Вам кажется, что баш — это проще, чем питон? (У меня на языке чешется сказать, что по той же логике брейнфак проще питона).
testo-lang Автор
Потому что 99% всей работы делают утилиты, на которые я полагаюсь в этой статье. Потрудитесь изучить финальный скрипт и увидите, что подавляющая часть этого скрипта — это вызов утилит virsh, virt-builder, virt-install и ssh. Разумеется, вызывать эти команды из баша удобнее, чем из питона.
Почему я выбрал именно эти утилиты, а не WhateverYouWant? Потому что, потрудитесь, наконец, прочитать дисклеймер.
Кроме того, если у Вас за 20 лет стажа так и не сложились отношения с башем, это не значит, что у других дела обстоят так же. Я, как и многие другие люди, не считаю его эзотерическим. Для того, чтобы скомпозировать вызов нескольких утилит, он вполне годится, что я и сделал в этой статье. В следующих статьях я его использовать уже не планирую.
amarao
99% этой работы называется "ansible". С башом у меня не "не сложилось", я просто знаю, что на баше писать кратно сложнее, чем на любом другом языке программирования. Даже тривиальные условия превращаются в чёртову магию (как проверить вхождение подстроки в баше: enjoy: https://stackoverflow.com/questions/229551/how-to-check-if-a-string-contains-a-substring-in-bash), а уж в районе обработки сигналов и т.д… Лучше не надо.