From 1bb8608e8865fa9a1c989ee141e3c0aac9eada8b Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Sun, 4 Oct 2026 11:08:43 +0200 Subject: [PATCH] os-apply: tell an UPDATED conffile from a KEPT one (dpkg's two shapes, measured live on demo-hp) Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS --- configs/felhom-os-apply | 15 +++++++++++++-- configs/test_felhom_os_apply.py | 17 ++++++++++++++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/configs/felhom-os-apply b/configs/felhom-os-apply index 016022d..ab51c46 100755 --- a/configs/felhom-os-apply +++ b/configs/felhom-os-apply @@ -389,10 +389,21 @@ class Apply: t0 = time.time() rc, out, err = self.g(APT_ENV + ["apt-get", "-y", "-q"] + DPKG_OPTS + args) secs = time.time() - t0 + # dpkg says "Installing new version of config file X" when X was NOT changed locally (the package's new + # version is taken), and "Configuration file 'X'" + "Keeping old config file" when it was (--force-confold + # keeps the local one; the package's version lands as X.dpkg-dist). Measured live 2026-10-04 (debian_version). + conflict = None for l in (out + err).splitlines(): - m = re.search(r"Installing new version of config file (\S+)|Configuration file '([^']+)'", l) + m = re.search(r"Installing new version of config file (\S+?)\s*\.\.\.", l) if m: - log(f"os-apply: CONFFILE kept {m.group(1) or m.group(2)}") + log(f"os-apply: CONFFILE updated {m.group(1)} (it was not changed locally)") + m = re.search(r"Configuration file '([^']+)'", l) + if m: + conflict = m.group(1) + if conflict and "Keeping old config file" in l: + log(f"os-apply: CONFFILE kept {conflict} (changed locally; the package's version is {conflict}.dpkg-dist)") + self.report.setdefault("conffiles_kept", []).append(conflict) + conflict = None self.g(["apt-get", "clean"]) if rc != 0: _, aud, _ = self.g(["dpkg", "--audit"]) diff --git a/configs/test_felhom_os_apply.py b/configs/test_felhom_os_apply.py index 13cb85e..3ed3d28 100644 --- a/configs/test_felhom_os_apply.py +++ b/configs/test_felhom_os_apply.py @@ -136,7 +136,7 @@ class Fake: if "=" in x and not x.startswith("-") and "::" not in x: n, v = x.split("=", 1) self.installed[n] = v - return 0, "Setting up libc6 ...\n", "" + return 0, getattr(self, "install_out", "Setting up libc6 ...\n"), "" if cmd == "df": return 0, f"Avail\n{self.free}\n", "" if cmd == "docker": @@ -438,6 +438,21 @@ class Refusals(unittest.TestCase): self.refused(f, "R13") +class Conffiles(unittest.TestCase): + # dpkg's two shapes, measured live 2026-10-04: an unchanged file is UPDATED; a locally changed one is KEPT. + def test_updated_vs_kept(self): + f = Fake() + f.install_out = ("Installing new version of config file /etc/debian_version ...\n" + "Configuration file '/etc/ssh/sshd_config'\n ==> Modified (by you or by a script) since installation.\n" + " ==> Keeping old config file as default.\n") + rc, rep = run(f) + self.assertEqual(rc, 0, rep) + self.assertIn("os-apply: CONFFILE updated /etc/debian_version (it was not changed locally)", f.logs) + self.assertTrue(any(l.startswith("os-apply: CONFFILE kept /etc/ssh/sshd_config") for l in f.logs), f.logs) + self.assertEqual(rep["conffiles_kept"], ["/etc/ssh/sshd_config"]) + self.assertFalse(any("kept /etc/debian_version" in l for l in f.logs), "an updated file must not be reported as kept") + + class Failure(unittest.TestCase): def test_install_failure_is_rc3_with_dpkg_state(self): f = Fake()