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


Статья предполагает наличие следующих навыков у читателя:


  • Уверенное пользование ОС семейства Linux;
  • Базовое понимание принципов виртуализации;
  • Знакомство с гипервизором QEMU и графическим клиентом virt-manager

Статья разбита на две части: в первой части мы познакомимся с основными инструментами, которые позволят нам создавать, развертывать и управлять виртуальными машинами используя исключительно командную строку. Эти знания нам пригодятся для второй части статьи (которую можно найти здесь: https://habr.com/ru/post/520648/), где мы соединим эти инструменты вместе и попробуем автоматизировать тесты конкретного сетевого приложения.


Дисклеймер

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


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


Что такое системное тестирование


Системное тестирование (или, как его ещё иногда называют, end-to-end тестирование) — это тестирование программы (или даже целой системы) с учётом окружения, в которой программе придётся работать. Помимо системного тестирования также существует unit-тестирование (тестирование конкретных функций и модулей), интеграционное тестирование (тестирование больших самостоятельных компонент программы или программы целиком) и ещё много других видов тестирования. Но почему же нас может заинтересовать именно системное?


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


  • Вид и версия ОС;
  • Разные настройки ОС (например, в области разграничения прав доступа);
  • Наличие или отсутствие определенных программ в ОС (в т.ч. драйверов);
  • Наличие сети и других компьютеров в ней;
  • Количество оперативной памяти или определённая архитектура процессора.

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


Почему именно виртуалки?


Может возникнуть вопрос, а зачем использовать именно виртуалки? Ведь для воспроизведения окружения для программы можно использовать и контейнеры (например, если Вас интересует поведение программы в связке с другими программами). И действительно, для контейнеров существует несколько решений по автоматизации тестов. Однако, существуют классы сценариев, которые нельзя протестировать с помощью контейнеров:


  1. Нельзя протестировать приложения не под Linux, а также гетерогенные стенды из нескольких машин;
  2. Нельзя протестировать GUI (или даже псевдо-GUI);
  3. Нельзя протестировать драйвера и работу с оборудованием.

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


Какой выбрать гипервизор?


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


Какие действия мы хотим автоматизировать?


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


  1. Как автоматически создать виртуалку;
  2. Как "раскатать" и настроить ОС;
  3. Как научиться управлять виртуалкой после её установки. В частности, нас интересует, как запускать процессы внутри виртуалки и копировать на неё файлы.

Заметка про тестирование методом чёрного ящика

По-хорошему, настоящая автоматизация системных тестов предполагает, что все эти действия мы будем выполнять так же, как и тестировщик, выполняющий ручное тестирование: нам нужно нажимать клавиши на виртуальной клавиатуре, двигать виртуальной мышью и анализировать происходящее на виртуальном экране. Такой подход позволяет автоматизировать любые тестовые сценарии, поскольку не делает никаких предположений о том, как устроена внутри тестируемая система. Однако, это очень сложный путь автоматизации. Мы, пока что, воспользуемся менее универсальным, но гораздо более простым способом.


Что ж, погнали!


Создаем виртуалку


Конечно, я думаю все пробовали создавать виртуальные машины вручную. Для этого в графическом интерфейсе рисуются всякие диалоговые окна, где надо выбрать конфигурацию, диски, сетевые адаптеры и прочее прочее. Так вот, для гипервизора QEMU всё это можно сделать и в консольном режиме с помощью утилиты virt-install. Давайте посмотрим как будет выглядеть простейшее создание виртуальной машины с помощью этой утилиты:


virt-install     --name my_super_vm     --ram 1024     --disk my_super_vm.qcow2,size=8     --cdrom /path/to/ubuntu_server.iso

Вот такой простой командой мы можем создать виртуальную машину с именем my_super_vm, 1024 Мегабайтами оперативной памяти, новым диском my_super_vm.qcow2 размером 8 Гигабайт. В виртуальном CD-приводе такой машины будет смонтирован установочный образ ubuntu_server.iso (конечно, этот образ надо предварительно скачать), который, как обычно, нужен для установки ОС на свежесозданную виртуалку.


Впрочем, если Вы выполните такую команду, то увидите как у вас запустилось графическое окно с VNC-клиентом, который подключается к консоли виртуальной машины. В этом окне Вы увидите начало установки Ubuntu Server 18.04. Конечно, реальный человек бы смог протыкать несколько клавиш на клавиатуре и установить Ubuntu Server, но мы лишены такой роскоши, ведь мы работаем исключительно консольными командами.


Но наше положение не такое уж и безвыходное, ведь установить ОС на виртуальную машину можно и по-другому.


Создаём диск из шаблона


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


Вызов команды virt-install, приведённый выше, выполняет сразу два действия: он создаёт одновременно и машину и диск. Из-за может сложиться неверное впечатление, что эти сущности неотделимы друг от друга.


Но на самом деле никто не мешает нам отдельно создать виртуальный диск, а затем подключить его к виртуальной машине. Вы можете подготовить свой образ диска, но в этой статье мы для простоты воспользуемся уже готовым. Проект libguestfs поддерживает в свободном доступе целый репозиторий таких образов. Кроме того, в рамках этого же проекта существует утилита virt-builder, которая позволяет легко скачать нужный образ диска и доработать его "напильником" под наши нужды.


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

В первую очередь Libguestfs — это программная библиотека, которая позволяет легко манипулировать содержимым образом виртуальных дисков. Под манипуляцией подразумеваются такие операции как форматирование дисков, копирование файлов на диск и из него и так далее. Кроме того, в рамках проекта реализован целый набор утилит на все случаи жизни, которые используют различные возможности библиотеки. Среди них есть очень простые утилиты (в стиле Unix-way), которые выполняют всего одно действие, например virt-copy-in. С другой стороны, есть команды-комбайны, которые умеют много всего и сразу, такие как virt-builder.


Итак, что же нам позволяет сделать virt-builder? С помощью этой утилиты мы можем на лету сформировать уже "подготовленный" диск с уже установленной Ubuntu Server. Сделать это можно так:


virt-builder ubuntu-18.04     --format qcow2     --output my_super_disk.qcow2

Что же здесь происходит? Мы говорим, что хотели бы создать диск в формате qcow2 (можно выбрать другой) и использовать в качестве основы шаблон ubuntu-18.04, который хранится в репозитории шаблонов в проекте libguestfs. Команда virt-builder скачает этот шаблон из Интернета и затем на его основе сгенерирует итоговый диск, в котором теперь будет установлена Ubuntu Server!


Кешируемость шаблонов

Приятной особенностью утилиты virt-builder является то, что шаблон диска кешируется после первого скачивания, поэтому последующие создания дисков будут проходить гораздо быстрее


Ну а теперь нам остаётся создать виртуальную машину, и указать в качестве диска (импортировать) подготовленный образ my_super_disk.qcow2:


virt-install     --import     --name my_super_vm     --ram 1024     --disk my_super_vm.qcow2

Обратите внимание, что исчез параметр --cdrom, он нам больше не нужен. Также добавился параметр --import. Этот параметр указывает, что виртуалка будет загружаться не с cdrom, а с виртуального диска (то есть это влияет на Bios Boot Options виртуальной машины). Ну а т.к. диск у нас теперь содержит установленную Ubuntu Server, такая загрузка пройдёт успешно.


Попробуйте выполнить эти команды и создать таким образом виртуальную машину my_super_vm. Вы сможете убедиться, что Ubuntu Server 18.04 действительно уже установлена и успешно загружается.


Соединяем сетью виртуалку и хост


Вот так вот мы практически незаметно научились автоматически устанавливать виртуалки и даже раскатывать на них готовую ОС. Теперь настало время поговорить о канале управления виртуалкой после её установки и настройки.


Ещё раз хотелось бы отметить, что эталоном автоматизации системных тестов должна выступать автоматизация действий человека: как он нажимает на клавиши, кликает мышкой и совершает другие действия. И ещё раз напомним, что это очень сложный (хоть и правильный) путь. Мы же воспользуемся тем фактом, что нам не требуется взаимодействовать с GUI, нам пока вполне хватит возможности выполнения произвольных bash-команд на гостевой системе.


Для этого существуют разные подходы, но мы с Вами в этой статье рассмотрим только самый простой и, в некоторой степени, топорный вариант: установка SSH-соединения между хостом и виртуалкой.


Альтернативные подходы

Не всегда канал управления виртуалкой через ssh является приемлемым вариантом. Такое бывает, если Вы тестируете приложение, которое каким-то образом завязано на сетевую подсистему. Например, в случае если вы как раз и разрабатываете ssh-сервер. Вы же не хотите, чтобы Ваши тесты полагались на работоспособность ещё неготовой программы? Либо Вы хотите проверить, как поведёт себя Ваша программа на виртуалке, где вообще нет сетевых интерфейсов.


В качестве альтернативы можно управлять гостевой системой через последовательный порт. При этом последовательный порт образует как бы трубу (pipe), проложенную между хостом и виртуалкой. Со стороны Linux-хоста эта труба видна как unix-socket, а со стороны гостевой системы — как последовательное устройство. Всё, что отправляется в один конец трубы, тут же появляется из другого. Соответственно, как и в случае с ssh, на гостевой системе должен работать некий сервер, ожидающий получения данных из последовательного порта. Примером такого сервера может служить qemu-guest-agent.


В случае, если Вы работаете с гипервизором Hyper-V, то помимо последовательного порта можно попробовать воспользоваться механизмом KVP (Key-Value Pairs) или Hyper-V Sockets.


Для этого нам потребуется сделать несколько манипуляций:


  1. Создать виртуальную сеть между хостом и виртуалкой;
  2. Настроить сетевой интерфейс на виртуалке;
  3. Задать пароль от пользователя root на виртуалке;
  4. Подредактировать SSH-настройке на виртуалке, чтобы можно было соединяться по SSH от имени рута с помощью пароля.

По поводу ssh без пароля

Лучше, конечно, было бы настроить вход по ключу. Это позволило бы немного упростить финальный скрипт. Единственная причина, почему я не стал так делать — это скользкий момент, связанный с sudo. Но вообще, принципиальных препятствий никаких нет.


Для начала займемся созданием виртальной сети между хостом и виртуалкой. Для этого нам надо сделать две простых операции:


  1. Создать виртуальную сеть;
  2. Подключить виртуалку к этой сети (хост уже будет подключен к сети по-умолчанию).

Итак, поехали.


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


Несколько слов про libvirt

Libvirt — это, опять же, программная библиотека, предназначенная для управления гипервизорами. Проект libvirt ставит перед собой задачу объять необъятное — сделать интерфейс управления, подходящий сразу для всех гипервизоров. Задача благая, но уж слишком амбициозная. На данный момент гипервизоры, отличные от QEMU, поддерживаются довольно слабо. Однако, если Вам требуется сделать что-то именно с QEMU, то проект libvirt и утилита virsh, входящая в его состав, являются самым быстрым и удобным вариантом.


В libvirt для создания различных виртуальных объектов (дисков, сетей, виртуалок) используются XML-схемы. XML-схема для создания сети выглядит примерно так:


<network>
    <name>net_for_ssh</name>
    <bridge name='net_for_ssh'/>
    <ip address='192.168.100.1' netmask='255.255.255.0'/>
</network>

Где 192.168.100.1 — это адрес, присваиваемый сетевому интерфейсу, который будет автоматически создан на хосте и ассоциирован с новой виртуальной сетью.


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


virsh net-define net_for_ssh.xml

После создания сеть создаётся в выключенном состоянии, поэтому её ещё надо включить:


virsh net-start net_for_ssh

Теперь надо подключить виртуалку к свежесозданной сети. Для этого при создании виртуалки необходимо добавить параметр --network:


virt-install     --import     --name my_super_vm     --ram 1024     --disk my_super_vm.qcow2     --network network=net_for_ssh     --noautoconsole

Обратите также внимание на аргумент --noautoconsole, который отключает автоматическое подключение к консоли виртуалки через VNC-клиент (впрочем, если Вам всё-равно хочется зайти посмотреть на виртуалку, Вы можете воспользоваться virt-manager).


Соединение готово, но пропинговать с хоста нашу виртуалку мы всё ещё не можем: сетевой интерфейс пока не настроен.


Настройка сетевого интерфейса на виртуалке


Как же мы будем настраивать интерфейс внутри виртуалки, если мы пока не можем выполнять на ней никаких команд (SSH-канал ведь ещё не настроен)? В этом нам снова поможет библиотека libguestfs и утилита virt-builder.


Дело в том, эта библиотека позволяет, в числе прочего, копировать файлы на виртуальный диск. Как мы знаем, в Ubuntu Server 18.04 за сетевые настройки отвечает netplan, и для того, чтобы сконфигурировать сетевой интерфейс, нам достаточно подложить специальный .yaml файлик в каталог /etc/netplan. И сделать это можно с помощью той же утилиты virt-builder с помощью параметра --copy-in:


Файл с настройками сетевого интерфейса netcfg_ssh.yaml
network:
  version: 2
  renderer: networkd
  ethernets:
    ens3:
      addresses:
        - 192.168.100.2/24

virt-builder ubuntu-18.04     --format qcow2     --output my_super_disk.qcow2     --copy-in netcfg_ssh.yaml:/etc/netplan/

И теперь при создании виртуального диска после раскатывания Ubuntu Server 18.04 из шаблона virt-builder дополнительно скопирует файл netcfg_ssh.yaml и подложит его в директорию /etc/netplan/ на файловой системе виртуального диска.


Теперь виртуальная машина должна пинговаться, проверим:


ping 192.168.100.2 -c5

Почти всё сделали, осталось лишь настроить SSH.


Настраиваем SSH на виртуалке


Для создания канала управления виртуалкой осталось сделать совсем чуть чуть:


  1. Прописать пароль для root-пользователя в виртуалке;
  2. Создать ключи на виртуалке для SSH-сервера, чтобы он стартовал без ругани;
  3. Прописать в конфиг SSH-сервера возможность подключаться от имени рута с использованием пароля.

Начнём с пароля для root. Здесь нас снова выручает virt-builder, который, на самом деле, позволяет Вам делать с виртуальным диском поистине удивительные вещи, одна из которых — прописать пароль для root-пользователя:


virt-builder ubuntu-18.04     --format qcow2     --output my_super_disk.qcow2     --root-password password:1111     --copy-in netcfg_ssh.yaml:/etc/netplan/

Теперь осталось сгенерировать ключи для SSH и подправить конфиг. Для этого нам надо всего-то надо выполнить пару команд:


ssh-keygen -A
sed -i \"s/.*PermitRootLogin.*/PermitRootLogin yes/g\" /etc/ssh/sshd_config

Вот только вопрос, а как же нам выполнить эти команды? Ведь virt-builder работает только с образами дисков, никаких виртуальных машин на этом этапе не создаётся. Однако, у virt-builder есть ещё пара козырей в рукаве. Он позволяет запускать программы гостевой системы без запуска собственно гостевой системы с помощью параметра --run-command:


virt-builder ubuntu-18.04     --format qcow2     --output my_super_disk.qcow2     --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_ssh.yaml:/etc/netplan/

А как это работает?

Хороший вопрос. Документация libguestfs ничего не говорит нам о деталях реализации этого механизма, а исходники довольно запутаны. Но, похоже, что здесь используется User Space Linux Kernel. Это не контейнеры, но очень на них похоже. В любом случае можно сделать следующие выводы:


  • в параметре --run-command можно смело вызвать любой бинарник, который есть на диске гостевой системы;
  • при этом можно смело пользоваться сетью. Например — устанавливать пакеты с помощью apt install.

И теперь… всё! Канал настроен! Можно было бы даже попробовать подключиться, если бы не одно "но". После выполнения команды virt-install виртуалка только-только начинает включаться. Между моментом включения виртуалки и запуском на ней ssh сервера может пройти несколько секунд. Поэтому нам потребуется организовать незатейливый механизм ожидания ssh сервера:


#!/bin/bash
SSH_CMD="sshpass -p 1111 ssh -o StrictHostKeyChecking=no"

while ! $SSH_CMD root@192.168.100.2 echo Hello world from my super vm!
do
    echo "Waiting for my super vm ..."
    sleep 1
done

Также, мне пришлось добавить параметр -o StrictHostKeyChecking=no к команде ssh для того, чтобы запрос на добавление сервера в список доверенных серверов не блокировал выполнение скрипта. Так же мне пришлось воспользоваться утилитой sshpass для того, чтобы автоматизировать ввод пароля.


Итоги


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