This commit is contained in:
Mark Gregory
2026-10-21 14:50:05 +01:00
parent 90987bd898
commit 9e501ca176
44 changed files with 461 additions and 222 deletions
+44 -9
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Generic read-only Linux health adapter for Pegasus Monitor 1.1.
"""Generic read-only Linux health adapter for Pegasus Monitor 1.2.
Install as /usr/local/bin/pegasus-monitor-health-json and run as an ordinary user.
No third-party Python packages required.
"""
@@ -64,13 +64,48 @@ if shutil.which('docker'):
else:
check('Docker', 'Docker access', 'warn', 'Docker is installed but unavailable to this SSH user')
for service in ('nginx', 'mysql', 'postgresql', 'redis-server', 'gitea'):
result = run(['systemctl', 'is-enabled', service])
if not result or result.returncode != 0:
continue
active = run(['systemctl', 'is-active', service])
working = active is not None and active.stdout.strip() == 'active'
check('Services', service, 'ok' if working else 'fail', 'active' if working else 'inactive')
# Discover common services without assuming they are installed or enabled.
# Each state is explicit so optional missing software never counts as a failure.
SERVICES = (
('nginx', 'nginx'), ('mysql', 'mysql'), ('mariadb', 'mariadb'),
('postgresql', 'postgresql'), ('redis-server', 'redis-server'),
('gitea', 'gitea'), ('docker', 'docker'), ('containerd', 'containerd'),
('php8.3-fpm', 'php8.3-fpm'), ('php8.4-fpm', 'php8.4-fpm'),
('php8.5-fpm', 'php8.5-fpm'), ('ssh', 'ssh'), ('cloudflared', 'cloudflared'),
('cockpit.socket', 'cockpit.socket'), ('libvirtd', 'libvirtd'),
)
services = []
for display, unit in SERVICES:
full_unit = unit if unit.endswith(('.service', '.socket')) else unit + '.service'
result = run(['systemctl', 'show', full_unit, '--no-pager',
'--property=LoadState', '--property=ActiveState',
'--property=UnitFileState'], timeout=5)
if result is None or not result.stdout.strip():
state, detail = 'unknown', 'systemctl unavailable'
else:
properties = dict(line.split('=', 1) for line in result.stdout.splitlines() if '=' in line)
load = properties.get('LoadState', 'not-found')
active = properties.get('ActiveState', 'unknown')
unit_state = properties.get('UnitFileState', 'unknown')
detail = f'Load: {load}; active: {active}; enablement: {unit_state}'
if load == 'not-found':
state = 'not_installed'
elif active == 'active':
state = 'running'
elif active == 'failed':
state = 'failed'
elif unit_state in ('disabled', 'masked'):
state = 'disabled'
elif active in ('inactive', 'deactivating'):
state = 'stopped'
else:
state = 'unknown'
services.append(dict(name=display, state=state, detail=detail))
# Uninstalled/disabled services are informational, not warnings or failures.
if state == 'failed':
check('Services', display, 'fail', detail)
elif state == 'stopped':
check('Services', display, 'warn', detail)
groups = []
for section in dict.fromkeys(c['section'] for c in checks):
@@ -80,4 +115,4 @@ for section in dict.fromkeys(c['section'] for c in checks):
counts = {s: sum(c['status'] == s for c in checks) for s in ('ok', 'warn', 'fail')}
overall = 'fail' if counts['fail'] else 'warn' if counts['warn'] else 'ok'
print(json.dumps(dict(schema=1, host=host, generatedAt=datetime.datetime.now(datetime.timezone.utc).isoformat(),
overall=overall, counts=counts, groups=groups, checks=checks, diagnostics=None)))
overall=overall, counts=counts, groups=groups, checks=checks, diagnostics=None, services=services)))