diff --git a/bundles/infobeamer-monitor/files/config.toml b/bundles/infobeamer-monitor/files/config.toml new file mode 100644 index 0000000..12dcdb7 --- /dev/null +++ b/bundles/infobeamer-monitor/files/config.toml @@ -0,0 +1,4 @@ +<% + from tomlkit import dumps as toml_dumps + from bundlewrap.utils.text import toml_clean +%>${toml_clean(toml_dumps(repo.libs.faults.resolve_faults(config), sort_keys=True))} diff --git a/bundles/infobeamer-monitor/files/infobeamer-monitor.service b/bundles/infobeamer-monitor/files/infobeamer-monitor.service new file mode 100644 index 0000000..7be13a2 --- /dev/null +++ b/bundles/infobeamer-monitor/files/infobeamer-monitor.service @@ -0,0 +1,15 @@ +[Unit] +Description=infobeamer-monitor +After=network.target + +[Service] +Type=exec +Restart=always +RestartSec=5s +ExecStart=/opt/infobeamer-cms/venv/bin/python monitor.py +User=infobeamer-cms +Group=infobeamer-cms +WorkingDirectory=/opt/infobeamer-monitor/ + +[Install] +WantedBy=multi-user.target diff --git a/bundles/infobeamer-monitor/files/monitor.py b/bundles/infobeamer-monitor/files/monitor.py new file mode 100644 index 0000000..b957633 --- /dev/null +++ b/bundles/infobeamer-monitor/files/monitor.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 + +import logging +from json import dumps +from time import sleep + +import paho.mqtt.client as mqtt +from requests import RequestException, get + +try: + # python 3.11 + from tomllib import loads as toml_load +except ImportError: + from rtoml import load as toml_load + +with open("config.toml") as f: + CONFIG = toml_load(f.read()) + + +logging.basicConfig( + format="[%(levelname)s %(name)s] %(message)s", + level=logging.INFO, +) + +LOG = logging.getLogger("main") +MLOG = logging.getLogger("mqtt") + +state = None + +client = mqtt.Client() +client.username_pw_set(CONFIG["mqtt"]["user"], CONFIG["mqtt"]["password"]) +client.connect(CONFIG["mqtt"]["host"], 1883, 60) +client.loop_start() + + +def mqtt_out(message, level="INFO", device=None): + key = "infobeamer" + if device: + key += f"/{device['id']}" + message = f"[{device['description']}] {message}" + + client.publish( + CONFIG["mqtt"]["topic"], + dumps( + { + "level": level, + "component": key, + "msg": message, + } + ), + ) + + +def mqtt_dump_state(device): + mqtt_out( + "Sync status: {} - Location: {} - Running Setup: {} ({}) - Resolution: {}".format( + "yes" if device["is_synced"] else "unknown", + device["location"], + device["setup"]["name"], + device["setup"]["id"], + device["run"].get("resolution", "unknown"), + ), + device=device, + ) + + +mqtt_out("Monitor starting up") +while True: + try: + try: + r = get("https://info-beamer.com/api/v1/device/list", auth=("", CONFIG["api_key"])) + r.raise_for_status() + ib_state = r.json()["devices"] + except RequestException as e: + LOG.exception("Could not get data from info-beamer") + mqtt_out( + f"Could not get data from info-beamer: {e!r}", + level="WARN", + ) + else: + new_state = {} + for device in ib_state: + did = str(device["id"]) + + if did in new_state: + mqtt_out("DUPLICATE DETECTED!", level="ERROR", device=device) + continue + + new_state[did] = device + must_dump_state = False + + if state is not None: + if did not in state: + LOG.info( + "new device found: {} [{}]".format( + did, + device["description"], + ) + ) + mqtt_out( + 'new device found with name "{}"!'.format( + device["description"] + ), + device=device, + ) + if device["is_online"]: + must_dump_state = True + + else: + if device["is_online"] != state[did]["is_online"]: + online_status = ( + "online from {}".format(device["run"]["public_addr"]) + if device["is_online"] + else "offline" + ) + + LOG.info("device {} is now {}".format(did, online_status)) + mqtt_out( + f"online status changed to {online_status}", + device=device, + ) + if device["is_online"]: + must_dump_state = True + + if device["is_online"]: + if device["maintenance"]: + mqtt_out( + "maintenance required: {}".join( + sorted(device["maintenance"]) + ), + level="WARN", + device=device, + ) + + if ( + device["is_synced"] != state[did]["is_synced"] + or device["location"] != state[did]["location"] + or device["setup"]["name"] != state[did]["setup"]["name"] + or device["run"].get("resolution") + != state[did]["run"].get("resolution") + ): + must_dump_state = True + + if must_dump_state: + mqtt_dump_state(device) + + else: + LOG.info("adding device {} to empty state".format(device["id"])) + + state = new_state + sleep(30) + except KeyboardInterrupt: + break + +mqtt_out("Monitor exiting") diff --git a/bundles/infobeamer-monitor/items.py b/bundles/infobeamer-monitor/items.py new file mode 100644 index 0000000..ff7c0fd --- /dev/null +++ b/bundles/infobeamer-monitor/items.py @@ -0,0 +1,33 @@ +assert node.has_bundle('infobeamer-cms') # uses same venv + +files['/opt/infobeamer-monitor/config.toml'] = { + 'content_type': 'mako', + 'context': { + 'config': node.metadata.get('infobeamer-monitor'), + }, + 'triggers': { + 'svc_systemd:infobeamer-monitor:restart', + }, +} + +files['/opt/infobeamer-monitor/monitor.py'] = { + 'mode': '0755', + 'triggers': { + 'svc_systemd:infobeamer-monitor:restart', + }, +} + +files['/usr/local/lib/systemd/system/infobeamer-monitor.service'] = { + 'triggers': { + 'action:systemd-reload', + 'svc_systemd:infobeamer-monitor:restart', + }, +} + +svc_systemd['infobeamer-monitor'] = { + 'needs': { + 'file:/opt/infobeamer-monitor/config.toml', + 'file:/opt/infobeamer-monitor/monitor.py', + 'file:/usr/local/lib/systemd/system/infobeamer-monitor.service', + }, +} diff --git a/nodes/voc/infobeamer-cms.py b/nodes/voc/infobeamer-cms.py index 3abaaf2..65c621c 100644 --- a/nodes/voc/infobeamer-cms.py +++ b/nodes/voc/infobeamer-cms.py @@ -2,6 +2,7 @@ nodes['voc.infobeamer-cms'] = { 'hostname': 'infobeamer-cms.c3voc.de', 'bundles': { 'infobeamer-cms', + 'infobeamer-monitor', 'redis', }, 'groups': { @@ -68,6 +69,15 @@ nodes['voc.infobeamer-cms'] = { 'Translations': 'translations', }, }, + 'infobeamer-monitor': { + 'api_key': vault.decrypt('encrypt$gAAAAABlitmDR1duKo_4KuMJBF_HbPO2GFo_gdoT1rvUKQ2kkugPbe2RljM4bxW5bmwhs5avjxiaSAvjnOBte9ioyPEr7cIh79WFEfMnsHeexlCHwMt6NV_t-8EAhuuEQEf3Py93g8zQ'), + 'mqtt': { + 'password': vault.decrypt('encrypt$gAAAAABhxakfhhwWn0vxhoO1FiMEpdCkomWvo0dHIuBrqDKav8WDpI6dXpb0hoXiWRsPV6p5m-8RlbfFbjPhz47AY-nFOOAAW6Yis3-IVD-U-InKJo9dvms='), + 'host': 'mqtt.c3voc.de', + 'topic': '/voc/alert', + 'user': vault.decrypt('encrypt$gAAAAABhxakKHC_kHmHP2mFHorb4niuNTH4F24w1D6m5JUxl117N7znlZA6fpMmY3_NcmBr2Ihw4hL3FjZr9Fm_1oUZ1ZQdADA=='), + }, + }, 'nginx': { 'vhosts': { 'redirect': {