Skip to content

Commit cc07222

Browse files
committed
Added commands rollback
1 parent cfe8697 commit cc07222

File tree

1 file changed

+76
-28
lines changed

1 file changed

+76
-28
lines changed

atomic-update.py

Lines changed: 76 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,27 @@ def sigint_handler(signum, frame):
216216
logging.error(f"Provided snapshot {continue_num} for option '--continue' does not exist")
217217
sys.exit(1)
218218

219+
# check if there's a snapshot number provided for rollback
220+
rollback_num = None
221+
if COMMAND == "rollback":
222+
try:
223+
rollback_num = int(ARG[0])
224+
if not rollback_num in range(1, 999999):
225+
logging.error("Invalid snapshot number provided for rollback. Must be between 1 to 999999 (inclusive)")
226+
sys.exit(1)
227+
except ValueError:
228+
logging.debug("Invalid value provided as snapshot number for rollback")
229+
sys.exit(1)
230+
except IndexError:
231+
logging.debug("No snapshot number provided for rollback")
232+
pass
233+
234+
if rollback_num:
235+
ret = os.system(f"btrfs subvolume list / | grep '@/.snapshots/{rollback_num}/snapshot'")
236+
if ret != 0:
237+
logging.error(f"Provided snapshot {rollback_num} for rollback does not exist")
238+
sys.exit(1)
239+
219240
# Bail out if we're not root
220241
if os.getuid() != 0:
221242
logging.error("Bailing out, program must be run with root privileges")
@@ -254,9 +275,9 @@ def sigint_handler(signum, frame):
254275
# Create secure temp dir
255276
TMP_DIR = tempfile.mkdtemp(dir="/tmp", prefix="atomic-update_")
256277

257-
# Handle command: dup
258-
if COMMAND == "dup":
259-
logging.info("Starting atomic distribution upgrade...")
278+
# Handle commands: dup, run
279+
if COMMAND in ["dup", "run"]:
280+
logging.info(f"Starting atomic {'distribution upgrade' if COMMAND == 'dup' else 'transaction'}...")
260281
# get snapper root config name
261282
snapper_root_config = get_snapper_root_config()
262283
logging.debug(f"Snapper root config name: {snapper_root_config}")
@@ -270,20 +291,20 @@ def sigint_handler(signum, frame):
270291
active_snap = default_snap
271292
if continue_num:
272293
active_snap = continue_num
273-
# create new read-write snapshot to perform dup in
294+
# create new read-write snapshot to perform atomic update in
274295
out, ret = shell_exec(f"snapper -c {snapper_root_config} create -c number " \
275296
f"-d 'Atomic update of #{active_snap}' " \
276297
f"-u 'atomic=yes' --from {active_snap} --read-write")
277298
if ret != 0:
278-
logging.error(f"Could not create read-write snapshot to perform dup in")
299+
logging.error(f"Could not create read-write snapshot to perform atomic update in")
279300
sys.exit(6)
280-
# get latest atomic snapshot
301+
# get latest atomic snapshot number we just created
281302
atomic_snap = get_atomic_snap(snapper_root_config)
282303
logging.debug(f"Latest atomic snapshot number: {atomic_snap}")
283304
logging.info(f"Using snapshot {active_snap} as base for new snapshot {atomic_snap}")
284305
snap_subvol = f"@/.snapshots/{atomic_snap}/snapshot"
285306
snap_dir = snap_subvol.lstrip("@")
286-
# check the latest atomic snapshot exists
307+
# check the latest atomic snapshot exists as btrfs subvolume
287308
out, ret = shell_exec(f"LC_ALL=C btrfs subvolume list / | grep '{snap_subvol}'")
288309
if ret != 0:
289310
logging.error(f"Could not find latest atomic snapshot subvolume {snap_subvol}. Discarding snapshot {atomic_snap}")
@@ -292,7 +313,7 @@ def sigint_handler(signum, frame):
292313
# find the device where root fs resides
293314
rootfs_device, ret = shell_exec("LC_ALL=C mount -l | grep 'on / type btrfs' | awk '{print $1}'")
294315
if ret != 0:
295-
logging.error("Could not find root filesystem device from mountpoints. Discarding snapshot {atomic_snap}")
316+
logging.error(f"Could not find root filesystem device from mountpoints. Discarding snapshot {atomic_snap}")
296317
shell_exec(f"snapper -c {snapper_root_config} delete {atomic_snap}")
297318
sys.exit(8)
298319
logging.debug(f"Btrfs root FS device: {rootfs_device}")
@@ -304,24 +325,35 @@ def sigint_handler(signum, frame):
304325
chroot {TMP_DIR} mount -a;
305326
"""
306327
shell_exec(commands)
307-
# check if dup has anything to do
308-
logging.info("Checking for packages to upgrade")
309-
xml_output, ret = shell_exec(f"LC_ALL=C zypper --root {TMP_DIR} --non-interactive --no-cd --xmlout dist-upgrade --dry-run")
310-
docroot = ET.fromstring(xml_output)
311-
for item in docroot.iter('install-summary'):
312-
num_pkgs = int(item.attrib["packages-to-change"])
313-
if not num_pkgs:
314-
logging.info("Nothing to do. Exiting...")
315-
cleanup()
316-
sys.exit()
317-
logging.info("Performing atomic distribution upgrade...")
318-
ret = os.system(f"zypper --root {TMP_DIR} {'' if CONFIRM else '--non-interactive'} --no-cd dist-upgrade")
319-
if ret != 0:
320-
logging.error(f"Zypper returned exit code {ret}. Discarding snapshot {atomic_snap}")
321-
shell_exec(f"snapper -c {snapper_root_config} delete {atomic_snap}")
322-
cleanup()
323-
sys.exit(9)
324-
logging.info(f"Distribution upgrade completed successfully")
328+
if COMMAND == "dup":
329+
# check if dup has anything to do
330+
logging.info("Checking for packages to upgrade")
331+
xml_output, ret = shell_exec(f"LC_ALL=C zypper --root {TMP_DIR} --non-interactive --no-cd --xmlout dist-upgrade --dry-run")
332+
docroot = ET.fromstring(xml_output)
333+
for item in docroot.iter('install-summary'):
334+
num_pkgs = int(item.attrib["packages-to-change"])
335+
if not num_pkgs:
336+
logging.info("Nothing to do. Exiting...")
337+
cleanup()
338+
sys.exit()
339+
logging.info("Performing distribution upgrade within chroot...")
340+
ret = os.system(f"zypper --root {TMP_DIR} {'' if CONFIRM else '--non-interactive'} --no-cd dist-upgrade")
341+
if ret != 0:
342+
logging.error(f"Zypper returned exit code {ret}. Discarding snapshot {atomic_snap}")
343+
shell_exec(f"snapper -c {snapper_root_config} delete {atomic_snap}")
344+
cleanup()
345+
sys.exit(9)
346+
logging.info(f"Distribution upgrade completed successfully")
347+
elif COMMAND == "run":
348+
exec_cmd = ' '.join(ARG)
349+
logging.info(f"Running command {exec_cmd!r} within chroot...")
350+
ret = os.system(f"chroot {snap_dir} {exec_cmd}")
351+
if ret != 0:
352+
logging.error(f"Command returned exit code {ret}. Discarding snapshot {atomic_snap}")
353+
shell_exec(f"snapper -c {snapper_root_config} delete {atomic_snap}")
354+
cleanup()
355+
sys.exit()
356+
logging.info("Command run successfully")
325357
if SHELL:
326358
logging.info(f"Opening chroot in snapshot {atomic_snap}")
327359
logging.info("Continue with 'exit' or discard with 'exit 1'")
@@ -338,6 +370,7 @@ def sigint_handler(signum, frame):
338370
if REBOOT:
339371
logging.info("Rebooting now...")
340372
os.system("systemctl reboot")
373+
sys.exit()
341374
if APPLY:
342375
logging.info(f"Using default snapshot {atomic_snap} to replace running system...")
343376
logging.info("Applying /usr...")
@@ -360,5 +393,20 @@ def sigint_handler(signum, frame):
360393
os.system("systemctl daemon-reexec")
361394
logging.info("Executing systemd-tmpfiles --create...")
362395
os.system("systemd-tmpfiles --create")
363-
logging.info("Applied default snapshot as new base for running system!")
364-
logging.info("Running processes will not be restarted automatically.")
396+
logging.info("Applied default snapshot as new base for running system")
397+
logging.info("Running processes will not be restarted automatically")
398+
sys.exit()
399+
400+
# Handle command: rollback
401+
elif COMMAND == "rollback":
402+
warn_opts = ["--apply", "--reboot"]
403+
if warn_opts in OPT:
404+
logging.warn(f"Options {', '.join(warn_opts)!r} do not apply to rollback command")
405+
if rollback_num:
406+
os.system(f"snapper rollback {rollback_num}")
407+
else:
408+
os.system("snapper rollback")
409+
410+
# If we're here, remind user to reboot
411+
logging.info("Please reboot your machine to activate the changes and avoid data loss")
412+
sys.exit()

0 commit comments

Comments
 (0)