"""Ansible --list inventory generation from DB records.""" from __future__ import annotations def generate_inventory(targets: list) -> dict: """Build an Ansible --list JSON inventory from a list of AnsibleTarget objects. Accepts any objects with .name, .address, .ansible_vars (dict), .groups (list of objects with .name and .ansible_vars). Uses duck typing so unit tests can pass SimpleNamespace objects. Var precedence (lowest → highest, matching Ansible default): group vars (sorted alphabetically by group name) → target ansible_vars → ansible_host """ inv: dict = {"all": {"hosts": []}, "_meta": {"hostvars": {}}} for target in targets: inv["all"]["hosts"].append(target.name) # Merge vars: group vars alphabetically, then host vars, then force ansible_host. merged: dict = {} for group in sorted(target.groups, key=lambda g: g.name): merged.update(group.ansible_vars or {}) merged.update(target.ansible_vars or {}) merged["ansible_host"] = target.address inv["_meta"]["hostvars"][target.name] = merged for group in target.groups: if group.name not in inv: inv[group.name] = { "hosts": [], "vars": dict(group.ansible_vars or {}), } inv[group.name]["hosts"].append(target.name) return inv def inventory_to_yaml(inv: dict) -> str: """Convert the ``--list`` dict from generate_inventory into a STATIC YAML inventory string. The ``--list`` shape (``all.hosts`` is a *list*, vars under ``_meta``) is only valid as the stdout of an executable dynamic-inventory script. When written to a plain file and passed via ``-i``, Ansible's yaml plugin requires ``all.hosts`` to be a *dict* keyed by hostname — otherwise it fails to parse and silently falls back to implicit localhost. This emits that valid form. """ import yaml hostvars = (inv.get("_meta") or {}).get("hostvars") or {} all_hosts = (inv.get("all") or {}).get("hosts") or [] root: dict = {"hosts": {h: (hostvars.get(h) or {}) for h in all_hosts}} children: dict = {} for key, val in inv.items(): if key in ("all", "_meta") or not isinstance(val, dict): continue grp: dict = {"hosts": {h: {} for h in (val.get("hosts") or [])}} if val.get("vars"): grp["vars"] = val["vars"] children[key] = grp if children: root["children"] = children return yaml.safe_dump({"all": root}, default_flow_style=False, sort_keys=True) async def fetch_scope_targets(db, scope: str) -> list: """Query AnsibleTarget objects for a given inventory scope string. Scopes: steward:all — every target steward:group: — targets in that group steward:target: — single target by ID Returns a list with groups pre-loaded (selectinload). Returns [] for repo:* scopes (caller handles those via legacy path). """ from sqlalchemy import select from sqlalchemy.orm import selectinload from steward.models.ansible_inventory import AnsibleTarget, AnsibleGroup if scope == "steward:all": stmt = ( select(AnsibleTarget) .options(selectinload(AnsibleTarget.groups)) .order_by(AnsibleTarget.name) ) elif scope.startswith("steward:group:"): group_id = scope[len("steward:group:"):] stmt = ( select(AnsibleTarget) .join(AnsibleTarget.groups) .where(AnsibleGroup.id == group_id) .options(selectinload(AnsibleTarget.groups)) .order_by(AnsibleTarget.name) ) elif scope.startswith("steward:target:"): target_id = scope[len("steward:target:"):] stmt = ( select(AnsibleTarget) .where(AnsibleTarget.id == target_id) .options(selectinload(AnsibleTarget.groups)) ) else: return [] result = await db.execute(stmt) return list(result.scalars().all())