diff options
| author | Craig Jennings <c@cjennings.net> | 2026-08-19 12:32:02 -0700 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-08-19 12:32:02 -0700 |
| commit | ec3a63caca4f2d955e594318a9a690e4c28af19e (patch) | |
| tree | 4ad5a438b38f92d49dbbcd2dd22e950831535f37 /tests/post-rebuild-check | |
| parent | afbf011aa0937b5702b6d8c1bfca0809ed809425 (diff) | |
| download | archsetup-ec3a63caca4f2d955e594318a9a690e4c28af19e.tar.gz archsetup-ec3a63caca4f2d955e594318a9a690e4c28af19e.zip | |
fix(check): read NTP sources only from files chrony is told to read
Check 6 globbed /etc/chrony.d unconditionally. A drop-in is inert unless chrony.conf names its directory, and Arch's stock chrony.conf names none, so a machine with the IP-addressed source on disk and no confdir line would show the literal and pass. That is a false pass on exactly the misconfiguration the check exists to catch, and it describes a file chrony never opens.
Sources now come only from chrony.conf plus whatever confdir or sourcedir it actually names. The config path is a seam so the confdir logic can be tested against a fixture instead of the real /etc.
This should have been in the previous commit, whose message already describes it. I staged before reviewing, fixed the finding, then committed the stale index.
Diffstat (limited to 'tests/post-rebuild-check')
| -rw-r--r-- | tests/post-rebuild-check/test_post_rebuild_check.py | 65 |
1 files changed, 65 insertions, 0 deletions
diff --git a/tests/post-rebuild-check/test_post_rebuild_check.py b/tests/post-rebuild-check/test_post_rebuild_check.py index 757039b..bad337e 100644 --- a/tests/post-rebuild-check/test_post_rebuild_check.py +++ b/tests/post-rebuild-check/test_post_rebuild_check.py @@ -30,6 +30,7 @@ Run from repo root: """ import os +import shutil import subprocess import tempfile import time @@ -124,6 +125,70 @@ class NtpBootstrap(unittest.TestCase): self.assertIn("no NTP sources are configured", r.stdout) self.assertEqual(r.returncode, 1) + # --- the confdir false pass ------------------------------------------ + # + # A drop-in is inert unless chrony.conf names its directory, and Arch's + # stock chrony.conf names none. Reading the drop-in without checking for + # confdir would find the IP-addressed source, call the machine healthy, and + # be describing a file chrony never opens — a false pass on exactly the + # misconfiguration this check exists to catch. + + def _chrony_fixture(self, main_lines, dropin_lines=None): + """Write a chrony.conf (plus an adjacent drop-in dir) and return its path.""" + d = tempfile.mkdtemp(prefix="prc-chrony-") + self.addCleanup(shutil.rmtree, d, True) + dropin_dir = os.path.join(d, "chrony.d") + os.makedirs(dropin_dir) + if dropin_lines is not None: + with open(os.path.join(dropin_dir, "10-bootstrap-ip-ntp.conf"), "w") as f: + f.write(dropin_lines) + conf = os.path.join(d, "chrony.conf") + with open(conf, "w") as f: + f.write(main_lines.replace("@DROPIN@", dropin_dir)) + return conf + + def _run_real_probe(self, chrony_conf): + """Run with PRC_NTP_SOURCES unset so the real chrony reader runs.""" + env = dict(os.environ) + env.update({"PRC_FAILED_UNITS": "", "PRC_UNIT_STATES": "", + "PRC_LOCAL_SCAN_ROOTS": "", "PRC_PROJECT_ROOTS": "", + "PRC_SIGNAL_ACCOUNTS": "+15045551234", + "PRC_CHRONY_CONF": chrony_conf}) + env.pop("PRC_NTP_SOURCES", None) + return subprocess.run(["sh", CHECK], capture_output=True, text=True, + timeout=30, env=env) + + def test_dropin_without_confdir_does_not_count(self): + # The regression. The IP-addressed source is present on disk but + # chrony.conf never points at it, so the machine is still deadlock-prone + # and the check has to say so. + conf = self._chrony_fixture("pool 2.arch.pool.ntp.org iburst\n", + "server 162.159.200.1 iburst\n") + r = self._run_real_probe(conf) + if "no NTP implementation is active" in r.stdout: + self.skipTest("no chronyd on this host — the reader branch can't run") + self.assertIn("every NTP source is named by hostname", r.stdout) + + def test_dropin_with_confdir_counts(self): + # The same two files, with chrony.conf actually naming the directory. + conf = self._chrony_fixture( + "pool 2.arch.pool.ntp.org iburst\nconfdir @DROPIN@\n", + "server 162.159.200.1 iburst\n") + r = self._run_real_probe(conf) + if "no NTP implementation is active" in r.stdout: + self.skipTest("no chronyd on this host — the reader branch can't run") + self.assertIn("check 6/6: NTP bootstrap — ok", r.stdout) + + def test_confdir_naming_an_empty_directory_is_not_a_pass(self): + # confdir present, nothing behind it: the sources are the hostname-only + # main file, so the finding stands. + conf = self._chrony_fixture( + "pool 2.arch.pool.ntp.org iburst\nconfdir @DROPIN@\n", None) + r = self._run_real_probe(conf) + if "no NTP implementation is active" in r.stdout: + self.skipTest("no chronyd on this host — the reader branch can't run") + self.assertIn("every NTP source is named by hostname", r.stdout) + def test_unset_seam_falls_through_to_the_real_probe(self): # Same contract as every other seam: unset means "really look", so a # caller who forgets the variable cannot silently skip the check. |
