Add ability to enroll RH-style systems (DNF5/DNF/RPM)
This commit is contained in:
parent
ad2abed612
commit
984b0fa81b
15 changed files with 1400 additions and 254 deletions
|
|
@ -1,6 +1,5 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
|
|
@ -97,58 +96,3 @@ def test_parse_status_conffiles_handles_continuations(tmp_path: Path):
|
|||
assert m["nginx"]["/etc/nginx/nginx.conf"] == "abcdef"
|
||||
assert m["nginx"]["/etc/nginx/mime.types"] == "123456"
|
||||
assert "other" not in m
|
||||
|
||||
|
||||
def test_read_pkg_md5sums_and_file_md5(tmp_path: Path, monkeypatch):
|
||||
import enroll.debian as d
|
||||
|
||||
# Patch /var/lib/dpkg/info/<pkg>.md5sums lookup to a tmp file.
|
||||
md5_file = tmp_path / "pkg.md5sums"
|
||||
md5_file.write_text("0123456789abcdef etc/foo.conf\n", encoding="utf-8")
|
||||
|
||||
def fake_exists(path: str) -> bool:
|
||||
return path.endswith("/var/lib/dpkg/info/p1.md5sums")
|
||||
|
||||
real_open = open
|
||||
|
||||
def fake_open(path: str, *args, **kwargs):
|
||||
if path.endswith("/var/lib/dpkg/info/p1.md5sums"):
|
||||
return real_open(md5_file, *args, **kwargs)
|
||||
return real_open(path, *args, **kwargs)
|
||||
|
||||
monkeypatch.setattr(d.os.path, "exists", fake_exists)
|
||||
monkeypatch.setattr("builtins.open", fake_open)
|
||||
|
||||
m = d.read_pkg_md5sums("p1")
|
||||
assert m == {"etc/foo.conf": "0123456789abcdef"}
|
||||
|
||||
content = b"hello world\n"
|
||||
p = tmp_path / "x"
|
||||
p.write_bytes(content)
|
||||
assert d.file_md5(str(p)) == hashlib.md5(content).hexdigest()
|
||||
|
||||
|
||||
def test_stat_triplet_fallbacks(tmp_path: Path, monkeypatch):
|
||||
import enroll.debian as d
|
||||
import sys
|
||||
|
||||
p = tmp_path / "f"
|
||||
p.write_text("x", encoding="utf-8")
|
||||
|
||||
class FakePwdMod:
|
||||
@staticmethod
|
||||
def getpwuid(_): # pragma: no cover
|
||||
raise KeyError
|
||||
|
||||
class FakeGrpMod:
|
||||
@staticmethod
|
||||
def getgrgid(_): # pragma: no cover
|
||||
raise KeyError
|
||||
|
||||
# stat_triplet imports pwd/grp inside the function, so patch sys.modules.
|
||||
monkeypatch.setitem(sys.modules, "pwd", FakePwdMod)
|
||||
monkeypatch.setitem(sys.modules, "grp", FakeGrpMod)
|
||||
owner, group, mode = d.stat_triplet(str(p))
|
||||
assert owner.isdigit()
|
||||
assert group.isdigit()
|
||||
assert mode.isdigit() and len(mode) == 4
|
||||
|
|
|
|||
25
tests/test_fsutil.py
Normal file
25
tests/test_fsutil.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from enroll.fsutil import file_md5, stat_triplet
|
||||
|
||||
|
||||
def test_file_md5_matches_hashlib(tmp_path: Path):
|
||||
p = tmp_path / "x"
|
||||
p.write_bytes(b"hello world")
|
||||
expected = hashlib.md5(b"hello world").hexdigest() # nosec
|
||||
assert file_md5(str(p)) == expected
|
||||
|
||||
|
||||
def test_stat_triplet_reports_mode(tmp_path: Path):
|
||||
p = tmp_path / "x"
|
||||
p.write_text("x", encoding="utf-8")
|
||||
os.chmod(p, 0o600)
|
||||
|
||||
owner, group, mode = stat_triplet(str(p))
|
||||
assert mode == "0600"
|
||||
assert owner # non-empty string
|
||||
assert group # non-empty string
|
||||
|
|
@ -2,6 +2,7 @@ import json
|
|||
from pathlib import Path
|
||||
|
||||
import enroll.harvest as h
|
||||
from enroll.platform import PlatformInfo
|
||||
from enroll.systemd import UnitInfo
|
||||
|
||||
|
||||
|
|
@ -10,6 +11,64 @@ class AllowAllPolicy:
|
|||
return None
|
||||
|
||||
|
||||
class FakeBackend:
|
||||
"""Minimal backend stub for harvest tests.
|
||||
|
||||
The real backends (dpkg/rpm) enumerate the live system (dpkg status, rpm
|
||||
databases, etc). These tests instead control all backend behaviour.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
name: str,
|
||||
owned_etc: set[str],
|
||||
etc_owner_map: dict[str, str],
|
||||
topdir_to_pkgs: dict[str, set[str]],
|
||||
pkg_to_etc_paths: dict[str, list[str]],
|
||||
manual_pkgs: list[str],
|
||||
owner_fn,
|
||||
modified_by_pkg: dict[str, dict[str, str]] | None = None,
|
||||
pkg_config_prefixes: tuple[str, ...] = ("/etc/apt/",),
|
||||
):
|
||||
self.name = name
|
||||
self.pkg_config_prefixes = pkg_config_prefixes
|
||||
self._owned_etc = owned_etc
|
||||
self._etc_owner_map = etc_owner_map
|
||||
self._topdir_to_pkgs = topdir_to_pkgs
|
||||
self._pkg_to_etc_paths = pkg_to_etc_paths
|
||||
self._manual = manual_pkgs
|
||||
self._owner_fn = owner_fn
|
||||
self._modified_by_pkg = modified_by_pkg or {}
|
||||
|
||||
def build_etc_index(self):
|
||||
return (
|
||||
self._owned_etc,
|
||||
self._etc_owner_map,
|
||||
self._topdir_to_pkgs,
|
||||
self._pkg_to_etc_paths,
|
||||
)
|
||||
|
||||
def owner_of_path(self, path: str):
|
||||
return self._owner_fn(path)
|
||||
|
||||
def list_manual_packages(self):
|
||||
return list(self._manual)
|
||||
|
||||
def specific_paths_for_hints(self, hints: set[str]):
|
||||
return []
|
||||
|
||||
def is_pkg_config_path(self, path: str) -> bool:
|
||||
for pfx in self.pkg_config_prefixes:
|
||||
if path == pfx or path.startswith(pfx):
|
||||
return True
|
||||
return False
|
||||
|
||||
def modified_paths(self, pkg: str, etc_paths: list[str]):
|
||||
# Test-controlled; ignore etc_paths.
|
||||
return dict(self._modified_by_pkg.get(pkg, {}))
|
||||
|
||||
|
||||
def test_harvest_dedup_manual_packages_and_builds_etc_custom(
|
||||
monkeypatch, tmp_path: Path
|
||||
):
|
||||
|
|
@ -22,7 +81,7 @@ def test_harvest_dedup_manual_packages_and_builds_etc_custom(
|
|||
real_exists = os.path.exists
|
||||
real_islink = os.path.islink
|
||||
|
||||
# Fake filesystem: two /etc files exist, only one is dpkg-owned.
|
||||
# Fake filesystem: two /etc files exist, only one is package-owned.
|
||||
# Also include some /usr/local files to populate usr_local_custom.
|
||||
files = {
|
||||
"/etc/openvpn/server.conf": b"server",
|
||||
|
|
@ -93,6 +152,7 @@ def test_harvest_dedup_manual_packages_and_builds_etc_custom(
|
|||
|
||||
# Avoid real system access
|
||||
monkeypatch.setattr(h, "list_enabled_services", lambda: ["openvpn.service"])
|
||||
monkeypatch.setattr(h, "list_enabled_timers", lambda: [])
|
||||
monkeypatch.setattr(
|
||||
h,
|
||||
"get_unit_info",
|
||||
|
|
@ -109,29 +169,30 @@ def test_harvest_dedup_manual_packages_and_builds_etc_custom(
|
|||
),
|
||||
)
|
||||
|
||||
# Debian package index: openvpn owns /etc/openvpn/server.conf; keyboard is unowned.
|
||||
def fake_build_index():
|
||||
owned_etc = {"/etc/openvpn/server.conf"}
|
||||
etc_owner_map = {"/etc/openvpn/server.conf": "openvpn"}
|
||||
topdir_to_pkgs = {"openvpn": {"openvpn"}}
|
||||
pkg_to_etc_paths = {"openvpn": ["/etc/openvpn/server.conf"], "curl": []}
|
||||
return owned_etc, etc_owner_map, topdir_to_pkgs, pkg_to_etc_paths
|
||||
# Package index: openvpn owns /etc/openvpn/server.conf; keyboard is unowned.
|
||||
owned_etc = {"/etc/openvpn/server.conf"}
|
||||
etc_owner_map = {"/etc/openvpn/server.conf": "openvpn"}
|
||||
topdir_to_pkgs = {"openvpn": {"openvpn"}}
|
||||
pkg_to_etc_paths = {"openvpn": ["/etc/openvpn/server.conf"], "curl": []}
|
||||
|
||||
monkeypatch.setattr(h, "build_dpkg_etc_index", fake_build_index)
|
||||
|
||||
# openvpn conffile hash mismatch => should be captured under service role
|
||||
monkeypatch.setattr(
|
||||
h,
|
||||
"parse_status_conffiles",
|
||||
lambda: {"openvpn": {"/etc/openvpn/server.conf": "old"}},
|
||||
backend = FakeBackend(
|
||||
name="dpkg",
|
||||
owned_etc=owned_etc,
|
||||
etc_owner_map=etc_owner_map,
|
||||
topdir_to_pkgs=topdir_to_pkgs,
|
||||
pkg_to_etc_paths=pkg_to_etc_paths,
|
||||
manual_pkgs=["openvpn", "curl"],
|
||||
owner_fn=lambda p: "openvpn" if "openvpn" in (p or "") else None,
|
||||
modified_by_pkg={
|
||||
"openvpn": {"/etc/openvpn/server.conf": "modified_conffile"},
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(h, "read_pkg_md5sums", lambda pkg: {})
|
||||
monkeypatch.setattr(h, "file_md5", lambda path: "new")
|
||||
|
||||
monkeypatch.setattr(
|
||||
h, "dpkg_owner", lambda p: "openvpn" if "openvpn" in p else None
|
||||
h, "detect_platform", lambda: PlatformInfo("debian", "dpkg", {})
|
||||
)
|
||||
monkeypatch.setattr(h, "list_manual_packages", lambda: ["openvpn", "curl"])
|
||||
monkeypatch.setattr(h, "get_backend", lambda info=None: backend)
|
||||
|
||||
monkeypatch.setattr(h, "collect_non_system_users", lambda: [])
|
||||
|
||||
def fake_stat_triplet(p: str):
|
||||
|
|
@ -207,6 +268,7 @@ def test_shared_cron_snippet_prefers_matching_role_over_lexicographic(
|
|||
monkeypatch.setattr(
|
||||
h, "list_enabled_services", lambda: ["apparmor.service", "ntpsec.service"]
|
||||
)
|
||||
monkeypatch.setattr(h, "list_enabled_timers", lambda: [])
|
||||
|
||||
def fake_unit_info(unit: str) -> UnitInfo:
|
||||
if unit == "apparmor.service":
|
||||
|
|
@ -235,31 +297,35 @@ def test_shared_cron_snippet_prefers_matching_role_over_lexicographic(
|
|||
|
||||
monkeypatch.setattr(h, "get_unit_info", fake_unit_info)
|
||||
|
||||
# Dpkg /etc index: no owned /etc paths needed for this test.
|
||||
monkeypatch.setattr(
|
||||
h,
|
||||
"build_dpkg_etc_index",
|
||||
lambda: (set(), {}, {}, {}),
|
||||
)
|
||||
monkeypatch.setattr(h, "parse_status_conffiles", lambda: {})
|
||||
monkeypatch.setattr(h, "read_pkg_md5sums", lambda pkg: {})
|
||||
monkeypatch.setattr(h, "file_md5", lambda path: "x")
|
||||
monkeypatch.setattr(h, "list_manual_packages", lambda: [])
|
||||
monkeypatch.setattr(h, "collect_non_system_users", lambda: [])
|
||||
|
||||
# Make apparmor *also* claim the ntpsec package (simulates overly-broad
|
||||
# package inference). The snippet routing should still prefer role 'ntpsec'.
|
||||
def fake_dpkg_owner(p: str):
|
||||
def fake_owner(p: str):
|
||||
if p == "/etc/cron.d/ntpsec":
|
||||
return "ntpsec"
|
||||
if "apparmor" in p:
|
||||
if "apparmor" in (p or ""):
|
||||
return "ntpsec" # intentionally misleading
|
||||
if "ntpsec" in p or "ntpd" in p:
|
||||
if "ntpsec" in (p or "") or "ntpd" in (p or ""):
|
||||
return "ntpsec"
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(h, "dpkg_owner", fake_dpkg_owner)
|
||||
backend = FakeBackend(
|
||||
name="dpkg",
|
||||
owned_etc=set(),
|
||||
etc_owner_map={},
|
||||
topdir_to_pkgs={},
|
||||
pkg_to_etc_paths={},
|
||||
manual_pkgs=[],
|
||||
owner_fn=fake_owner,
|
||||
modified_by_pkg={},
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
h, "detect_platform", lambda: PlatformInfo("debian", "dpkg", {})
|
||||
)
|
||||
monkeypatch.setattr(h, "get_backend", lambda info=None: backend)
|
||||
|
||||
monkeypatch.setattr(h, "stat_triplet", lambda p: ("root", "root", "0644"))
|
||||
monkeypatch.setattr(h, "collect_non_system_users", lambda: [])
|
||||
|
||||
def fake_copy(bundle_dir: str, role_name: str, abs_path: str, src_rel: str):
|
||||
dst = Path(bundle_dir) / "artifacts" / role_name / src_rel
|
||||
|
|
@ -268,11 +334,7 @@ def test_shared_cron_snippet_prefers_matching_role_over_lexicographic(
|
|||
|
||||
monkeypatch.setattr(h, "_copy_into_bundle", fake_copy)
|
||||
|
||||
class AllowAll:
|
||||
def deny_reason(self, path: str):
|
||||
return None
|
||||
|
||||
state_path = h.harvest(str(bundle), policy=AllowAll())
|
||||
state_path = h.harvest(str(bundle), policy=AllowAllPolicy())
|
||||
st = json.loads(Path(state_path).read_text(encoding="utf-8"))
|
||||
|
||||
# Cron snippet should end up attached to the ntpsec role, not apparmor.
|
||||
|
|
|
|||
|
|
@ -322,3 +322,96 @@ def test_copy2_replace_overwrites_readonly_destination(tmp_path: Path):
|
|||
assert dst.read_text(encoding="utf-8") == "new"
|
||||
mode = stat.S_IMODE(dst.stat().st_mode)
|
||||
assert mode & stat.S_IWUSR # destination should remain mergeable
|
||||
|
||||
|
||||
def test_manifest_includes_dnf_config_role_when_present(tmp_path: Path):
|
||||
bundle = tmp_path / "bundle"
|
||||
out = tmp_path / "ansible"
|
||||
|
||||
# Create a dnf_config artifact.
|
||||
(bundle / "artifacts" / "dnf_config" / "etc" / "dnf").mkdir(
|
||||
parents=True, exist_ok=True
|
||||
)
|
||||
(bundle / "artifacts" / "dnf_config" / "etc" / "dnf" / "dnf.conf").write_text(
|
||||
"[main]\n", encoding="utf-8"
|
||||
)
|
||||
|
||||
state = {
|
||||
"host": {"hostname": "test", "os": "redhat", "pkg_backend": "rpm"},
|
||||
"users": {
|
||||
"role_name": "users",
|
||||
"users": [],
|
||||
"managed_files": [],
|
||||
"excluded": [],
|
||||
"notes": [],
|
||||
},
|
||||
"services": [],
|
||||
"package_roles": [],
|
||||
"manual_packages": [],
|
||||
"manual_packages_skipped": [],
|
||||
"apt_config": {
|
||||
"role_name": "apt_config",
|
||||
"managed_files": [],
|
||||
"excluded": [],
|
||||
"notes": [],
|
||||
},
|
||||
"dnf_config": {
|
||||
"role_name": "dnf_config",
|
||||
"managed_files": [
|
||||
{
|
||||
"path": "/etc/dnf/dnf.conf",
|
||||
"src_rel": "etc/dnf/dnf.conf",
|
||||
"owner": "root",
|
||||
"group": "root",
|
||||
"mode": "0644",
|
||||
"reason": "dnf_config",
|
||||
}
|
||||
],
|
||||
"excluded": [],
|
||||
"notes": [],
|
||||
},
|
||||
"etc_custom": {
|
||||
"role_name": "etc_custom",
|
||||
"managed_files": [],
|
||||
"excluded": [],
|
||||
"notes": [],
|
||||
},
|
||||
"usr_local_custom": {
|
||||
"role_name": "usr_local_custom",
|
||||
"managed_files": [],
|
||||
"excluded": [],
|
||||
"notes": [],
|
||||
},
|
||||
"extra_paths": {
|
||||
"role_name": "extra_paths",
|
||||
"include_patterns": [],
|
||||
"exclude_patterns": [],
|
||||
"managed_files": [],
|
||||
"excluded": [],
|
||||
"notes": [],
|
||||
},
|
||||
}
|
||||
|
||||
bundle.mkdir(parents=True, exist_ok=True)
|
||||
(bundle / "state.json").write_text(json.dumps(state, indent=2), encoding="utf-8")
|
||||
|
||||
manifest(str(bundle), str(out))
|
||||
|
||||
pb = (out / "playbook.yml").read_text(encoding="utf-8")
|
||||
assert "- dnf_config" in pb
|
||||
|
||||
tasks = (out / "roles" / "dnf_config" / "tasks" / "main.yml").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
# Ensure the role exists and contains some file deployment logic.
|
||||
assert "Deploy any other managed files" in tasks
|
||||
|
||||
|
||||
def test_render_install_packages_tasks_contains_dnf_branch():
|
||||
from enroll.manifest import _render_install_packages_tasks
|
||||
|
||||
txt = _render_install_packages_tasks("role", "role")
|
||||
assert "ansible.builtin.apt" in txt
|
||||
assert "ansible.builtin.dnf" in txt
|
||||
assert "ansible.builtin.package" in txt
|
||||
assert "pkg_mgr" in txt
|
||||
|
|
|
|||
93
tests/test_platform.py
Normal file
93
tests/test_platform.py
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import enroll.platform as platform
|
||||
|
||||
|
||||
def test_read_os_release_parses_kv_and_strips_quotes(tmp_path: Path):
|
||||
p = tmp_path / "os-release"
|
||||
p.write_text(
|
||||
"""
|
||||
# comment
|
||||
ID=fedora
|
||||
ID_LIKE=\"rhel centos\"
|
||||
NAME=\"Fedora Linux\"
|
||||
EMPTY=
|
||||
NOEQUALS
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
osr = platform._read_os_release(str(p))
|
||||
assert osr["ID"] == "fedora"
|
||||
assert osr["ID_LIKE"] == "rhel centos"
|
||||
assert osr["NAME"] == "Fedora Linux"
|
||||
assert osr["EMPTY"] == ""
|
||||
assert "NOEQUALS" not in osr
|
||||
|
||||
|
||||
def test_detect_platform_prefers_os_release(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
platform,
|
||||
"_read_os_release",
|
||||
lambda path="/etc/os-release": {"ID": "fedora", "ID_LIKE": "rhel"},
|
||||
)
|
||||
# If os-release is decisive we shouldn't need which()
|
||||
monkeypatch.setattr(platform.shutil, "which", lambda exe: None)
|
||||
|
||||
info = platform.detect_platform()
|
||||
assert info.os_family == "redhat"
|
||||
assert info.pkg_backend == "rpm"
|
||||
|
||||
|
||||
def test_detect_platform_fallbacks_to_dpkg_when_unknown(monkeypatch):
|
||||
monkeypatch.setattr(platform, "_read_os_release", lambda path="/etc/os-release": {})
|
||||
monkeypatch.setattr(
|
||||
platform.shutil, "which", lambda exe: "/usr/bin/dpkg" if exe == "dpkg" else None
|
||||
)
|
||||
|
||||
info = platform.detect_platform()
|
||||
assert info.os_family == "debian"
|
||||
assert info.pkg_backend == "dpkg"
|
||||
|
||||
|
||||
def test_get_backend_unknown_prefers_rpm_if_present(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
platform.shutil, "which", lambda exe: "/usr/bin/rpm" if exe == "rpm" else None
|
||||
)
|
||||
|
||||
b = platform.get_backend(
|
||||
platform.PlatformInfo(os_family="unknown", pkg_backend="unknown", os_release={})
|
||||
)
|
||||
assert isinstance(b, platform.RpmBackend)
|
||||
|
||||
|
||||
def test_rpm_backend_modified_paths_labels_conffiles(monkeypatch):
|
||||
b = platform.RpmBackend()
|
||||
|
||||
# Pretend rpm -V says both files changed, but only one is a config file.
|
||||
monkeypatch.setattr(b, "_modified_files", lambda pkg: {"/etc/foo.conf", "/etc/bar"})
|
||||
monkeypatch.setattr(b, "_config_files", lambda pkg: {"/etc/foo.conf"})
|
||||
|
||||
out = b.modified_paths("mypkg", ["/etc/foo.conf", "/etc/bar", "/etc/dnf/dnf.conf"])
|
||||
assert out["/etc/foo.conf"] == "modified_conffile"
|
||||
assert out["/etc/bar"] == "modified_packaged_file"
|
||||
# Package-manager config paths are excluded.
|
||||
assert "/etc/dnf/dnf.conf" not in out
|
||||
|
||||
|
||||
def test_specific_paths_for_hints_differs_between_backends():
|
||||
# We can exercise this without instantiating DpkgBackend (which reads dpkg status)
|
||||
class Dummy(platform.PackageBackend):
|
||||
name = "dummy"
|
||||
pkg_config_prefixes = ("/etc/apt/",)
|
||||
|
||||
d = Dummy()
|
||||
assert d.is_pkg_config_path("/etc/apt/sources.list")
|
||||
assert not d.is_pkg_config_path("/etc/ssh/sshd_config")
|
||||
|
||||
r = platform.RpmBackend()
|
||||
paths = set(r.specific_paths_for_hints({"nginx"}))
|
||||
assert "/etc/sysconfig/nginx" in paths
|
||||
assert "/etc/sysconfig/nginx.conf" in paths
|
||||
131
tests/test_rpm.py
Normal file
131
tests/test_rpm.py
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import enroll.rpm as rpm
|
||||
|
||||
|
||||
def test_rpm_owner_returns_none_when_unowned(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
rpm,
|
||||
"_run",
|
||||
lambda cmd, allow_fail=False, merge_err=False: (
|
||||
1,
|
||||
"file /etc/x is not owned by any package\n",
|
||||
),
|
||||
)
|
||||
assert rpm.rpm_owner("/etc/x") is None
|
||||
|
||||
|
||||
def test_rpm_owner_parses_name(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
rpm, "_run", lambda cmd, allow_fail=False, merge_err=False: (0, "bash\n")
|
||||
)
|
||||
assert rpm.rpm_owner("/bin/bash") == "bash"
|
||||
|
||||
|
||||
def test_strip_arch_strips_known_arches():
|
||||
assert rpm._strip_arch("vim-enhanced.x86_64") == "vim-enhanced"
|
||||
assert rpm._strip_arch("foo.noarch") == "foo"
|
||||
assert rpm._strip_arch("weird.token") == "weird.token"
|
||||
|
||||
|
||||
def test_list_manual_packages_prefers_dnf_repoquery(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
rpm.shutil, "which", lambda exe: "/usr/bin/dnf" if exe == "dnf" else None
|
||||
)
|
||||
|
||||
def fake_run(cmd, allow_fail=False, merge_err=False):
|
||||
# First repoquery form returns usable output.
|
||||
if cmd[:3] == ["dnf", "-q", "repoquery"]:
|
||||
return 0, "vim-enhanced.x86_64\nhtop\nvim-enhanced.x86_64\n"
|
||||
raise AssertionError(f"unexpected cmd: {cmd}")
|
||||
|
||||
monkeypatch.setattr(rpm, "_run", fake_run)
|
||||
|
||||
pkgs = rpm.list_manual_packages()
|
||||
assert pkgs == ["htop", "vim-enhanced"]
|
||||
|
||||
|
||||
def test_list_manual_packages_falls_back_to_history(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
rpm.shutil, "which", lambda exe: "/usr/bin/dnf" if exe == "dnf" else None
|
||||
)
|
||||
|
||||
def fake_run(cmd, allow_fail=False, merge_err=False):
|
||||
# repoquery fails
|
||||
if cmd[:3] == ["dnf", "-q", "repoquery"]:
|
||||
return 1, ""
|
||||
if cmd[:3] == ["dnf", "-q", "history"]:
|
||||
return (
|
||||
0,
|
||||
"Installed Packages\nvim-enhanced.x86_64\nLast metadata expiration check: 0:01:00 ago\n",
|
||||
)
|
||||
raise AssertionError(f"unexpected cmd: {cmd}")
|
||||
|
||||
monkeypatch.setattr(rpm, "_run", fake_run)
|
||||
|
||||
pkgs = rpm.list_manual_packages()
|
||||
assert pkgs == ["vim-enhanced"]
|
||||
|
||||
|
||||
def test_build_rpm_etc_index_uses_fallback_when_rpm_output_mismatches(monkeypatch):
|
||||
# Two files in /etc, one owned, one unowned.
|
||||
monkeypatch.setattr(
|
||||
rpm, "_walk_etc_files", lambda: ["/etc/owned.conf", "/etc/unowned.conf"]
|
||||
)
|
||||
|
||||
# Simulate chunk query producing unexpected extra line (mismatch) -> triggers per-file fallback.
|
||||
monkeypatch.setattr(
|
||||
rpm,
|
||||
"_run",
|
||||
lambda cmd, allow_fail=False, merge_err=False: (0, "ownedpkg\nEXTRA\nTHIRD\n"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
rpm, "rpm_owner", lambda p: "ownedpkg" if p == "/etc/owned.conf" else None
|
||||
)
|
||||
|
||||
owned, owner_map, topdir_to_pkgs, pkg_to_etc = rpm.build_rpm_etc_index()
|
||||
|
||||
assert owned == {"/etc/owned.conf"}
|
||||
assert owner_map["/etc/owned.conf"] == "ownedpkg"
|
||||
assert "owned.conf" in topdir_to_pkgs
|
||||
assert pkg_to_etc["ownedpkg"] == ["/etc/owned.conf"]
|
||||
|
||||
|
||||
def test_build_rpm_etc_index_parses_chunk_output(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
rpm, "_walk_etc_files", lambda: ["/etc/ssh/sshd_config", "/etc/notowned"]
|
||||
)
|
||||
|
||||
def fake_run(cmd, allow_fail=False, merge_err=False):
|
||||
# One output line per input path.
|
||||
return 0, "openssh-server\nfile /etc/notowned is not owned by any package\n"
|
||||
|
||||
monkeypatch.setattr(rpm, "_run", fake_run)
|
||||
|
||||
owned, owner_map, topdir_to_pkgs, pkg_to_etc = rpm.build_rpm_etc_index()
|
||||
|
||||
assert "/etc/ssh/sshd_config" in owned
|
||||
assert "/etc/notowned" not in owned
|
||||
assert owner_map["/etc/ssh/sshd_config"] == "openssh-server"
|
||||
assert "ssh" in topdir_to_pkgs
|
||||
assert "openssh-server" in topdir_to_pkgs["ssh"]
|
||||
assert pkg_to_etc["openssh-server"] == ["/etc/ssh/sshd_config"]
|
||||
|
||||
|
||||
def test_rpm_config_files_and_modified_files_parsing(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
rpm,
|
||||
"_run",
|
||||
lambda cmd, allow_fail=False, merge_err=False: (
|
||||
0,
|
||||
"/etc/foo.conf\n/usr/bin/tool\n",
|
||||
),
|
||||
)
|
||||
assert rpm.rpm_config_files("mypkg") == {"/etc/foo.conf", "/usr/bin/tool"}
|
||||
|
||||
# rpm -V returns only changed/missing files
|
||||
out = "S.5....T. c /etc/foo.conf\nmissing /etc/bar\n"
|
||||
monkeypatch.setattr(
|
||||
rpm, "_run", lambda cmd, allow_fail=False, merge_err=False: (1, out)
|
||||
)
|
||||
assert rpm.rpm_modified_files("mypkg") == {"/etc/foo.conf", "/etc/bar"}
|
||||
Loading…
Add table
Add a link
Reference in a new issue