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
Miguel Jacq b149b2e5d7
All checks were successful
CI / test (push) Successful in 18m7s
Lint / test (push) Successful in 41s
Remove salt
2026-06-17 18:13:06 +10:00
..
roles Remove salt 2026-06-17 18:13:06 +10:00
__init__.py Separate up the ansible renderer. Simplify the package management bits by using ansible.builtin.package 2026-06-17 16:40:36 +10:00
context.py Separate up the ansible renderer. Simplify the package management bits by using ansible.builtin.package 2026-06-17 16:40:36 +10:00
jinjaturtle.py Separate up the ansible renderer. Simplify the package management bits by using ansible.builtin.package 2026-06-17 16:40:36 +10:00
layout.py Support for detecting Docker images 2026-06-17 18:05:02 +10:00
model.py Support for detecting Docker images 2026-06-17 18:05:02 +10:00
readme.py Separate up the ansible renderer. Simplify the package management bits by using ansible.builtin.package 2026-06-17 16:40:36 +10:00
tasks.py Remove salt 2026-06-17 18:13:06 +10:00
vars.py Support for detecting Docker images 2026-06-17 18:05:02 +10:00
yamlutil.py Separate up the ansible renderer. Simplify the package management bits by using ansible.builtin.package 2026-06-17 16:40:36 +10:00

from __future__ import annotations

import os
import re
from typing import Any, Callable, Dict, List, Set


def _markdown_list(items: List[str]) -> str:
    values = [str(item) for item in items if str(item)]
    return "\n".join(f"- {item}" for item in values) or "- (none)"


def _managed_file_lines(
    managed_files: List[Dict[str, Any]], *, include_reason: bool
) -> List[str]:
    out: List[str] = []
    for mf in managed_files:
        path = str(mf.get("path") or "")
        if not path:
            continue
        if include_reason:
            out.append(f"{path} ({mf.get('reason')})")
        else:
            out.append(path)
    return out


def _excluded_lines(excluded: List[Dict[str, Any]]) -> List[str]:
    return [f"{e.get('path')} ({e.get('reason')})" for e in excluded if e.get("path")]


def _read_artifact_lines(bundle_dir: str, role: str, src_rel: str) -> List[str]:
    art_path = os.path.join(bundle_dir, "artifacts", role, src_rel)
    try:
        with open(art_path, "r", encoding="utf-8", errors="replace") as f:
            return [line.rstrip("\n") for line in f]
    except OSError:
        return []


def _apt_config_readme(
    *,
    bundle_dir: str,
    role: str,
    snapshot: Dict[str, Any],
    managed_files: List[Dict[str, Any]],
    managed_dirs: List[Dict[str, Any]],
    excluded: List[Dict[str, Any]],
    notes: List[Any],
) -> str:
    source_paths: List[str] = []
    keyring_paths: List[str] = []
    repo_hosts: Set[str] = set()
    url_re = re.compile(r"(?:https?|ftp)://([^/\s]+)", re.IGNORECASE)

    for mf in managed_files:
        path = str(mf.get("path") or "")
        src_rel = str(mf.get("src_rel") or "")
        if not path or not src_rel:
            continue
        if path == "/etc/apt/sources.list" or path.startswith(
            "/etc/apt/sources.list.d/"
        ):
            source_paths.append(path)
            for line in _read_artifact_lines(bundle_dir, role, src_rel):
                s = line.strip()
                if not s or s.startswith("#"):
                    continue
                for match in url_re.finditer(s):
                    repo_hosts.add(match.group(1))
        if (
            path.startswith("/etc/apt/trusted.gpg")
            or path.startswith("/etc/apt/keyrings/")
            or path.startswith("/usr/share/keyrings/")
        ):
            keyring_paths.append(path)

    return f"""# apt_config

APT configuration harvested from the system (sources, pinning, and keyrings).

## Repository hosts
{_markdown_list(sorted(repo_hosts))}

## Source files
{_markdown_list(sorted(set(source_paths)))}

## Keyrings
{_markdown_list(sorted(set(keyring_paths)))}

## Managed files
{_markdown_list(_managed_file_lines(managed_files, include_reason=True))}

## Excluded
{_markdown_list(_excluded_lines(excluded))}

## Notes
{_markdown_list([str(n) for n in notes])}
"""


def _dnf_config_readme(
    *,
    bundle_dir: str,
    role: str,
    snapshot: Dict[str, Any],
    managed_files: List[Dict[str, Any]],
    managed_dirs: List[Dict[str, Any]],
    excluded: List[Dict[str, Any]],
    notes: List[Any],
) -> str:
    repo_paths: List[str] = []
    key_paths: List[str] = []
    repo_hosts: Set[str] = set()
    url_re = re.compile(r"(?:https?|ftp)://([^/\s]+)", re.IGNORECASE)
    file_url_re = re.compile(r"file://(/[^\s]+)")

    for mf in managed_files:
        path = str(mf.get("path") or "")
        src_rel = str(mf.get("src_rel") or "")
        if not path or not src_rel:
            continue
        if path.startswith("/etc/yum.repos.d/") and path.endswith(".repo"):
            repo_paths.append(path)
            for line in _read_artifact_lines(bundle_dir, role, src_rel):
                s = line.strip()
                if not s or s.startswith("#") or s.startswith(";"):
                    continue
                for match in url_re.finditer(s):
                    repo_hosts.add(match.group(1))
                for match in file_url_re.finditer(s):
                    key_paths.append(match.group(1))
        if path.startswith("/etc/pki/rpm-gpg/"):
            key_paths.append(path)

    return f"""# dnf_config

DNF/YUM configuration harvested from the system (repos, config files, and RPM GPG keys).

## Repository hosts
{_markdown_list(sorted(repo_hosts))}

## Repo files
{_markdown_list(sorted(set(repo_paths)))}

## GPG keys
{_markdown_list(sorted(set(key_paths)))}

## Managed files
{_markdown_list(_managed_file_lines(managed_files, include_reason=True))}

## Excluded
{_markdown_list(_excluded_lines(excluded))}

## Notes
{_markdown_list([str(n) for n in notes])}
"""


def _simple_managed_files_readme(
    title: str,
    description: str,
    *,
    include_reason: bool,
) -> Callable[..., str]:
    def _builder(
        *,
        bundle_dir: str,
        role: str,
        snapshot: Dict[str, Any],
        managed_files: List[Dict[str, Any]],
        managed_dirs: List[Dict[str, Any]],
        excluded: List[Dict[str, Any]],
        notes: List[Any],
    ) -> str:
        return f"""# {title}

{description}

## Managed files
{_markdown_list(_managed_file_lines(managed_files, include_reason=include_reason))}

## Excluded
{_markdown_list(_excluded_lines(excluded))}

## Notes
{_markdown_list([str(n) for n in notes])}
"""

    return _builder


def _extra_paths_readme(
    *,
    bundle_dir: str,
    role: str,
    snapshot: Dict[str, Any],
    managed_files: List[Dict[str, Any]],
    managed_dirs: List[Dict[str, Any]],
    excluded: List[Dict[str, Any]],
    notes: List[Any],
) -> str:
    include_pats = snapshot.get("include_patterns", []) or []
    exclude_pats = snapshot.get("exclude_patterns", []) or []
    return f"""# {role}

User-requested extra file harvesting.

## Include patterns
{_markdown_list([str(p) for p in include_pats])}

## Exclude patterns
{_markdown_list([str(p) for p in exclude_pats])}

## Managed directories
{_markdown_list([str(d.get('path') or '') for d in managed_dirs])}

## Managed files
{_markdown_list(_managed_file_lines(managed_files, include_reason=False))}

## Excluded
{_markdown_list(_excluded_lines(excluded))}

## Notes
{_markdown_list([str(n) for n in notes])}
"""