Автоматизация системных тестов на базе QEMU (Часть 2/2)

Это вторая часть статьи, посвященной автоматизации системного тестирования на основе виртуальных машин. Первую часть можно найти здесь.

В этой части статьи мы будем использовать навыки, полученные в первой части, для реальной автоматизации системных тестов. В конце статьи мы получим скрипт, который каждый желающий может запустить у себя на компьютере и совершенно с нуля получить развёрнутый стенд из трёх машин, установленным тестируемым приложением, а также пройденными настоящими системными тестами (мы напишем три теста).


Дисклеймер

Напоминаю, что статья не ставит себе целью предложить универсальное решение, подходящее под прогон большого количества тестов, и не ставит себе целью преуменьшить значение существующих решений по автоматизации тестирования. Статья лишь является «входным мостиком» в мир автоматизации системного тестирования и помогает понять основные принципы, которыми надо руководствоваться при этом. Выбор решений продиктован исключительно моим личным выбором с точки зрения удобства, доступности и низкого порога вхождения.

Итак, в прошлой части мы запаслись внушительным арсеналом из навыков работы с виртуальными машинами из командной строки: научились устанавливать виртуалки, раскатывать на них ОС (на примере Ubuntu Server 18.04), соединять виртуалку с хостом по сети и даже организовывать канал управления через SSH. Всё это нам пригодится в этой статье, но прежде чем перейти к практике, нужно обсудить несколько вопросов.


Что же мы хотим получить?

Самый главный вопрос, на который нужно дать ответ — это «Какой результат мы хотим получить?». Да, в прошлый раз мы много говорили об автоматизации установки, развертывания и настройки виртуалок, но в отрыве от конечной цели это всё не имеет большого смысла.

Лично для меня системные тесты «всё в одном» выглядят так: я скачиваю из VCS несколько небольших файликов (сам скрипт с запуском, плюс, возможно, несколько вспомогательных артефактов), подкладываю куда нужно тестируемую программу (в виде инсталлятора или пакета, например), нажимаю одну кнопку и иду пить кофе. Когда я возвращаюсь, я хочу либо увидеть что все тесты прошли, либо что такие-то тесты сломались. Я не хочу заниматься какой-либо настройкой стенда, не хочу разворачивать виртуалки или что-то там настраивать. Хочу скачать скрипт и воспользоваться им.

Более того, этот скрипт должен быть повторно используемым: если я подложу новую сборку программы, я хочу снова запустить скрипт и он должен без вопросов снова отработать. В идеале скрипт должен кешировать промежуточные результаты, чтобы мне не приходилось каждый раз ждать пока создадутся и настроятся виртуалки (ведь эти действия должны выполниться только один раз).

Также было бы неплохо получить ещё один скрипт, который бы позволял при необходимости очистить все созданные сущности и освободить место на диске.


Что будем тестировать

Для статьи в качестве подопытной программы я выбрал интересную кандидатуру. Мы будем тестировать мини-фаервол, написанный с ипользованием библиотеки Data Plane Development Kit (DPDK). Если вы не знакомы с DPDK — не пугайтесь, мы не собираемся его изучать, мы возьмём готовое приложение из тех примеров, которые поставляются вместе с DPDK. Приложение на DPDK идеально подходит к этой статье, потому что совершенно непонятно, как именно можно автоматизировать end-to-end тесты для таких приложений.


Несколько слов про DPDK и что в нем такого особенного

DPDK (Data Plane Development Kit) — под этим громким названием скрывается просто набор библиотек, написанных на языке C. Эти библиотеки упрощают создание приложений, которые занимаются обработкой сетевого трафика. Примечательной особенностью этой библиотеки является тот факт, что она взаимодействует с сетевыми адаптерами напрямую. Обычно оборудованием управляет операционная система, она же выполняет приём и обработку сетевых пакетов. Платформа DPDK позволяет отодвинуть операционную систему в сторону и взять всё управление в свои руки. Зачем это нужно? Такой подход позволяет добиться впечатляющих показателей производительности. Дело в том, что ядро операционной системы, например Linux, выполняет с сетевыми пакетами очень много манипуляций, которые нам, возможно, и не нужны. Оно и понятно, ведь Linux — он как швейцарский нож, подходит для решения практически любой задачи. Если же нужно решить всего одну относительно простую задачу и сделать это максимально эффективно, то DPDK будет хорошим выбором.

Само приложение имеет очень простой принцип работы:


  1. оно принимает сетевые пакеты с одного из сетевых интерфейсов;
  2. сопоставляет полученный пакет со списком правил фильтрации;
  3. если пакет попал под правило DROP — то пакет отбрасывается;
  4. если пакет попал под правило ACCEPT — то пакет выплёвывается из другого сетевого адаптера;

Соответственно, нам нужно проверить, что:


  • приложение устанавливается на ОС и успешно запускается;
  • правила фильтрации отрабатывают так, как и было задумано;


План работ

Как и в предыдущей части, давайте для начала представим, какие действия нам нужно было бы проделать вручную, если бы мы хотели протестировать такой базовый фаерволл с использованием виртуалок:


  1. Создать три виртуальных машины (client, middlebox, server), установить везде Ubuntu Server 18.04 (например);
  2. Создать две виртуальные сети: сеть между client и middlebox (назовём эту сеть для краткости net_1) и сеть между middlebox и server (назовём её net_2);
  3. Подключить машины к этим сетям;
  4. Настроить виртуальные машины, привести их в боевое состояние;
  5. Установить наше приложение-фаервол в машину middlebox;
  6. Прогнать сами тесты.

Вот все эти вещи мы и хотим автоматизировать в нашем скрипте. При этом добавим пару оговорок:


  1. Как мы помним из первой части статьи, для выполнения команд на виртуалках нам потребуется организовать SSH-канал управления между виртуалками и хостом, это будет один из пунктов, который нам нужно будет дополнительно сделать, хотя для ручного тестирования этот пункт явно необязателен;
  2. Хоть теоретически хост может связаться с виртуалкой по SSH используя виртуальные сети net_1 и net_2, но лучше использовать для этого отдельную сеть (назовём её net_for_ssh). Глобальных причин для этого две:


  • Т.к. мы тестируем фаерволл, то не хотелось бы, чтобы управляющий трафик оказывал какое-либо влияние на ход проведения тестов;
  • В случае, если что-то пойдет не так (например, фаерволл свалится), мы бы не хотели чтобы управление у виртуалки отваливалось.


За работу!

Для тестирования мини-фаерволла мы развернём такой стенд:


q9blwkgoonetfkpuwvtihpil4vs.png

С учётом знаний, которые мы получили в предыдущей части статьи, автоматизировать развёртывание стенда совсем несложно:


run_tests.sh
#!/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

Для запуска этого скрипта потребуются следующие артефакты:


net_for_ssh.xml

    net_for_ssh
    
    


net_1.xml

    net_1
    
    


net_2.xml

    net_2
    
    


netcfg_client.yaml
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


netcfg_middlebox.yaml
network:
  version: 2
  renderer: networkd
  ethernets:
    ens3:
      addresses:
        - 192.168.100.3/24


netcfg_server.yaml
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

Большая часть команд и приёмов Вам должна быть уже известна из первой части статьи, а мы лишь остановимся на некоторых интересных моментах:


  1. Мы используем параметр --install команды virt-builder, чтобы установить на виртуалки дополнительные пакеты. Это просто удобное сокращение для --run-command "apt install ...". Собственно, Вам даже не обязательно знать, какой пакетный менеджер работает на гостевой системе — virt-builder сам разберётся. Для машины client мы устанавливаем пакеты wget, для server — nginx (чтобы тестировать фаерволл с помощью http-запросов на сервер). Для middlebox мы устанавливаем зависимости, необходимые для настройки и запуска DPDK-приложений;
  2. Для машин client и server мы указываем, какие МАС-адреса нужно присвоить сетевым адаптерам, смотрящих в сторону фаерволла. Это пригодится нам при прогоне тестов;
  3. Для машины middlebox мы указываем топологию виртуального процессора (параметр --vcpus): нам требуется один CPU c двумя ядрами без поддержки технологии hyperthreading. Два ядра — это минимальное количество ядер, необходимое для запуска DPDK приложения. Кроме того мы указываем параметр --cpu host, что означает, что процессор на вируалке должен иметь те же возможности, что и процессор на хостовой системе. Дело в том, что по-умолчанию QEMU создаёт виртуальный процессор, который не поддерживает даже SSE3 инструкции. А без этого, опять же, DPDK приложение не запустится.
  4. Также для машины middlebox мы указываем модель сетевых адаптеров, участвующих в машрутизации и фильтрации трафика: e1000. Та модель адаптера, которая создаётся по-умолчанию на данный момент не поддерживается библиотекой DPDK.

На текущий момент наш скрипт run_tests.sh сможет корректно отработать только один раз (или даже вообще ни разу, если Вы проделывали у себя шаги из первой части статьи). При повторном запуске у Вас будут возникать ошибки, связанные с двумя моментами:


  1. Нельзя повторно создать уже созданную сеть;
  2. Нельзя повторно создать уже созданную виртуальную машину.

Для того, чтобы нам было проще дорабатывать скрипт run_tests.sh, нам нужно создать скрипт, который будет удалять все наши наработки. Выглядеть этот скрипт будет так:


run_clean.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, будет отрабатывать всегда. Основные моменты в этом скрипте:


  1. Удаляет машины или сети только если они созданы (существование виртуалок проверяется с помощью команды virsh list --all, а существование сетей — с помощью команды virsh net-list -all);
  2. Чтобы удалить машину/сеть, сначала нужно убедиться, что эта машина/сеть выключена, иначе удалить её не получится;
  3. Виртуалки удаляются вместе со снепшотами (параметр --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 вернёт значение, отличное от нуля.


Переходим к самим тестам

В рамках статьи мы напишем три системных теста:


  1. Проверим, что приложение успешно устанавливается и запускается;
  2. Проверим, что приложение в принципе машрутизирует трафик;
  3. Проверим, что приложение может блокировать определенный вид трафика.

Давайте взглянем на первый тест:

$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


О содержимом скрипта

Вот развёрнутый список того, что делает этот скрипт (для интересующихся):


  1. Устанавливает скопированный на виртуалку deb-пакет;
  2. Резервирует и монтирует 256 гигантских страниц по 2 мегабайта (DPDK-приложения по-умолчанию используют гигантские страницы для размещения своих данных);
  3. Подгружает poll-mode драйвер uio_pci_generic (он поставляется в составе Ubuntu Server). Этот драйвер необходим для того, чтобы DPDK приложение смогло через него получить прямой доступ к сетевым адаптерам;
  4. Отвязывает интерфейсы ens4 (в сторону клиента) и ens5 (в сторону сервера) от стандартного сетевого драйвера и привязывает их к драйверу uio_pci_generic;
  5. Создаёт файл rule_ipv4.db с правилами маршрутизации для пакетов IPv4 и кладёт туда два правила: все пакеты с адресом назначения 192.168.102.0/24 отправлять на порт 1 (то есть все пакеты от клиента к серверу надо отправлять на порт, который смотрит в сторону сервера), а все пакеты с адресом назначения 192.168.101.0/24 отправлять на порт 0 (то есть в сторону клиента);
  6. Также создаёт аналогичный файл для rule_ipv6.db, но туда кладёт одно правило по умолчанию «Все пакеты отправляй на порт 0». Реальных IPv6 пакетов генерироваться в рамках тестов не будет, но без этого файла DPDK приложение запускаться не будет;
  7. Запускает тестируемое l3fwd приложение и отправляет его работать в фон с помощью daemon. Подробно останавливаться на формате запуска не будем, но для интересующихся этот формат можно посмотреть на странице с документацией l3fwd: https://doc.dpdk.org/guides/sample_app_ug/l3_forward.html

Посмотрите, какие манипуляция с окружением нам приходится делать чтобы проверить приложение DPDK с точки зрения конечного пользователя:


  1. Установить пакет .deb в систему;
  2. Подправить параметры ядра;
  3. Смонтировать раздел к файловой системе;
  4. Загрузить модуль ядра;
  5. Привязать сетевые интерфейсы, участвующие в маршрутизации, к драйверу uio_pci_generic;
  6. Запустить приложение в фоне.

В этом и состоит сущность системного тестирования: мы не просто тестируем программу «в вакууме», мы не делаем никаких заглушек и ухищрений чтобы она хоть как-то заработала. Вместо этого мы помещаем программу в реальную обстановку и производим все манипуляции, которые пришлось бы сделать на реальном компьютере реальному пользователю. Это даёт колоссальный уровень спокойствия за тестируемую программу.

И этот лейтмотив только усиливается во втором тесте: вместо того, чтобы проверять работу 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-записями

Перед тем, как пускать трафик, мы добавили две статические 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 для идеи автоматизации системных тестов с использованием виртуальных машин. Но строить полноценную большую и промышленную систему тестов на основе баш-скриптов было бы… довольно оптимистично. Впрочем, есть и более элегентное, законченное и отработаное решение, которое может помочь Вам клепать системные тесты как горячие пирожки на любых операционных системах и даже не задумываться всерьез обо всей сложной внутренней кухне, которая при всём при этом происходит. Но это уже тема для нашей следующей статьи…

© Habrahabr.ru