#!/usr/bin/env python3 from __future__ import annotations import os import re import sys from pathlib import Path STACK_DIR = Path(__file__).resolve().parent.parent ENV_FILE = Path(os.environ.get('ENV_FILE', STACK_DIR / '.env')) TEMPLATE = STACK_DIR / 'keepalived.conf.template' OUTPUT = STACK_DIR / 'keepalived.conf' def load_env(path: Path) -> dict[str, str]: env = dict(os.environ) if path.exists(): for raw in path.read_text(encoding='utf-8').splitlines(): line = raw.strip() if not line or line.startswith('#') or '=' not in line: continue k, v = line.split('=', 1) env.setdefault(k.strip(), v.strip().strip('"').strip("'")) return env def main() -> int: env = load_env(ENV_FILE) required = [ 'LAN_INTERFACE', 'VIP_CIDR', 'KEEPALIVED_ROUTER_ID', 'KEEPALIVED_STATE', 'KEEPALIVED_PRIORITY', 'KEEPALIVED_PASSWORD', 'PD_PIHOLE_WEB_PORT' ] missing = [k for k in required if not env.get(k)] if missing: print(f'Missing required vars in {ENV_FILE}: {", ".join(missing)}', file=sys.stderr) return 1 keepalived_password = env['KEEPALIVED_PASSWORD'] if len(keepalived_password) > 8: print('KEEPALIVED_PASSWORD must be 8 characters or fewer for VRRP PASS auth.', file=sys.stderr) return 1 try: web_port_hex = f"{int(env['PD_PIHOLE_WEB_PORT']):04X}" except ValueError: print('PD_PIHOLE_WEB_PORT must be an integer.', file=sys.stderr) return 1 router_id_name = f"pihole_{re.sub(r'[^a-z0-9]+', '_', env['KEEPALIVED_STATE'].lower())}" nopreempt_line = ' nopreempt' if env.get('KEEPALIVED_NOPREEMPT', 'true').lower() in {'1','true','yes','on'} else '' rendered = TEMPLATE.read_text(encoding='utf-8') replacements = { 'ROUTER_ID_NAME': router_id_name, 'PIHOLE_WEB_PORT_HEX': web_port_hex, 'NOPREEMPT_LINE': nopreempt_line, } replacements.update({k: env[k] for k in required}) for key, value in replacements.items(): rendered = rendered.replace('${' + key + '}', value) OUTPUT.write_text(rendered.rstrip() + '\n', encoding='utf-8') print(f'Rendered {OUTPUT}') return 0 if __name__ == '__main__': raise SystemExit(main())