from __future__ import annotations from dataclasses import dataclass from pathlib import Path from aim.exceptions import ExternalCommandFailed from aim.external import command_available, run_external @dataclass(frozen=True) class PlaybookSpec: key: str name: str filename: str category: str platforms: tuple[str, ...] extra_vars: tuple[str, ...] = () customer_specific: bool = False ask_pass: bool = False require_vault: bool = False PLAYBOOKS: tuple[PlaybookSpec, ...] = ( PlaybookSpec("checkmk_cleanup", "Cleanup CheckMK", "checkmk_cleanup.yml", "CheckMK", ("linux", "windows"), ("checkmk_cleanup_enabled=true",)), PlaybookSpec("checkmk_install_agent", "Install CheckMK Agent", "checkmk_install_agent.yml", "CheckMK", ("linux", "windows")), PlaybookSpec("checkmk_update_config", "Update CheckMK Config", "checkmk_update_config.yml", "CheckMK", ("linux", "windows")), PlaybookSpec("debug_ping", "Ping", "debug_ping.yml", "Debug", ("linux", "windows")), PlaybookSpec("debug_server_role_selection", "Server Role Selection", "debug_server_role_selection.yml", "Debug", ("linux", "windows")), PlaybookSpec("debug_disk_usage", "Disk Usage", "debug_disk_usage.yml", "Debug", ("windows",)), PlaybookSpec("patch_os", "Patch OS", "patch_os.yml", "Maintenance", ("linux", "windows")), PlaybookSpec("reboot_system", "Reboot System", "reboot_system.yml", "Maintenance", ("linux", "windows")), PlaybookSpec("backup_eventlog", "Backup Event Log", "backup_eventlog.yml", "Maintenance", ("windows",)), PlaybookSpec("start_stopped_services", "Start Stopped Services", "start_stopped_services.yml", "Maintenance", ("windows",)), PlaybookSpec("configure_sophos_initial", "Initial Bitformer Config", "configure_sophos_initial_bitformer_config.yml", "Sophos XGS", ("sophosxgs",), ask_pass=True, require_vault=True), PlaybookSpec("configure_sophosxgs", "Configure Sophos XGS", "configure_sophosxgs.yml", "Sophos XGS", ("sophosxgs",), customer_specific=True, ask_pass=True, require_vault=True), ) CATEGORY_ORDER = ("CheckMK", "Debug", "Maintenance", "Sophos XGS") class PlaybookManager: def __init__(self, customers, config): self.customers = customers self.config = config @property def root(self) -> Path: return self.config.root_dir / "playbooks" def categories(self) -> list[str]: return list(CATEGORY_ORDER) def specs_for_category(self, category: str) -> list[PlaybookSpec]: return [spec for spec in PLAYBOOKS if spec.category == category] def path(self, spec: PlaybookSpec, customer: str | None = None) -> Path: if spec.customer_specific: if not customer: raise ValueError(f"Customer is required for playbook {spec.name}") return self.root / "customers" / customer / spec.filename return self.root / spec.filename def available(self, spec: PlaybookSpec, customer: str | None = None) -> bool: return self.path(spec, customer).is_file() def run(self, customer: str, inventory: Path, spec: PlaybookSpec, *, limit: str | None = None) -> None: playbook = self.path(spec, customer) if not playbook.is_file(): raise FileNotFoundError(playbook) if not inventory.is_file(): raise FileNotFoundError(inventory) if not command_available("ansible-playbook"): raise ExternalCommandFailed("Required command not found: ansible-playbook") args = ["ansible-playbook", "-i", str(inventory), str(playbook)] vault = inventory.parent / "group_vars" / "all" / "vault.yml" if spec.require_vault and not vault.is_file(): raise FileNotFoundError(f"Required customer vault not found: {vault}") if vault.is_file(): args.extend(["--vault-id", f"{customer}@prompt"]) if spec.ask_pass: args.append("--ask-pass") if limit: args.extend(["--limit", limit]) for extra_var in spec.extra_vars: args.extend(["-e", extra_var]) result = run_external(args, cwd=self.config.root_dir) if result.returncode: raise ExternalCommandFailed(f"Playbook failed with exit code {result.returncode}")