Как уже говорилось ранее, DeviceManager — программная прослойка, которая используется для постановки заданий устройствам и получения результатов их выполнения. Сегодня речь пойдёт об организации процесса обновления программы и осуществления мониторинга рабочих мест, так как без этого проблематично развивать продукт и осуществлять его поддержку.
Автоматизация процесса обновления
Мысли автоматизации процесса обновления появились практически сразу после появления продукта, так как обновлять более 100 рабочих мест вручную — трудозатратно, к тому же каждое из них в зависимости от предназначения настраивается по-разному. На практике места обновлялись по мере внедрения новых плагинов в DeviceManager.
Обеспечивать поддержку ещё трудозатратнее: потратив несколько часов на проблему могло выясниться, что это “пофиксили” в более ранних версиях, а это рабочее место просто забыли обновить.
Наделять DeviceManager функционалом по обновлению было рискованно: в случае сбоя обновления рабочее место полностью выходило из строя. А нужен был надежный механизм, как автомат Калашникова, который помогал в решении множества проблем, а не создавал их.
Мы решили применить подход с использованием дополнительного легковесного программного обеспечения — Launcher.
Launcher должен отвечать нескольким требованиям:
- малый объем;
- низкое ресурсопотребление;
- высокая устойчивость к сбоям;
- гибкость.
Под гибкостью подразумевается умение обновлять не только DeviceManager или другие программы, а в первую очередь самого себя. Тем самым нам необходимо выполнить только первичную установку программы на рабочее место, дальше обновления и работа будут происходить автономно.
Организация обмена данных
Но гибкости не добиться без управляющего звена, которое будет руководить обновлениями, обеспечивать мониторинг и необходимую информацию. В качестве этого звена будет выступать МИС (Медицинская информационная система).
Для поддержания этой концепции со стороны МИС было проделано много работы и это заслуживает отдельной статьи. Я лишь сделаю краткий обзор:
Обновление начинается с загрузки в МИС нового дистрибутива в специальный справочник с указанием номера версии. Есть возможность пометить версию как стабильную. Тогда она автоматически обновится на всех рабочих местах.
Для конфигов был добавлен справочник по настройке дистрибутивов, в котором хранятся шаблоны разных плагинов и приборов.
Для конфигурирования рабочих мест был заведен журнал рабочих узлов. Каждое рабочее место можно настроить: указать список программ для установки, их версии и конфиги.
Для мониторинга текущего состояния предусмотрен запрос в Launcher — на основе ответа из которого в графическом виде отображается индикатор:
- Зелёный — всё в норме;
- Желтый — требуется обновление;
- Красный — не запущено;
- Серый — не установлено.
Если требуется дополнительная информация — предусмотрена команда из МИС на загрузку логов. В ответ Launcher собирает все лог-файлы по сетевым сервисам с рабочего узла, архивирует и высылает в МИС. Launcher при работе использует ротацию логов, что позволяет контролировать количество лог-файлов и занимаемый ими объём.
Обновление происходит по расписанию, в строго установленное время, которое задается в МИС. Но в случае необходимости при помощи соответствующей кнопки можно выполнить экстренное обновление выбранного рабочего места.
Этого инструментария достаточно, чтобы автоматизировать обновление, наблюдать за процессом и корректировать его в случае чего.
Процесс разработки
За счет использования framework Qt удалось сократить количество внешних зависимостей до одной — библиотеки libarchive, необходимой для работы с архивами различных расширений.
Это обусловлено тем, что дистрибутивы загружаются в МИС в виде zip архива и в процессе обновления этот архив нужно распаковать. А при формировании логов для отправки на запрос МИС необходимо производить сжатие для сокращения объема данных.
Для использования представленных ниже примеров кода необходимо подключить следующие заголовочные файлы: #include <archive.h> и #include <archive_entry.h>.
bool unpackingArchive(const QString &nameArchive, const QString &pathToUnpacking)
{
archive *a;
archive_entry *entry;
int r;
a = archive_read_new();
archive_read_support_compression_all(a);
archive_read_support_format_all(a);
r = archive_read_open_filename(a, nameArchive.toStdString().c_str(),1024);
if (r != ARCHIVE_OK)
{
QFile::remove(nameArchive);
return false;
}
QDir().mkpath(pathToUnpacking);
while (archive_read_next_header(a, &entry) == ARCHIVE_OK)
{
__LA_MODE_T filetype = archive_entry_filetype(entry);
if (filetype == AE_IFREG)
{
int64_t entry_size;
const QString currentFile = archive_entry_pathname(entry);
if (currentFile.contains("/"))
QDir().mkpath(pathToUnpacking + "/" + currentFile.left(currentFile.lastIndexOf("/")));
entry_size = archive_entry_size(entry);
QByteArray fileContents;
fileContents.resize(static_cast<int>(entry_size));
archive_read_data(a, fileContents.data(), static_cast<size_t>(entry_size));
const QString nameFile = pathToUnpacking + "/" + currentFile;
QFile file(nameFile);
if (!file.open(QFile::WriteOnly))
{
QString errorMessage = "Error open file " +
QFileInfo(file).absoluteFilePath() + "; error string - " + file.errorString();
qCritical() << errorMessage;
return false;
}
file.write(fileContents,entry_size);
file.close();
}
}
archive_read_close(a);
return true;
}
bool packingZipArchive(const QString &pathToFolder, const QString &nameArchive)
{
QDir dir(pathToFolder);
QFileInfoList zipedList = dir.entryInfoList(QDir::Files);
zipedList += dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);
archive *zip = archive_write_new();
archive_write_set_format_zip(zip);
if (archive_write_open_filename(zip, nameArchive.toStdString().c_str()) != ARCHIVE_OK)
{
qCritical() << "Open zip file error: " << archive_error_string(zip);
return false;
}
archive_entry *entry = archive_entry_new();
for (const QFileInfo &i : zipedList)
{
writeFile(entry, i, zip, dir.absolutePath());
}
archive_entry_free(entry);
archive_write_close(zip);
archive_write_free(zip);
return true;
}
void writeFile(archive_entry *entry, const QFileInfo &info, archive *arch, const QString &archiveRootDir)
{
if (info.isDir())
{
QFileInfoList condition;
condition << QDir(info.absoluteFilePath()).entryInfoList(QDir::Files) << QDir(info.absoluteFilePath()).entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);
for (const QFileInfo &i :condition)
writeFile(entry, i, arch, archiveRootDir);
}
else
{
struct stat st;
QFile file(info.absoluteFilePath());
if (!file.open(QFile::ReadOnly))
{
qCritical() << "File " << info.absoluteFilePath() << " not open: " << file.errorString();
return;
}
qDebug() << "Add file " << info.absoluteFilePath();
if (fstat(file.handle(), &st) == -1)
{
qWarning() << "Error read metadata from file " << info.absoluteFilePath();
}
else
archive_entry_copy_stat(entry,&st);
QString subPath = info.absoluteFilePath().remove(archiveRootDir);
QRegExp regex("^(./|/)");
if (subPath.indexOf(regex) > -1)
subPath = subPath.remove(regex);
archive_entry_set_pathname(entry, subPath.toStdString().c_str());
archive_entry_set_size(entry, info.size());
archive_entry_set_filetype(entry, AE_IFREG);
if (archive_write_header(arch, entry) != ARCHIVE_OK)
{
qCritical() << "Open zip file error: " << archive_error_string(arch);
}
else
{
QByteArray data = file.readAll();
archive_write_data(arch, data.data(), static_cast<size_t>(data.size()));
}
archive_entry_clear(entry);
}
}
Процесс обновления Launcher
В конце хотелось бы поговорить о процедуре смены версии самого Launcher. Дело в том, что использование в процессе автоматизации Launcher не является до конца надежным методом. При обновлении также может произойти сбой, это не приведет к молниеносному выходу из строя рабочего места, но доставит немало хлопот по восстановлению.
Чтобы этого избежать было решено проводить обновление следующим образом:
Запущенная версия Launcher проверяет доступность новых версий в МИС. При наличии обновления происходит скачивание нового дистрибутива, его распаковка и настройка. Затем текущая версия запускает обновлённый экземпляр программы, не завершая свою работу. После запуска обновлённая версия производит поиск запущенного ранее экземпляра и, если находит, то закрывает его. В окончание процесса обновления удаляются все старые файлы запущенной в начале версии Launcher. Тем самым достигается “бесшовное” обновление, риск сбоя при котором крайне мал.
Итог
Благодаря автоматизации процесса обновления сильно упростилась процедура поддержки рабочих мест. Системному администратору при обновлении больше не нужен доступ к самим местам, нужна только МИС и предварительно настроенный Launcher, который берет на себя всю работу по обновлению, настройке и запуску DeviceManager на рабочих местах.
P.S. В рамках данной статьи не удалось полностью раскрыть организацию работы по обновлению со стороны МИС, но это мы исправим в следующих публикациях. До новых встреч!
Dima_Sharihin
Ничего не понял. Если у вас deb/rpm-пакетный дистрибьютив, то кто мешал просто сделать свой пакет или настроить регулярную доставку обновлений? Тогда нужно было бы только сверять текущую версию с актуальной.
Если у вас именно что embedded девайс — то для таких вещей есть что-то вроде rauc, mender и куча других готовых решений "под ключ".
kamazz147 Автор
Есть две главные причины: во-первых, в работе используются различные OC и для них нужна единая логика работы, без особой донастройки (в стиле «поставил и забыл»).
Во-вторых, у нас Launcher выполняет минимальный мониторинг: состояние сервисов («жив/не жив»), сверяет конфиги, текущие версии и т.п. и передает в МИС.
Всё это можно реализовать на основе существующих продуктов, но это скорее всего будет зоопарк технологий, и будет требовать настройки на каждом месте.