Merge pull request #236 from living-ip/fix/systemd-252-first-install
Some checks are pending
CI / lint-and-test (push) Waiting to run

Handle systemd 252 first-install omissions
This commit is contained in:
Fawaz 2026-07-23 09:04:28 -07:00 committed by GitHub
commit 0701db4aa5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 31 additions and 3 deletions

View file

@ -694,10 +694,14 @@ def _parse_properties(raw: bytes) -> dict[str, str]:
separator == "=" and key in SERVICE_PROPERTIES and key not in value, "systemd property output was invalid" separator == "=" and key in SERVICE_PROPERTIES and key not in value, "systemd property output was invalid"
) )
value[key] = item value[key] = item
# systemd 252 omits EnvironmentFiles entirely when its string array is # systemd 252 omits empty array/command properties. For a nonexistent
# empty. A nonempty array is emitted and remains subject to the exact # service, there cannot be an effective command to conceal, so normalize
# no-EnvironmentFile checks below. # the three omitted Exec* properties to their explicit empty form. Loaded
# units must still report every command property for exact verification.
value.setdefault("EnvironmentFiles", "") value.setdefault("EnvironmentFiles", "")
if value.get("LoadState") == "not-found":
for property_name in SERVICE_EXEC_PROPERTIES:
value.setdefault(property_name, "")
_require(set(value) == set(SERVICE_PROPERTIES), "systemd property output was incomplete") _require(set(value) == set(SERVICE_PROPERTIES), "systemd property output was incomplete")
return value return value

View file

@ -118,6 +118,7 @@ class FakeHost:
self.race_applied = False self.race_applied = False
self.property_overrides: dict[str, str] = {} self.property_overrides: dict[str, str] = {}
self.omitted_properties: set[str] = set() self.omitted_properties: set[str] = set()
self.not_found_omitted_properties: set[str] = set()
self.not_found_security_defaults: dict[str, str] = {} self.not_found_security_defaults: dict[str, str] = {}
self.unstable_readbacks = False self.unstable_readbacks = False
self.show_count = 0 self.show_count = 0
@ -241,6 +242,7 @@ class FakeHost:
f"{name}={value[name]}\n" f"{name}={value[name]}\n"
for name in installer.SERVICE_PROPERTIES for name in installer.SERVICE_PROPERTIES
if name not in self.omitted_properties if name not in self.omitted_properties
and (loaded or name not in self.not_found_omitted_properties)
).encode() ).encode()
return subprocess.CompletedProcess(arguments, 0, stdout=payload, stderr=b"") return subprocess.CompletedProcess(arguments, 0, stdout=payload, stderr=b"")
@ -475,6 +477,28 @@ def test_systemd_252_omitted_environment_files_property_is_normalized(tmp_path:
assert host.restart_calls == 1 assert host.restart_calls == 1
def test_systemd_252_omitted_not_found_commands_are_normalized(tmp_path: Path) -> None:
bundle, release, image = release_bundle(tmp_path, "2")
paths = install_paths(tmp_path)
host = FakeHost(paths)
host.add_release(release, image)
host.not_found_security_defaults = dict(PRODUCTION_SHAPED_NOT_FOUND_SECURITY)
host.not_found_omitted_properties.update({"EnvironmentFiles", *installer.SERVICE_EXEC_PROPERTIES})
receipt = installer.install_release(
bundle,
paths=paths,
source_binding=source_binding(),
runner=host,
sleeper=lambda _seconds: None,
execute_restart=True,
)
assert receipt["status"] == "pass"
assert receipt["release_sha256"] == release["release_sha256"]
assert host.restart_calls == 1
def test_systemd_252_omission_does_not_hide_a_later_dropin(tmp_path: Path) -> None: def test_systemd_252_omission_does_not_hide_a_later_dropin(tmp_path: Path) -> None:
bundle, release, image = release_bundle(tmp_path, "2") bundle, release, image = release_bundle(tmp_path, "2")
paths = install_paths(tmp_path) paths = install_paths(tmp_path)