Name: parasol-app Version: 1.6 Release: 1 Summary: The parasol app package License: FIXME BuildArch: x86_64 Requires: python3.11 BuildRequires: python3-pip, python3, python3-requests, python3-pyyaml, python3-psutil %description The parasol app package. %prep %build cat > parasol-app.service <<"EOF" [Unit] Description=Parasol App After=network.target [Service] Type=simple ExecStart=/usr/bin/parasol-app Restart=on-failure RestartSec=5s StandardOutput=journal StandardError=journal [Install] WantedBy=multi-user.target EOF cat > parasol-app <<"EOF" #!/usr/bin/python3 import os import socket import syslog import psutil import requests import threading import time import uuid import yaml from datetime import datetime, timezone from requests.exceptions import ConnectionError _webhook_thread = None APP_VERSION = "1.6" CONFIG_PATH = "/etc/parasol-app.conf" CONFIG_D_PATH = "/etc/parasol-app.d/" last_webhook_high_cpu_state = None def get_non_localhost_ips(): """Return list of non-localhost IPv4 addresses. Excludes 127.x.""" ips = [] for addrs in psutil.net_if_addrs().values(): for a in addrs: if a.family == socket.AF_INET and a.address and not a.address.startswith('127.'): ips.append(a.address) return ips def resolve_alert_hostname(): """FQDN or short hostname for labels.hostname; None if unusable.""" bad = ("", "localhost", "localhost.localdomain") fqdn = (socket.getfqdn() or "").strip() if fqdn and fqdn.lower() not in bad: return fqdn short = (socket.gethostname() or "").strip() if short and short.lower() not in bad: return short return None def _rfc3339_now(): return datetime.now(timezone.utc).strftime("%%Y-%%m-%%dT%%H:%%M:%%S.%%f")[:-3] + "Z" def call_webhook(high_cpu): global _webhook_thread if _webhook_thread is not None and _webhook_thread.is_alive(): _webhook_thread.join() ips = get_non_localhost_ips() server_ip = ips[0] if ips else "unknown" webhook_url = (config.get("alert_webhook_url") or "").strip() token = (config.get("alert_webhook_token") or "").strip() session_id = str(uuid.uuid4()) def _do_post(): try: time.sleep(2) if high_cpu: keyword = "plugin enabled" else: keyword = "plugin disabled" syslog.syslog(syslog.LOG_INFO, f"'{keyword}' in session '{session_id}'") if not webhook_url: syslog.syslog(syslog.LOG_WARNING, "no alert_webhook_url in config, skipping webhook") return headers = {"Content-Type": "application/json"} if token: headers["Authorization"] = f"Bearer {token}" now = _rfc3339_now() labels_alert = { "alertname": "High CPU load", "instance": server_ip, "severity": "critical", } _hn = resolve_alert_hostname() if _hn: labels_alert["hostname"] = _hn if high_cpu: desc = "CPU usage is above the threshold" payload = { "version": "4", "groupKey": "{}/{}:{}".format("{}", "{}", server_ip), "receiver": "parasol-app", "status": "firing", "externalURL": "https://prometheus.example.com/", "commonLabels": {}, "commonAnnotations": {}, "alerts": [ { "status": "firing", "labels": labels_alert, "annotations": { "description": desc, "summary": "High CPU on parasol-app ({})".format(server_ip), }, "startsAt": now, "endsAt": "0001-01-01T00:00:00Z", "generatorURL": "http://{}:9090/graph".format(server_ip), } ], "truncatedAlerts": 0, } else: desc = "CPU usage has returned to normal — parasol-app below threshold." payload = { "version": "4", "groupKey": "{}/{}:{}".format("{}", "{}", server_ip), "receiver": "parasol-app", "status": "resolved", "externalURL": "https://prometheus.example.com/", "alerts": [ { "status": "resolved", "labels": labels_alert, "annotations": { "description": desc, "summary": "Resolved ParasolAppHighCPU on {}".format(server_ip), }, "startsAt": now, "endsAt": now, "generatorURL": "http://{}:9090/graph".format(server_ip), } ], "truncatedAlerts": 0, } response = requests.post(webhook_url, json=payload, headers=headers, timeout=30) syslog.syslog(syslog.LOG_INFO, "Alertmanager webhook POST: {}".format(response.status_code)) except ConnectionError: syslog.syslog(syslog.LOG_WARNING, "unable to connect to webhook, continuing") _webhook_thread = threading.Thread(target=_do_post) _webhook_thread.start() def read_yaml_file(file_path): with open(file_path, 'r') as file: data = yaml.safe_load(file) return data def conf_d_dir_has_perf_mode(conf_d_path): if len(os.listdir(conf_d_path)) == 0: return False return True config = read_yaml_file(CONFIG_PATH) while True: use_high_cpu = conf_d_dir_has_perf_mode(CONFIG_D_PATH) and APP_VERSION == "1.5" # no update on startup if last_webhook_high_cpu_state is None: last_webhook_high_cpu_state = use_high_cpu # bail out if we already called the webhook and the state didnt change if last_webhook_high_cpu_state != use_high_cpu: call_webhook(high_cpu = use_high_cpu) last_webhook_high_cpu_state = use_high_cpu if not use_high_cpu: time.sleep(2) EOF pip3 install pyinstaller /usr/bin/python3 -m PyInstaller -F parasol-app %install mkdir -p %{buildroot}/etc/systemd/system install -m 644 parasol-app.service %{buildroot}/etc/systemd/system/ mkdir -p %{buildroot}/usr/bin install -m 755 dist/parasol-app %{buildroot}/usr/bin/ mkdir -p %{buildroot}/etc mkdir -p %{buildroot}/etc/parasol-app.d/ %files /etc/systemd/system/parasol-app.service /usr/bin/parasol-app %post systemctl daemon-reload %changelog * Tue Feb 17 2026 Example User - 1.5-1 new package