chore: Python 3.13, mypy --strict in CI, eight bug fixes, docs
Squashes three changes: ## chore: support Python 3.13 and make mypy --strict pass in CI Add 3.13 to the CI matrix and classifiers, run mypy in CI, fix the 14 strict errors it reported, and correct the README's minimum Python. ## fix: eight bugs found in a full review, each with a regression test - channel: run_on_transport lost output sshd sent after the exit status; it now reads stdout and stderr to EOF before returning. - ssh_server: a failed way back (restore or reload) raised out of harden_ssh_server after the host was touched; it is now reported in the result, with rolled_back False. - host_status: a newer RPM kernel (RHEL, Fedora) was never seen, because its release, not its arch, followed the last "-". - package_updates: an apt line wrapped at a blank lost the blank, and with it the package. - kernel: an empty payload (gzip or base64 missing) read as a report. - listening: "[fe80::1]%eth0:546", as ss prints it, kept its brackets. - ping_sweep: a reply whose "success" was not a dict aborted the sweep. - lag: a member of unknown speed (-1) lowered the LAG's speed. ## docs: bring the README and docstrings in line with the code The firewall worked example showed the runtime stubs the README forbids; the class table missed PhoneDriver and MediaDriver; ConfigLifecycleMixin is mixed in by drivers, not by a role base; services, mac_acl and packages named a class, a key and an exception that do not exist.
This commit is contained in:
1 parent
e58b65a3db
commit
dd8977750d
25 files changed
+163
-52
No files matched your search
@@ -167,3 +167,20 @@ def test_close_closes_the_channel():
|
||||
ParamikoExecStream(ch).close()
|
||||
|
||||
assert ch.closed
|
||||
|
||||
|
||||
def test_run_keeps_output_that_arrives_after_the_exit_status():
|
||||
"""sshd may send exit-status before the last stdout bytes."""
|
||||
|
||||
class EarlyExit(FakeChannel):
|
||||
def exit_status_ready(self):
|
||||
return True
|
||||
|
||||
def recv_ready(self):
|
||||
return False
|
||||
|
||||
channel = EarlyExit(stdout=[b"head ", b"tail"], stderr=[b"warn"])
|
||||
|
||||
result = run_on_transport(FakeTransport(channel), "cmd")
|
||||
|
||||
assert result == CommandResult("head tail", "warn", 0)
|
||||
@@ -131,6 +131,13 @@ class TestKernelRebootPending:
|
||||
("7.0.14-19-pve", ["7.0.14-19-pve", "7.0.2-6-pve"], None),
|
||||
# Arch: the running kernel's modules were replaced by the upgrade.
|
||||
("6.10.5-arch1-1", ["6.10.9-arch1-1"], "6.10.9-arch1-1"),
|
||||
# RHEL/Fedora: the release changes with every update, the arch does not.
|
||||
(
|
||||
"5.14.0-427.13.1.el9_4.x86_64",
|
||||
["5.14.0-427.13.1.el9_4.x86_64", "5.14.0-503.11.1.el9_5.x86_64"],
|
||||
"5.14.0-503.11.1.el9_5.x86_64",
|
||||
),
|
||||
("6.9.7-200.fc40.x86_64", ["6.9.7-200.fc40.x86_64"], None),
|
||||
],
|
||||
)
|
||||
def test_the_newer_kernel_of_the_running_flavour(self, running, installed, newer):
|
||||
|
||||
@@ -85,6 +85,14 @@ class TestParsing:
|
||||
with pytest.raises(ValueError):
|
||||
parse_kernel_facts("sh: gzip: not found\n")
|
||||
|
||||
def test_an_empty_payload_raises(self):
|
||||
"""gzip or base64 missing on the host: the frame is there, the report is not."""
|
||||
with pytest.raises(ValueError):
|
||||
parse_kernel_facts("KFACTS_BEGIN\nKFACTS_END\n")
|
||||
empty = base64.b64encode(gzip.compress(b"")).decode()
|
||||
with pytest.raises(ValueError):
|
||||
parse_kernel_facts(f"KFACTS_BEGIN\n{empty}\nKFACTS_END\n")
|
||||
|
||||
def test_a_damaged_payload_raises(self):
|
||||
with pytest.raises(ValueError):
|
||||
parse_kernel_facts("KFACTS_BEGIN\nnot base64 at all!\nKFACTS_END\n")
|
||||
|
||||
@@ -74,3 +74,12 @@ def test_does_not_modify_its_input():
|
||||
add_lag_interfaces(ifaces)
|
||||
|
||||
assert list(ifaces) == ["3"]
|
||||
|
||||
|
||||
def test_an_unknown_member_speed_does_not_make_the_lag_negative():
|
||||
ifaces = {
|
||||
"3": _port(speed=-1.0, trunk_group="Trk3"),
|
||||
"4": _port(speed=1000.0, trunk_group="Trk3"),
|
||||
}
|
||||
|
||||
assert add_lag_interfaces(ifaces)["Trk3"]["speed"] == 1000.0
|
||||
@@ -84,6 +84,7 @@ class TestParsing:
|
||||
("0.0.0.0%ens18:68", "0.0.0.0", "ens18"),
|
||||
("[::ffff:127.0.0.1]:8125", "::ffff:127.0.0.1", None),
|
||||
("[fe80::1%eth0]:546", "fe80::1", "eth0"),
|
||||
("[fe80::1]%eth0:546", "fe80::1", "eth0"), # what iproute2 ss prints
|
||||
(":::22", "::", None), # iproute2 4.9 prints IPv6 without brackets
|
||||
],
|
||||
)
|
||||
|
||||
@@ -81,6 +81,16 @@ class TestAptUpgradable:
|
||||
"3.0.13-0ubuntu3.5"
|
||||
)
|
||||
|
||||
def test_a_line_wrapped_at_a_blank_keeps_the_blank(self):
|
||||
wrapped = (
|
||||
"openssl/noble-updates 3.0.13-0ubuntu3.6\n"
|
||||
" amd64 [upgradable from: 3.0.13-0ubuntu3.5]\n__APT_RC=0\n"
|
||||
)
|
||||
|
||||
assert _by_name(parse_apt_upgradable(wrapped))["openssl"]["current_version"] == (
|
||||
"3.0.13-0ubuntu3.5"
|
||||
)
|
||||
|
||||
def test_one_package_for_several_architectures_is_one_entry(self):
|
||||
multiarch = (
|
||||
"libc6/noble-updates 2.39-0ubuntu8.5 amd64 [upgradable from: 2.39-0ubuntu8.4]\n"
|
||||
|
||||
@@ -219,3 +219,10 @@ def test_ping_sweep_stops_when_stop_requested():
|
||||
|
||||
assert result["scanned"] == 1
|
||||
assert len(driver.calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("success", [True, "yes", ["x"]])
|
||||
def test_a_malformed_success_is_no_answer_not_an_abort(success):
|
||||
entry = PingSweepMixin._parse_ping_reply("192.0.2.1", {"success": success})
|
||||
|
||||
assert entry == {"ip": "192.0.2.1", "alive": False, "rtt_ms": None, "error": "malformed ping reply"}
|
||||
@@ -335,3 +335,17 @@ def test_restoring_an_earlier_dropin_leaks_nothing_either():
|
||||
host.files[DROPIN_PATH] = "# netOrk\nHostKeyAlgorithms ssh-ed25519\n"
|
||||
host.harden_ssh_server({"mac": ["hmac-sha1"]}, verify=lambda: False)
|
||||
assert host.files[DROPIN_PATH] == "# netOrk\nHostKeyAlgorithms ssh-ed25519\n"
|
||||
|
||||
|
||||
def test_mixin_reports_a_failed_way_back_instead_of_raising():
|
||||
class ReloadFails(FakeHost):
|
||||
def run_command(self, command, **kwargs):
|
||||
if "reload" in command or "HUP" in command:
|
||||
return CommandResult("", "Failed to reload ssh.service", 1)
|
||||
return super().run_command(command, **kwargs)
|
||||
|
||||
host = ReloadFails()
|
||||
result = host.harden_ssh_server({"mac": ["hmac-sha1"]})
|
||||
|
||||
assert not result.applied and not result.rolled_back
|
||||
assert "putting the previous configuration back failed" in result.message
|
||||
Reference in new issue
Block a user