Fix notification of individual services when related config changes, even when roles are grouped
All checks were successful
CI / test (push) Successful in 19m18s
Lint / test (push) Successful in 42s

This commit is contained in:
Miguel Jacq 2026-06-20 15:31:42 +10:00
parent 08066595f1
commit 097022f782
Signed by: mig5
GPG key ID: 03906B4110AAD3B8
8 changed files with 472 additions and 62 deletions

View file

@ -38,3 +38,70 @@ def test_cm_module_uses_shared_state_io(tmp_path):
assert CMModule.state_path(tmp_path) == written
assert CMModule.load_state(tmp_path) == state
assert CMModule._load_state(tmp_path) == state
def test_active_service_units_for_package_snapshot_is_conservative():
entries = [
{
"kind": "service",
"snapshot": {
"unit": "docker.service",
"role_name": "docker",
"packages": ["docker.io"],
"active_state": "active",
},
},
{
"kind": "service",
"snapshot": {
"unit": "docker-cleanup.service",
"role_name": "docker_cleanup",
"packages": ["docker.io"],
"active_state": "inactive",
},
},
]
by_package = CMModule.active_service_units_by_package(entries)
assert by_package == {
"docker.io": [{"unit": "docker.service", "role_name": "docker"}]
}
assert CMModule.active_service_units_for_package_snapshot(
{"package": "docker.io", "role_name": "docker"}, by_package
) == ["docker.service"]
def test_active_service_units_for_package_snapshot_avoids_ambiguous_restarts():
entries = [
{
"kind": "service",
"snapshot": {
"unit": "alpha.service",
"role_name": "alpha",
"packages": ["shared"],
"active_state": "active",
},
},
{
"kind": "service",
"snapshot": {
"unit": "beta.service",
"role_name": "beta",
"packages": ["shared"],
"active_state": "active",
},
},
]
by_package = CMModule.active_service_units_by_package(entries)
assert (
CMModule.active_service_units_for_package_snapshot(
{"package": "shared", "role_name": "shared"}, by_package
)
== []
)
assert CMModule.active_service_units_for_package_snapshot(
{"package": "shared", "role_name": "beta"}, by_package
) == ["beta.service"]

View file

@ -284,6 +284,12 @@ def test_manifest_writes_roles_and_playbook_with_clean_when(tmp_path: Path):
assert "foo_systemd_enabled: true" in defaults
assert "foo_systemd_state: stopped" in defaults
handlers = (out / "roles" / "foo" / "handlers" / "main.yml").read_text(
encoding="utf-8"
)
assert "- name: Restart service" in handlers
assert "state: restarted" in handlers
# Playbook should include users, etc_custom, packages, and services
pb = (out / "playbook.yml").read_text(encoding="utf-8")
assert "role: users" in pb
@ -626,6 +632,154 @@ def test_manifest_groups_systemd_units_into_common_role(tmp_path: Path):
tasks = (out / "roles" / "net" / "tasks" / "main.yml").read_text(encoding="utf-8")
assert "Ensure grouped unit enablement matches harvest" in tasks
assert 'no_log: "{{ enroll_hide_systemd_status | default(true) | bool }}"' in tasks
assert "Restart managed services" not in tasks
defaults_text = (out / "roles" / "net" / "defaults" / "main.yml").read_text(
encoding="utf-8"
)
assert "notify:" in defaults_text
assert "- Restart managed service NetworkManager.service" in defaults_text
assert (
"Restart managed service NetworkManager-dispatcher.service" not in defaults_text
)
handlers = (out / "roles" / "net" / "handlers" / "main.yml").read_text(
encoding="utf-8"
)
assert "Run systemd daemon-reload" in handlers
assert "- name: Restart managed service NetworkManager.service" in handlers
assert "name: NetworkManager.service" in handlers
assert "state: restarted" in handlers
assert "Restart managed services" not in handlers
assert "Restart managed service NetworkManager-dispatcher.service" not in handlers
def test_manifest_common_package_file_notifies_matching_active_service(tmp_path: Path):
bundle = tmp_path / "bundle"
out = tmp_path / "ansible"
(bundle / "artifacts" / "docker" / "etc" / "docker").mkdir(
parents=True, exist_ok=True
)
(bundle / "artifacts" / "docker" / "etc" / "docker" / "daemon.json").write_text(
'{"log-driver":"json-file"}\n', encoding="utf-8"
)
state = {
"schema_version": 3,
"host": {"hostname": "test", "os": "debian", "pkg_backend": "dpkg"},
"inventory": {
"packages": {
"docker.io": {
"version": "1.0",
"arches": ["amd64"],
"installations": [
{"version": "1.0", "arch": "amd64", "section": "admin"}
],
"section": "admin",
"observed_via": [
{"kind": "systemd_unit", "ref": "docker.service"},
{"kind": "package_role", "ref": "docker"},
],
"roles": ["docker"],
}
}
},
"roles": {
"users": {
"role_name": "users",
"users": [],
"managed_files": [],
"excluded": [],
"notes": [],
},
"services": [
{
"unit": "docker.service",
"role_name": "docker",
"packages": ["docker.io"],
"active_state": "active",
"sub_state": "running",
"unit_file_state": "enabled",
"condition_result": "yes",
"managed_files": [],
"managed_dirs": [],
"managed_links": [],
"excluded": [],
"notes": [],
}
],
"packages": [
{
"package": "docker.io",
"role_name": "docker",
"section": "admin",
"has_config": True,
"managed_files": [
{
"path": "/etc/docker/daemon.json",
"src_rel": "etc/docker/daemon.json",
"owner": "root",
"group": "root",
"mode": "0644",
"reason": "modified_conffile",
}
],
"managed_dirs": [],
"managed_links": [],
"excluded": [],
"notes": [],
}
],
"apt_config": {
"role_name": "apt_config",
"managed_files": [],
"excluded": [],
"notes": [],
},
"dnf_config": {
"role_name": "dnf_config",
"managed_files": [],
"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": [],
},
},
}
_write_state(bundle, state)
manifest.manifest(str(bundle), str(out))
defaults = (out / "roles" / "admin" / "defaults" / "main.yml").read_text(
encoding="utf-8"
)
assert "dest: /etc/docker/daemon.json" in defaults
assert "- Restart managed service docker.service" in defaults
handlers = (out / "roles" / "admin" / "handlers" / "main.yml").read_text(
encoding="utf-8"
)
assert "- name: Restart managed service docker.service" in handlers
assert "name: docker.service" in handlers
assert "Restart managed services" not in handlers
def test_manifest_fqdn_implies_no_common_roles(tmp_path: Path):

View file

@ -184,6 +184,9 @@ def test_manifest_puppet_writes_control_repo_style_output(tmp_path: Path):
assert node_data["foo::files"]["/etc/foo/foo.conf"]["source"] == (
"puppet:///modules/foo/nodes/test.example/etc/foo.conf"
)
assert node_data["foo::files"]["/etc/foo/foo.conf"]["notify"] == (
"Service['foo.service']"
)
assert node_data["foo::services"]["foo.service"] == {
"ensure": "running",
"enable": True,
@ -483,6 +486,7 @@ def test_manifest_puppet_uses_default_node_and_common_package_modules(tmp_path:
assert "package { 'foo':" in net_pp
assert "file { '/etc/foo/foo.conf':" in net_pp
assert "source => 'puppet:///modules/net/etc/foo.conf'" in net_pp
assert "notify => Service['foo.service']" in net_pp
assert "service { 'foo.service':" in net_pp
assert (out / "modules" / "net" / "files" / "etc" / "foo.conf").exists()
assert not (out / "modules" / "curl").exists()

View file

@ -164,6 +164,8 @@ def test_manifest_salt_writes_single_site_state_tree(tmp_path: Path):
assert '- name: "foo"' in net_sls
assert '"/etc/foo/foo.conf":' in net_sls
assert 'source: "salt://roles/net/files/etc/foo.conf"' in net_sls
assert "watch_in:" in net_sls
assert 'service: "enroll_service_net_foo_service_20435514"' in net_sls
assert "file.symlink:" in net_sls
assert "service.running:" in net_sls
assert (out / "states" / "roles" / "net" / "files" / "etc" / "foo.conf").exists()
@ -531,6 +533,9 @@ def test_manifest_salt_uses_jinjaturtle_templates(monkeypatch, tmp_path: Path):
pillar = yaml.safe_load(pillar_path.with_suffix(".sls").read_text(encoding="utf-8"))
file_data = pillar["enroll"]["roles"]["foo"]["files"]["/etc/foo/foo.conf"]
assert file_data["source"] == "salt://roles/foo/templates/etc/foo.conf.j2"
assert file_data["watch_in"] == [
{"service": "enroll_service_foo_foo_service_20435514"}
]
assert file_data["template"] == "jinja"
assert file_data["context"] == {"foo_setting": True}