Standardise more into CMModule parent class for the 3 child renderers
This commit is contained in:
parent
7379587a28
commit
899724097e
5 changed files with 1487 additions and 2251 deletions
2524
enroll/ansible.py
2524
enroll/ansible.py
File diff suppressed because it is too large
Load diff
490
enroll/cm.py
490
enroll/cm.py
|
|
@ -1,8 +1,19 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import shlex
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, Iterable, Iterator, List, Mapping, Set
|
from typing import (
|
||||||
|
Any,
|
||||||
|
Callable,
|
||||||
|
ClassVar,
|
||||||
|
Dict,
|
||||||
|
Iterable,
|
||||||
|
Iterator,
|
||||||
|
List,
|
||||||
|
Mapping,
|
||||||
|
Set,
|
||||||
|
)
|
||||||
|
|
||||||
from .state import load_state, state_path, write_state
|
from .state import load_state, state_path, write_state
|
||||||
|
|
||||||
|
|
@ -25,9 +36,18 @@ class CMModule:
|
||||||
files: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
files: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
||||||
links: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
links: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
||||||
services: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
services: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
||||||
|
firewall_runtime: Dict[str, Any] = field(default_factory=dict)
|
||||||
notes: List[str] = field(default_factory=list)
|
notes: List[str] = field(default_factory=list)
|
||||||
|
|
||||||
def has_resources(self) -> bool:
|
managed_owner_attr: ClassVar[str] = "owner"
|
||||||
|
firewall_runtime_dir: ClassVar[str] = "/etc/enroll/firewall"
|
||||||
|
firewall_runtime_artifacts: ClassVar[tuple[tuple[str, str, str], ...]] = (
|
||||||
|
("ipset_save", "ipset.save", "0600"),
|
||||||
|
("iptables_v4_save", "iptables.v4", "0600"),
|
||||||
|
("iptables_v6_save", "iptables.v6", "0600"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def has_core_resources(self) -> bool:
|
||||||
return bool(
|
return bool(
|
||||||
self.packages
|
self.packages
|
||||||
or self.groups
|
or self.groups
|
||||||
|
|
@ -36,9 +56,20 @@ class CMModule:
|
||||||
or self.files
|
or self.files
|
||||||
or self.links
|
or self.links
|
||||||
or self.services
|
or self.services
|
||||||
|
or self.firewall_runtime
|
||||||
or self.notes
|
or self.notes
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def has_resources(self) -> bool:
|
||||||
|
return self.has_core_resources()
|
||||||
|
|
||||||
|
def has_resources_or_attrs(self, *attrs: str) -> bool:
|
||||||
|
"""Return true if core resources or named renderer extras are present."""
|
||||||
|
|
||||||
|
return self.has_core_resources() or any(
|
||||||
|
bool(getattr(self, attr, None)) for attr in attrs
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def state_path(bundle_dir: str | Path) -> Path:
|
def state_path(bundle_dir: str | Path) -> Path:
|
||||||
"""Return the canonical state.json path for a harvest bundle."""
|
"""Return the canonical state.json path for a harvest bundle."""
|
||||||
|
|
@ -142,6 +173,412 @@ class CMModule:
|
||||||
def add_snapshot_notes(self, snap: Dict[str, Any]) -> None:
|
def add_snapshot_notes(self, snap: Dict[str, Any]) -> None:
|
||||||
self.notes.extend(str(n) for n in (snap.get("notes", []) or []))
|
self.notes.extend(str(n) for n in (snap.get("notes", []) or []))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def package_name_from_snapshot(snap: Dict[str, Any]) -> str:
|
||||||
|
return str(snap.get("package") or "").strip()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def package_names_from_snapshot(snap: Dict[str, Any]) -> Iterator[str]:
|
||||||
|
for pkg in snap.get("packages", []) or []:
|
||||||
|
pkg_s = str(pkg or "").strip()
|
||||||
|
if pkg_s:
|
||||||
|
yield pkg_s
|
||||||
|
|
||||||
|
def add_package_snapshot(self, snap: Dict[str, Any]) -> None:
|
||||||
|
pkg = self.package_name_from_snapshot(snap)
|
||||||
|
if pkg:
|
||||||
|
self.packages.add(pkg)
|
||||||
|
|
||||||
|
def add_service_packages_from_snapshot(self, snap: Dict[str, Any]) -> None:
|
||||||
|
self.packages.update(self.package_names_from_snapshot(snap))
|
||||||
|
|
||||||
|
def service_unit_from_snapshot(self, snap: Dict[str, Any]) -> str:
|
||||||
|
return str(snap.get("unit") or "").strip()
|
||||||
|
|
||||||
|
def service_enabled_from_snapshot(self, snap: Dict[str, Any]) -> bool:
|
||||||
|
unit_file_state = str(snap.get("unit_file_state") or "")
|
||||||
|
return unit_file_state in ("enabled", "enabled-runtime")
|
||||||
|
|
||||||
|
def service_state_from_snapshot(
|
||||||
|
self,
|
||||||
|
snap: Dict[str, Any],
|
||||||
|
*,
|
||||||
|
running: str,
|
||||||
|
stopped: str,
|
||||||
|
) -> str:
|
||||||
|
return running if snap.get("active_state") == "active" else stopped
|
||||||
|
|
||||||
|
def add_service_snapshot_state(
|
||||||
|
self,
|
||||||
|
snap: Dict[str, Any],
|
||||||
|
*,
|
||||||
|
state_key: str,
|
||||||
|
running: str,
|
||||||
|
stopped: str,
|
||||||
|
include_manage: bool = False,
|
||||||
|
) -> None:
|
||||||
|
"""Add the common systemd service parts, parameterised per renderer."""
|
||||||
|
|
||||||
|
self.add_service_packages_from_snapshot(snap)
|
||||||
|
unit = self.service_unit_from_snapshot(snap)
|
||||||
|
if not unit:
|
||||||
|
return
|
||||||
|
|
||||||
|
data: Dict[str, Any] = {
|
||||||
|
"name": unit,
|
||||||
|
state_key: self.service_state_from_snapshot(
|
||||||
|
snap, running=running, stopped=stopped
|
||||||
|
),
|
||||||
|
"enable": self.service_enabled_from_snapshot(snap),
|
||||||
|
}
|
||||||
|
if include_manage:
|
||||||
|
data["manage"] = True
|
||||||
|
self.services[unit] = data
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def normalise_flatpak_item(
|
||||||
|
item: Any,
|
||||||
|
*,
|
||||||
|
method: str,
|
||||||
|
user: str | None = None,
|
||||||
|
home: str | None = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
out = dict(item)
|
||||||
|
elif isinstance(item, str):
|
||||||
|
out = {"name": item}
|
||||||
|
else:
|
||||||
|
out = {"name": str(item)}
|
||||||
|
|
||||||
|
out["method"] = str(out.get("method") or method or "system").strip() or "system"
|
||||||
|
if user and not out.get("user"):
|
||||||
|
out["user"] = user
|
||||||
|
if home and not out.get("home"):
|
||||||
|
out["home"] = home
|
||||||
|
ref = str(out.get("ref") or "").strip()
|
||||||
|
if ref and not out.get("name"):
|
||||||
|
out["name"] = ref.rsplit("/", 1)[-1]
|
||||||
|
name = str(out.get("name") or out.get("app_id") or "").strip()
|
||||||
|
if name:
|
||||||
|
out["name"] = name
|
||||||
|
remote = str(out.get("remote") or "").strip()
|
||||||
|
if remote:
|
||||||
|
out["remote"] = remote
|
||||||
|
branch = str(out.get("branch") or out.get("origin") or "").strip()
|
||||||
|
if branch:
|
||||||
|
out["branch"] = branch
|
||||||
|
if ref:
|
||||||
|
out["ref"] = ref
|
||||||
|
return out
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def normalise_flatpak_remote(item: Any) -> Dict[str, Any]:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
out = dict(item)
|
||||||
|
else:
|
||||||
|
out = {"name": str(item)}
|
||||||
|
name = str(out.get("name") or out.get("remote") or "").strip()
|
||||||
|
url = str(out.get("url") or out.get("from_url") or "").strip()
|
||||||
|
method = (
|
||||||
|
str(out.get("method") or out.get("scope") or "system").strip() or "system"
|
||||||
|
)
|
||||||
|
if name:
|
||||||
|
out["name"] = name
|
||||||
|
if url:
|
||||||
|
out["url"] = url
|
||||||
|
out["method"] = "user" if method == "user" else "system"
|
||||||
|
return out
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def normalise_snap_item(item: Any) -> Dict[str, Any]:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
out = dict(item)
|
||||||
|
elif isinstance(item, str):
|
||||||
|
out = {"name": item}
|
||||||
|
else:
|
||||||
|
out = {"name": str(item)}
|
||||||
|
|
||||||
|
name = str(out.get("name") or "").strip()
|
||||||
|
if name:
|
||||||
|
out["name"] = name
|
||||||
|
channel = str(out.get("tracking") or out.get("channel") or "").strip()
|
||||||
|
if channel:
|
||||||
|
out["channel"] = channel
|
||||||
|
raw_notes = out.get("notes") or []
|
||||||
|
if isinstance(raw_notes, str):
|
||||||
|
raw_notes = [raw_notes]
|
||||||
|
notes = [str(note).lower() for note in raw_notes]
|
||||||
|
confinement = str(out.get("confinement") or "").strip().lower()
|
||||||
|
out["classic"] = bool(
|
||||||
|
out.get("classic")
|
||||||
|
or confinement == "classic"
|
||||||
|
or any("classic" in note for note in notes)
|
||||||
|
)
|
||||||
|
out["devmode"] = bool(
|
||||||
|
out.get("devmode")
|
||||||
|
or any("devmode" in note or "dev mode" in note for note in notes)
|
||||||
|
)
|
||||||
|
out["dangerous"] = bool(
|
||||||
|
out.get("dangerous") or any("dangerous" in note for note in notes)
|
||||||
|
)
|
||||||
|
revision = str(out.get("revision") or "").strip()
|
||||||
|
if revision and not channel:
|
||||||
|
out["revision"] = revision
|
||||||
|
return out
|
||||||
|
|
||||||
|
def prepare_flatpak_remote(self, item: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def prepare_flatpak_item(self, item: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def prepare_snap_item(self, item: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def user_records_from_snapshot(snap: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||||
|
records: List[Dict[str, Any]] = []
|
||||||
|
for raw in snap.get("users", []) or []:
|
||||||
|
if not isinstance(raw, dict):
|
||||||
|
continue
|
||||||
|
name = str(raw.get("name") or "").strip()
|
||||||
|
if not name:
|
||||||
|
continue
|
||||||
|
primary_group = str(raw.get("primary_group") or name).strip()
|
||||||
|
supplementary = sorted(
|
||||||
|
{
|
||||||
|
str(group).strip()
|
||||||
|
for group in (raw.get("supplementary_groups") or [])
|
||||||
|
if str(group).strip()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
records.append(
|
||||||
|
{
|
||||||
|
"name": name,
|
||||||
|
"uid": raw.get("uid"),
|
||||||
|
"gid": raw.get("gid"),
|
||||||
|
"primary_group": primary_group,
|
||||||
|
"home": raw.get("home") or f"/home/{name}",
|
||||||
|
"shell": raw.get("shell"),
|
||||||
|
"gecos": raw.get("gecos"),
|
||||||
|
"supplementary_groups": supplementary,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return records
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def user_group_names_from_records(records: Iterable[Mapping[str, Any]]) -> Set[str]:
|
||||||
|
groups: Set[str] = set()
|
||||||
|
for record in records:
|
||||||
|
primary_group = str(record.get("primary_group") or "").strip()
|
||||||
|
if primary_group:
|
||||||
|
groups.add(primary_group)
|
||||||
|
groups.update(
|
||||||
|
str(group).strip()
|
||||||
|
for group in (record.get("supplementary_groups") or [])
|
||||||
|
if str(group).strip()
|
||||||
|
)
|
||||||
|
return groups
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def package_service_entries(
|
||||||
|
roles: Mapping[str, Any],
|
||||||
|
inventory_packages: Mapping[str, Any],
|
||||||
|
*,
|
||||||
|
use_common_roles: bool,
|
||||||
|
) -> Iterator[Dict[str, Any]]:
|
||||||
|
for svc in roles.get("services", []) or []:
|
||||||
|
if not isinstance(svc, dict):
|
||||||
|
continue
|
||||||
|
own_label = str(svc.get("role_name") or svc.get("unit") or "service")
|
||||||
|
role_label = (
|
||||||
|
section_label_for_packages(
|
||||||
|
svc.get("packages", []) or [], inventory_packages
|
||||||
|
)
|
||||||
|
if use_common_roles
|
||||||
|
else own_label
|
||||||
|
)
|
||||||
|
yield {"kind": "service", "snapshot": svc, "role_label": role_label}
|
||||||
|
|
||||||
|
for pkg in roles.get("packages", []) or []:
|
||||||
|
if not isinstance(pkg, dict):
|
||||||
|
continue
|
||||||
|
own_label = str(pkg.get("role_name") or pkg.get("package") or "package")
|
||||||
|
role_label = (
|
||||||
|
package_section_label(pkg, inventory_packages)
|
||||||
|
if use_common_roles
|
||||||
|
else own_label
|
||||||
|
)
|
||||||
|
yield {"kind": "package", "snapshot": pkg, "role_label": role_label}
|
||||||
|
|
||||||
|
def add_user_flatpaks_snapshot(self, snap: Dict[str, Any]) -> None:
|
||||||
|
home_by_user = {
|
||||||
|
str(u.get("name")): str(u.get("home") or "")
|
||||||
|
for u in (snap.get("users", []) or [])
|
||||||
|
if isinstance(u, dict) and u.get("name")
|
||||||
|
}
|
||||||
|
for remote in snap.get("user_flatpak_remotes", []) or []:
|
||||||
|
item = self.normalise_flatpak_remote(remote)
|
||||||
|
user = str(item.get("user") or "").strip()
|
||||||
|
if user and not item.get("home"):
|
||||||
|
item["home"] = home_by_user.get(user) or f"/home/{user}"
|
||||||
|
if item.get("method") == "user" and item.get("name") and item.get("url"):
|
||||||
|
self.flatpak_remotes.append( # type: ignore[attr-defined]
|
||||||
|
self.prepare_flatpak_remote(item)
|
||||||
|
)
|
||||||
|
for uname, flatpaks in (snap.get("user_flatpaks", {}) or {}).items():
|
||||||
|
user = str(uname)
|
||||||
|
for fp in flatpaks or []:
|
||||||
|
item = self.normalise_flatpak_item(
|
||||||
|
fp, method="user", user=user, home=home_by_user.get(user) or None
|
||||||
|
)
|
||||||
|
if item.get("name"):
|
||||||
|
self.flatpaks.append( # type: ignore[attr-defined]
|
||||||
|
self.prepare_flatpak_item(item)
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_flatpak_snapshot(self, snap: Dict[str, Any]) -> None:
|
||||||
|
for remote in snap.get("remotes", []) or []:
|
||||||
|
item = self.normalise_flatpak_remote(remote)
|
||||||
|
if item.get("name") and item.get("url"):
|
||||||
|
self.flatpak_remotes.append( # type: ignore[attr-defined]
|
||||||
|
self.prepare_flatpak_remote(item)
|
||||||
|
)
|
||||||
|
for fp in snap.get("system_flatpaks", []) or []:
|
||||||
|
item = self.normalise_flatpak_item(fp, method="system")
|
||||||
|
if item.get("name"):
|
||||||
|
self.flatpaks.append( # type: ignore[attr-defined]
|
||||||
|
self.prepare_flatpak_item(item)
|
||||||
|
)
|
||||||
|
self.add_snapshot_notes(snap)
|
||||||
|
|
||||||
|
def add_snap_snapshot(self, snap: Dict[str, Any]) -> None:
|
||||||
|
for raw in snap.get("system_snaps", []) or []:
|
||||||
|
item = self.normalise_snap_item(raw)
|
||||||
|
if item.get("name"):
|
||||||
|
self.snaps.append( # type: ignore[attr-defined]
|
||||||
|
self.prepare_snap_item(item)
|
||||||
|
)
|
||||||
|
self.add_snapshot_notes(snap)
|
||||||
|
|
||||||
|
def firewall_runtime_snapshot_has_artifacts(self, snap: Mapping[str, Any]) -> bool:
|
||||||
|
return any(
|
||||||
|
str(snap.get(key) or "").strip()
|
||||||
|
for key, _dest, _mode in self.firewall_runtime_artifacts
|
||||||
|
)
|
||||||
|
|
||||||
|
def firewall_runtime_source_refs(self, snap: Mapping[str, Any]) -> Dict[str, str]:
|
||||||
|
return {
|
||||||
|
key: str(snap.get(key) or "").strip()
|
||||||
|
for key, _dest, _mode in self.firewall_runtime_artifacts
|
||||||
|
if str(snap.get(key) or "").strip()
|
||||||
|
}
|
||||||
|
|
||||||
|
def firewall_runtime_dest_path(self, dest_name: str) -> str:
|
||||||
|
return f"{self.firewall_runtime_dir}/{dest_name}"
|
||||||
|
|
||||||
|
def firewall_runtime_ipset_sets(self, snap: Mapping[str, Any]) -> List[str]:
|
||||||
|
return [
|
||||||
|
str(x).strip() for x in (snap.get("ipset_sets") or []) if str(x).strip()
|
||||||
|
]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def shell_quote(value: Any) -> str:
|
||||||
|
return shlex.quote(str(value or ""))
|
||||||
|
|
||||||
|
def firewall_ipset_restore_cmd(self, path: str, sets: List[str]) -> str:
|
||||||
|
flush_parts = [f"ipset flush {self.shell_quote(name)} || true" for name in sets]
|
||||||
|
flush = "; ".join(flush_parts)
|
||||||
|
restore = f"ipset restore -exist < {self.shell_quote(path)}"
|
||||||
|
if flush:
|
||||||
|
return f"/bin/sh -c {self.shell_quote(flush + '; ' + restore)}"
|
||||||
|
return f"/bin/sh -c {self.shell_quote(restore)}"
|
||||||
|
|
||||||
|
def firewall_runtime_commands(self, runtime: Mapping[str, Any]) -> Dict[str, Any]:
|
||||||
|
out: Dict[str, Any] = {}
|
||||||
|
ipset_path = str(runtime.get("ipset_save") or "")
|
||||||
|
if ipset_path:
|
||||||
|
sets = [str(x) for x in (runtime.get("ipset_sets") or []) if str(x)]
|
||||||
|
out["ipset_restore_cmd"] = self.firewall_ipset_restore_cmd(ipset_path, sets)
|
||||||
|
ipt4_path = str(runtime.get("iptables_v4_save") or "")
|
||||||
|
if ipt4_path:
|
||||||
|
out["iptables_v4_restore_cmd"] = (
|
||||||
|
f"iptables-restore {self.shell_quote(ipt4_path)}"
|
||||||
|
)
|
||||||
|
ipt6_path = str(runtime.get("iptables_v6_save") or "")
|
||||||
|
if ipt6_path:
|
||||||
|
out["iptables_v6_restore_cmd"] = (
|
||||||
|
f"ip6tables-restore {self.shell_quote(ipt6_path)}"
|
||||||
|
)
|
||||||
|
return out
|
||||||
|
|
||||||
|
def _managed_owner_attrs(self, owner: Any) -> Dict[str, Any]:
|
||||||
|
return {self.managed_owner_attr: owner or "root"}
|
||||||
|
|
||||||
|
def add_firewall_runtime_snapshot(
|
||||||
|
self,
|
||||||
|
snap: Dict[str, Any],
|
||||||
|
*,
|
||||||
|
bundle_dir: str,
|
||||||
|
artifact_role: str,
|
||||||
|
files_dir: Path,
|
||||||
|
copy_artifact: Callable[..., str | None],
|
||||||
|
source_uri: Callable[[str, str], str],
|
||||||
|
file_prefix: str | None = None,
|
||||||
|
dir_attrs: Mapping[str, Any] | None = None,
|
||||||
|
file_attrs: Mapping[str, Any] | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Add captured live firewall state using renderer-supplied file hooks."""
|
||||||
|
|
||||||
|
self.add_service_packages_from_snapshot(snap)
|
||||||
|
attrs: Dict[str, Any] = {
|
||||||
|
**self._managed_owner_attrs("root"),
|
||||||
|
"group": "root",
|
||||||
|
"mode": "0750",
|
||||||
|
"reason": "firewall_runtime",
|
||||||
|
}
|
||||||
|
if dir_attrs:
|
||||||
|
attrs.update(dir_attrs)
|
||||||
|
self.add_managed_dir(self.firewall_runtime_dir, **attrs)
|
||||||
|
|
||||||
|
runtime: Dict[str, Any] = {}
|
||||||
|
for key, dest_name, mode in self.firewall_runtime_artifacts:
|
||||||
|
src_rel = str(snap.get(key) or "").strip()
|
||||||
|
if not src_rel:
|
||||||
|
continue
|
||||||
|
role_rel = copy_artifact(
|
||||||
|
bundle_dir,
|
||||||
|
artifact_role,
|
||||||
|
src_rel,
|
||||||
|
files_dir,
|
||||||
|
dst_prefix=file_prefix,
|
||||||
|
)
|
||||||
|
if not role_rel:
|
||||||
|
self.notes.append(
|
||||||
|
f"Firewall runtime artifact {src_rel!r} was referenced but not found."
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
file_data: Dict[str, Any] = {
|
||||||
|
**self._managed_owner_attrs("root"),
|
||||||
|
"group": "root",
|
||||||
|
"mode": mode,
|
||||||
|
"source": source_uri(self.module_name, role_rel),
|
||||||
|
"reason": "firewall_runtime",
|
||||||
|
}
|
||||||
|
if file_attrs:
|
||||||
|
file_data.update(file_attrs)
|
||||||
|
dest = self.firewall_runtime_dest_path(dest_name)
|
||||||
|
self.add_managed_file(dest, **file_data)
|
||||||
|
runtime[key] = dest
|
||||||
|
|
||||||
|
ipset_sets = self.firewall_runtime_ipset_sets(snap)
|
||||||
|
if ipset_sets:
|
||||||
|
runtime["ipset_sets"] = ipset_sets
|
||||||
|
if runtime:
|
||||||
|
runtime.update(self.firewall_runtime_commands(runtime))
|
||||||
|
self.firewall_runtime.update(runtime)
|
||||||
|
self.add_snapshot_notes(snap)
|
||||||
|
|
||||||
def remove_directory_resource_conflicts(self) -> None:
|
def remove_directory_resource_conflicts(self) -> None:
|
||||||
for path in set(self.files) | set(self.links):
|
for path in set(self.files) | set(self.links):
|
||||||
self.dirs.pop(path, None)
|
self.dirs.pop(path, None)
|
||||||
|
|
@ -204,6 +641,55 @@ def role_order_key(role: str) -> tuple[int, str]:
|
||||||
return (priority.get(role, 50), role)
|
return (priority.get(role, 50), role)
|
||||||
|
|
||||||
|
|
||||||
|
def markdown_list(items: Iterable[Any], *, empty: str = "None.") -> str:
|
||||||
|
values = [str(item) for item in items if str(item)]
|
||||||
|
return "\n".join(f"- {item}" for item in values) or f"- {empty}"
|
||||||
|
|
||||||
|
|
||||||
|
def path_reason_lines(
|
||||||
|
items: Iterable[Mapping[str, Any]], *, source_key: str = "path"
|
||||||
|
) -> List[str]:
|
||||||
|
lines: List[str] = []
|
||||||
|
for item in items or []:
|
||||||
|
path = str(item.get(source_key) or "")
|
||||||
|
if not path:
|
||||||
|
continue
|
||||||
|
reason = str(item.get("reason") or "")
|
||||||
|
lines.append(f"{path} ({reason})" if reason else path)
|
||||||
|
return lines
|
||||||
|
|
||||||
|
|
||||||
|
def iter_role_snapshots(roles: Mapping[str, Any]) -> Iterator[Mapping[str, Any]]:
|
||||||
|
for value in roles.values():
|
||||||
|
if isinstance(value, list):
|
||||||
|
for item in value:
|
||||||
|
if isinstance(item, Mapping):
|
||||||
|
yield item
|
||||||
|
elif isinstance(value, Mapping):
|
||||||
|
yield value
|
||||||
|
|
||||||
|
|
||||||
|
def snapshot_note_lines(roles: Mapping[str, Any]) -> List[str]:
|
||||||
|
notes: List[str] = []
|
||||||
|
for snap in iter_role_snapshots(roles):
|
||||||
|
source = str(
|
||||||
|
snap.get("role_name") or snap.get("unit") or snap.get("package") or "role"
|
||||||
|
)
|
||||||
|
notes.extend(f"`{source}`: {note}" for note in snap.get("notes", []) or [])
|
||||||
|
return notes
|
||||||
|
|
||||||
|
|
||||||
|
def snapshot_excluded_lines(roles: Mapping[str, Any]) -> List[str]:
|
||||||
|
excluded: List[str] = []
|
||||||
|
for snap in iter_role_snapshots(roles):
|
||||||
|
source = str(
|
||||||
|
snap.get("role_name") or snap.get("unit") or snap.get("package") or "role"
|
||||||
|
)
|
||||||
|
for line in path_reason_lines(snap.get("excluded", []) or []):
|
||||||
|
excluded.append(f"`{source}`: {line}")
|
||||||
|
return excluded
|
||||||
|
|
||||||
|
|
||||||
def _drop_duplicate_set_items(
|
def _drop_duplicate_set_items(
|
||||||
module: CMModule,
|
module: CMModule,
|
||||||
values: Set[str],
|
values: Set[str],
|
||||||
|
|
|
||||||
358
enroll/puppet.py
358
enroll/puppet.py
|
|
@ -12,10 +12,9 @@ import yaml
|
||||||
|
|
||||||
from .cm import (
|
from .cm import (
|
||||||
CMModule,
|
CMModule,
|
||||||
package_section_label,
|
|
||||||
resolve_catalog_conflicts,
|
resolve_catalog_conflicts,
|
||||||
role_order_key,
|
role_order_key,
|
||||||
section_label_for_packages,
|
markdown_list,
|
||||||
)
|
)
|
||||||
from .state import inventory_packages_from_state, roles_from_state
|
from .state import inventory_packages_from_state, roles_from_state
|
||||||
|
|
||||||
|
|
@ -32,108 +31,43 @@ class PuppetRole(CMModule):
|
||||||
self.flatpak_remotes: List[Dict[str, Any]] = []
|
self.flatpak_remotes: List[Dict[str, Any]] = []
|
||||||
self.flatpaks: List[Dict[str, Any]] = []
|
self.flatpaks: List[Dict[str, Any]] = []
|
||||||
self.snaps: List[Dict[str, Any]] = []
|
self.snaps: List[Dict[str, Any]] = []
|
||||||
self.firewall_runtime: Dict[str, Any] = {}
|
|
||||||
|
|
||||||
def has_resources(self) -> bool:
|
def has_resources(self) -> bool:
|
||||||
return (
|
return self.has_resources_or_attrs(
|
||||||
super().has_resources()
|
"container_images", "flatpak_remotes", "flatpaks", "snaps"
|
||||||
or bool(self.container_images)
|
|
||||||
or bool(self.flatpak_remotes)
|
|
||||||
or bool(self.flatpaks)
|
|
||||||
or bool(self.snaps)
|
|
||||||
or bool(self.firewall_runtime)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def add_package_snapshot(self, snap: Dict[str, Any]) -> None:
|
|
||||||
pkg = str(snap.get("package") or "").strip()
|
|
||||||
if pkg:
|
|
||||||
self.packages.add(pkg)
|
|
||||||
|
|
||||||
def add_service_snapshot(self, snap: Dict[str, Any]) -> None:
|
def add_service_snapshot(self, snap: Dict[str, Any]) -> None:
|
||||||
for pkg in snap.get("packages", []) or []:
|
self.add_service_snapshot_state(
|
||||||
pkg_s = str(pkg or "").strip()
|
snap, state_key="ensure", running="running", stopped="stopped"
|
||||||
if pkg_s:
|
)
|
||||||
self.packages.add(pkg_s)
|
|
||||||
unit = str(snap.get("unit") or "").strip()
|
|
||||||
if unit:
|
|
||||||
unit_file_state = str(snap.get("unit_file_state") or "")
|
|
||||||
self.services[unit] = {
|
|
||||||
"name": unit,
|
|
||||||
"ensure": (
|
|
||||||
"running" if snap.get("active_state") == "active" else "stopped"
|
|
||||||
),
|
|
||||||
"enable": unit_file_state in ("enabled", "enabled-runtime"),
|
|
||||||
}
|
|
||||||
|
|
||||||
def add_users_snapshot(self, snap: Dict[str, Any]) -> None:
|
def add_users_snapshot(self, snap: Dict[str, Any]) -> None:
|
||||||
for u in snap.get("users", []) or []:
|
records = self.user_records_from_snapshot(snap)
|
||||||
if not isinstance(u, dict):
|
self.groups.update(self.user_group_names_from_records(records))
|
||||||
continue
|
for record in records:
|
||||||
name = str(u.get("name") or "").strip()
|
name = str(record.get("name") or "")
|
||||||
if not name:
|
|
||||||
continue
|
|
||||||
primary_group = str(u.get("primary_group") or name).strip()
|
|
||||||
if primary_group:
|
|
||||||
self.groups.add(primary_group)
|
|
||||||
supplementary = sorted(
|
|
||||||
{
|
|
||||||
str(g).strip()
|
|
||||||
for g in (u.get("supplementary_groups") or [])
|
|
||||||
if str(g).strip()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
self.groups.update(supplementary)
|
|
||||||
self.users[name] = {
|
self.users[name] = {
|
||||||
"name": name,
|
"name": name,
|
||||||
"uid": u.get("uid"),
|
"uid": record.get("uid"),
|
||||||
"gid": u.get("gid"),
|
"gid": record.get("gid"),
|
||||||
"primary_group": primary_group or None,
|
"primary_group": record.get("primary_group") or None,
|
||||||
"home": u.get("home") or f"/home/{name}",
|
"home": record.get("home"),
|
||||||
"shell": u.get("shell"),
|
"shell": record.get("shell"),
|
||||||
"gecos": u.get("gecos"),
|
"gecos": record.get("gecos"),
|
||||||
"supplementary_groups": supplementary,
|
"supplementary_groups": record.get("supplementary_groups") or [],
|
||||||
}
|
}
|
||||||
|
|
||||||
home_by_user = {
|
self.add_user_flatpaks_snapshot(snap)
|
||||||
str(u.get("name")): str(u.get("home") or "")
|
|
||||||
for u in (snap.get("users", []) or [])
|
|
||||||
if isinstance(u, dict) and u.get("name")
|
|
||||||
}
|
|
||||||
for remote in snap.get("user_flatpak_remotes", []) or []:
|
|
||||||
item = _normalise_flatpak_remote(remote)
|
|
||||||
user = str(item.get("user") or "").strip()
|
|
||||||
if user and not item.get("home"):
|
|
||||||
item["home"] = home_by_user.get(user) or f"/home/{user}"
|
|
||||||
if item.get("method") == "user" and item.get("name") and item.get("url"):
|
|
||||||
self.flatpak_remotes.append(_prepare_flatpak_remote(item))
|
|
||||||
for uname, flatpaks in (snap.get("user_flatpaks", {}) or {}).items():
|
|
||||||
user = str(uname)
|
|
||||||
for fp in flatpaks or []:
|
|
||||||
item = _normalise_flatpak_item(
|
|
||||||
fp, method="user", user=user, home=home_by_user.get(user) or None
|
|
||||||
)
|
|
||||||
if item.get("name"):
|
|
||||||
self.flatpaks.append(_prepare_flatpak_item(item))
|
|
||||||
|
|
||||||
def add_flatpak_snapshot(self, snap: Dict[str, Any]) -> None:
|
def prepare_flatpak_remote(self, item: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
for remote in snap.get("remotes", []) or []:
|
return _prepare_flatpak_remote(item)
|
||||||
item = _normalise_flatpak_remote(remote)
|
|
||||||
if item.get("name") and item.get("url"):
|
|
||||||
self.flatpak_remotes.append(_prepare_flatpak_remote(item))
|
|
||||||
for fp in snap.get("system_flatpaks", []) or []:
|
|
||||||
item = _normalise_flatpak_item(fp, method="system")
|
|
||||||
if item.get("name"):
|
|
||||||
self.flatpaks.append(_prepare_flatpak_item(item))
|
|
||||||
for note in snap.get("notes", []) or []:
|
|
||||||
self.notes.append(str(note))
|
|
||||||
|
|
||||||
def add_snap_snapshot(self, snap: Dict[str, Any]) -> None:
|
def prepare_flatpak_item(self, item: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
for raw in snap.get("system_snaps", []) or []:
|
return _prepare_flatpak_item(item)
|
||||||
item = _normalise_snap_item(raw)
|
|
||||||
if item.get("name"):
|
def prepare_snap_item(self, item: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
self.snaps.append(_prepare_snap_item(item))
|
return _prepare_snap_item(item)
|
||||||
for note in snap.get("notes", []) or []:
|
|
||||||
self.notes.append(str(note))
|
|
||||||
|
|
||||||
def add_firewall_runtime_snapshot(
|
def add_firewall_runtime_snapshot(
|
||||||
self,
|
self,
|
||||||
|
|
@ -144,58 +78,16 @@ class PuppetRole(CMModule):
|
||||||
module_files_dir: Path,
|
module_files_dir: Path,
|
||||||
file_prefix: Optional[str] = None,
|
file_prefix: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.packages.update(
|
super().add_firewall_runtime_snapshot(
|
||||||
str(p).strip() for p in (snap.get("packages") or []) if str(p).strip()
|
snap,
|
||||||
|
bundle_dir=bundle_dir,
|
||||||
|
artifact_role=artifact_role,
|
||||||
|
files_dir=module_files_dir,
|
||||||
|
copy_artifact=_copy_artifact,
|
||||||
|
source_uri=_source_uri,
|
||||||
|
file_prefix=file_prefix,
|
||||||
|
dir_attrs={"require": "File['/etc/enroll']"},
|
||||||
)
|
)
|
||||||
self.add_managed_dir(
|
|
||||||
"/etc/enroll/firewall",
|
|
||||||
owner="root",
|
|
||||||
group="root",
|
|
||||||
mode="0750",
|
|
||||||
require="File['/etc/enroll']",
|
|
||||||
reason="firewall_runtime",
|
|
||||||
)
|
|
||||||
runtime: Dict[str, Any] = {}
|
|
||||||
for key, dest_name, mode in (
|
|
||||||
("ipset_save", "ipset.save", "0600"),
|
|
||||||
("iptables_v4_save", "iptables.v4", "0600"),
|
|
||||||
("iptables_v6_save", "iptables.v6", "0600"),
|
|
||||||
):
|
|
||||||
src_rel = str(snap.get(key) or "").strip()
|
|
||||||
if not src_rel:
|
|
||||||
continue
|
|
||||||
role_rel = _copy_artifact(
|
|
||||||
bundle_dir,
|
|
||||||
artifact_role,
|
|
||||||
src_rel,
|
|
||||||
module_files_dir,
|
|
||||||
dst_prefix=file_prefix,
|
|
||||||
)
|
|
||||||
if not role_rel:
|
|
||||||
self.notes.append(
|
|
||||||
f"Firewall runtime artifact {src_rel!r} was referenced but not found."
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
dest = f"/etc/enroll/firewall/{dest_name}"
|
|
||||||
self.add_managed_file(
|
|
||||||
dest,
|
|
||||||
owner="root",
|
|
||||||
group="root",
|
|
||||||
mode=mode,
|
|
||||||
source=_source_uri(self.module_name, role_rel),
|
|
||||||
reason="firewall_runtime",
|
|
||||||
)
|
|
||||||
runtime[key] = dest
|
|
||||||
ipset_sets = [
|
|
||||||
str(x).strip() for x in (snap.get("ipset_sets") or []) if str(x).strip()
|
|
||||||
]
|
|
||||||
if ipset_sets:
|
|
||||||
runtime["ipset_sets"] = ipset_sets
|
|
||||||
if runtime:
|
|
||||||
runtime.update(_firewall_runtime_commands(runtime))
|
|
||||||
self.firewall_runtime.update(runtime)
|
|
||||||
for note in snap.get("notes", []) or []:
|
|
||||||
self.notes.append(str(note))
|
|
||||||
|
|
||||||
def add_container_images_snapshot(self, snap: Dict[str, Any]) -> None:
|
def add_container_images_snapshot(self, snap: Dict[str, Any]) -> None:
|
||||||
for raw in snap.get("images", []) or []:
|
for raw in snap.get("images", []) or []:
|
||||||
|
|
@ -374,70 +266,6 @@ def _container_tag_cmd(engine: str, pull_ref: str, tag_ref: str) -> str:
|
||||||
return f"{engine} tag {_shell_quote(pull_ref)} {_shell_quote(tag_ref)}"
|
return f"{engine} tag {_shell_quote(pull_ref)} {_shell_quote(tag_ref)}"
|
||||||
|
|
||||||
|
|
||||||
def _normalise_flatpak_item(
|
|
||||||
item: Dict[str, Any],
|
|
||||||
*,
|
|
||||||
method: str,
|
|
||||||
user: Optional[str] = None,
|
|
||||||
home: Optional[str] = None,
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
out = dict(item)
|
|
||||||
out["method"] = str(out.get("method") or method or "system").strip() or "system"
|
|
||||||
if user and not out.get("user"):
|
|
||||||
out["user"] = user
|
|
||||||
if home and not out.get("home"):
|
|
||||||
out["home"] = home
|
|
||||||
ref = str(out.get("ref") or "").strip()
|
|
||||||
if ref and not out.get("name"):
|
|
||||||
out["name"] = ref.rsplit("/", 1)[-1]
|
|
||||||
name = str(out.get("name") or out.get("app_id") or "").strip()
|
|
||||||
if name:
|
|
||||||
out["name"] = name
|
|
||||||
remote = str(out.get("remote") or "").strip()
|
|
||||||
if remote:
|
|
||||||
out["remote"] = remote
|
|
||||||
branch = str(out.get("branch") or out.get("origin") or "").strip()
|
|
||||||
if branch:
|
|
||||||
out["branch"] = branch
|
|
||||||
if ref:
|
|
||||||
out["ref"] = ref
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def _normalise_flatpak_remote(item: Dict[str, Any]) -> Dict[str, Any]:
|
|
||||||
out = dict(item)
|
|
||||||
name = str(out.get("name") or out.get("remote") or "").strip()
|
|
||||||
url = str(out.get("url") or out.get("from_url") or "").strip()
|
|
||||||
method = str(out.get("method") or out.get("scope") or "system").strip() or "system"
|
|
||||||
if name:
|
|
||||||
out["name"] = name
|
|
||||||
if url:
|
|
||||||
out["url"] = url
|
|
||||||
out["method"] = "user" if method == "user" else "system"
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def _normalise_snap_item(item: Dict[str, Any]) -> Dict[str, Any]:
|
|
||||||
out = dict(item)
|
|
||||||
name = str(out.get("name") or "").strip()
|
|
||||||
if name:
|
|
||||||
out["name"] = name
|
|
||||||
channel = str(out.get("tracking") or out.get("channel") or "").strip()
|
|
||||||
if channel:
|
|
||||||
out["channel"] = channel
|
|
||||||
notes = [str(note).lower() for note in (out.get("notes") or [])]
|
|
||||||
confinement = str(out.get("confinement") or "").strip().lower()
|
|
||||||
out["classic"] = confinement == "classic" or any(
|
|
||||||
"classic" in note for note in notes
|
|
||||||
)
|
|
||||||
out["devmode"] = any("devmode" in note or "dev mode" in note for note in notes)
|
|
||||||
out["dangerous"] = any("dangerous" in note for note in notes)
|
|
||||||
revision = str(out.get("revision") or "").strip()
|
|
||||||
if revision and not channel:
|
|
||||||
out["revision"] = revision
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def _flatpak_scope(item: Dict[str, Any]) -> str:
|
def _flatpak_scope(item: Dict[str, Any]) -> str:
|
||||||
return "--user" if str(item.get("method") or "system") == "user" else "--system"
|
return "--user" if str(item.get("method") or "system") == "user" else "--system"
|
||||||
|
|
||||||
|
|
@ -596,30 +424,6 @@ def _state_title(prefix: str, value: Any) -> str:
|
||||||
return f"enroll-{prefix}-{safe}"
|
return f"enroll-{prefix}-{safe}"
|
||||||
|
|
||||||
|
|
||||||
def _firewall_ipset_restore_cmd(path: str, sets: List[str]) -> str:
|
|
||||||
flush_parts = [f"ipset flush {_shell_quote(name)} || true" for name in sets]
|
|
||||||
flush = "; ".join(flush_parts)
|
|
||||||
restore = f"ipset restore -exist < {_shell_quote(path)}"
|
|
||||||
if flush:
|
|
||||||
return f"/bin/sh -c {_shell_quote(flush + '; ' + restore)}"
|
|
||||||
return f"/bin/sh -c {_shell_quote(restore)}"
|
|
||||||
|
|
||||||
|
|
||||||
def _firewall_runtime_commands(runtime: Dict[str, Any]) -> Dict[str, Any]:
|
|
||||||
out: Dict[str, Any] = {}
|
|
||||||
ipset_path = str(runtime.get("ipset_save") or "")
|
|
||||||
if ipset_path:
|
|
||||||
sets = [str(x) for x in (runtime.get("ipset_sets") or []) if str(x)]
|
|
||||||
out["ipset_restore_cmd"] = _firewall_ipset_restore_cmd(ipset_path, sets)
|
|
||||||
ipt4_path = str(runtime.get("iptables_v4_save") or "")
|
|
||||||
if ipt4_path:
|
|
||||||
out["iptables_v4_restore_cmd"] = f"iptables-restore {_shell_quote(ipt4_path)}"
|
|
||||||
ipt6_path = str(runtime.get("iptables_v6_save") or "")
|
|
||||||
if ipt6_path:
|
|
||||||
out["iptables_v6_restore_cmd"] = f"ip6tables-restore {_shell_quote(ipt6_path)}"
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def _render_firewall_runtime_execs(
|
def _render_firewall_runtime_execs(
|
||||||
lines: List[str], runtime: Dict[str, Any], *, indent: str = " "
|
lines: List[str], runtime: Dict[str, Any], *, indent: str = " "
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
@ -757,57 +561,29 @@ def _collect_puppet_roles(
|
||||||
file_prefix=node_file_prefix,
|
file_prefix=node_file_prefix,
|
||||||
)
|
)
|
||||||
|
|
||||||
for svc in roles.get("services", []) or []:
|
for entry in CMModule.package_service_entries(
|
||||||
if not isinstance(svc, dict):
|
roles, inventory_packages, use_common_roles=use_common_modules
|
||||||
continue
|
):
|
||||||
original_role_name = _puppet_name(
|
snap = entry.get("snapshot") or {}
|
||||||
str(svc.get("role_name") or svc.get("unit") or "service"),
|
kind = str(entry.get("kind") or "package")
|
||||||
fallback="service",
|
fallback = "service" if kind == "service" else "package"
|
||||||
|
source_label = str(
|
||||||
|
snap.get("role_name") or snap.get("unit") or snap.get("package") or fallback
|
||||||
|
)
|
||||||
|
original_role_name = _puppet_name(source_label, fallback=fallback)
|
||||||
|
role_name = _puppet_name(
|
||||||
|
str(entry.get("role_label") or source_label),
|
||||||
|
fallback="package_group" if use_common_modules else fallback,
|
||||||
)
|
)
|
||||||
if use_common_modules:
|
|
||||||
role_name = _puppet_name(
|
|
||||||
section_label_for_packages(
|
|
||||||
[
|
|
||||||
str(p).strip()
|
|
||||||
for p in (svc.get("packages") or [])
|
|
||||||
if str(p).strip()
|
|
||||||
],
|
|
||||||
inventory_packages,
|
|
||||||
),
|
|
||||||
fallback="package_group",
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
role_name = original_role_name
|
|
||||||
prole = ensure_role(role_name)
|
prole = ensure_role(role_name)
|
||||||
prole.add_service_snapshot(svc)
|
if kind == "service":
|
||||||
prole.add_managed_content(
|
prole.add_service_snapshot(snap)
|
||||||
svc,
|
|
||||||
bundle_dir=bundle_dir,
|
|
||||||
artifact_role=str(svc.get("role_name") or original_role_name),
|
|
||||||
module_files_dir=modules_dir / prole.module_name / "files",
|
|
||||||
file_prefix=node_file_prefix,
|
|
||||||
)
|
|
||||||
|
|
||||||
for pkg in roles.get("packages", []) or []:
|
|
||||||
if not isinstance(pkg, dict):
|
|
||||||
continue
|
|
||||||
original_role_name = _puppet_name(
|
|
||||||
str(pkg.get("role_name") or pkg.get("package") or "package"),
|
|
||||||
fallback="package",
|
|
||||||
)
|
|
||||||
if use_common_modules:
|
|
||||||
role_name = _puppet_name(
|
|
||||||
package_section_label(pkg, inventory_packages),
|
|
||||||
fallback="package_group",
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
role_name = original_role_name
|
prole.add_package_snapshot(snap)
|
||||||
prole = ensure_role(role_name)
|
|
||||||
prole.add_package_snapshot(pkg)
|
|
||||||
prole.add_managed_content(
|
prole.add_managed_content(
|
||||||
pkg,
|
snap,
|
||||||
bundle_dir=bundle_dir,
|
bundle_dir=bundle_dir,
|
||||||
artifact_role=str(pkg.get("role_name") or original_role_name),
|
artifact_role=str(snap.get("role_name") or original_role_name),
|
||||||
module_files_dir=modules_dir / prole.module_name / "files",
|
module_files_dir=modules_dir / prole.module_name / "files",
|
||||||
file_prefix=node_file_prefix,
|
file_prefix=node_file_prefix,
|
||||||
)
|
)
|
||||||
|
|
@ -1577,19 +1353,13 @@ def _render_readme(
|
||||||
host = state.get("host", {}) if isinstance(state.get("host"), dict) else {}
|
host = state.get("host", {}) if isinstance(state.get("host"), dict) else {}
|
||||||
hostname = host.get("hostname") or "unknown"
|
hostname = host.get("hostname") or "unknown"
|
||||||
hiera_mode = bool(fqdn)
|
hiera_mode = bool(fqdn)
|
||||||
role_lines = (
|
role_lines = markdown_list(
|
||||||
"\n".join(
|
f"`{r.module_name}` from Enroll role `{r.role_name}`" for r in puppet_roles
|
||||||
f"- `{r.module_name}` from Enroll role `{r.role_name}`"
|
)
|
||||||
for r in puppet_roles
|
node_lines = markdown_list(f"`{n}`" for n in (node_names or []))
|
||||||
)
|
notes_text = markdown_list(
|
||||||
or "- None."
|
f"`{r.module_name}`: {note}" for r in puppet_roles for note in r.notes
|
||||||
)
|
)
|
||||||
node_lines = "\n".join(f"- `{n}`" for n in (node_names or [])) or "- None."
|
|
||||||
notes: List[str] = []
|
|
||||||
for r in puppet_roles:
|
|
||||||
for note in r.notes:
|
|
||||||
notes.append(f"`{r.module_name}`: {note}")
|
|
||||||
notes_text = "\n".join(f"- {n}" for n in notes) or "- None."
|
|
||||||
if hiera_mode:
|
if hiera_mode:
|
||||||
layout = f"""- `manifests/site.pp` declares node blocks and includes classes listed in Hiera key `enroll::classes`.
|
layout = f"""- `manifests/site.pp` declares node blocks and includes classes listed in Hiera key `enroll::classes`.
|
||||||
- `hiera.yaml` configures per-node lookup from `data/nodes/%{{trusted.certname}}.yaml` with a fallback to `data/common.yaml`.
|
- `hiera.yaml` configures per-node lookup from `data/nodes/%{{trusted.certname}}.yaml` with a fallback to `data/common.yaml`.
|
||||||
|
|
@ -1599,11 +1369,10 @@ def _render_readme(
|
||||||
apply = f"""Run from this generated output directory, passing the node certname so Hiera selects the right node data:
|
apply = f"""Run from this generated output directory, passing the node certname so Hiera selects the right node data:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo puppet apply --modulepath ./modules --hiera_config ./hiera.yaml --certname {fqdn} manifests/site.pp --noop
|
sudo puppet apply --modulepath ./modules --hiera_config ./hiera.yaml --certname {fqdn} manifests/site.pp --noop --test
|
||||||
```
|
```
|
||||||
|
|
||||||
If you depend on other pre-installed Puppet modules (such as for supporting Docker image version enforcement, which Enroll may
|
If you depend on other pre-installed Puppet modules, you may need to pass in other modulepaths as well, e.g:
|
||||||
have harvested information on), you may need to pass in other modulepaths as well, e.g:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo puppet apply --modulepath ./modules:/etc/puppet/code/modules --hiera_config ./hiera.yaml --certname {fqdn} manifests/site.pp --noop
|
sudo puppet apply --modulepath ./modules:/etc/puppet/code/modules --hiera_config ./hiera.yaml --certname {fqdn} manifests/site.pp --noop
|
||||||
|
|
@ -1618,11 +1387,10 @@ For Puppet agent/control-repo use, place this output where `hiera.yaml`, `data/`
|
||||||
apply = """Run from this generated output directory so Puppet can find `./modules`, or pass an absolute module path:
|
apply = """Run from this generated output directory so Puppet can find `./modules`, or pass an absolute module path:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo puppet apply --modulepath ./modules manifests/site.pp --noop
|
sudo puppet apply --modulepath ./modules manifests/site.pp --noop --test
|
||||||
```
|
```
|
||||||
|
|
||||||
If you depend on other pre-installed Puppet modules (such as for supporting Docker image version enforcement, which Enroll may
|
If you depend on other pre-installed Puppet modules, you may need to pass in other modulepaths as well, e.g:
|
||||||
have harvested information on), you may need to pass in other modulepaths as well, e.g:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo puppet apply --modulepath ./modules:/etc/puppet/code/modules manifests/site.pp --noop
|
sudo puppet apply --modulepath ./modules:/etc/puppet/code/modules manifests/site.pp --noop
|
||||||
|
|
@ -1661,7 +1429,7 @@ This Puppet target reuses the existing harvest state without changing harvesting
|
||||||
|
|
||||||
## Current limitations
|
## Current limitations
|
||||||
|
|
||||||
- JinjaTurtle templating is currently Ansible-oriented and is not applied to Puppet output.
|
- JinjaTurtle templating is currently Ansible/Salt-oriented and is not applied to Puppet output - there are no erb templates, just raw files.
|
||||||
- Review generated resources before applying them broadly across unlike hosts.
|
- Review generated resources before applying them broadly across unlike hosts.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
|
||||||
344
enroll/salt.py
344
enroll/salt.py
|
|
@ -12,10 +12,9 @@ import yaml
|
||||||
|
|
||||||
from .cm import (
|
from .cm import (
|
||||||
CMModule,
|
CMModule,
|
||||||
package_section_label,
|
|
||||||
resolve_catalog_conflicts,
|
resolve_catalog_conflicts,
|
||||||
role_order_key,
|
role_order_key,
|
||||||
section_label_for_packages,
|
markdown_list,
|
||||||
)
|
)
|
||||||
from .jinjaturtle import jinjify_artifact, resolve_jinjaturtle_mode
|
from .jinjaturtle import jinjify_artifact, resolve_jinjaturtle_mode
|
||||||
from .state import inventory_packages_from_state, roles_from_state
|
from .state import inventory_packages_from_state, roles_from_state
|
||||||
|
|
@ -25,6 +24,8 @@ from .yamlutil import yaml_dump_mapping, yaml_load_mapping_file
|
||||||
class SaltRole(CMModule):
|
class SaltRole(CMModule):
|
||||||
"""Salt-specific view of a renderer-neutral CMModule."""
|
"""Salt-specific view of a renderer-neutral CMModule."""
|
||||||
|
|
||||||
|
managed_owner_attr = "user"
|
||||||
|
|
||||||
def __init__(self, role_name: str) -> None:
|
def __init__(self, role_name: str) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
role_name=role_name,
|
role_name=role_name,
|
||||||
|
|
@ -34,110 +35,47 @@ class SaltRole(CMModule):
|
||||||
self.flatpak_remotes: List[Dict[str, Any]] = []
|
self.flatpak_remotes: List[Dict[str, Any]] = []
|
||||||
self.flatpaks: List[Dict[str, Any]] = []
|
self.flatpaks: List[Dict[str, Any]] = []
|
||||||
self.snaps: List[Dict[str, Any]] = []
|
self.snaps: List[Dict[str, Any]] = []
|
||||||
self.firewall_runtime: Dict[str, Any] = {}
|
|
||||||
|
|
||||||
def has_resources(self) -> bool:
|
def has_resources(self) -> bool:
|
||||||
return (
|
return self.has_resources_or_attrs(
|
||||||
super().has_resources()
|
"container_images", "flatpak_remotes", "flatpaks", "snaps"
|
||||||
or bool(self.container_images)
|
|
||||||
or bool(self.flatpak_remotes)
|
|
||||||
or bool(self.flatpaks)
|
|
||||||
or bool(self.snaps)
|
|
||||||
or bool(self.firewall_runtime)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sls_name(self) -> str:
|
def sls_name(self) -> str:
|
||||||
return f"roles.{self.module_name}"
|
return f"roles.{self.module_name}"
|
||||||
|
|
||||||
def add_package_snapshot(self, snap: Dict[str, Any]) -> None:
|
|
||||||
pkg = str(snap.get("package") or "").strip()
|
|
||||||
if pkg:
|
|
||||||
self.packages.add(pkg)
|
|
||||||
|
|
||||||
def add_service_snapshot(self, snap: Dict[str, Any]) -> None:
|
def add_service_snapshot(self, snap: Dict[str, Any]) -> None:
|
||||||
for pkg in snap.get("packages", []) or []:
|
self.add_service_snapshot_state(
|
||||||
pkg_s = str(pkg or "").strip()
|
snap, state_key="state", running="running", stopped="dead"
|
||||||
if pkg_s:
|
)
|
||||||
self.packages.add(pkg_s)
|
|
||||||
unit = str(snap.get("unit") or "").strip()
|
|
||||||
if unit:
|
|
||||||
unit_file_state = str(snap.get("unit_file_state") or "")
|
|
||||||
self.services[unit] = {
|
|
||||||
"name": unit,
|
|
||||||
"state": "running" if snap.get("active_state") == "active" else "dead",
|
|
||||||
"enable": unit_file_state in ("enabled", "enabled-runtime"),
|
|
||||||
}
|
|
||||||
|
|
||||||
def add_users_snapshot(self, snap: Dict[str, Any]) -> None:
|
def add_users_snapshot(self, snap: Dict[str, Any]) -> None:
|
||||||
for u in snap.get("users", []) or []:
|
records = self.user_records_from_snapshot(snap)
|
||||||
if not isinstance(u, dict):
|
self.groups.update(self.user_group_names_from_records(records))
|
||||||
continue
|
for record in records:
|
||||||
name = str(u.get("name") or "").strip()
|
name = str(record.get("name") or "")
|
||||||
if not name:
|
|
||||||
continue
|
|
||||||
primary_group = str(u.get("primary_group") or name).strip()
|
|
||||||
if primary_group:
|
|
||||||
self.groups.add(primary_group)
|
|
||||||
supplementary = sorted(
|
|
||||||
{
|
|
||||||
str(g).strip()
|
|
||||||
for g in (u.get("supplementary_groups") or [])
|
|
||||||
if str(g).strip()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
self.groups.update(supplementary)
|
|
||||||
user_data: Dict[str, Any] = {
|
user_data: Dict[str, Any] = {
|
||||||
"name": name,
|
"name": name,
|
||||||
"uid": u.get("uid"),
|
"uid": record.get("uid"),
|
||||||
"gid": primary_group or u.get("gid"),
|
"gid": record.get("primary_group") or record.get("gid"),
|
||||||
"home": u.get("home") or f"/home/{name}",
|
"home": record.get("home"),
|
||||||
"shell": u.get("shell"),
|
"shell": record.get("shell"),
|
||||||
"groups": supplementary,
|
"groups": record.get("supplementary_groups") or [],
|
||||||
}
|
}
|
||||||
user_data.update(_gecos_attrs(u.get("gecos")))
|
user_data.update(_gecos_attrs(record.get("gecos")))
|
||||||
self.users[name] = user_data
|
self.users[name] = user_data
|
||||||
|
|
||||||
home_by_user = {
|
self.add_user_flatpaks_snapshot(snap)
|
||||||
str(u.get("name")): str(u.get("home") or "")
|
|
||||||
for u in (snap.get("users", []) or [])
|
|
||||||
if isinstance(u, dict) and u.get("name")
|
|
||||||
}
|
|
||||||
for remote in snap.get("user_flatpak_remotes", []) or []:
|
|
||||||
item = _normalise_flatpak_remote(remote)
|
|
||||||
user = str(item.get("user") or "").strip()
|
|
||||||
if user and not item.get("home"):
|
|
||||||
item["home"] = home_by_user.get(user) or f"/home/{user}"
|
|
||||||
if item.get("method") == "user" and item.get("name") and item.get("url"):
|
|
||||||
self.flatpak_remotes.append(_prepare_flatpak_remote(item))
|
|
||||||
for uname, flatpaks in (snap.get("user_flatpaks", {}) or {}).items():
|
|
||||||
user = str(uname)
|
|
||||||
for fp in flatpaks or []:
|
|
||||||
item = _normalise_flatpak_item(
|
|
||||||
fp, method="user", user=user, home=home_by_user.get(user) or None
|
|
||||||
)
|
|
||||||
if item.get("name"):
|
|
||||||
self.flatpaks.append(_prepare_flatpak_item(item))
|
|
||||||
|
|
||||||
def add_flatpak_snapshot(self, snap: Dict[str, Any]) -> None:
|
def prepare_flatpak_remote(self, item: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
for remote in snap.get("remotes", []) or []:
|
return _prepare_flatpak_remote(item)
|
||||||
item = _normalise_flatpak_remote(remote)
|
|
||||||
if item.get("name") and item.get("url"):
|
|
||||||
self.flatpak_remotes.append(_prepare_flatpak_remote(item))
|
|
||||||
for fp in snap.get("system_flatpaks", []) or []:
|
|
||||||
item = _normalise_flatpak_item(fp, method="system")
|
|
||||||
if item.get("name"):
|
|
||||||
self.flatpaks.append(_prepare_flatpak_item(item))
|
|
||||||
for note in snap.get("notes", []) or []:
|
|
||||||
self.notes.append(str(note))
|
|
||||||
|
|
||||||
def add_snap_snapshot(self, snap: Dict[str, Any]) -> None:
|
def prepare_flatpak_item(self, item: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
for raw in snap.get("system_snaps", []) or []:
|
return _prepare_flatpak_item(item)
|
||||||
item = _normalise_snap_item(raw)
|
|
||||||
if item.get("name"):
|
def prepare_snap_item(self, item: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
self.snaps.append(_prepare_snap_item(item))
|
return _prepare_snap_item(item)
|
||||||
for note in snap.get("notes", []) or []:
|
|
||||||
self.notes.append(str(note))
|
|
||||||
|
|
||||||
def add_firewall_runtime_snapshot(
|
def add_firewall_runtime_snapshot(
|
||||||
self,
|
self,
|
||||||
|
|
@ -148,58 +86,16 @@ class SaltRole(CMModule):
|
||||||
role_files_dir: Path,
|
role_files_dir: Path,
|
||||||
file_prefix: Optional[str] = None,
|
file_prefix: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.packages.update(
|
super().add_firewall_runtime_snapshot(
|
||||||
str(p).strip() for p in (snap.get("packages") or []) if str(p).strip()
|
snap,
|
||||||
|
bundle_dir=bundle_dir,
|
||||||
|
artifact_role=artifact_role,
|
||||||
|
files_dir=role_files_dir,
|
||||||
|
copy_artifact=_copy_artifact,
|
||||||
|
source_uri=_source_uri,
|
||||||
|
file_prefix=file_prefix,
|
||||||
|
dir_attrs={"require": [{"file": "/etc/enroll"}]},
|
||||||
)
|
)
|
||||||
self.add_managed_dir(
|
|
||||||
"/etc/enroll/firewall",
|
|
||||||
user="root",
|
|
||||||
group="root",
|
|
||||||
mode="0750",
|
|
||||||
require=[{"file": "/etc/enroll"}],
|
|
||||||
reason="firewall_runtime",
|
|
||||||
)
|
|
||||||
runtime: Dict[str, Any] = {}
|
|
||||||
for key, dest_name, mode in (
|
|
||||||
("ipset_save", "ipset.save", "0600"),
|
|
||||||
("iptables_v4_save", "iptables.v4", "0600"),
|
|
||||||
("iptables_v6_save", "iptables.v6", "0600"),
|
|
||||||
):
|
|
||||||
src_rel = str(snap.get(key) or "").strip()
|
|
||||||
if not src_rel:
|
|
||||||
continue
|
|
||||||
role_rel = _copy_artifact(
|
|
||||||
bundle_dir,
|
|
||||||
artifact_role,
|
|
||||||
src_rel,
|
|
||||||
role_files_dir,
|
|
||||||
dst_prefix=file_prefix,
|
|
||||||
)
|
|
||||||
if not role_rel:
|
|
||||||
self.notes.append(
|
|
||||||
f"Firewall runtime artifact {src_rel!r} was referenced but not found."
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
dest = f"/etc/enroll/firewall/{dest_name}"
|
|
||||||
self.add_managed_file(
|
|
||||||
dest,
|
|
||||||
user="root",
|
|
||||||
group="root",
|
|
||||||
mode=mode,
|
|
||||||
source=_source_uri(self.module_name, role_rel),
|
|
||||||
reason="firewall_runtime",
|
|
||||||
)
|
|
||||||
runtime[key] = dest
|
|
||||||
ipset_sets = [
|
|
||||||
str(x).strip() for x in (snap.get("ipset_sets") or []) if str(x).strip()
|
|
||||||
]
|
|
||||||
if ipset_sets:
|
|
||||||
runtime["ipset_sets"] = ipset_sets
|
|
||||||
if runtime:
|
|
||||||
runtime.update(_firewall_runtime_commands(runtime))
|
|
||||||
self.firewall_runtime.update(runtime)
|
|
||||||
for note in snap.get("notes", []) or []:
|
|
||||||
self.notes.append(str(note))
|
|
||||||
|
|
||||||
def add_container_images_snapshot(self, snap: Dict[str, Any]) -> None:
|
def add_container_images_snapshot(self, snap: Dict[str, Any]) -> None:
|
||||||
for raw in snap.get("images", []) or []:
|
for raw in snap.get("images", []) or []:
|
||||||
|
|
@ -413,70 +309,6 @@ def _container_tag_cmd(engine: str, pull_ref: str, tag_ref: str) -> str:
|
||||||
return f"{engine} tag {_shell_quote(pull_ref)} {_shell_quote(tag_ref)}"
|
return f"{engine} tag {_shell_quote(pull_ref)} {_shell_quote(tag_ref)}"
|
||||||
|
|
||||||
|
|
||||||
def _normalise_flatpak_item(
|
|
||||||
item: Dict[str, Any],
|
|
||||||
*,
|
|
||||||
method: str,
|
|
||||||
user: Optional[str] = None,
|
|
||||||
home: Optional[str] = None,
|
|
||||||
) -> Dict[str, Any]:
|
|
||||||
out = dict(item)
|
|
||||||
out["method"] = str(out.get("method") or method or "system").strip() or "system"
|
|
||||||
if user and not out.get("user"):
|
|
||||||
out["user"] = user
|
|
||||||
if home and not out.get("home"):
|
|
||||||
out["home"] = home
|
|
||||||
ref = str(out.get("ref") or "").strip()
|
|
||||||
if ref and not out.get("name"):
|
|
||||||
out["name"] = ref.rsplit("/", 1)[-1]
|
|
||||||
name = str(out.get("name") or out.get("app_id") or "").strip()
|
|
||||||
if name:
|
|
||||||
out["name"] = name
|
|
||||||
remote = str(out.get("remote") or "").strip()
|
|
||||||
if remote:
|
|
||||||
out["remote"] = remote
|
|
||||||
branch = str(out.get("branch") or out.get("origin") or "").strip()
|
|
||||||
if branch:
|
|
||||||
out["branch"] = branch
|
|
||||||
if ref:
|
|
||||||
out["ref"] = ref
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def _normalise_flatpak_remote(item: Dict[str, Any]) -> Dict[str, Any]:
|
|
||||||
out = dict(item)
|
|
||||||
name = str(out.get("name") or out.get("remote") or "").strip()
|
|
||||||
url = str(out.get("url") or out.get("from_url") or "").strip()
|
|
||||||
method = str(out.get("method") or out.get("scope") or "system").strip() or "system"
|
|
||||||
if name:
|
|
||||||
out["name"] = name
|
|
||||||
if url:
|
|
||||||
out["url"] = url
|
|
||||||
out["method"] = "user" if method == "user" else "system"
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def _normalise_snap_item(item: Dict[str, Any]) -> Dict[str, Any]:
|
|
||||||
out = dict(item)
|
|
||||||
name = str(out.get("name") or "").strip()
|
|
||||||
if name:
|
|
||||||
out["name"] = name
|
|
||||||
channel = str(out.get("tracking") or out.get("channel") or "").strip()
|
|
||||||
if channel:
|
|
||||||
out["channel"] = channel
|
|
||||||
notes = [str(note).lower() for note in (out.get("notes") or [])]
|
|
||||||
confinement = str(out.get("confinement") or "").strip().lower()
|
|
||||||
out["classic"] = confinement == "classic" or any(
|
|
||||||
"classic" in note for note in notes
|
|
||||||
)
|
|
||||||
out["devmode"] = any("devmode" in note or "dev mode" in note for note in notes)
|
|
||||||
out["dangerous"] = any("dangerous" in note for note in notes)
|
|
||||||
revision = str(out.get("revision") or "").strip()
|
|
||||||
if revision and not channel:
|
|
||||||
out["revision"] = revision
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def _flatpak_scope(item: Dict[str, Any]) -> str:
|
def _flatpak_scope(item: Dict[str, Any]) -> str:
|
||||||
return "--user" if str(item.get("method") or "system") == "user" else "--system"
|
return "--user" if str(item.get("method") or "system") == "user" else "--system"
|
||||||
|
|
||||||
|
|
@ -583,30 +415,6 @@ def _prepare_snap_item(item: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def _firewall_ipset_restore_cmd(path: str, sets: List[str]) -> str:
|
|
||||||
flush_parts = [f"ipset flush {_shell_quote(name)} || true" for name in sets]
|
|
||||||
flush = "; ".join(flush_parts)
|
|
||||||
restore = f"ipset restore -exist < {_shell_quote(path)}"
|
|
||||||
if flush:
|
|
||||||
return f"/bin/sh -c {_shell_quote(flush + '; ' + restore)}"
|
|
||||||
return f"/bin/sh -c {_shell_quote(restore)}"
|
|
||||||
|
|
||||||
|
|
||||||
def _firewall_runtime_commands(runtime: Dict[str, Any]) -> Dict[str, Any]:
|
|
||||||
out: Dict[str, Any] = {}
|
|
||||||
ipset_path = str(runtime.get("ipset_save") or "")
|
|
||||||
if ipset_path:
|
|
||||||
sets = [str(x) for x in (runtime.get("ipset_sets") or []) if str(x)]
|
|
||||||
out["ipset_restore_cmd"] = _firewall_ipset_restore_cmd(ipset_path, sets)
|
|
||||||
ipt4_path = str(runtime.get("iptables_v4_save") or "")
|
|
||||||
if ipt4_path:
|
|
||||||
out["iptables_v4_restore_cmd"] = f"iptables-restore {_shell_quote(ipt4_path)}"
|
|
||||||
ipt6_path = str(runtime.get("iptables_v6_save") or "")
|
|
||||||
if ipt6_path:
|
|
||||||
out["iptables_v6_restore_cmd"] = f"ip6tables-restore {_shell_quote(ipt6_path)}"
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def _append_firewall_runtime_states(lines: List[str], runtime: Dict[str, Any]) -> None:
|
def _append_firewall_runtime_states(lines: List[str], runtime: Dict[str, Any]) -> None:
|
||||||
specs = [
|
specs = [
|
||||||
(
|
(
|
||||||
|
|
@ -807,60 +615,29 @@ def _collect_salt_roles(
|
||||||
overwrite_templates=not bool(fqdn),
|
overwrite_templates=not bool(fqdn),
|
||||||
)
|
)
|
||||||
|
|
||||||
for svc in roles.get("services", []) or []:
|
for entry in CMModule.package_service_entries(
|
||||||
if not isinstance(svc, dict):
|
roles, inventory_packages, use_common_roles=use_common_roles
|
||||||
continue
|
):
|
||||||
original_role_name = _salt_name(
|
snap = entry.get("snapshot") or {}
|
||||||
str(svc.get("role_name") or svc.get("unit") or "service"),
|
kind = str(entry.get("kind") or "package")
|
||||||
fallback="service",
|
fallback = "service" if kind == "service" else "package"
|
||||||
|
source_label = str(
|
||||||
|
snap.get("role_name") or snap.get("unit") or snap.get("package") or fallback
|
||||||
|
)
|
||||||
|
original_role_name = _salt_name(source_label, fallback=fallback)
|
||||||
|
role_name = _salt_name(
|
||||||
|
str(entry.get("role_label") or source_label),
|
||||||
|
fallback="package_group" if use_common_roles else fallback,
|
||||||
)
|
)
|
||||||
if use_common_roles:
|
|
||||||
role_name = _salt_name(
|
|
||||||
section_label_for_packages(
|
|
||||||
[
|
|
||||||
str(p).strip()
|
|
||||||
for p in (svc.get("packages") or [])
|
|
||||||
if str(p).strip()
|
|
||||||
],
|
|
||||||
inventory_packages,
|
|
||||||
),
|
|
||||||
fallback="package_group",
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
role_name = original_role_name
|
|
||||||
srole = ensure_role(role_name)
|
srole = ensure_role(role_name)
|
||||||
srole.add_service_snapshot(svc)
|
if kind == "service":
|
||||||
srole.add_managed_content(
|
srole.add_service_snapshot(snap)
|
||||||
svc,
|
|
||||||
bundle_dir=bundle_dir,
|
|
||||||
artifact_role=str(svc.get("role_name") or original_role_name),
|
|
||||||
role_files_dir=states_dir / "roles" / srole.module_name / "files",
|
|
||||||
file_prefix=node_file_prefix,
|
|
||||||
jt_exe=jt_exe,
|
|
||||||
jt_enabled=jt_enabled,
|
|
||||||
overwrite_templates=not bool(fqdn),
|
|
||||||
)
|
|
||||||
|
|
||||||
for pkg in roles.get("packages", []) or []:
|
|
||||||
if not isinstance(pkg, dict):
|
|
||||||
continue
|
|
||||||
original_role_name = _salt_name(
|
|
||||||
str(pkg.get("role_name") or pkg.get("package") or "package"),
|
|
||||||
fallback="package",
|
|
||||||
)
|
|
||||||
if use_common_roles:
|
|
||||||
role_name = _salt_name(
|
|
||||||
package_section_label(pkg, inventory_packages),
|
|
||||||
fallback="package_group",
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
role_name = original_role_name
|
srole.add_package_snapshot(snap)
|
||||||
srole = ensure_role(role_name)
|
|
||||||
srole.add_package_snapshot(pkg)
|
|
||||||
srole.add_managed_content(
|
srole.add_managed_content(
|
||||||
pkg,
|
snap,
|
||||||
bundle_dir=bundle_dir,
|
bundle_dir=bundle_dir,
|
||||||
artifact_role=str(pkg.get("role_name") or original_role_name),
|
artifact_role=str(snap.get("role_name") or original_role_name),
|
||||||
role_files_dir=states_dir / "roles" / srole.module_name / "files",
|
role_files_dir=states_dir / "roles" / srole.module_name / "files",
|
||||||
file_prefix=node_file_prefix,
|
file_prefix=node_file_prefix,
|
||||||
jt_exe=jt_exe,
|
jt_exe=jt_exe,
|
||||||
|
|
@ -1621,17 +1398,12 @@ def _render_readme(
|
||||||
) -> str:
|
) -> str:
|
||||||
host = state.get("host", {}) if isinstance(state.get("host"), dict) else {}
|
host = state.get("host", {}) if isinstance(state.get("host"), dict) else {}
|
||||||
hostname = host.get("hostname") or "unknown"
|
hostname = host.get("hostname") or "unknown"
|
||||||
role_lines = (
|
role_lines = markdown_list(
|
||||||
"\n".join(
|
f"`{r.sls_name}` from Enroll role `{r.role_name}`" for r in salt_roles
|
||||||
f"- `{r.sls_name}` from Enroll role `{r.role_name}`" for r in salt_roles
|
)
|
||||||
)
|
notes_text = markdown_list(
|
||||||
or "- None."
|
f"`{r.sls_name}`: {note}" for r in salt_roles for note in r.notes
|
||||||
)
|
)
|
||||||
notes: List[str] = []
|
|
||||||
for r in salt_roles:
|
|
||||||
for note in r.notes:
|
|
||||||
notes.append(f"`{r.sls_name}`: {note}")
|
|
||||||
notes_text = "\n".join(f"- {n}" for n in notes) or "- None."
|
|
||||||
|
|
||||||
if fqdn:
|
if fqdn:
|
||||||
node_display = (
|
node_display = (
|
||||||
|
|
|
||||||
|
|
@ -433,7 +433,8 @@ def test_manifest_groups_excluded_package_paths_into_common_roles(tmp_path: Path
|
||||||
|
|
||||||
assert (out / "roles" / "net").exists()
|
assert (out / "roles" / "net").exists()
|
||||||
assert not (out / "roles" / "secret_agent").exists()
|
assert not (out / "roles" / "secret_agent").exists()
|
||||||
readme = (out / "roles" / "net" / "README.md").read_text(encoding="utf-8")
|
assert not (out / "roles" / "net" / "README.md").exists()
|
||||||
|
readme = (out / "README.md").read_text(encoding="utf-8")
|
||||||
assert "/etc/secret-agent/key" in readme
|
assert "/etc/secret-agent/key" in readme
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1461,24 +1462,32 @@ def test_copy2_replace_atomic(tmp_path: Path):
|
||||||
|
|
||||||
|
|
||||||
def test_render_firewall_runtime_tasks_empty():
|
def test_render_firewall_runtime_tasks_empty():
|
||||||
result = ansible_tasks._render_firewall_runtime_tasks("firewall_runtime")
|
result = ansible_tasks._render_role_tasks(
|
||||||
|
ansible_tasks.AnsibleRole("firewall_runtime"), firewall_runtime=True
|
||||||
|
)
|
||||||
# Function always returns at least a basic playbook structure
|
# Function always returns at least a basic playbook structure
|
||||||
assert isinstance(result, str)
|
assert isinstance(result, str)
|
||||||
assert len(result) > 0
|
assert len(result) > 0
|
||||||
|
|
||||||
|
|
||||||
def test_render_firewall_runtime_tasks_with_iptables():
|
def test_render_firewall_runtime_tasks_with_iptables():
|
||||||
result = ansible_tasks._render_firewall_runtime_tasks("firewall_runtime")
|
result = ansible_tasks._render_role_tasks(
|
||||||
|
ansible_tasks.AnsibleRole("firewall_runtime"), firewall_runtime=True
|
||||||
|
)
|
||||||
assert len(result) >= 1
|
assert len(result) >= 1
|
||||||
|
|
||||||
|
|
||||||
def test_render_firewall_runtime_tasks_with_ipset():
|
def test_render_firewall_runtime_tasks_with_ipset():
|
||||||
result = ansible_tasks._render_firewall_runtime_tasks("firewall_runtime")
|
result = ansible_tasks._render_role_tasks(
|
||||||
|
ansible_tasks.AnsibleRole("firewall_runtime"), firewall_runtime=True
|
||||||
|
)
|
||||||
assert len(result) >= 1
|
assert len(result) >= 1
|
||||||
|
|
||||||
|
|
||||||
def test_render_firewall_runtime_tasks_with_ipv6():
|
def test_render_firewall_runtime_tasks_with_ipv6():
|
||||||
result = ansible_tasks._render_firewall_runtime_tasks("firewall_runtime")
|
result = ansible_tasks._render_role_tasks(
|
||||||
|
ansible_tasks.AnsibleRole("firewall_runtime"), firewall_runtime=True
|
||||||
|
)
|
||||||
assert len(result) >= 1
|
assert len(result) >= 1
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1608,7 +1617,8 @@ def test_manifest_renders_flatpak_and_snap_details(tmp_path: Path):
|
||||||
users_tasks = (out / "roles" / "users" / "tasks" / "main.yml").read_text(
|
users_tasks = (out / "roles" / "users" / "tasks" / "main.yml").read_text(
|
||||||
encoding="utf-8"
|
encoding="utf-8"
|
||||||
)
|
)
|
||||||
users_readme = (out / "roles" / "users" / "README.md").read_text(encoding="utf-8")
|
assert not (out / "roles" / "users" / "README.md").exists()
|
||||||
|
users_readme = (out / "README.md").read_text(encoding="utf-8")
|
||||||
flatpak_defaults = (out / "roles" / "flatpak" / "defaults" / "main.yml").read_text(
|
flatpak_defaults = (out / "roles" / "flatpak" / "defaults" / "main.yml").read_text(
|
||||||
encoding="utf-8"
|
encoding="utf-8"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Reference in a new issue