Ansible для детского сада. Часть 6. Пока что не СУБД для Linux Server Update Services (LSUS)
А за скрюченной рекой
В скрюченном домишке
Жили летом и зимой
Скрюченные мышки.
Корней Чуковский. Скрюченная песня
У меня постоянное ощущение того, что я описываю не велосипед с костылями, а велосипед, который давно изобретен, на котором все катались лет 15 назад, если не 20. Что-то типа «введение в линукс и все вокруг для 10 класса». Что на информатике учат.
Поскольку этот текст пишется 50\50 – половина сделана еще осенью 2024, а половину я переделываю в ходе самообучения, и когда время и желание есть, то не удивляйтесь, что в тексте попадаются странные решения.
Для лиги лени: много примеров, как делать не надо, и как точно не работает
Ansible для детского сада в скольки то частях. Часть 1.Про все сразу
Ansible для детского сада в скольки то частях. Часть 2. Костылируем жалкое подобие WSUS - Linux Server Update Services (LSUS)
Ansible для детского сада. Часть 3. Настраиваем подобие безопасности и все остальное
Подготовка Git
Ansible для детского сада. Часть 4. Первичная настройка конечного клиента
4.1 Теоретически все просто
4.2 Сначала готовим открытую часть сертификата без пароля через Putty в Windows и еще раз вспоминаем что генерирует Putty в Windows, а что нужно в ssh
4.3 Детский скрипт, и как не надо делать
4.4 Отлаживаем SSH, снова и опять
4.5 Проблемы с переносом строки и не только
4.6 Проверяем, что получилось
4.7 Почти переходим к второму Ansible плейбуку
4.8 Воюем с пробелами в Anisble и восстанавливаем пример из первой статьи
4.9 Возвращаемся к основной задаче
Ansible для детского сада в скольки то частях. Часть 5. Приделываем костыли
5.1 Теоретическое обоснование костылей, или «зачем».
5.2 Выбор dataflow
5.3 Собираем Ansible facts без плейбука
5.4 Разбираем полученную выгрузку
Ansible для детского сада. Часть 6. Пока что не СУБД для Linux Server Update Services (LSUS)
6.1 Введение
6.2 Еще раз про готовые решения – Spacewalk, Satellite, Foreman + katello, Rudder
6.3 Переходим к проектированию на минималках
6.4 Начинаем писать код
6.4 Начинаем писать код или Powershell класс в классе. Часть 1
6.5 Продолжаем писать код или Powershell класс в классе. Часть 2
Ansible для детского сада. Часть 7, опять не связанная с Ansible
Ansible для детского сада. Часть 8. Теперь костыльная БД для Linux Server Update Services (LSUS)
Ansible для детского сада. Часть 6. Пока что не СУБД для Linux Server Update Services (LSUS)
Все, что мне было надо, я писал во второй части –
С целевой структурой данных ситуация сложнее. Для своего предпоследнего пет проекта под похожие задачи я просто развернул базу данных (Postgre), и туда клал разное. Нужно ли на первом шаге такое решение? Не знаю, мне не нужно, мне и бинарной таблицы хватит. Но что туда класть? Очевидно, туда должны попасть: FQDN, IP, дистрибутив, версия дистрибутива, ядро сейчас, последние дата и время доступности, аптайм. Должно ли туда попадать предыдущее состояние объекта, и какие-то еще настройки? Не очень важно, всегда можно расширить схему данных, добавить к объекту еще пару свойств.
И вот тут самое время подумать. Что я хочу от системы, какую задачу она решает. И как хранит данные для этого, и как глубоко.
Какие технические данные хранить уже понятно, с чем сравнивать, в общем, тоже.
6.2 Еще раз про готовые решения – Spacewalk, Satellite, Foreman + katello, Rudder
Повторю из второй части
У Microsoft был Baseline Security Analyzer
У Microsoft есть Security Compliance Toolkit (SCT)
У Microsoft есть Azure Update Manager operation(AUM).
В опенсорсе был Spacewalk. Последний релиз - 2.10 / March 18, 2020
У RH был Satellite. Это Foreman + katello+ support. Foreman 3.16 and Katello 4.18
Ivanti Patch for Endpoint Manager ? Ага:
Release Date September 18, 2025 The U.S. Cybersecurity and Infrastructure Security Agency (CISA) has published an analysis of the malware deployed in attacks exploiting vulnerabilities affecting Ivanti Endpoint Manager Mobile (EPMM). The Cybersecurity and Infrastructure Security Agency (CISA) obtained two sets of malware from an organization compromised by cyber threat actors exploiting CVE-2025-4427 and CVE-2025-4428 in Ivanti Endpoint Manager Mobile (Ivanti EPMM). Each set contains loaders for malicious listeners that enable cyber threat actors to run arbitrary code on the compromised server. Malicious Listener for Ivanti Endpoint Mobile Management Systems
Rudder ? Ничего про него не знаю.
Вопрос в сложности развертывания, это совсем не WSUS с его далее – далее - готово – пропишите WSUS в GPO
6.3 Переходим к проектированию на минималках
Функциональные требования: что делает этот софт – понятно, структурирует информацию про то, что надо обновить. Но, кстати, в таком случае нужно еще поле «пометки для объекта, почему не обновлено», и это уже точно нужно вести в CMDB (Configuration Management Database).
Не функциональные требования: как он это хранит.
И дальше UI \ UX.
Для начала, нужен уникальный идентификатор объекта.
В Windows это Machine ID, в Linux /etc/machine-id. Кроме этого, скорее всего, у сервера будет уникальный MAC или, для Cloud init, MAC останется, но со своими особенностями.
Что касается «истории состояния», то можно долго думать, читать про всякие 36 нормальных форм, Кимбалла, Data Vaul и вот это все.
Или можно зарезервировать строки данных:
Состояние00 «состояние сейчас»
Состояние01 «состояние при предыдущем опросе, вне зависимости от даты»
Состояние24 «состояние сутки назад»
Состояние168 «состояние неделю назад»
Состояние720 «состояние месяц назад»
Это позволяет организовать простую логику «было – стало» и перемещать данные по цепочке и до заполнения таблицы или таблиц. Вторая и третья нормальные формы SQL, вот это все.
На этом этапе и начинается разделение между «держим в каждом объекте 5 лишних групп, зато это один простой массив» и «держим данные в связанных таблицах с одним первичным ключом, зато модно стильно SQL».
Или, можно немного упороться, и сделать объект из 5 объектов, то есть 3 мерную таблицу.
Это, с одной стороны, штатный и понятный (мне) механизм, с другой снимает требование по заведению SQL.
Получаемый объект будет в формате:
Сервер.Состояние00.Параметр01
Сервер.Состояние01. Параметр01
и так далее.
Давайте пробовать.
То есть, с точки зрения «вспомнить как Python работает с модулем psycopg2 и СУБД» полезнее сделать с базой данных, с точки зрения скорости \ удобства сейчас – проще сделать класс и класс из классов. Так что перенесу задачу «по базе данных» на часть 7.
Пока писал текст, понял что задача «хранение данных в базе» все равно требует считывания данных в какой-то объект или группу объектов, как-то с этим всем все равно потом работать.
Переусложнение не нужно.
Логика первичного заполнения
В первый момент времени не заполнено ничего. Поэтому при каждом запуске придется выполнять пре-проверку.
Получить сегодняшнее число, считать Data00 и выбрать Data00.timestamp
Взять Data00.timestamp. Если Null, то заполнить текущим временем и данными.
Можно прописать везде 01.01.1970 (Unix Epoch) при первом запуске и проверять, что там. Это гораздо проще сравнивать при следующих шагах.
Дальше в чем-то проще.
Взять текущую дату. Сравнить с датой в Data01. Если данным в Data01 больше 24 часов, то переместить их в Data24. Ну или как-то так. Можно наоборот – посмотреть данные в Состояние720 и Состояние168, сравнить с текущей отметкой времени, переместить.
На этом месте я понял, что было два пути:
1. Более правильный в сложных системах: планировать еще пару дней, нарисовать схему движения данных, итд. Возможно, сделать схему более гибкой.
2. Начать писать код, и поправить все что нужно, по мере необходимости. В том числе, расширить схему объекта, если надо.
6.4 Начинаем писать код или Powershell класс в классе.Часть 1
Почему Powershell, а не питон ? Да потому, что часть из этого кода пишется для людей, которые Powershell читать и использовать еще могут, а питон уже сложно, сложно ничего не понятно.
В таких статьях всегда есть соблазн «просто взять и скопипастить уже готовое» или «взять и сделать через AI». AI я использую, но не очень люблю. Даже не смотря на то, что Copilot очень нравится и коллегам, и мне.
Поэтому будет, как обычно, много примеров «неправильно» или «непонятно».
Всегда есть соблазн написать сразу итоговое «хорошо». На мой взгляд, надо писать и пример «вот так плохо, потому что».
Как я писал раньше, вот так, разумеется, работать будет:
Class AnsibleFacts01{
[string]$IsSUCCESS
[string]$FQDN
[string]$IP
[string]$Ansible_kernel_version
[string]$Comment}$NewHost1 = [AnsibleFacts01]::new()
$NewHost1.Comment = "123"
$NewHost1
И даже вот так система сработает без ошибок:
Class AnsibleFactsForData{[AnsibleFacts01]$Data00
[AnsibleFacts01]$Data01}
$NewHost2 = [AnsibleFactsForData]::new()
Но уже на этапе
$NewHost2.Data00.Comment = "123"
система скажет
The property 'Comment' cannot be found on this object.
Почему? Потому. Overloading constructors, вот это все по руководству Petri PowerShell Classes – Your Ultimate Guide
Поэтому что? Поэтому есть три пути. Webcam, дальше вы знаете.
Путь первый. Конструктор и инициализация.
Для начала перепишу первый класс, а точнее скопирую пример из Using other classes with a class in Powershell
class SomeOtherClass {
[string] $Status
[string] $ReasonSomeOtherClass () { }
SomeOtherClass ([string] $Status, [string] $Reason) {
$this.Status = $Status
$this.Reason = $Reason }}class SomeClass {
[string] $Server
[string] $File
[SomeOtherClass] $OtherClass
SomeMasterClass () { }
SomeMasterClass ([string] $Server, [string] $File, [SomeOtherClass] $Class) {
$this.Server = $Server
$this.File = $File
$this.OtherClass = $Class}}
Но к такому примеру нужна инициализация: (там же)
$testing = [SomeClass]@{
Server = 'Server01'
File = 'SomeFile'
OtherClass = [SomeOtherClass]@{
Status = 'OK!'
Reason = 'Idk' }}
В моем примере (выше) это будет выглядеть как:
Class AnsibleFactsForData01{
[AnsibleFacts01]$Data00
[AnsibleFacts01]$Data01}$NewHost1 = [AnsibleFactsForData01]::new()
$NewHost1.Data00 = [AnsibleFacts01]@{Comment = '456'}
Посмотрю что получилось:
$NewHost1.Data00
Поменяю значение и еще раз посмотрю что получилось:
$NewHost1.Data00.Comment = '6'
$NewHost1.Data00
Вот такая вот история. Хотите, не хотите, а будьте добры инициализировать, а после этого уже как хотите.
Путь второй, тоже не очень
Class AnsibleFactsForData02{
[AnsibleFacts01]$Data00
[AnsibleFacts01]$Data01
# AnsibleFactsForData02 () { } Ввиду тупости, я не понимаю зачем эта строка, поэтому ее закомментировал.
AnsibleFactsForData02 () {
$this.Data00 = [AnsibleFacts01]::new()}}$NewHost02 = [AnsibleFactsForData02]::new()
$NewHost02.Data00.Comment = '12'
Проверим что вышло:
$NewHost02.Data00
Выглядит, что все работает.
Теперь проверим:
$NewHost02.Data01.Comment = '34'
И, конечно, так не работает.
Потому что внутри AnsibleFactsForData02 я потрогал только $this.Data00, и не потрогал $this.Data01
Раз я его не потрогал, то под него не выделилась память, и все такое. Но у меня есть оправдание, когда в школе люди учили typedef struct, constructor и прочие ~className (Destructors), я прогуливал.
Тут должен быть третий путь, но я его не придумал.
Но в моем примере можно вместо класса с классами внутри использовать простой массив классов, только придется запомнить, что [0] это сейчас, [1] это в прошлый раз, итд.
Осталось записать это в файл.
6.5 Продолжаем писать код или Powershell класс в классе. Часть 2
Упрощаем и отрезаем. Получаем:
Class Ans_F{ # AnsibleFacts
[string]$IsSUCCESS
[string]$FQDN
[string]$IP
[string]$Ansible_kernel_version
[string]$Comment}Class A4D { #AnsibleFactsForData
[Ans_F]$D00
[Ans_F]$D01
[Ans_F]$D24 # «состояние сутки назад»
[Ans_F]$D168 # «состояние неделю назад»
[Ans_F]$D720 # «состояние месяц назад»
# AnsibleFactsForData02 () { } не понимаю зачем эта строка, поэтому закомментировал.
A4D () {
$this.D00 = [Ans_F]::new()
$this.D01 = [Ans_F]::new()
$this.D24 = [Ans_F]::new()
$this.D168 = [Ans_F]::new()
$this.D720 = [Ans_F]::new() }}
$NewHost02 = [A4D]::new()
$NewHost02.D00.Comment = '12'
$NewHost02.D01.Comment = '34'
$NewHost02.D00$NewHost03 = [A4D]::new()
$NewHost03.D168.Comment = '56'$HostList = @()
$HostList += $NewHost02
$HostList += $NewHost03
$ScriptPath = $PSScriptRoot
$OutFileName = 'Ansible2.xml'
$OutFullName = $ScriptPath + "\" + $OutFileName
Export-Clixml -Path $OutFullName -InputObject $HostList$HostListExport = Import-Clixml -Path $OutFullName
$HostListExport[1].D168.Comment
Есть минус, про Select при выборе формата – можно забыть.