@@ -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
220241if 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
255276TMP_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):
304325chroot { 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