-
Для создания mqtt клиента используйте
MQTTClient(на базе paho) из библиотеки wb_common. Оба варианта запуска показаны в main.py. Функция клиента start() по умолчанию запускает его в отдельном потоке, так сделан классThreadedServiceTemplate. Создание отдельного потока можно выключить и запускать клиента вручную через start() + loop_forever() в текущем потоке, так сделан классOneThreadServiceTemplate. Использовать функцию loop() для работы не рекомендуется, тк она не делает автоматический реконнект в случае разрыва соединения с брокером. -
Для перехвата сигналов используйте библиотеку signal: signal.SIGTERM (остановка через systemctl stop) и signal.SIGINT (остановка через Ctrl-C в консоли). В обработчике сигнала нужно вызвать stop(). Если вы используете loop_forever(), то в обработчике не нужно принудительно завершать основной процесс, выполнение после обработки сигнала вернётся в loop_forever(), которая завершится. Больше информации и примеров http://www.steves-internet-guide.com/client-connections-python-mqtt/.
-
Запуск. Весь основной код оформляется отдельным python-пакетом. В пакете создается основной файл (например
main.pyили по имени сервиса), в нем — функцияmain, которая может быть как импортирована в другой код, так и запущена при обычном запуске файла из консоли. В папкеbin/создается обёртка, которая импортирует и запускаетmainиз пакета.Обёртку в
bin/делаем всегда, даже если сервису не нужен CLI. Причин три. Единообразие — любой сервис WB запускается одинаково и его точка входа лежит в предсказуемом месте. Готовое место под--version, которую мы добавляем каждому сервису. И запас на будущее — если из сервиса понадобится сделать настоящую утилиту с флагами, пакетирование переделывать не придется. -
--version. Каждый сервис умеет печатать свою версию. Флаг регистрируется там же, где живет парсер, через_PrintVersionAction— версия читается только когда флаг реально передан, поэтому сервис, запущенный из исходников без установленного пакета, работает как раньше. Делаем именно--versionбез короткой формы, потому что-vв наших утилитах часто занят под verbose. Сам version.py про argparse не знает ничего и остается пригодным для пакетов вообще без CLI.
Источник версии один — debian/changelog. Дальше она расходится так.
setup.pyпри сборке зоветget_version_from_changelog()и кладет версию в метаданные python-пакета. Суффикс бранчевой сборки (~exp~...) при этом отрезается, потому что PEP 440 символ~не разрешает.- Код в рантайме зовет
get_version(), который читает версию из метаданных установленного пакета черезimportlib.metadata. Искать файлы рядом с собой не нужно, поэтому одна и та же строка доступна и CLI, и сервису, и любому библиотечному коду. Имя дистрибутива берется из__package__— по спецификации нормализации имен серии точек и подчеркиваний равны дефису, поэтомуwb.python_service_templateнаходит метаданныеwb-python-service-template. tests/test_version.pyпроверяет ту же строку из метаданных. Тест намеренно не читает changelog сам — во время сборки pytest работает из.pybuild/<интерпретатор>/build, где каталогаdebian/нет, а через метаданные тест заодно покрываетget_version(), то есть ту функцию, которой пользуется реальный код.
Метаданные пакета появляются только на стадии install, уже после тестов. Поэтому в debian/rules стоит хук PYBUILD_BEFORE_TEST, который создает их в сборочном каталоге перед тестами. Убирать их после не нужно, pybuild удаляет метаданные из сборочного каталога сам перед установкой.
--version печатает версию релиза, без суффикса бранчевой сборки. Полная deb-версия всегда видна в dpkg -l и в диагностическом архиве.
Тесты запускает pybuild на шаге dh_auto_test, командой pytest tests из сборочного каталога.
python3-pytest в Build-Depends обязателен, и он там не для того, чтобы запускать тесты руками. Именно по наличию этого пакета pybuild выбирает pytest, а без него молча уходит на unittest discover, и тесты в стиле голых функций просто не соберутся.
Доктесты включены по умолчанию флагом --doctest-modules в pytest.ini. Пример — parse_changelog_version() в version.py, где примеры вызова документируют разбор строки changelog внятнее, чем это сделал бы комментарий. Доктесты уместны ровно там, где пример и есть самая понятная документация функции, и падают вместе со сборкой, если поведение разошлось с примером.
Из-за --doctest-modules на сборке импортируется каждый модуль пакета, а не только те, которые тянут за собой тесты. Отсюда два требования к любому модулю под wb/. Первое, все его зависимости должны стоять в Build-Depends, а не только в Depends. Второе, на уровне модуля не должно быть кода, который что-то делает при импорте, то есть читает конфиг, создает клиента или ходит в сеть, потому что на сборщике ни конфига, ни сети нет. Нарушение любого из двух роняет сборку на стадии сбора тестов.
Прогоняться должны все тесты вместе с доктестами, и одинаково в двух местах — на сборке и локально командой pytest без параметров из корня проекта. Ради этого в addopts перечислены оба каталога. Без wb доктесты не достают до модулей пакета, потому что явный путь tests от pybuild перебивает testpaths. Без tests ломается обратное, при запуске без параметров единственным корнем сбора становится wb и обычные тесты выпадают из прогона. Оба пути резолвятся от каталога запуска, поэтому звать pytest надо из корня. pythonpath = . нужен, чтобы пакет вообще импортировался — голый pytest не кладет текущий каталог в sys.path, в отличие от python -m pytest. Он резолвится от каталога с pytest.ini, а его pybuild копирует в сборочный каталог, поэтому строка верна и локально, и на сборке.
Локально перед pytest нужен один шаг, python3 setup.py --quiet egg_info. Тесты версии читают метаданные пакета, а вне сборки их никто не создает, поэтому без этого шага тесты версии падают. На сборке то же самое делает хук PYBUILD_BEFORE_TEST из debian/rules.
Проверки, у которых нет потребителя в рабочем коде, живут в тестах, а не в пакете. Поэтому регулярное выражение для формата версии лежит в tests/test_version.py, а в version.py такой функции нет намеренно.
Если у сервиса появляется CLI с подкомандами, к нему стоит положить файл автодополнения. Пример — wb-homeui-gates.bash в homeui, он сгенерирован один раз утилитой shtab по argparse-парсеру и закоммичен как есть.
Файл кладется в completions/<имя-утилиты>.bash, а в debian/<пакет>.install добавляется строка
completions/wb-python-service-template.bash /usr/share/bash-completion/completions
В самом шаблоне такого файла нет намеренно. У шаблонного CLI всего два флага, дополнять в них нечего. shtab тоже подключаем только когда у утилиты действительно много подкоманд и опций, иначе к сборке добавляется зависимость и шаг генерации ради пары строк.
Аннотации типов обязательны — у всех параметров и у всех возвращаемых значений, включая -> None. Требование записано в codestyle.
Докстринг пишем там, где он говорит то, чего нет в имени и подписи — контракт возвращаемого значения, выбор между несколькими вариантами, неочевидное поведение. Докстринг, пересказывающий имя функции, читать только мешает, поэтому в шаблоне их нет ни у MQTT-колбэков, ни у обработчиков сигналов. Есть у обоих классов-примеров, потому что из имени не видно, когда какой брать, у их run(), потому что он возвращает код выхода, и у main().
Форма одна, многострочная. """ на отдельной строке, за ней суммарная фраза, при необходимости пустая строка и подробности, дальше секции в стиле Google (Args:, Returns:, Raises:). Однострочную форму """Текст.""" не используем, даже когда описание помещается в строку.
Исходная система - где происходит сборка deb-пакета. В терминологии debian - build-система Целевая система - куда устанавливается пакет. В терминологии debian - host-система
Для сборки deb-пакетов в Wiren Board используется кросс-компиляция. Это значит, что сборка будет происходить на машине с архитектурой, отличной от той, на которой всё будет запускаться, и нужно подсказать сборщику, как правильно разрешать зависимости.
-
Собрать пакет вручную и посмотреть что получается можно внутри devcontainer-a командой
dpkg-buildpackage -rfakeroot -us -ucДиректория debian в проекте (исходная система) будет содержать структуру директорий относительно корня целевой системы. Перед сборкой не забыть установить build-depends.
-
/debian/control для Python
Для source пакета в
Build-Dependsнужно использоватьpython3-allвместоpython3, потому что это отвязывает процедуру сборки от конкретной реализации Python, иdistutilsавтоматически подтягивается с подходящей версией. Писатьpython3-all:anyв пакетах-библиотеках обычно не имеет смысла, тк используемarch-all.Source: wb-my-lib ... Build-Depends: python3-all, dh-python, ...Для binary пакетов
Package: wb-my-lib Architecture: all Depends: ${python3:Depends}, ${misc:Depends}, ...Architecture: all(не путать сArchitecture: any) нужен для того, чтобы собранный пакет был платформо-независимый.python3в зависимости не записывается, его заменяет${python3:Depends}. -
Кодогенерация при кросс-компиляции
У нас этот сценарий применяется в wb-mqtt-serial. Кодогенерация там - сборка шаблонов с помощью
j2cli. Для того чтобы установитьj2cli, совместимый с архитектурой сборщика, используем:native.Source: wb-mqtt-serial ... Build-Depends: ..., j2cli:native -
Jenkinsfile
В шаблоне вызов пайплайна закомментирован намеренно. Иначе Jenkins собирал бы сам шаблон и публиковал его в релизный фид, где шаблону не место. В настоящем пакете эти строки надо раскомментировать.
buildDebArchAll defaultRunPythonChecks: true, defaultRunLintian: true, defaultAngryPylint: true
defaultRunPythonChecksдобавляет стадию проверок кода. На ней по всем файлам питона в репозитории проходятblack,isortиpylintс конфигами из codestyle.defaultRunLintianдобавляет стадию после сборки, на которой готовый deb проверяет дебиановский линтерlintianс профилем WB из того же codestyle. Он смотрит на упаковку, а не на код, и валит стадию на замечаниях уровня error.defaultAngryPylintделает ошибкиpylintфатальными для сборки. Без него они попадают только в лог, а стадия остаётся зелёной. -
Резервное копирование конфигов
Сервис wb-configs-early умеет делать резервное копирование файлов перед началом работы системы и перед выключением. Он перемещает сам файл конфига в раздел, который не сбрасывается при установке обновлений, а сам конфиг в исходном расположении
/etc/заменяет на симлинк. Для этого нужно в папку/etc/wb-configs.dположить скрипт, который вызоветwb_moveдля всех нужных конфигов (в данном примере это10wb-python-service-template).
-
Почему all-пакеты не собираются в sbuild с
--host=armhf?Альтернативная формулировка - почему all-пакеты нельзя было раньше собрать через
wbdev cdeb.Во-первых, потому что в Debian обычно делят сборку all-пакетов и кросс-компиляцию, вот (пруф).
Во-вторых, это могло бы заработать, если бы мейнтейнеры Python-библиотек в Debian озадачивались добавлением
Multi-Arch: foreignв свои пакеты. На ноябрь 2022 этой пометки нет как минимум вpython3-paho-mqttиpython3-jinja2, а эти пакеты мы часто используем.В итоге оказалось проще пойти каноничным путём и собирать
allиanyотдельно. -
Для чего при сборке cdeb запускаются и
--arch-all, и--arch-any?Раньше для сборки пакетов отдельно использовались
wbdev cdeb(дляany) иwbdev ndeb(дляall). Это было очень актуально во временаwheezy, когда не было ещё multiarch, и пакеты для Wiren Board приходилось собирать в chroot с qemu. Тогда для экономии времени сборки (qemu медленный) сделали отдельно командуndeb, которой собиралиall-пакеты прямо в Docker-окружении.Потом мы перешли на sbuild, и разница между
cdebиndebстала размываться, потому что для обеих команд уже использовалось sbuild-окружение, только с разным аргументом--host.Так получилось, что при переходе на релизную систему, когда для сборки стало нужно указывать, для какой платформы происходит сборка (wb5/wb6/wb7), добавление наших репозиториев (
http://deb.wirenboard.com/wbX/<deb-release>) делалось только вcdeb. Дляndebпоявился репозиторийdev-tools, который нужен скорее для сборки пакетов, нужных нам на производстве и для CI.Пока наши all-пакеты не зависели друг от друга при сборке, можно было продолжать пользоваться такой системой, выбирая в зависимости от типа пакета
cdebилиndeb. При разработке wb-device-manager нам понадобились all-зависимости из нашего репозитория.В этот момент @webconn решил, что пора объединять
cdebиndeb- так получается меньше пайплайнов для CI и не надо дублировать код в devenv. Также меньше возможностей ошибиться при сборке - скрипты сами сделают всё, что нужно.ndebс этого момента стал deprecated, о чём пишется WARNING. -
Почему в Python-библиотеках не пишем Multi-Arch в пакетах?
Как минимум потому что это не делают даже в Debian.
Перед тем как открывать PR, убедитесь, что:
- Код проходит тесты (из корня проекта
python3 setup.py --quiet egg_info && pytest); - Концы строк во всех файлах остаются
LF(это обеспечивает.gitattributes); - Пакет успешно собирается и на выходе получается deb-файл;
- Пакет проверен на реальном контроллере:
- Устанавливается;
- После установки импортируется namespace-пакет;
- Пакет сообщает свою версию по
--version; - Сервис запускается сам после установки;
- Сервис автоматически стартует после перезагрузки контроллера;
- Пакет удаляется.
Выполняется на самом Wiren Board (арх all, подойдёт любой).
Репозитории WB на контроллере должны быть настроены — из них тянется python3-wb-common.
# 1. Инструменты сборки (один раз на контроллер)
sudo apt update
sudo apt install -y build-essential devscripts equivs git
# 2. Копия из своей ветки
GIT_BRANCH_NAME="feature/branch-name"
git clone -b "${GIT_BRANCH_NAME}" --single-branch \
https://github.com/wirenboard/wb-python-service-template.git
cd wb-python-service-template
# 3. Build-зависимости из debian/control и сборка пакета
sudo mk-build-deps -ir debian/control
dpkg-buildpackage -us -uc -bДалее нужно проверить что пакета корректно собран и сервис поднимается сам
# 1. Установка собранного пакета
sudo apt install -y ../wb-python-service-template_*.deb
# 2. Проверка импорта namespace-пакета и версии из метаданных пакета
python3 -c "import wb.python_service_template.main as m; print('import OK:', m.main)"
wb-python-service-template --version # ожидается: версия из debian/changelog
# 3. Проверка работы сервиса
systemctl is-enabled wb-python-service-template # ожидается: enabled
systemctl status wb-python-service-template # ожидается: active (running)
# 4. Проверка автозапуска после перезагрузки
sudo reboot
# после загрузки снова зайти по SSH и проверить, что сервис поднялся сам:
systemctl status wb-python-service-template # ожидается: active (running)
# 5. Удаление после проверки
sudo apt purge -y wb-python-service-templateПри успешной сборке pytest прогонит и тесты из tests/, и доктесты из модулей пакета
(шаг сборки dh_auto_test), а после установки сервис включится автоматически благодаря
секции [Install] в юните.