linuxrouter/routlin/maintenance.py

160 lines
5.2 KiB
Python
Raw Normal View History

2026-06-01 22:12:11 -04:00
#!/usr/bin/env python3
"""
maintenance.py -- Periodic maintenance tasks run by the routlin-maintenance systemd timer.
Tasks performed on each run:
2026-06-11 01:31:57 -04:00
1. DDNS update (delegates to ddns.py)
2. FreeRADIUS log rotation
3. ARP cache refresh
4. DNS metrics collection (delegates to metrics.py)
2026-06-01 22:12:11 -04:00
Usage:
2026-06-11 01:31:57 -04:00
python3 maintenance.py
2026-06-01 22:12:11 -04:00
"""
2026-06-03 03:23:19 -04:00
import ipaddress
2026-06-01 22:12:11 -04:00
import json
import re
2026-06-11 01:31:57 -04:00
import subprocess
2026-06-01 22:12:11 -04:00
import sys
from pathlib import Path
2026-06-11 01:31:57 -04:00
SCRIPT_DIR = Path(__file__).parent
CONFIG_FILE = SCRIPT_DIR / "config.json"
DDNS_SCRIPT = SCRIPT_DIR / "ddns.py"
METRICS_SCRIPT = SCRIPT_DIR / "metrics.py"
RADIUS_LOG_FILE = Path("/var/log/freeradius/radius.log")
ARP_CACHE_FILE = Path("/var/lib/misc/arp-cache.json")
2026-06-01 22:12:11 -04:00
# ===================================================================
2026-06-11 01:31:57 -04:00
# Config
2026-06-01 22:12:11 -04:00
# ===================================================================
def load_config():
if not CONFIG_FILE.exists():
print(f"ERROR: Config file not found: {CONFIG_FILE}", file=sys.stderr)
sys.exit(1)
with open(CONFIG_FILE) as f:
2026-06-11 01:31:57 -04:00
return json.load(f)
2026-06-01 22:12:11 -04:00
# ===================================================================
2026-06-11 01:31:57 -04:00
# DDNS - delegate to ddns.py
2026-06-01 22:12:11 -04:00
# ===================================================================
2026-06-11 01:31:57 -04:00
def run_ddns():
subprocess.run([sys.executable, str(DDNS_SCRIPT), "--update"])
2026-06-01 22:12:11 -04:00
# ===================================================================
# FreeRADIUS log rotation
# ===================================================================
2026-06-03 00:45:04 -04:00
def _clear_radius_log_dir(log_dir, reason):
try:
files = [p for p in log_dir.iterdir() if p.is_file()]
if not files:
return
for p in files:
try:
p.unlink()
except PermissionError:
print(f"WARNING: Cannot delete {p} (permission denied).")
except OSError as e:
print(f"WARNING: Error deleting {p}: {e}")
print(f"FreeRADIUS logs cleared ({reason}).")
except PermissionError:
print(f"WARNING: Cannot read {log_dir} (permission denied).")
except OSError as e:
print(f"WARNING: Error clearing FreeRADIUS log dir: {e}")
2026-06-01 22:12:11 -04:00
def rotate_radius_log(radius_cfg):
2026-06-03 00:45:04 -04:00
general = radius_cfg.get("general", {})
log_dir = RADIUS_LOG_FILE.parent
if not log_dir.exists():
2026-06-01 22:12:11 -04:00
return
2026-06-03 00:45:04 -04:00
if not general.get("logging", False):
_clear_radius_log_dir(log_dir, "logging disabled")
return
max_kb = general.get("log_max_kb", 1024)
max_bytes = int(max_kb * 1024)
2026-06-01 22:12:11 -04:00
try:
2026-06-03 00:45:04 -04:00
files = [p for p in log_dir.iterdir() if p.is_file()]
total = sum(p.stat().st_size for p in files)
if total > max_bytes:
_clear_radius_log_dir(log_dir, f"total {total // 1024} KB exceeded {max_kb} KB")
2026-06-01 22:12:11 -04:00
except PermissionError:
2026-06-03 00:45:04 -04:00
print(f"WARNING: Cannot read {log_dir} (permission denied).")
2026-06-01 22:12:11 -04:00
except OSError as e:
2026-06-03 00:45:04 -04:00
print(f"WARNING: Error checking FreeRADIUS log dir: {e}")
2026-06-01 22:12:11 -04:00
# ===================================================================
2026-06-11 01:31:57 -04:00
# ARP cache
2026-06-01 22:12:11 -04:00
# ===================================================================
2026-06-03 03:20:52 -04:00
ARP_MAX_AGE_SECS = 4 * 3600
2026-06-03 03:23:19 -04:00
def refresh_arp_cache(cfg):
vlan_networks = []
2026-06-11 01:31:57 -04:00
for v in cfg.get('vlans', []):
2026-06-03 03:23:19 -04:00
subnet = v.get('subnet')
mask = v.get('subnet_mask')
if subnet and mask:
try:
vlan_networks.append(ipaddress.IPv4Network(f'{subnet}/{mask}', strict=False))
except ValueError:
pass
2026-06-03 02:57:59 -04:00
try:
2026-06-03 03:20:52 -04:00
result = subprocess.run(['ip', '-stats', 'neigh'], capture_output=True, text=True, timeout=5)
2026-06-11 01:31:57 -04:00
best = {}
2026-06-03 02:57:59 -04:00
for line in result.stdout.splitlines():
parts = line.split()
if 'lladdr' not in parts:
continue
2026-06-11 01:31:57 -04:00
if ':' in parts[0]:
2026-06-03 02:57:59 -04:00
continue
2026-06-03 03:23:19 -04:00
try:
addr = ipaddress.IPv4Address(parts[0])
if vlan_networks and not any(addr in n for n in vlan_networks):
continue
except ValueError:
continue
2026-06-03 03:03:38 -04:00
iface = parts[2] if len(parts) > 2 else ''
if iface.startswith('br-') or iface == 'docker0':
continue
2026-06-03 03:20:52 -04:00
state = parts[-1]
if state in ('FAILED', 'PERMANENT', 'NOARP', 'INCOMPLETE'):
continue
used_match = re.search(r'used\s+(\d+)/', line)
used_secs = int(used_match.group(1)) if used_match else 0
2026-06-03 03:34:35 -04:00
if state != 'REACHABLE' and used_secs > ARP_MAX_AGE_SECS:
2026-06-03 03:20:52 -04:00
continue
2026-06-03 02:57:59 -04:00
idx = parts.index('lladdr')
mac = parts[idx + 1].lower()
2026-06-03 03:20:52 -04:00
if mac not in best or used_secs < best[mac][0]:
2026-06-03 03:32:00 -04:00
best[mac] = (used_secs, {'ip': parts[0], 'state': 'REACHABLE'})
2026-06-03 03:20:52 -04:00
ARP_CACHE_FILE.write_text(json.dumps({m: e for m, (_, e) in best.items()}))
2026-06-03 02:57:59 -04:00
except Exception as exc:
print(f"WARNING: Could not refresh ARP cache: {exc}")
2026-06-11 01:31:57 -04:00
# ===================================================================
# Main
# ===================================================================
2026-06-01 22:12:11 -04:00
def main():
2026-06-11 01:31:57 -04:00
run_ddns()
cfg = load_config()
rotate_radius_log(cfg.get("radius", {}))
2026-06-03 03:23:19 -04:00
refresh_arp_cache(cfg)
2026-06-11 01:31:57 -04:00
subprocess.run([sys.executable, str(METRICS_SCRIPT), "--collect"])
2026-06-01 22:12:11 -04:00
2026-06-09 21:28:38 -04:00
2026-06-01 22:12:11 -04:00
if __name__ == "__main__":
main()