Всем привет! Меня зовут Александр, я старший инженер по верификации в YADRO. В блоге уже были статьи о том, как мои коллеги из других отделов мучают наши дисковые массивы, — одна из них тут. Разумеется, делается это с благой целью: чтобы улучшить параметры и избавиться от багов.
Наш отдел тоже работает с системами хранения данных, но без издевательств над массивами: мы проводим сертификационное тестирование. Результат наших трудов — сертификат, подписанный с обеих сторон и подтверждающий совместимость нашего изделия и стороннего программного продукта или аппаратного средства. Например, дискового массива TATLIN.UNIFIED Gen2 с RED OS 8.0. Для коммерческих и государственных организаций такие сертификаты — серьезный аргумент при планировании закупки оборудования или ПО.
Вот только из-за приличного объема ручного тестирования сертификация отнимает много времени и сил, и это несмотря на опыт и навыки. Но мы не стоим на месте и стараемся автоматизировать процесс. Многое пока только в планах, но кое-что уже удалось реализовать. О нескольких удачных приемах автоматизации сертифи��ационного тестирования как раз и расскажу сегодня — сможете применить в своих проектах.

Методики разрабатываем сами
Мы проводим тестирование по нашим внутренним программам и методикам. Практика показывает, что они есть не у всех вендоров операционных систем, да и нам хочется быть уверенными в своем железе и максимально полно выявлять ограничения при работе с нашим оборудованием. Если у партнера есть методика и требования, мы берем их за основу, но с нашими расширениями и дополнениями. Если методики нет, то максимум, что можно сделать в плане автоматизации, — простейшие smoke-тесты. Продуманная методика позволяет качественно провести ручное тестирование и упростить автоматизацию.
Сейчас мы ищем в команду старшего инженера по верификации. Присоединяйтесь — будем рады!
Обычно методики пишутся под ручное тестирование. С одной стороны, это хорошо, ведь информация изложена по-человечески, иногда вплоть до команд массивов, операционных систем и сетевого оборудования. C другой, из-за ориентации на человека они не всегда применимы для автоматизации. С чем это связано:
Тесты выполняются в определенном порядке. Состояние стенда после одного теста служит основой для проведения следующего. Если что-то ломается по ходу тестирования, опытный инженер быстро приведет все в порядок.
Причина предыдущего пункта — экономия трудозатрат тестировщика. Иногда этим же обуславливается отказ от сложных или массовых (с точки зрения выполнения или анализа результатов), но интересных тестов.
Если в пределах одного теста прогоняются несколько сущностей и происходит сбой, эксперт разберется, что считать успехом, а что отметить негативной записью в протоколе.
Вывод здесь такой. Прежде чем писать код, нужно переработать существующие методики и адаптировать их к автоматизации:
1. Произвести декомпозицию «жирных» тестов. Система тестирования должна дать четкое и однозначное заключение о результате проверки конкретного компонента или подсистемы.
2. Каждому тесту оформить стадии setup/teardown. Понятно, что будет много копипасты, но при реализации все это уйдет в функции, методы классов и фикстуры. Мы делаем независимые друг от друга тесты, которые не привязаны к порядку выполнения. Каждый тест считает, что стенд находится в нулевом состоянии, — и это же состояние он обязан оставить после себя. Так мы сразу устраняем основную проблему расширения ручных методик: куда вставить новый тест, чтобы не сломать имеющиеся.
3. Тщательно проработать критерии успешности и неуспешности тестов. Не получится оценивать результат «на глазок». Описания, что при работающем многопутевом доступе (multipath) трафик должен равномерно распределяться по активным путям, уже недостаточно. Но если указать допустимое процентное отклонение между трафиком путей, это можно измерить и оценить программным методом.
4. Добавить новые тесты, пусть даже на начальной стадии они просто осядут в бэклоге. Может оказаться, что запрограммировать их — не такая уж и сложная задача.
Пример одного из тестов
Блочный доступ. Общие проверки. Презентация LUN по одному пути | |
Описание | Предварительная настройка |
Проверяется доступность ресурсов дискового массива для тестируемой операционной системы путем подключения LUN по протоколу блочного доступа. | Обеспечить связность массива и сервера по СХД или IP-сети для FC и iSCSI,NVMe-over-TCP соответственно, по одному пути. На массиве разметить LUN и презентовать тестируемой системе. |
Выполняемые действия | Ожидаемый результат |
На тестируемой системе, при необходимости, произвести процедуры обнаружения и добавления блочных устройств. | Вывод утилиты lsblk покажет новое блочное устройство с параметрами, соответствующими выделенному LUN и дискового массива. |
Как устроена архитектура
У нас все реализовано на Python с использованием pytest. Если вернуться к примеру теста, видно, что он универсален и не привязан к конкретному протоколу блочного доступа. В таком случае все протоколозависимые действия размещены в разделе «Предварительная настройка». Эта особенность позволяет полностью реализовать концепцию pytest с фикстурами для этапов setup/teardown и самими тестами, которые параметризуются тестируемым протоколом доступа.
Расписывать другие преимущества pytest не вижу смысла: это де-факто промышленный стандарт тестировщиков, для нашей задачи он подходит идеально. А вот архитектура комплекса заслуживает пояснения.
Прежде всего взглянем на типичный стенд:

На схеме видим дисковый массив TATLIN.FLEX, сервер (физический, но можно использовать и виртуальный) с развернутой операционной системой ALSE 1.8 и некоторое количество активного сетевого оборудования. Схема типовая, а вот состав и номенклатура весьма разнообразны.
Физическая схема (L2 по модели OSI) с вкраплениями информации о более высоком уровне (L3) нужна для удобства тестировщика. Еще есть дополнительные информационные системы: TMS TestY, NetBox, Jira, Bitbucket и другие. Отображать их нет смысла: взаимодействовать с ними можно только через рабочую станцию инженера и при его активном участии.
На первый взгляд, кажется вполне логичным разворачивать специализированный софт на сервере с тестируемой операционной системой и там-же выполнять процесс тестирования. Но при всей простоте реализации тут много минусов. Перечислю самые неприятные:
На тестируемую систему будет установлено дополнительное программное обеспечение и сопутствующие библиотеки. Если ОС не сможет предоставить все необходимое для запуска системы тестирования, нужно будет подключать «левые» репозитории — и таким образом вносить нежелательные изменения.
Тестируемая система может зависнуть или потребовать вмешательств от инженера. Тогда тест остановится или прервется.
Мы проводим тестирование в выделенных тестовых сегментах, у которых нет доступа к нашим информационным системам и сегменту управления оборудованием. Без информационной безопасности сейчас никуда — это надо уважать и принимать. Так что настраивать и изменять инфраструктуру по ходу тестирования нужно будет отдельно, вручную. Еще придется поработать биороботом, чтобы перенести информацию из различных систем в тесты и выгрузить результаты в учетную систему тестирования.
Эти минусы исчезают, если развернуть комплекс автотестирования на отдельной машине, — например, на рабочей станции инженера или на CI-инфраструктуре. Доступы во внутренние информационные системы есть, управлять оборудованием — легко, все необходимое на операционной системе делается удаленно с помощью SSH. Если от тестируемой системы нет реакции, по таймауту автоматически перезагружаем сервер с помощью bmc/ipmi.
Может казаться, что код получится сложным, но это не так. Дальше мы с вами в этом убедимся.
Работа с железом
Выше я уже писал, что для стенда задействовано много разнообразного оборудования — это и сетевые коммутаторы четырех видов, включая наш KORNFELD, и коммутаторы FC, и наши дисковые массивы различных линеек. Как все это настраивать, как с ними общаться в процессе тестирования?
Ответ прост – Ansible. Практически под каждое оборудование есть модули, а если нет, то их легко написать самостоятельно. К тому же, вынося платформозависимый код в плейбуки Ansible, мы упрощаем код самой системы тестирования. В перспективе быстро добавляем новые типы инфраструктурного оборудования без погружения в их низкоуровневое API.
Осталось определиться, как организовать работу с Ansible. Какие тут варианты:
Запускать плейбуки через subprocess. Желательно сделать обертку, чтобы было удобно и красиво. И да, тут придется подумать над интерфейсами и парсерами результатов.
Поискать в плагинах pytest все со словом Ansible в названии и описании. Такие плагины есть, но функциональность так себе.
В итоге наш выбор пал на вариант с использованием модуля Python по имени ansible-runner. Он настолько интересный, что остановлюсь на нем подробнее.
Что там с Ansible-runner
Модуль умеет запускать как плейбуки, так и команды (модули Ansible) напрямую — ad hoc. От нас нужно подготовить инвентарь (inventory) и плейбуки.
Плейбук запускается одной командой:
r = ansible_runner.run(
playbook=BLKLUNMP[conn_method],
limit=f'{stand["storage"]},{stand["server"]}',
private_data_dir=os.getcwd(),
extravars=stand,
rotate_artifacts=5)
assert r.status in 'successful', 'setup storage failed'Здесь все очевидно:
playbook— имя файла с выполняемым плейбуком. Здесь приведен кусочек кода из проекта, где имя плейбука выбирается из словаря, в зависимости от тестируемого протокола доступа.limit— позволяет ограничить выполнение выбранными хостами. Аналог ключа--limitпри вызове ansible-playbook.private_data_dir— тут хранятся inventory и прочая иерархия входных данных.extravars— позволяет передать дополнительные переменные в плейбук. Аналог ключа--extra-varsпри вызовеansible-playbook. У этих переменных максимальный приоритет. Они позволяют передать много полезной информации для выполнения плейбука: имена, размеры и прочие параметры LUN, которые нужно сгенерировать для теста, названия портов и зон на коммутаторах SAN, параметры сетевых подключений и так далее. Понятно, что некоторые параметры можно исхитриться и высчитать или сгенерировать средствами Ansible, используя, например, фильтры и Jinja, но с чистым Python это проще и быстрее.rotate_artifacts=5– показывает, сколько каталогов с артефактами хранить между запусками. Полезно при разработке и отладке тестов.
Если обратили внимание, в примере параметр limit содержит два разных типа оборудования — сервер и хранилище. На самом деле есть места, где их даже больше: добавлены коммутаторы FC и сети Ethernet. В плейбуках прописано несколько пьес (play), и исполняются они с различными типами оборудования через привязки к группам. Если хост оказался TATLIN.FLEX, выполняется play с привязкой к группе tatlin_flex, а если TATLIN.UNIFIED — tatlin_unified. Для коммутаторов Brocade привязка будет своя, для операционки — ��налогично. Главное, чтобы в inventory все тестируемое оборудование было аккуратно разложено по соответствующим группам. Если такой подход не нравится, никто не запрещает использовать отдельные плейбуки для каждого типа оборудования.
Всего вышеописанного для полноценного тестирования нам все же недостаточно. К примеру, для теста, который я показывал в начале статьи, на этапе setup мы выполнили плейбук, где настроили различные зоны, создали на массиве LUN с необходимыми параметрами из extravars и отдали его инициатору. Дальше в тесте нам нужно четко определить, что операционная система видит новое блочное устройство — и это именно наш LUN. И что размер блока, серийный номер/WWN и другие параметры соответствуют тому, что мы передавали в плейбук и что размечено на самом массиве.
Для этого в плейбук, сразу после создания LUN, вставляем вывод информации:
- name: create lun
delegate_to: localhost
flex_lun:
lunname: "{{ lunname }}"
size: 10
raid: "{{ pool }}"
luntype: scsi
state: present
force: true
register: lun
- name: print UUID
debug:
msg: "{{ lun.msg.uuid }}"Дальше после вызова ansible_runner.run, можно просто отпарсить r.stdout и найти требуемое значение uuid — по нему мы однозначно идентифицируем блочное устройство операционной системы, связанное с нашим LUN. Но ansible-runner сильно упрощает задачу получения информации из плейбука, ведь тут все уже разложено по полочкам, или событиям. Вот пример кода для разбора событий после вызова ansible_runner.run:
for events in r.events:
if event['event'] == 'runner_on_ok':
if event['event_data']['task'] == 'print UUID'
uuid = event['event_data']['res']['msg']Тот же подход используется, если в плейбуке выводили что-либо в цикле.
Ну и чтобы закрыть тему Ansible, затрону две интересные возможности:
ansible_runner.get_inventory– позволяет прочитать inventory. Если завернуть его в pytest-фикстуру уровня сессии, можно получать значения всех переменных inventory из любого теста. Не нужно ползать по каталогам и парсить YAML самостоятельно.В фикстуру уровня сессии можно вставить плейбук или прямой вызов модуля, который просто собирает факты с оборудования стенда. Потом эту информацию можно обработать для дальнейшей передачи в следующие плейбуки через
extravars. Это намного быстрее и удобнее, чем собирать ее в самих плейбуках.
Пример с доступом к инвентарю и вывод после сбора фактов
Не удержался и набросал небольшой пример. Собрал факты через ‘ad hoc’ вызова модуля setup. В самом тесте только выводится информация о WWPN-адресе первой оптической карточки.
import os
import pytest
import ansible_runner
@pytest.fixture(scope='session')
def ansible_inventory():
inventory,_err = ansible_runner.get_inventory(
action='list',
inventories=['inventory'],
response_format='json',
private_data_dir=os.getcwd(),
quiet=True
)
yield inventory
@pytest.fixture(scope='session')
def ansible_facts_server():
r = ansible_runner.run(
host_pattern='CERT-ONYX',
module='setup',
private_data_dir=os.getcwd(),
quiet=True,
)
for event in r.events():
if event['event'] == 'runner_on_ok':
server_facts=event['event_data']['res']['ansible_facts']
yield server_facts
def test_333(ansible_inventory, ansible_server_facts):
inventory_wwpn0 = ansible_inventory['_meta']['hostvars']['CERT-ONYX']['fc'][0]['wwpn']
print(f'interface fc0 wwpn from inventory: {inventory_wwpn0}')
facts_wwpn0 = ansible_facts_server['ansible_fibre_channel_wwn'][0]
# приводим к стандартному виду — октеты разделенные двоеточием
facts_wwpn0 = ':'.join(facts_wwpn0[i:i+2] for i in range(0,16,2))
print(f'interface fc0 wwpn from ansible_facts: {facts_wwpn0}')Ansible, безусловно, упрощает задачу подготовки стенда и самого тестирования. Но подчеркну, что мы относимся к нему без фанатизма: многие вещи проще выполнить родными средствами Python. В нашем случае мы взаимодействуем с тестируемым хостом (тестируемой операционной системой) через модуль Python под названием sh. Раньше в проекте он использовался для локального выполнения команд, но оказалось, что и удаленное выполнение через SSH удобно и наглядно вписывается в исходный код тестов. В документации доходчиво показано, как работать с перенаправлением ввода и вывода, обрабатывать коды возврата, выборочно подавлять ошибки и все в таком духе. Изначально для взаимодействия с удаленным хостом по SSH мы использовали paramiko, но после знакомства с sh все было переписано, и возвращаться обратно пока желания не возникает.
Синтетический пример с удаленным получением информации обо всех блочных устройствах прилагается. Доступ к серверу настроен по ключам:
import sh
remote_host=sh.ssh.bake('root@cert-onyx')
block_devices_json=remote_host.lsblk('-OJ')Тут есть небольшая хитрость. У lsblk ключ -J, который выводит информацию в формате JSON. Это упрощает дальнейшую работу с данными, ведь писать свой парсер не нужно. Сейчас у многих утилит есть варианты вывода в JSON, и это предпочтительный способ общения с ними.
Запуск утилит мы выполняем два раза: один раз для обработки с выводом в JSON, второй — в стандартном виде для записи в лог, если потребуется проводить анализ при помощи человека. Другие артефакты тестирования с хоста тоже собираем этим модулем.
Автоматизация с TestY
Выше я упоминал внутренние информационные системы, к которым хочется иметь доступ в процессе тестирования. С Jira понятно: берем идентификатор задачи и состав стенда. Netbox используем для получения топологии стенда и формирования inventory. А что у нас с TestY?
TestY (Test Management System, TMS) — наша собственная система управления тестированием. Выложена в open source. В блоге есть несколько подробных статей от ее разработчиков — например, тут.
Так зачем нам TMS для автоматизации? Давайте снова вернемся к началу. Ручное тестирование мы проводим с помощью методики, описывающей, как производить отдельные тесты и интерпретировать их результаты. У нас есть программа испытаний с перечнем и параметрами тестов, которые нужно провести. Часто все это объединяется в один документ под названием «Программа и Методика испытаний», или ПМИ.
При автоматизированном подходе можно также опираться на «бумажный» вариант ПМИ и производить тестирование, вручную запуская требуемые тесты и занося их результаты в протокол. Это несколько трудозатратно и скучно. Нас же интересует полностью автоматическое тестирование – чтобы мы могли подготовить стенд, входные параметры испытаний, запустить средство автотестирования и в итоге получить результаты не в виде «сырых» данных, а с конкретной оценкой «пройдено» или «не пройдено». Понятное дело, с подтверждающими материалами.
В случае с TestY мы заносим методики с тестами в сущности под названиями Test Suite и Test Case соответственно. Потом, используя один или несколько Test Suite, формируем Test Plan (тест-план, программа испытаний). В нем может находиться несколько одинаковых тестов, которые генерируются на основе Test Case с разными параметрами.
Вся эта информация в электронном виде, да еще и с доступом по REST API, позволяет минимизировать ручной труд — достаточно подготовить тест-план и передать его в параметрах системе автотестирования. Данных в нем хватает для самостоятельного выполнения тестов. А дальше, в процессе автотестирования, нам нужно внести результаты с артефактами в TestY.
Из того, что я описал, вырисовываются две технические задачи:
отфильтровать и выполнить из имеющихся тестов только те, что есть в тест-плане TestY;
занести результат их выполнения в TestY.
Интегрируемся с Testy
В pytest есть механизм, который на каждом этапе жизненного цикла позволяет изменять и расширять стандартную функциональность. Реализуется он через hook-функции.
На этапе коллекционирования тестов pytest соберет все доступное из каталогов и даст нам возможность поработать со списком тестов в хуке pytest_collection_modifyitems. Здесь нужно сопоставить собранные pytest-тесты, включая их параметры, с перечнем тестов из TestY, тоже с параметрами. Параметры мы специально делаем идентичными. Они легко поддаются сопоставлению, а вот имена тестов сравнить сложнее.
Имя теста в TestY — это просто строка с осмысленным названием, которое позволяет оператору ориентироваться в тест-плане. Оно не уникально и может произвольно изменяться в процессе жизненного цикла Test Suite. Раньше мы добавляли в начало названия теста TestY имя теста pytest в квадратных скобках. Это не слишком мешало восприятию и позволяло связать тест в TestY и его реализацию в pytest. Но как только в TestY появились кастомные атрибуты, мы сделали совсем красиво: имя теперь храним в атрибуте ‘shortname’.
Сопоставляем имена тестов и их параметры мы не просто так. Во-первых, все, что не удалось сопоставить, отбрасываем — это позволяет нам точно идти по тест-плану. Во-вторых, через user_properties сохраняем в метаданных pytest-теста уникальный идентификатор (testid) теста из Testy. Дальше он будет использоваться для указания, в какой тест занести результат выполнения и артефакты тестирования.
На этом работа с тестами не заканчивается. Бывает, в тест-план попадают тесты, которые железо не поддерживает. Пример: к тестированию TATLIN.UNIFIED Gen2 v 3.1 добавили тестирование NVMe-over-TCP, а оно появилось только с версии 3.2. Просто тихонечко отбросить эти тесты было бы некорректно, поэтому из тестирования мы их исключаем, но при этом сразу заносим в Testy результат SKIPPED с пояснением причины. Ну а дальше запускаем pytest и радостно наблюдаем, как тесты проходят, падают и рисуют разноцветные точки — в общем, наслаждаемся стандартным процессом.
Теперь давайте запишем результаты в TestY. Здесь тоже задействуем хук, который называется pytest_report_teststatus. Он может быть вызван на стадиях setup, call и teardown (содержится в атрибуте report.when). В зависимости от этой самой стадии и логических атрибутов report.passed, report.failed, report.skipped, мы генерируем отчет и заливаем его в TestY для теста с идентификатором testid:
Ошибка произошла на стадии
setup. Тесту задается статусBROKEN. В комментарий добавляемreport.longrepr, содержащий контекст с ошибкой.Хук вызван на стадии
‘call’со статусамиreport.passed,report.failed,report.skipped. В TestY формируем результатPASSED,FAILED,SKIPPED, обогащаем артефактами тестирования и, при наличии, контекстом с ошибкой.Все события на стадию
‘teardown’пропускаем. Результат самого тестирования уже получен и перекрывать его сбоями зачистки стенда не стоит.
В планах — разобраться, как обработать результаты xfailed, xpassed. И отобразить их в TestY с результатом RETEST, чтобы потом обратить на них внимание.
На сегодня у меня все, но продукт еще не закончен. Есть много идей как по новым тестам, так и по доработкам существующих, не охвачен весь спектр оборудования и операционных систем. Но подходы, которые мы выработали и реализовали, позволят нам улучшить код и безболезненно развивать его при расширении модельного ряда наших продуктов и новых релизов партнерских ОС. А главное, теперь я запускаю тестирование и спокойно ложусь спать, ведь результаты, которым можно доверять, будут готовы уже к утру.