Skip to content

Commit 874c5b8

Browse files
committed
Improve snapshot verification
basic essential program runtime checks to detect missing and incompatible libraries.
1 parent 55db295 commit 874c5b8

File tree

1 file changed

+53
-13
lines changed

1 file changed

+53
-13
lines changed

atomic-update

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,32 @@ from shlex import quote
2828
import xml.etree.ElementTree as ET
2929

3030
# Constants
31-
VERSION = "0.1.14"
31+
VERSION = "0.1.15"
3232
ZYPPER_PID_FILE = "/run/zypp.pid"
3333
VALID_CMD = ["dup", "run", "rollback"]
3434
VALID_OPT = ["--reboot", "--apply", "--shell", "--continue", "--no-verify", \
3535
"--interactive", "--debug", "--help", "--version"]
3636

37+
# Required programs / dependecies
38+
REQUIRED_DEP = ["zypper", "snapper", "btrfs", "echo", "ps", "sed", "awk", "bash", "sort", \
39+
"env", "chroot", "mount", "umount", "rmdir", "findmnt", "systemd-nspawn", \
40+
"systemctl", "machinectl", "systemd-analyze"]
41+
42+
# The exit code of these programs (if it exists) in addition to the required programs
43+
# will be checked pre/post each transaction/update
44+
CHK_PROGRAMS = [
45+
"Xorg",
46+
"Xwayland",
47+
"pipewire",
48+
"wireplumber",
49+
"firefox",
50+
"thunderbird",
51+
"gdm",
52+
"gnome-shell",
53+
"gnome-control-center",
54+
# TODO: add the display manager, graphical shell, and settings app binary for other DE/WMs
55+
]
56+
3757
# Command help/usage info
3858
help_text = """
3959
Usage: atomic-update [options] command
@@ -97,8 +117,25 @@ def get_atomic_snap(snapper_root_config, status):
97117
except:
98118
pass
99119

100-
# Function to verify snapshot by booting it up as a container
101-
def verify_snapshot():
120+
# Function to verify snapshot's ability to run important programs -
121+
# acts as a basic check for missing and incompatible libraries
122+
def verify_programs(TMP_MOUNT_DIR):
123+
failed_programs = []
124+
programs = REQUIRED_DEP + CHK_PROGRAMS
125+
logging.debug(f"Verifying programs: {', '.join(programs)}")
126+
for program in programs:
127+
version_str = "-version" if program in ["Xorg", "Xwayland"] else "--version"
128+
command = f"chroot {TMP_MOUNT_DIR} bash -c '" \
129+
f"command -v {program} || exit 0 && sudo -u nobody {program} {version_str}" \
130+
f"' > /dev/null 2>&1"
131+
out, ret = shell_exec(command)
132+
if ret != 0:
133+
failed_programs.append(program)
134+
logging.debug(f"Failed programs: {', '.join(failed_programs)}")
135+
return failed_programs
136+
137+
# Function to verify snapshot's systemd units by booting it up as a container
138+
def verify_units():
102139
logging.debug("Booting container")
103140
cmd = ["systemd-nspawn", "--directory", TMP_MOUNT_DIR, "--ephemeral", "--boot", \
104141
"systemd.mask=local-fs.target", "systemd.mask=auditd.service", "systemd.mask=kdump.service"]
@@ -305,14 +342,11 @@ if os.getuid() != 0:
305342
sys.exit(2)
306343

307344
# Bail out if required dependecies are not available
308-
programs = ["zypper", "snapper", "btrfs", "echo", "ps", "sed", "awk", "bash", "sort", \
309-
"env", "chroot", "mount", "umount", "rmdir", "findmnt", "systemd-nspawn", \
310-
"systemctl", "machinectl", "systemd-analyze"]
311-
for program in programs:
345+
for program in REQUIRED_DEP:
312346
if not shell_exec(f"command -v {program}")[0]:
313347
logging.error(f"Bailing out, missing required dependecy {program!r} in PATH ({os.environ.get('PATH')}) " \
314348
f"for user {os.environ.get('USER')!r}. The following programs " \
315-
f"are required for atomic-update to function: {', '.join(programs)}"
349+
f"are required for atomic-update to function: {', '.join(REQUIRED_DEP)}"
316350
)
317351
sys.exit(3)
318352

@@ -403,7 +437,8 @@ chroot {TMP_MOUNT_DIR} mount -a -O no_netdev;
403437
# verify snapshot prior to performing update
404438
if not NO_VERIFY:
405439
logging.info("Verifying snapshot prior to update...")
406-
pre_all_units, pre_failed_units = verify_snapshot()
440+
pre_all_units, pre_failed_units = verify_units()
441+
pre_failed_progs = verify_programs(TMP_MOUNT_DIR)
407442
if COMMAND == "dup":
408443
# check if dup has anything to do
409444
logging.info("Checking for packages to upgrade...")
@@ -461,12 +496,17 @@ chroot {TMP_MOUNT_DIR} bash -c "export PS1='atomic-update:\${{PWD}} # '; exec ba
461496
# verify snapshot after update
462497
if not NO_VERIFY:
463498
logging.info("Verifying snapshot post update...")
464-
post_all_units, post_failed_units = verify_snapshot()
499+
post_all_units, post_failed_units = verify_units()
465500
newly_failed_units = list( set(post_failed_units) - set(pre_failed_units) )
466501
update_failed_units = [unit for unit in newly_failed_units if unit in pre_all_units]
467-
if update_failed_units:
468-
logging.error(f"Discarding snapshot {atomic_snap} as the following " \
469-
f"systemd units have failed since update: {', '.join(update_failed_units)}")
502+
post_failed_progs = verify_programs(TMP_MOUNT_DIR)
503+
newly_failed_progs = list( set(post_failed_progs) - set(pre_failed_progs) )
504+
if update_failed_units or newly_failed_progs:
505+
msg = f"Discarding snapshot {atomic_snap} as new errors were detected after the update. "
506+
msg += f"The following programs have failed to run: {', '.join(newly_failed_progs)}. " if newly_failed_progs else ""
507+
msg += f"The following systemd units have failed: {', '.join(update_failed_units)}. " if update_failed_units else ""
508+
msg = msg.rstrip()
509+
logging.error(msg)
470510
shell_exec(f"snapper -c {snapper_root_config} delete {atomic_snap}")
471511
cleanup()
472512
sys.exit()

0 commit comments

Comments
 (0)