From 91a2314e2633b361178057be98ffffa0d8742727 Mon Sep 17 00:00:00 2001 From: Kunal Mehta Date: Thu, 7 Nov 2024 15:48:10 -0500 Subject: [PATCH] Add a basic noble migration check script Perform a number of checks to ensure the system is ready for the noble migration. The results are written to a JSON file in /etc/ that other things like the JI and the upgrade script itself can read from. The script is run hourly on a systemd timer but can also be run interactively for administrators who want slightly more details. Refs #7322. --- .../testinfra/common/test_release_upgrades.py | 20 +++ .../securedrop-noble-migration-check.service | 7 + .../securedrop-noble-migration-check.timer | 10 ++ .../bin/securedrop-noble-migration-check.py | 137 ++++++++++++++++++ securedrop/debian/rules | 2 + 5 files changed, 176 insertions(+) create mode 100644 securedrop/debian/config/lib/systemd/system/securedrop-noble-migration-check.service create mode 100644 securedrop/debian/config/lib/systemd/system/securedrop-noble-migration-check.timer create mode 100755 securedrop/debian/config/usr/bin/securedrop-noble-migration-check.py diff --git a/molecule/testinfra/common/test_release_upgrades.py b/molecule/testinfra/common/test_release_upgrades.py index 5eb87a2c3b..eb66e8c05f 100644 --- a/molecule/testinfra/common/test_release_upgrades.py +++ b/molecule/testinfra/common/test_release_upgrades.py @@ -1,3 +1,6 @@ +import time + +import pytest import testutils test_vars = testutils.securedrop_test_vars @@ -27,3 +30,20 @@ def test_release_manager_upgrade_channel(host): _, channel = raw_output.split("=") assert channel == "never" + + +def test_migration_check(host): + """Verify our migration check script works""" + if host.system_info.codename != "focal": + pytest.skip("only applicable/testable on focal") + + with host.sudo(): + # remove state file so we can see if it works + if host.file("/etc/securedrop-noble-migration.json").exists: + host.run("rm /etc/securedrop-noble-migration.json") + cmd = host.run("systemctl start securedrop-noble-migration-check") + assert cmd.rc == 0 + while host.service("securedrop-noble-migration-check").is_running: + time.sleep(1) + + assert host.file("/etc/securedrop-noble-migration.json").exists diff --git a/securedrop/debian/config/lib/systemd/system/securedrop-noble-migration-check.service b/securedrop/debian/config/lib/systemd/system/securedrop-noble-migration-check.service new file mode 100644 index 0000000000..09039ca998 --- /dev/null +++ b/securedrop/debian/config/lib/systemd/system/securedrop-noble-migration-check.service @@ -0,0 +1,7 @@ +[Unit] +Description=Check noble migration readiness + +[Service] +Type=oneshot +ExecStart=/usr/bin/securedrop-noble-migration-check.py +User=root diff --git a/securedrop/debian/config/lib/systemd/system/securedrop-noble-migration-check.timer b/securedrop/debian/config/lib/systemd/system/securedrop-noble-migration-check.timer new file mode 100644 index 0000000000..1fcdcd0ac2 --- /dev/null +++ b/securedrop/debian/config/lib/systemd/system/securedrop-noble-migration-check.timer @@ -0,0 +1,10 @@ +[Unit] +Description=Check noble migration readiness + +[Timer] +OnCalendar=hourly +Persistent=true +RandomizedDelaySec=5m + +[Install] +WantedBy=timers.target diff --git a/securedrop/debian/config/usr/bin/securedrop-noble-migration-check.py b/securedrop/debian/config/usr/bin/securedrop-noble-migration-check.py new file mode 100755 index 0000000000..50c8e6db7e --- /dev/null +++ b/securedrop/debian/config/usr/bin/securedrop-noble-migration-check.py @@ -0,0 +1,137 @@ +#!/usr/bin/python3 +""" +Check migration of a SecureDrop server from focal to noble + +This script is run as root on both the app and mon servers. +""" + +import grp +import json +import os +import shutil +import subprocess +import sys +from pathlib import Path +from urllib.parse import urlparse + + +def os_codename() -> str: + with open("/etc/os-release") as f: + os_release = f.readlines() + for line in os_release: + if line.startswith("VERSION_CODENAME="): + return line.split("=")[1].strip().strip('"') + raise ValueError("Could not determine Ubuntu codename") + + +def check_ssh_group() -> bool: + """Check the UNIX "ssh" group is empty""" + try: + group = grp.getgrnam("ssh") + except KeyError: + print("ssh: group does not exist") + return True + + if len(group.gr_mem) > 0: + print(f"ssh: group is not empty: {group.gr_mem}") + return False + + print("ssh: group is empty") + return True + + +def check_ufw_removed() -> bool: + """Check that ufw is removed""" + if Path("/usr/sbin/ufw").exists(): + print("ufw: ufw is still installed") + return False + else: + print("ufw: ufw was removed") + return True + + +def check_free_space() -> bool: + """Check the root partition has more than 10G free""" + root = shutil.disk_usage("/") + if root.free < (10 * 1024 * 1024 * 1024): + print("free space: not enough free space") + return False + else: + print("free space: enough free space") + return True + + +def check_apt() -> bool: + """Verify only expected sources are configured""" + try: + output = subprocess.check_output(["apt-get", "indextargets"], text=True) + except subprocess.CalledProcessError: + print("apt: error invoking apt-get indextargets") + return False + + for line in output.splitlines(): + if line.startswith("URI:"): + parsed = urlparse(line.split(":", 1)[1].strip()) + if parsed.hostname not in [ + "archive.ubuntu.com", + "security.ubuntu.com", + "apt.freedom.press", + "apt-test.freedom.press", + ]: + print(f"apt: unexpected source: {parsed.hostname}") + return False + + print("apt: all sources are expected") + return True + + +def check_systemd() -> bool: + """ "No systemd units are failed""" + try: + subprocess.check_output(["systemctl", "is-failed"]) + print("systemd: some systemd units are failed") + return False + except subprocess.CalledProcessError: + print("systemd: all systemd units are running") + return True + + +def main() -> None: + if os.geteuid() != 0: + print("You need to run this as root") + sys.exit(1) + + state_path = Path("/etc/securedrop-noble-migration.json") + codename = os_codename() + + if codename != "focal": + print(f"Unsupported Ubuntu version: {codename}") + # nothing to do, write an empty JSON blob + state_path.write_text(json.dumps({})) + return + + state = {} + state["ssh"] = check_ssh_group() + state["ufw"] = check_ufw_removed() + state["free_space"] = check_free_space() + state["apt"] = check_apt() + state["systemd"] = check_systemd() + + state_path.write_text(json.dumps(state)) + + if all(state.values()): + print("All ready for migration!") + sys.exit(0) + else: + print("""\ + +Some errors were found that will block migration. + +If you are unsure what to do, please contact the SecureDrop +support team: . +""") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/securedrop/debian/rules b/securedrop/debian/rules index 57f753447f..fba82162bc 100755 --- a/securedrop/debian/rules +++ b/securedrop/debian/rules @@ -85,6 +85,7 @@ override_dh_systemd_enable: dh_systemd_enable --no-enable securedrop-remove-pending-sources.service dh_systemd_enable --no-enable securedrop-remove-packages.service dh_systemd_enable --no-enable securedrop-cleanup-ossec.service + dh_systemd_enable --no-enable securedrop-noble-migration-check.service dh_systemd_enable # This is basically the same as the enable stanza above, just whether the @@ -95,4 +96,5 @@ override_dh_systemd_start: dh_systemd_start --no-start securedrop-remove-pending-sources.service dh_systemd_start --no-start securedrop-remove-packages.service dh_systemd_start --no-start securedrop-cleanup-ossec.service + dh_systemd_start --no-start securedrop-noble-migration-check.service dh_systemd_start