Implement runit support for Fenris monitoring (issue #84)

Deliver end-to-end native monitoring path under runit with existing
CLI/TUI controls and truthful status, preserving systemd behavior.

Changes:
- Add init system abstraction layer (src/fenris/init_system.py) that
  detects systemd vs runit and provides unified interface for timer
  control, on-demand collection, and service state queries
- Create runit service files (units/runit/) with completion-relative
  5-minute cadence, 2-minute boot delay, bounded execution (90s),
  no catch-up, and serialized runs via flock
- Update monitor.py to use abstraction layer instead of direct systemctl
- Update status.py to use abstraction layer for service state queries
- Update all packaging scripts (deb, rpm) for init-system-aware setup
- Update Makefile to install runit service files alongside systemd units
- Add 46 tests for init system abstraction layer

Spec: ADR 0008, §8.4, §8.5, §8.6, §8.7, §8.8
Closes #84

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
This commit is contained in:
xavierk
2026-09-15 00:15:24 +05:30
co-authored by CommandCodeBot
parent 37a0ed7030
commit e27052d09a
15 changed files with 1390 additions and 227 deletions
+13 -54
View File
@@ -15,9 +15,7 @@ When run as a script, uses the fenris package from the installed wheel.
import argparse
import json
import os
import subprocess
import sys
import sqlite3
from datetime import datetime, timezone
from pathlib import Path
@@ -39,6 +37,11 @@ from fenris.monitoring_periods import (
close_period,
get_open_period,
)
from fenris.init_system import (
enable_timer,
disable_timer,
collect_now,
)
DEFAULT_STORE_PATH = Path("/var/lib/fenris/observations.db")
@@ -70,25 +73,8 @@ def cmd_enable(args: argparse.Namespace) -> None:
else:
print("Monitoring period already open (id=%d)" % open_period["id"])
# Enable and start the timer
if args.now:
result = subprocess.run(
["systemctl", "enable", "--now", "fenris-collect.timer"],
capture_output=True,
text=True,
)
else:
result = subprocess.run(
["systemctl", "enable", "fenris-collect.timer"],
capture_output=True,
text=True,
)
if result.returncode != 0:
print("Error enabling timer:", result.stderr, file=sys.stderr)
sys.exit(1)
print("Timer enabled" + (" and started" if args.now else ""))
# Enable the timer (init-system aware)
enable_timer(args.now)
finally:
conn.close()
@@ -117,25 +103,8 @@ def cmd_disable(args: argparse.Namespace) -> None:
else:
print("No open monitoring period (no-op)")
# Disable and stop the timer
if args.now:
result = subprocess.run(
["systemctl", "disable", "--now", "fenris-collect.timer"],
capture_output=True,
text=True,
)
else:
result = subprocess.run(
["systemctl", "disable", "fenris-collect.timer"],
capture_output=True,
text=True,
)
if result.returncode != 0:
print("Error disabling timer:", result.stderr, file=sys.stderr)
sys.exit(1)
print("Timer disabled" + (" and stopped" if args.now else ""))
# Disable the timer (init-system aware)
disable_timer(args.now)
finally:
conn.close()
@@ -143,22 +112,12 @@ def cmd_disable(args: argparse.Namespace) -> None:
def cmd_collect(args: argparse.Namespace) -> None:
"""Trigger on-demand collection.
Starts fenris-collect.service, blocks until exit, reports outcome.
Starts fenris-collect, blocks until exit, reports outcome.
Spec §8.7: fenris sample routes through fenris-monitor → systemctl start,
which blocks until the oneshot exits; outcome reported synchronously.
Spec §8.7: fenris sample routes through fenris-monitor → collect,
which blocks until the collection exits; outcome reported synchronously.
"""
result = subprocess.run(
["systemctl", "start", "fenris-collect.service"],
capture_output=True,
text=True,
)
if result.returncode == 0:
print("Collection completed successfully")
else:
print("Collection failed:", result.stderr, file=sys.stderr)
sys.exit(1)
collect_now()
def cmd_baseline_set(args: argparse.Namespace) -> None: