Як я читав показання датчиків через SNMP (Python+AgentX+systemd+Raspberry Pi) і спорудив ще одну мониторилку

Всім привіт.

image

Ліричний відступСтаття лежить в чернетках вже пару тижнів, бо не було часу таки допилити описуваний об'єкт. Але під натиском товаришів, які своїми статтями вже покрили половину того, що я хотів сказати, вирішив піти за принципом «release fast, release early, release crap» опублікувати те, що є. Тим більше, що розробка на 80% закінчена.
З моменту публікації статті про «В міру Універсальний Пристрій Управління» минуло чимало часу (а якщо бути точним, більше року). Чимало, але недостатньо, щоб я таки написав нормальну програмну начинку для цього пристрою. Адже не для краси ж воно є — воно повинно збирати дані з датчиків і робити так, щоб ці дані виявлялися в системі моніторингу (в моєму випадку Zabbix)

Частина перша — software
За час, що минув з програмної начинки було реалізовано наступне:

  • Тестовий скрипт для демонстрації, що всі підключений працює
  • Скрипт для заббикса, щоб збирати покази з термодатчиків
Були спроби написати окремі мониторилки для ntpd і для gpsd. Багато часу було витрачено на супер-мониторилку, яка повинна була вміти читати конфіг, запускати процеси збору даних з різних джерел відповідно конфіг, збирати дані з цих процесів і виводити на екранчик свідчення, одночасно даючи можливість заббиксу читати ці дані. За фактом вийшло реалізувати диспетчер процесів, який читав конфіг і плодив потрібні процеси, і рисовалку на екрані, яка вийшла досить крутий — навіть вміє читати layout з конфига і змінювати вміст екрану по таймеру, при цьому збираючи дані від процесів у той момент, коли вони потрібні. Немає в цій супер-мониторилке тільки одного — власне процесів, які б збирали дані. Ну і плюс були ідеї зробити систему сигналів, щоб функції кнопок призначати, враховувати пріоритети різних джерел даних, ну і так далі, але все вперлося у вільний час і в те, що ця супер-мониторилка виходила вже дуже роздутою і потужною.

На якийсь час я забив на розробку повноцінної програмної начинки. Ненуачо, скрипт працює, а правило «працює — не чіпай», як кажуть, святе правило адміністратора. Але ось невдача — чим більше хочеться моніторити, тим більше треба скриптів писати і тим більше треба додавати виключень в SELinux для заббикса (я ж не тільки raspi моніторю) — у дефолтної політиці заббиксу (як і rsyslog, наприклад) заборонено викликати довільні програми, і це зрозуміло. Відключати SELinux для заббикса зовсім або писати свою політику під кожен бінарники, який буде смикатися, дуже не хотілося. Тому довелося думати.

А давайте взагалі розберемося, як можна збирати дані в систему моніторингу:

  • За того, хто ініціатор:

    • Активний моніторинг — спостережуваний вузол ініціює передачу даних (push)
    • Пасивний моніторинг — спостереження вузол ініціює передачу даних (pull)

  • За методом збору даних:

    • Через агента на спостережуваному сайті, використовуючи тільки підтримувані агентом метрики
    • Через агента на спостережуваному сайті, розширюючи агент скриптами

    • За SNMP
    • Примітивний ping
    • telnet
    • … і так далі
Я використовую pull-моніторинг, не з релігійних причин, а просто так склалося. Насправді різниці між push і pull небагато, особливо на малих навантаженнях (на одній з попередніх робіт робив Nagios+NSCA, великої різниці не помітив, елементи все одно руками створювати). Можна було б використовувати zabbix_sender, якщо б у мене вже був push-моніторинг, але його немає, а на нема й суду нема, а заважати одне з іншим як-то неакуратненько. А от у питанні, за яким протоколом моніторити, начебто вибір великий, та не дуже — discovery підтримується тільки через агента або через SNMP, що залишає нас вже тільки з двома варіантами. Агент відпадає через описаної проблеми з SELinux. Вуаля, у нас залишається pull-моніторинг через SNMP.

Урраа! А чого ура-то? В лінуксі ніби як є snmpd, але як примусити його віддавати те, що нам потрібно, але про що snmpd не має ні найменшого поняття? Виявляється, у snmpd є цілих 3 (принципово різних) способу віддавати довільні дані по довільним OIDам:

  • Запустити зовнішній скрипт (директиви exec/sh/execfix/extend/extendfix/pass/pass_persist) — погано потенційних проблем з SELinux і з-за того, що неконтрольована купа скриптів в кінцевому підсумку перетвориться на звалище. Та й кажуть, у pass_persist все погано з передачею бінарних даних. Не знаю, може, безсовісно брешуть, але мені ідея плодити мільйон скриптів в будь-якому випадку не подобалася;

  • Написати щось на вбудованому perl або завантажити .so — не знаю і не хочу знати перл, не хочу писати so-шки, я ж не програміст, щоб на С писати;

  • Отримати дані з зовнішнього агента (proxy, SMUX, AgentX) — а ось це звучить добре, loose coupling ж, не залежить від мови. Давайте розбиратися:

    • proxy — запросити OID у SNMP-агента на зазначеному сайті. Треба реалізовувати цілком протокол SNMP, що мені абсолютно ні до чого, та й навіщо запитувати щось у іншого сайту, задіяти мережу, коли я хочу дані локально отримати. Я знаю про існування 127.0.0.1, але в будь-якому випадку, реалізовувати SNMP не посміхається зовсім;

    • SMUX — потрібна підтримка протоколу smux в зухвалій агента в тому числі, а man каже, що за замовчуванням net-snmp збирається без підтримки smux (і так вже ntpd перезбирати для підтримки pps, ще й перезбирати net-snmp на raspi не посміхається). Та й smux це — всього лише обгортка для пакетів SNMP, просто додана можливість для субагента зареєструватися на агента;


    • AgentX — по суті той же, що і SMUX, тільки протокол простіше, а пакет легше. Ну і вкомпилен за замовчуванням в net-snmp, що теж приємно. Звучить як наш вибір.
Я пишу на пітоні, тому пішов шукати, а не реалізував хто вже протокол agentx. І адже знайшлися такі гарні люди — https://github.com/rayed/pyagentx і https://github.com/pief/python-netsnmpagent. Другий проект начебто жвавіше, але перший здався простіше. Я почав з першого (pyagentx), він працює і робить все, що треба. Але от коли я став думати, а як в цю бібліотеку передавати дані, захотілося таки розібратися з другим пакетом (python-netsnmpagent). Проблема з pyagentx полягає в тому, що так, як воно написано, воно не може отримувати дані від викликають функцій, а отже, запит свіжих даних має відбуватися прямо у функції, яка посилає оновлення в snmpd, що не завжди зручно і не завжди можливо. Можна було, звичайно, отпочковать щось своє і перевизначити функції, але по суті довелося б переписати клас майже цілком, чого робити не хотілося — на коліні ж розробляємо, все повинно бути просто і швидко. Однак небажання розбиратися з python-netsnmpagent таки перемогло і я знайшов спосіб передати дані в updater з pyagentx, але про це нижче.

Наступне питання був такий — а як повинна виглядати архитетура? Спроба написати диспетчер, форкающий джерела даних і читає дані з них, вже була і закінчилася не дуже добре (див. вище), так що було вирішено відмовитися від реалізації диспетчера. І так вдало склалося, що то я десь побачив статтю про systemd, то просто в черговий раз полоскотало давнє бажання розібратися з ним ближче, і я вирішив, що диспетчером в моєму випадку буде systemd. Haters gonna hate, а ми будемо розбиратися, коли воно вже навіть на raspi з коробки є.

Які корисні можливості systemd для себе я виявив:

  • Безкоштовно демонізація — пишемо юніт служби з типом simple (або notify) і отримуємо демона, не написавши ні строчки коду для цього. Прощайте python-daemon та/або daemonize
  • Автоматичний перезапуск впали юнітів — ну тут коментарі зайві, рятує від непостійних помилок
  • Сокет-активація і взагалі управління сокетами — дуже приємно, коли хтось, хто хоче записати в сокет, може це зробити, навіть якщо той, хто буде читати з сокета, ще не готовий це зробити. Більш того, читача можна активувати за фактом запису в сокет, що може заощадити скільки-то оперативної пам'яті (втім, не то, щоб її не вистачало...)
  • Template-юніти — якщо у мене багато однакових датчиків, можна наплодити багато процесів з одного юніта, передати всім різні параметри і радіти
  • (виявлено занадто пізно, поки не реалізовано) юніти-таймери — дозволяють періодично запускати якийсь юніт. Чому не cron — тому що у cron мінімальний період 1 хвилина, а я хочу частіше опитувати датчики. Чому не sleep() — тому що активне очікування та тому що період починає дрейфувати — так, датчик ми смикаємо кожні N секунд, але з урахуванням читання й обробки даних період оновлення даних буде не N секунд, а N+x, тобто при кожному читанні період оновлення даних буде з'їжджати на x
З урахуванням цих знахідок в голові намалювалася архітектура:

  • systemd відкриває сокет для зв'язку між процесами-датчиками і процесом-колектором, всі процеси-датчики пишуть в один і той же сокет
  • systemd запускає юніти для процесів-датчиків
  • процес-датчик читає дані з датчика, пише їх в сокет і засинає (systemd timer unit я на той момент ще не знайшов)
  • як тільки дані з якогось датчика записані в сокет, systemd запускає процес-колектор, який бере оновлення від датчика, чарівним чином його обробляє і зберігає у внутрішньому стані. Процес-колектор не вмирає
  • процес-колектор породжує окремий потік (саме потік, не процес, щоб уникнути IPC між процесами, що в пітоні для даної задачі кілька сумне, нижче напишу, чому я так думаю), в якому відбувається передача внутрішнього стану в snmpd по протоколу agentx
Одне дуже нехороше місце — це поділюване внутрішній стан між потоком-колектором і потоком-agentx. Але я собі це простив, тому що в пітоні є чарівний GIL, який вирішує питання синхронізації між двома потоками. Хоча це, звичайно, дуже погано і не по книзі. Була думка винести поділюване стан в окремий процес і змусити процес-agentx і процес-колектор працювати з процесом станом через сокет, але заломало мене робити ще один сокет, писати ще один юніт і так далі.

Чому мені не сподобалося IPC в пітоні стосовно до даної задачі:

  • Queue працює нормально, але ці черги неіменовані, інстанси Queue треба передавати в форкаемый процес. У моєму випадку це означає повністю переписати pyagentx
  • Manager, можливо, вирішив би мою проблему, але знову ж таки, це означає повністю переписати pyagentx
  • posix/sysv ipc чудово, там є іменовані черзі, але ці черги обмежені в розмірі, на деяких системах — зовсім убого обмежені (пишуть [гортати до «Usage tips»], що на macos, наприклад, не більше 2КБ на чергу і навіть налаштувати не можна). Не те, щоб мені треба було запускатися на купі різних систем з різною мірою убогості реалізацією sysv ipc, але займатися тюнінгом теж не хотілося. Хочу, щоб відразу і добре
  • знову posix/sysv ipc — черзі блокують, то є якийсь мінімальний таймаут повинен бути, перш ніж читання з черги поверне «порожньо». У випадку з pyagentx блокування на читанні з черги в update() дуже небажана, та й взагалі це убого
  • і знову posix/sysv ipc — проблема з іменуванням черг. Незважаючи на те, що черги повідомлень іменовані, іменовані вони не ім'ям, а ключем. Так як ключ не є ієрархічним або семантично очевидним, легко вибрати неунікальний ключ. У реалізації posix/sysv ipc для пітона є можливість згенерувати ключ черги автоматично, але ось невдача — якби я міг щось передати в pyagentx, я б передав туди Queue і не мучився. Можна генерувати ключ з допомогою ftok, але пишуть [гортати до «Usage tips»], що ftok дає не більше впевненості в ключа, ніж
    int random() {return 4;}
  • (більше нічого на думку не спало, що не вовлекало б зовнішній брокер черг, а завдання не така вже, щоб ще і брокер черг тримати — зайвий сервіс, зайвий головний біль)
dbus виглядав рішенням усіх лих, так і є він скрізь, де є systemd, але от біда — pydbus вимагає GLib >=2.46, щоб публікувати API, а в raspbian тільки 2.42. dbus-python оголошено застарілим і непідтримуваних. Коротше, поки смажений півень в попу не клюне, буду розділяти стан небезпечним чином.

При використанні SNMP у своїх брудних цілях є ще одна заковика — а як вибрати OIDы для своїх наборів даних? Для цього є спеціальна гілка в private, яка називається enterprises— .1.3.6.1.4.1.<enterprise_id>. Отримати унікальний enterprise ID можна у IANA. Коли схема OIDов визначена, непогано було б написати MIB, щоб самому не забути, де що, ну і щоб систем моніторингу було легше. Введення в написання МІВов є тут.

В якийсь момент я виявив ntpsnmpd з відповідним МІВом і зрадів було до лисині, але коли скомпилил це диво, виявив, що автор спромігся тільки реалізувати кілька констант верхнього рівня і на цьому видихався. Я трохи поковырялся в коді і так до кінця і не зрозумів, яким хитрим чином автор взаємодіяв з ntpd (або ntpq), щоб витягнути ті константи, не парся висновок. Одне я зрозумів точно — готового python API немає, а значить, ловити нема чого, доведеться цей MIB самому реалізовувати.

П'ятихвилинка ненавистіНі, ну от правда, невже за всі ці роки ніхто так і не написав аналогів ntpd, smartctl, lm_sensors і інших утиліт без API? Ніхто не прикрутив до них snmp-агентів? Таких аналогів, щоб не треба було парсити текстовий висновок? Ні, я розумію, юниксвей і все таке, але це не той випадок. Добре б її була можливість вивести дані в машиночитаемом форматі, але ні, все тільки для людей. Причому судячи з плачам в інтернеті (російською та забугорном), я не один такий нещасний. Ну, припустимо, lm_sensors можна пробачити, тому що ті ж дані можна машиночитаемом форматі віднімати з sysfs, але решта-то?

У загальному і цілому, вся ця конструкція працює і є досить живучою. Discovery в заббиксе працює, итемы створюються, графіки малюються, тригери шлють алерти — чого ще для щастя треба? Код ще не финализирован, так що не публікую.

Частина друга — hardware
Не скрізь можна вкрячить юнитовый корпус, але і вішати соплі по стінах теж не хочеться. Є дуже витончене рішення — DIN-рейка. Купи конструктивів з рейкою продаються на ринку, в які можна і блок живлення рейковий поставити (я використовую MeanWell DR-15-5), і всякі автомати, пзв-что_угодно. Відповідно, захотілося корпус на DIN-рейку для raspi. В якості кандидатів розглядалися ось ці два товариша — модель від Italtronic і RasPiBox. Перевага RasPiBox в тому, що там вже є плата для прототипування і введення харчування здійснюється через гвинтові контакти (через стабілізатор на GPIO), що зручно, але може бути небезпечно. Але коштує він більше, ніж в 3 рази дорожче, займає більше місця на рейці і не має прозорого віконця. Модель від Italtronic також не ідеальна — ширина її така, що всі готові LCD-екрани 16х2 туди не влазять по ширині, тобто цінність прозорого віконця різко падає, але за низьку ціну я був готовий цей недолік пробачити.

Корпус виявився досить зручний, має місця під кріплення (а точніше, під установку) двох друкованих плат або листа чого завгодно. Я підкладки роблю з акрилу, загорнутого в токонепроводящую ESD-захисну плівку, пиляю дремелем:



Плати всередині тримаються тільки на силі тертя і на невеликих уступах з двох сторін, тобто ніякого жорсткого кріплення всередині не передбачено. Незважаючи на деяку величину, корпус маленький і місця над самою raspi залишається не так вже багато, особливо якщо вставляти плату на нижній рівень. А плата мені потрібна, так як потрібно розмістити кілька світлодіодів і плату з RTC.

Фотографії корпусу






Підключати до нової мониторилке хочу датчики температури і вологості. Для температури наш вибір — ds18b20, він працює, але варто порівнювати свідчення з повіреним термометром, датчик може брехати на півградуса по специфікації. Для компенсації додав примітивну корекцію показань на константу в конфіг, перевіряв ось таким термометром:



Виявилося, що мої примірники ds18b20 цілком собі не брешуть. А ось наступний датчик як раз-таки бреше, причому аж на 0,6 градуса. Втім, знову ж таки, залежить від примірники — один брехав, інший майже не брехав.

З вологістю виявилося все не так просто. Дешеве або не працює з raspi зовсім (бо аналогове), чи ні бібліотек (хочу, щоб відразу і добре), або дороге як авіаційні кабелі. Компроміс між зручністю і жабою був знайдений в датчику Adafruit BME280, який бонусом ще й температуру з тиском показує (але може брехати, як я вище зазначив).

Якщо ds18b20 можна просто загорнути в термоусадку і радіти, з ВМЕ280 такий фокус не пройде. Ідей про корпус було чимало — і залишити як є, припаявши дроти і заливши їх клейовими соплями (вушка для кріплення вже є, виходить), і зробити міні-корпус з того ж акрилу, що і підкладки під компоненти, і вчинити що-небудь з 3D-принтером, благо є один в зоні досяжності… Але потім я згадав про яйця:



Це ж ідеальний корпус. Для датчика місця вистачає, роз'єм поставити можна, зручний доступ для обслуговування, підвісити можна скрізь або просто закинути куди-небудь.

Підключати датчики до raspi вирішив через DB9. В USB ліній мало, розетка RJ45 не влізла за габаритами. Датчик-яйце вирішив підключити по USB, тому що в шафі виявилися залишки різаних USB-кабелів — не пропадати ж добру:



Для захисту GPIO-гребінки на raspi і для зручності розбирання корпусу взяв ще одну гребінку і припаялся до неї. Гребінка кутова, що дало трохи більше місця по вертикалі, але я трохи не подрассчитал і ця гребінка уткнулася в резистори для світлодіодів. Все, звичайно, намертво загорнуте в термоусадку, але момент, який в майбутньому варто пам'ятати. У підсумку половинки корпусу все ще можна розняти, щоб, скажімо, поміняти батарейку в rtc або саму raspi. Все інше (точніше, флешка) доступно для заміни без відкривання корпусу.

Фотографії напівготовності і готовності





Одна рекомендація — не економте на кнопках. Я ось заощадив, так кнопка не тільки деренчить (з цим можна боротися, в бібліотеці RPi.GPIO захист від брязкоту передбачена), але ще і спрацьовує тільки в дуже конкретному положенні. Кнопку я передбачав для програмного відключення пристрою на випадок, якщо треба відключити живлення (вже кілька разів вбив ФС на флешці неакуратним вимиканням), але виявилося, що мало щось передбачити — треба ще й читати документацію. Якщо ви, як і я, не читаєте документацію, то знайте — overlay gpio_shutdown робить зовсім не те, що можна було б припустити, а всього лише виставляє на деякій піне високий/низький рівень при відключенні, щоб, наприклад, зовнішній блок живлення міг згаснути. Для того, щоб відключати raspi по кнопці, є сильний модуль rpi_power_switch (але його компилять треба, а для цього kernel-headers потрібні) або користувальницький демон Adafruit-GPIO-Halt. У мене буде свій hostd, який буде блимати світлодіодами, от заодно і на кнопку реагувати буде.

Висновок
Вийшов програмно-апаратний комплекс для моніторингу, розширюваний, використовує актуальні технології, стійкий до збоїв. Частини можна оновлювати і перезапускати незалежно інших частин (спасибі systemd, це не вимагало від мене як від розробника ніяких зусиль). А найголовніше — вийшло отримати багато задоволення від процесу, і від результату. Ну і маленький візок нових знань додалася.

Спасибі за читання!
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.