251 lines
9.7 KiB
Python
251 lines
9.7 KiB
Python
"""
|
|
mod_timers.py -- systemd timer installation and removal.
|
|
|
|
Manages the blocklist refresh timer, DDNS maintenance timer, and
|
|
generic interval timers (health check, etc.).
|
|
"""
|
|
|
|
import subprocess
|
|
|
|
import mod_shared as shared
|
|
|
|
BLIST_TIMER_NAME = f"{shared.PRODUCT_NAME}-dns-blocklist-update"
|
|
BLIST_TIMER_FILE = shared.SYSTEMD_DIR / f"{BLIST_TIMER_NAME}.timer"
|
|
BLIST_TIMER_SVC_FILE = shared.SYSTEMD_DIR / f"{BLIST_TIMER_NAME}.service"
|
|
|
|
MAINT_TIMER_NAME = f"{shared.PRODUCT_NAME}-maintenance"
|
|
MAINT_TIMER_FILE = shared.SYSTEMD_DIR / f"{MAINT_TIMER_NAME}.timer"
|
|
MAINT_TIMER_SVC_FILE = shared.SYSTEMD_DIR / f"{MAINT_TIMER_NAME}.service"
|
|
|
|
HEALTH_TIMER_NAME = f"{shared.PRODUCT_NAME}-health-check"
|
|
HEALTH_TIMER_FILE = shared.SYSTEMD_DIR / f"{HEALTH_TIMER_NAME}.timer"
|
|
HEALTH_TIMER_SVC_FILE = shared.SYSTEMD_DIR / f"{HEALTH_TIMER_NAME}.service"
|
|
HEALTH_TIMER_INTERVAL_SEC = 300
|
|
|
|
CAPTIVE_QUEUE_TIMER_NAME = f"{shared.PRODUCT_NAME}-captive-queue"
|
|
CAPTIVE_QUEUE_TIMER_FILE = shared.SYSTEMD_DIR / f"{CAPTIVE_QUEUE_TIMER_NAME}.timer"
|
|
CAPTIVE_QUEUE_TIMER_SVC_FILE = shared.SYSTEMD_DIR / f"{CAPTIVE_QUEUE_TIMER_NAME}.service"
|
|
CAPTIVE_QUEUE_TIMER_INTERVAL = 10
|
|
|
|
CAPTIVE_CHECK_TIMER_NAME = f"{shared.PRODUCT_NAME}-captive-check"
|
|
CAPTIVE_CHECK_TIMER_FILE = shared.SYSTEMD_DIR / f"{CAPTIVE_CHECK_TIMER_NAME}.timer"
|
|
CAPTIVE_CHECK_TIMER_SVC_FILE = shared.SYSTEMD_DIR / f"{CAPTIVE_CHECK_TIMER_NAME}.service"
|
|
CAPTIVE_CHECK_TIMER_INTERVAL = 300
|
|
|
|
|
|
# ===================================================================
|
|
# Blocklist timer
|
|
# ===================================================================
|
|
|
|
def _parse_time_to_calendar(time_str):
|
|
"""Convert HH:MM string to a systemd OnCalendar value. Raises ValueError on bad format."""
|
|
parts = time_str.strip().split(":")
|
|
if len(parts) != 2:
|
|
raise ValueError(f"Invalid daily_execute_time_24hr_local: '{time_str}'. Expected HH:MM.")
|
|
hh, mm = parts
|
|
return f"*-*-* {hh.zfill(2)}:{mm.zfill(2)}:00"
|
|
|
|
def install_timer(data):
|
|
"""Install the daily blocklist refresh timer. Returns error string on failure, None on success."""
|
|
general = data.get("dns_blocking", {}).get("general", {})
|
|
execute_time = general.get("daily_execute_time_24hr_local", "02:30")
|
|
try:
|
|
on_calendar = _parse_time_to_calendar(execute_time)
|
|
except ValueError as e:
|
|
return str(e)
|
|
|
|
timer_content = "\n".join([
|
|
"# Generated by core.py -- do not edit manually.",
|
|
"",
|
|
"[Unit]",
|
|
"Description=Daily blocklist refresh",
|
|
"",
|
|
"[Timer]",
|
|
f"OnCalendar={on_calendar}",
|
|
"Persistent=true",
|
|
"",
|
|
"[Install]",
|
|
"WantedBy=timers.target",
|
|
"",
|
|
])
|
|
|
|
blocklist_script = shared.SCRIPT_DIR / "dns-blocklists.py"
|
|
service_content = "\n".join([
|
|
"# Generated by core.py -- do not edit manually.",
|
|
"",
|
|
"[Unit]",
|
|
"Description=Daily blocklist refresh",
|
|
"After=network-online.target",
|
|
"Wants=network-online.target",
|
|
"",
|
|
"[Service]",
|
|
"Type=oneshot",
|
|
f"ExecStart=/usr/bin/python3 {blocklist_script}",
|
|
"",
|
|
])
|
|
|
|
for path, content in ((BLIST_TIMER_FILE, timer_content), (BLIST_TIMER_SVC_FILE, service_content)):
|
|
if not path.exists() or path.read_text() != content:
|
|
path.write_text(content)
|
|
print(f"Written: {path}")
|
|
|
|
subprocess.run(["systemctl", "daemon-reload"], capture_output=True, text=True)
|
|
subprocess.run(["systemctl", "enable", "--now", f"{BLIST_TIMER_NAME}.timer"],
|
|
capture_output=True, text=True)
|
|
print(f"Timer {BLIST_TIMER_NAME}.timer enabled (runs daily at {execute_time}).")
|
|
|
|
|
|
# ===================================================================
|
|
# Interval timers
|
|
# ===================================================================
|
|
|
|
def install_interval_timers(names, timer_files, svc_files, descriptions, exec_starts, interval_secs):
|
|
for name, timer_file, svc_file, description, exec_start, interval_sec in zip(
|
|
names, timer_files, svc_files, descriptions, exec_starts, interval_secs):
|
|
timer_content = "\n".join([
|
|
"# Generated by core.py -- do not edit manually.",
|
|
"",
|
|
"[Unit]",
|
|
f"Description={description}",
|
|
"",
|
|
"[Timer]",
|
|
f"OnActiveSec={interval_sec}s",
|
|
f"OnUnitActiveSec={interval_sec}s",
|
|
"AccuracySec=10s",
|
|
"",
|
|
"[Install]",
|
|
"WantedBy=timers.target",
|
|
"",
|
|
])
|
|
service_content = "\n".join([
|
|
"# Generated by core.py -- do not edit manually.",
|
|
"",
|
|
"[Unit]",
|
|
f"Description={description}",
|
|
"",
|
|
"[Service]",
|
|
"Type=oneshot",
|
|
f"ExecStart={exec_start}",
|
|
"",
|
|
])
|
|
for path, content in ((timer_file, timer_content), (svc_file, service_content)):
|
|
if not path.exists() or path.read_text() != content:
|
|
path.write_text(content)
|
|
print(f"Written: {path}")
|
|
|
|
subprocess.run(["systemctl", "daemon-reload"], capture_output=True, text=True)
|
|
for name, interval_sec in zip(names, interval_secs):
|
|
subprocess.run(["systemctl", "enable", f"{name}.timer"], capture_output=True, text=True)
|
|
active = subprocess.run(
|
|
["systemctl", "is-active", f"{name}.timer"],
|
|
capture_output=True, text=True
|
|
).stdout.strip() == "active"
|
|
verb = "restart" if active else "start"
|
|
subprocess.run(["systemctl", verb, f"{name}.timer"], capture_output=True, text=True)
|
|
print(f"Timer {name}.timer enabled (runs every {interval_sec}s).")
|
|
|
|
def remove_timers(names, timer_files, svc_files, daemon_reload=False):
|
|
for name, timer_file, svc_file in zip(names, timer_files, svc_files):
|
|
subprocess.run(["systemctl", "disable", "--now", f"{name}.timer"],
|
|
capture_output=True, text=True)
|
|
for f in (timer_file, svc_file):
|
|
if f.exists():
|
|
f.unlink()
|
|
print(f"Removed: {f}")
|
|
else:
|
|
print(f"Not found, skipping: {f}")
|
|
if daemon_reload:
|
|
subprocess.run(["systemctl", "daemon-reload"], capture_output=True, text=True)
|
|
|
|
|
|
# ===================================================================
|
|
# DDNS maintenance timer
|
|
# ===================================================================
|
|
|
|
def _parse_ddns_interval(interval_str):
|
|
"""Convert interval string (e.g. 5m, 2h, 1d) to systemd OnUnitActiveSec value."""
|
|
s = interval_str.strip()
|
|
if s.endswith("m"): return f"{s[:-1]}min"
|
|
if s.endswith("h"): return f"{s[:-1]}h"
|
|
if s.endswith("d"): return f"{s[:-1]}day"
|
|
raise ValueError(f"Invalid timer_interval format: '{s}'. Use e.g. 5m, 2h, 1d.")
|
|
|
|
def install_maint_timer(data):
|
|
ddns = data.get("ddns", {})
|
|
interval = ddns.get("general", {}).get("timer_interval", "10m")
|
|
script_path = shared.SCRIPT_DIR / "maintenance.py"
|
|
try:
|
|
systemd_unit = _parse_ddns_interval(interval)
|
|
except ValueError as e:
|
|
print(f"DDNS timer: {e}")
|
|
return
|
|
|
|
service_content = "\n".join([
|
|
"# Generated by core.py -- do not edit manually.",
|
|
"",
|
|
"[Unit]",
|
|
"Description=DDNS IP update",
|
|
"After=network-online.target",
|
|
"Wants=network-online.target",
|
|
"",
|
|
"[Service]",
|
|
"Type=oneshot",
|
|
f"ExecStart=/usr/bin/python3 {script_path} --update",
|
|
"",
|
|
])
|
|
timer_content = "\n".join([
|
|
"# Generated by core.py -- do not edit manually.",
|
|
"",
|
|
"[Unit]",
|
|
"Description=DDNS IP update timer",
|
|
"",
|
|
"[Timer]",
|
|
f"OnActiveSec={systemd_unit}",
|
|
f"OnUnitActiveSec={systemd_unit}",
|
|
"OnBootSec=1min",
|
|
"AccuracySec=10s",
|
|
"",
|
|
"[Install]",
|
|
"WantedBy=timers.target",
|
|
"",
|
|
])
|
|
for path, content in ((MAINT_TIMER_SVC_FILE, service_content), (MAINT_TIMER_FILE, timer_content)):
|
|
if not path.exists() or path.read_text() != content:
|
|
path.write_text(content)
|
|
print(f"Written: {path}")
|
|
subprocess.run(["systemctl", "daemon-reload"], capture_output=True, text=True)
|
|
active = subprocess.run(
|
|
["systemctl", "is-active", f"{MAINT_TIMER_NAME}.timer"],
|
|
capture_output=True, text=True
|
|
).stdout.strip() == "active"
|
|
verb = "restart" if active else "enable --now"
|
|
subprocess.run(["systemctl"] + verb.split() + [f"{MAINT_TIMER_NAME}.timer"],
|
|
capture_output=True, text=True)
|
|
print(f"Timer {MAINT_TIMER_NAME}.timer enabled (runs every {interval}).")
|
|
|
|
|
|
# ===================================================================
|
|
# Captive portal timers
|
|
# ===================================================================
|
|
|
|
def install_captive_timers():
|
|
install_interval_timers(
|
|
names=[CAPTIVE_QUEUE_TIMER_NAME, CAPTIVE_CHECK_TIMER_NAME],
|
|
timer_files=[CAPTIVE_QUEUE_TIMER_FILE, CAPTIVE_CHECK_TIMER_FILE],
|
|
svc_files=[CAPTIVE_QUEUE_TIMER_SVC_FILE, CAPTIVE_CHECK_TIMER_SVC_FILE],
|
|
descriptions=["Captive portal queue processor", "Captive portal session expiry checker"],
|
|
exec_starts=[
|
|
f"/bin/bash {shared.SCRIPT_DIR / 'do_captive_queue.sh'}",
|
|
f"/usr/bin/python3 {shared.SCRIPT_DIR / 'check_captive_users.py'}",
|
|
],
|
|
interval_secs=[CAPTIVE_QUEUE_TIMER_INTERVAL, CAPTIVE_CHECK_TIMER_INTERVAL],
|
|
)
|
|
|
|
|
|
def remove_captive_timers():
|
|
remove_timers(
|
|
names=[CAPTIVE_QUEUE_TIMER_NAME, CAPTIVE_CHECK_TIMER_NAME],
|
|
timer_files=[CAPTIVE_QUEUE_TIMER_FILE, CAPTIVE_CHECK_TIMER_FILE],
|
|
svc_files=[CAPTIVE_QUEUE_TIMER_SVC_FILE, CAPTIVE_CHECK_TIMER_SVC_FILE],
|
|
daemon_reload=True,
|
|
)
|