This repository has been archived on 2026-06-22. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.
enroll/enroll/ansible_renderer/model.py
Miguel Jacq ebc27e1111
Some checks failed
Lint / test (push) Waiting to run
CI / test (push) Has been cancelled
Support for detecting Docker images
2026-06-17 18:05:02 +10:00

227 lines
7.6 KiB
Python

from __future__ import annotations
import re
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Set
from ..cm import CMModule, package_section_label, section_label_for_packages
from ..role_names import avoid_reserved_role_name
@dataclass
class AnsibleRoleCollection:
services: List[Dict[str, Any]]
packages: List[Dict[str, Any]]
common_role_groups: Dict[str, List[Dict[str, Any]]]
class AnsibleRole(CMModule):
"""Ansible-specific view of a renderer-neutral CMModule."""
def __init__(
self,
role_name: str,
*,
var_prefix: Optional[str] = None,
section_label: Optional[str] = None,
grouped: bool = False,
) -> None:
super().__init__(role_name=role_name, module_name=role_name)
self.var_prefix = var_prefix or role_name
self.section_label = section_label
self.grouped = grouped
self.entries: List[Dict[str, Any]] = []
self.excluded: List[Dict[str, Any]] = []
self.origin_lines: List[str] = []
def add_package_snapshot(self, snap: Dict[str, Any]) -> None:
pkg = str(snap.get("package") or "").strip()
source_role = str(snap.get("role_name") or pkg or self.role_name)
self.entries.append({"kind": "package", "snapshot": snap})
if pkg:
self.packages.add(pkg)
self.origin_lines.append(f"package `{pkg}` from role `{source_role}`")
self.add_managed_content(snap)
def add_service_snapshot(self, snap: Dict[str, Any]) -> None:
unit = str(snap.get("unit") or "").strip()
source_role = str(snap.get("role_name") or unit or self.role_name)
self.entries.append({"kind": "service", "snapshot": snap})
for pkg in snap.get("packages", []) or []:
pkg_s = str(pkg or "").strip()
if pkg_s:
self.packages.add(pkg_s)
if unit:
unit_file_state = str(snap.get("unit_file_state") or "")
self.services.setdefault(
unit,
{
"name": unit,
"manage": True,
"enabled": unit_file_state in ("enabled", "enabled-runtime"),
"state": (
"started" if snap.get("active_state") == "active" else "stopped"
),
},
)
self.origin_lines.append(f"service `{unit}` from role `{source_role}`")
self.add_managed_content(snap)
def add_managed_content(self, snap: Dict[str, Any]) -> None:
for d in self.managed_dirs_from_snapshot(snap):
path = str(d.get("path") or "").strip()
self.add_managed_dir(
path,
dest=path,
owner=d.get("owner") or "root",
group=d.get("group") or "root",
mode=d.get("mode") or "0755",
)
for mf in self.managed_files_from_snapshot(snap):
path = str(mf.get("path") or "").strip()
src_rel = str(mf.get("src_rel") or "").strip()
if not path or not src_rel:
continue
self.add_managed_file(
path,
dest=path,
src_rel=src_rel,
owner=mf.get("owner") or "root",
group=mf.get("group") or "root",
mode=mf.get("mode") or "0644",
reason=mf.get("reason") or "managed_file",
)
for ml in self.managed_links_from_snapshot(snap):
path = str(ml.get("path") or "").strip()
target = str(ml.get("target") or "").strip()
if not path or not target:
continue
self.add_managed_link(path, dest=path, src=target)
self.excluded.extend(snap.get("excluded", []) or [])
self.add_snapshot_notes(snap)
@property
def sorted_packages(self) -> List[str]:
return sorted(self.packages)
@property
def systemd_units_var(self) -> List[Dict[str, Any]]:
return [self.services[k] for k in sorted(self.services)]
class AnsibleManifestPlan:
"""Track generated Ansible roles without scattering category lists."""
_ORDER = (
"apt_config",
"dnf_config",
"package",
"service",
"etc_custom",
"usr_local_custom",
"extra_paths",
"flatpak",
"snap",
"container_images",
"users",
"tail_package",
"sysctl",
"firewall_runtime",
)
def __init__(self) -> None:
self._roles: Dict[str, List[str]] = {category: [] for category in self._ORDER}
self._tail_packages: List[str] = []
def add(self, category: str, role: str) -> None:
if category not in self._roles:
raise ValueError(f"unknown Ansible role category: {category}")
if role and role not in self._roles[category]:
self._roles[category].append(role)
def roles(self, category: str) -> List[str]:
return list(self._roles.get(category, []))
def has(self, category: str, role: str) -> bool:
return role in self._roles.get(category, [])
def mark_tail_package(self, role: str) -> None:
if self.has("package", role) and role not in self._tail_packages:
self._tail_packages.append(role)
def ordered_roles(self) -> List[str]:
tail = set(self._tail_packages)
package_roles = [r for r in self._roles["package"] if r not in tail]
out: List[str] = []
for category in self._ORDER:
if category == "package":
out.extend(package_roles)
elif category == "tail_package":
out.extend(self._tail_packages)
else:
out.extend(self._roles[category])
return out
def _role_id(raw: str) -> str:
"""Return an Ansible-safe role identifier from an arbitrary label."""
s = re.sub(r"[^A-Za-z0-9]+", "_", raw or "misc")
s = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", s)
s = s.lower()
s = re.sub(r"_+", "_", s).strip("_")
if not s:
s = "misc"
if not re.match(r"^[a-z_]", s):
s = "r_" + s
return s
def _section_role_name(label: str, occupied_roles: Set[str]) -> str:
"""Create a stable section role name, avoiding generated-role collisions."""
base = avoid_reserved_role_name(_role_id(label), prefix="section")
role = base if base not in occupied_roles else f"section_{base}"
n = 2
while role in occupied_roles:
role = f"section_{base}_{n}"
n += 1
occupied_roles.add(role)
return role
def _collect_ansible_roles(
roles: Dict[str, Any],
inventory_packages: Dict[str, Any],
*,
use_common_roles: bool,
) -> AnsibleRoleCollection:
services = roles.get("services", []) or []
packages = roles.get("packages", []) or []
common_role_groups: Dict[str, List[Dict[str, Any]]] = {}
if use_common_roles:
for svc in services:
label = section_label_for_packages(
svc.get("packages", []) or [], inventory_packages
)
common_role_groups.setdefault(label, []).append(
{"kind": "service", "snapshot": svc}
)
for pr in packages:
label = package_section_label(pr, inventory_packages)
common_role_groups.setdefault(label, []).append(
{"kind": "package", "snapshot": pr}
)
return AnsibleRoleCollection(
services=[], packages=[], common_role_groups=common_role_groups
)
return AnsibleRoleCollection(
services=services,
packages=packages,
common_role_groups=common_role_groups,
)