Iniital release
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Read-only JSON adapter for an existing pegasus-health --verbose command."""
|
||||
import datetime
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
COMMAND = ['/usr/local/bin/pegasus-health', '--verbose']
|
||||
GROUPS = [
|
||||
('Ubuntu 26.04', ['Failed systemd units']),
|
||||
('Nginx and PHP-FPM', ['nginx','php8.3-fpm','php8.4-fpm','Nginx configuration']),
|
||||
('MySQL, PostgreSQL and Redis', ['mysql','postgresql','redis-server','MySQL','PostgreSQL','Redis']),
|
||||
('Docker', ['docker','Docker containers discovered']),
|
||||
('Gitea and Gitea MCP', ['gitea','gitea-mcp','Gitea HTTP']),
|
||||
('Cloudflare Tunnel', ['cloudflared']),
|
||||
('Cockpit', ['cockpit.socket','Cockpit']),
|
||||
('Domino VM and NRPC', ['domino651 VM','domino651 autostart','Domino NRPC']),
|
||||
('OpenVPN and IPsec', ['openvpn','OpenVPN UDP','IKEv2 UDP','IPsec NAT-T UDP']),
|
||||
('PPPoE networking', ['Interface ppp0','Default route','DNS resolution']),
|
||||
('Backup drive', ['/backup mounted','Backup filesystem UUID']),
|
||||
('Backup timer', ['pegasus-backup.timer']),
|
||||
('Time Machine removed', ['Time Machine removed']),
|
||||
('Public websites', ['absdev.net','git.absdev.net','saturn.absdev.net','cloud.absdev.net']),
|
||||
]
|
||||
|
||||
def output():
|
||||
try:
|
||||
p = subprocess.run(COMMAND, capture_output=True, text=True, timeout=180, check=False)
|
||||
except (OSError, subprocess.TimeoutExpired) as exc:
|
||||
print(json.dumps({'schema': 1, 'error': str(exc), 'overall': 'fail', 'groups': [], 'checks': [], 'counts': {'ok':0,'warn':0,'fail':1}}))
|
||||
return 2
|
||||
checks = []
|
||||
section = ''
|
||||
plain = re.sub(r'\x1b\[[0-9;]*m', '', p.stdout)
|
||||
for line in plain.splitlines():
|
||||
head = re.match(r'^=== (.+) ===$', line)
|
||||
if head:
|
||||
section = head.group(1)
|
||||
match = re.match(r'^\[(OK\s*|WARN|FAIL)\]\s+(.+?)(?:\s+—\s+(.*))?$', line)
|
||||
if match:
|
||||
checks.append({'name': match[2].strip(), 'status': match[1].strip().lower(), 'detail': (match[3] or '').strip(), 'section': section})
|
||||
lookup = {c['name']: c['status'] for c in checks}
|
||||
groups = []
|
||||
for title, names in GROUPS:
|
||||
relevant = names + ([c['name'] for c in checks if c['name'].startswith('Container ')] if title == 'Docker' else [])
|
||||
states = [lookup.get(name, 'fail') for name in relevant]
|
||||
state = 'fail' if 'fail' in states else 'warn' if 'warn' in states else 'ok'
|
||||
label = title
|
||||
if title == 'Docker':
|
||||
running = sum(c['status'] == 'ok' for c in checks if c['name'].startswith('Container '))
|
||||
label = f'Docker — {running} containers'
|
||||
groups.append({'name':label, 'status':state, 'missing':[n for n in relevant if n not in lookup]})
|
||||
counts_match = re.search(r'SUMMARY:\s*(\d+) OK\s*\|\s*(\d+) WARN\s*\|\s*(\d+) FAIL', plain)
|
||||
counts = dict(zip(('ok','warn','fail'), (int(x) for x in counts_match.groups()))) if counts_match else {'ok':0,'warn':0,'fail':1}
|
||||
if not checks:
|
||||
counts['fail'] = max(1, counts['fail'])
|
||||
overall = 'fail' if counts['fail'] or any(g['status']=='fail' for g in groups) else 'warn' if counts['warn'] or any(g['status']=='warn' for g in groups) else 'ok'
|
||||
print(json.dumps({'schema':1, 'host':'pegasus', 'generatedAt':datetime.datetime.now(datetime.timezone.utc).isoformat(), 'overall':overall, 'counts':counts,'groups':groups,'checks':checks, 'diagnostics':plain[-24000:], 'commandExitCode':p.returncode}, ensure_ascii=False))
|
||||
return {'ok':0,'warn':1,'fail':2}[overall]
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(output())
|
||||
Reference in New Issue
Block a user