def delete_snapshot(self, snapshot: Subvolume) -> None: logger = self._logger filesystem_path = snapshot.filesystem_path logical_path = snapshot.logical_path try: filesystem_path_str = str(filesystem_path) is_subvolume = filesystem_path.exists() and btrfsutil.is_subvolume( filesystem_path_str) if is_subvolume: root_dir_str = str(constants.ROOT_DIR) num_id = snapshot.num_id deleted_subvolumes = checked_cast( list[int], btrfsutil.deleted_subvolumes(root_dir_str)) if num_id not in deleted_subvolumes: logger.info(f"Deleting the '{logical_path}' snapshot.") btrfsutil.delete_subvolume(filesystem_path_str) else: logger.warning( f"The '{logical_path}' snapshot has already " "been deleted but not yet cleaned up.") else: logger.warning( f"The '{filesystem_path}' directory is not a subvolume.") except btrfsutil.BtrfsUtilError as e: logger.exception("btrfsutil call failed!") raise SubvolumeError( f"Could not delete the '{logical_path}' snapshot!") from e
def test_create_snapshot(self): subvol = os.path.join(self.mountpoint, 'subvol') btrfsutil.create_subvolume(subvol) os.mkdir(os.path.join(subvol, 'dir')) for i, arg in enumerate(self.path_or_fd(subvol)): with self.subTest(type=type(arg)): snapshots_dir = os.path.join(self.mountpoint, 'snapshots{}'.format(i)) os.mkdir(snapshots_dir) snapshot = os.path.join(snapshots_dir, 'snapshot') btrfsutil.create_snapshot(subvol, snapshot + '1') self.assertTrue(btrfsutil.is_subvolume(snapshot + '1')) self.assertTrue(os.path.exists(os.path.join(snapshot + '1', 'dir'))) btrfsutil.create_snapshot(subvol, (snapshot + '2').encode()) self.assertTrue(btrfsutil.is_subvolume(snapshot + '2')) self.assertTrue(os.path.exists(os.path.join(snapshot + '2', 'dir'))) if HAVE_PATH_LIKE: btrfsutil.create_snapshot(subvol, PurePath(snapshot + '3')) self.assertTrue(btrfsutil.is_subvolume(snapshot + '3')) self.assertTrue(os.path.exists(os.path.join(snapshot + '3', 'dir'))) nested_subvol = os.path.join(subvol, 'nested') more_nested_subvol = os.path.join(nested_subvol, 'more_nested') btrfsutil.create_subvolume(nested_subvol) btrfsutil.create_subvolume(more_nested_subvol) os.mkdir(os.path.join(more_nested_subvol, 'nested_dir')) snapshot = os.path.join(self.mountpoint, 'snapshot') btrfsutil.create_snapshot(subvol, snapshot + '1') # Dummy subvolume. self.assertEqual(os.stat(os.path.join(snapshot + '1', 'nested')).st_ino, 2) self.assertFalse(os.path.exists(os.path.join(snapshot + '1', 'nested', 'more_nested'))) btrfsutil.create_snapshot(subvol, snapshot + '2', recursive=True) self.assertTrue(os.path.exists(os.path.join(snapshot + '2', 'nested/more_nested/nested_dir'))) transid = btrfsutil.create_snapshot(subvol, snapshot + '3', recursive=True, no_wait=True) self.assertTrue(os.path.exists(os.path.join(snapshot + '3', 'nested/more_nested/nested_dir'))) self.assertGreater(transid, 0) btrfsutil.create_snapshot(subvol, snapshot + '4', read_only=True) self.assertTrue(btrfsutil.get_subvolume_read_only(snapshot + '4'))
def test_create_snapshot(self): subvol = os.path.join(self.mountpoint, 'subvol') btrfsutil.create_subvolume(subvol) os.mkdir(os.path.join(subvol, 'dir')) for i, arg in enumerate(self.path_or_fd(subvol)): with self.subTest(type=type(arg)): snapshots_dir = os.path.join(self.mountpoint, 'snapshots{}'.format(i)) os.mkdir(snapshots_dir) snapshot = os.path.join(snapshots_dir, 'snapshot') btrfsutil.create_snapshot(subvol, snapshot + '1') self.assertTrue(btrfsutil.is_subvolume(snapshot + '1')) self.assertTrue(os.path.exists(os.path.join(snapshot + '1', 'dir'))) btrfsutil.create_snapshot(subvol, (snapshot + '2').encode()) self.assertTrue(btrfsutil.is_subvolume(snapshot + '2')) self.assertTrue(os.path.exists(os.path.join(snapshot + '2', 'dir'))) if HAVE_PATH_LIKE: btrfsutil.create_snapshot(subvol, PurePath(snapshot + '3')) self.assertTrue(btrfsutil.is_subvolume(snapshot + '3')) self.assertTrue(os.path.exists(os.path.join(snapshot + '3', 'dir'))) nested_subvol = os.path.join(subvol, 'nested') more_nested_subvol = os.path.join(nested_subvol, 'more_nested') btrfsutil.create_subvolume(nested_subvol) btrfsutil.create_subvolume(more_nested_subvol) os.mkdir(os.path.join(more_nested_subvol, 'nested_dir')) snapshot = os.path.join(self.mountpoint, 'snapshot') btrfsutil.create_snapshot(subvol, snapshot + '1') # Dummy subvolume. self.assertEqual(os.stat(os.path.join(snapshot + '1', 'nested')).st_ino, 2) self.assertFalse(os.path.exists(os.path.join(snapshot + '1', 'nested', 'more_nested'))) btrfsutil.create_snapshot(subvol, snapshot + '2', recursive=True) self.assertTrue(os.path.exists(os.path.join(snapshot + '2', 'nested/more_nested/nested_dir'))) transid = btrfsutil.create_snapshot(subvol, snapshot + '3', recursive=True, async_=True) self.assertTrue(os.path.exists(os.path.join(snapshot + '3', 'nested/more_nested/nested_dir'))) self.assertGreater(transid, 0) btrfsutil.create_snapshot(subvol, snapshot + '4', read_only=True) self.assertTrue(btrfsutil.get_subvolume_read_only(snapshot + '4'))
def test_is_subvolume(self): dir = os.path.join(self.mountpoint, 'foo') os.mkdir(dir) for arg in self.path_or_fd(self.mountpoint): with self.subTest(type=type(arg)): self.assertTrue(btrfsutil.is_subvolume(arg)) for arg in self.path_or_fd(dir): with self.subTest(type=type(arg)): self.assertFalse(btrfsutil.is_subvolume(arg)) with self.assertRaises(btrfsutil.BtrfsUtilError) as e: btrfsutil.is_subvolume(os.path.join(self.mountpoint, 'bar')) # This is a bit of an implementation detail, but really this is testing # that the exception is initialized correctly. self.assertEqual(e.exception.btrfsutilerror, btrfsutil.ERROR_STATFS_FAILED) self.assertEqual(e.exception.errno, errno.ENOENT)
def _create_writable_snapshot_from(self, source: Subvolume) -> Subvolume: logger = self._logger snapshot_manipulation = self.package_config.snapshot_manipulation destination_directory = snapshot_manipulation.destination_directory if not destination_directory.exists(): directory_permissions = constants.SNAPSHOTS_ROOT_DIR_PERMISSIONS octal_permissions = "{0:o}".format(directory_permissions) try: logger.info( f"Creating the '{destination_directory}' destination " f"directory with {octal_permissions} permissions.") destination_directory.mkdir(mode=directory_permissions, parents=True) except OSError as e: logger.exception("Path.mkdir() call failed!") raise SubvolumeError( f"Could not create the '{destination_directory}' destination directory!" ) from e destination = source.to_destination(destination_directory) source_logical_path = source.logical_path snapshot_directory = destination.filesystem_path try: logger.info( "Creating a new writable snapshot from the read-only " f"'{source_logical_path}' snapshot at '{snapshot_directory}'.") snapshot_directory_str = str(snapshot_directory) is_subvolume = snapshot_directory.exists( ) and btrfsutil.is_subvolume(snapshot_directory_str) if not is_subvolume: source_filesystem_path_str = str(source.filesystem_path) btrfsutil.create_snapshot(source_filesystem_path_str, snapshot_directory_str, read_only=False) else: logger.warning( f"The '{snapshot_directory}' directory is already a subvolume." ) except btrfsutil.BtrfsUtilError as e: logger.exception("btrfsutil call failed!") raise SubvolumeError( f"Could not create a new writable snapshot at '{snapshot_directory}'!" ) from e writable_snapshot = self.get_subvolume_from(snapshot_directory) return none_throws(writable_snapshot).as_newly_created_from(source)
def test_create_subvolume(self): subvol = os.path.join(self.mountpoint, 'subvol') btrfsutil.create_subvolume(subvol + '1') self.assertTrue(btrfsutil.is_subvolume(subvol + '1')) btrfsutil.create_subvolume((subvol + '2').encode()) self.assertTrue(btrfsutil.is_subvolume(subvol + '2')) if HAVE_PATH_LIKE: btrfsutil.create_subvolume(PurePath(subvol + '3')) self.assertTrue(btrfsutil.is_subvolume(subvol + '3')) pwd = os.getcwd() try: os.chdir(self.mountpoint) btrfsutil.create_subvolume('subvol4') self.assertTrue(btrfsutil.is_subvolume('subvol4')) finally: os.chdir(pwd) btrfsutil.create_subvolume(subvol + '5/') self.assertTrue(btrfsutil.is_subvolume(subvol + '5')) btrfsutil.create_subvolume(subvol + '6//') self.assertTrue(btrfsutil.is_subvolume(subvol + '6')) transid = btrfsutil.create_subvolume(subvol + '7', no_wait=True) self.assertTrue(btrfsutil.is_subvolume(subvol + '7')) self.assertGreater(transid, 0) # Test creating subvolumes under '/' in a chroot. pid = os.fork() if pid == 0: try: os.chroot(self.mountpoint) os.chdir('/') btrfsutil.create_subvolume('/subvol8') self.assertTrue(btrfsutil.is_subvolume('/subvol8')) with self.assertRaises(btrfsutil.BtrfsUtilError): btrfsutil.create_subvolume('/') os._exit(0) except Exception: traceback.print_exc() os._exit(1) wstatus = os.waitpid(pid, 0)[1] self.assertTrue(os.WIFEXITED(wstatus)) self.assertEqual(os.WEXITSTATUS(wstatus), 0)
def test_create_subvolume(self): subvol = os.path.join(self.mountpoint, 'subvol') btrfsutil.create_subvolume(subvol + '1') self.assertTrue(btrfsutil.is_subvolume(subvol + '1')) btrfsutil.create_subvolume((subvol + '2').encode()) self.assertTrue(btrfsutil.is_subvolume(subvol + '2')) if HAVE_PATH_LIKE: btrfsutil.create_subvolume(PurePath(subvol + '3')) self.assertTrue(btrfsutil.is_subvolume(subvol + '3')) pwd = os.getcwd() try: os.chdir(self.mountpoint) btrfsutil.create_subvolume('subvol4') self.assertTrue(btrfsutil.is_subvolume('subvol4')) finally: os.chdir(pwd) btrfsutil.create_subvolume(subvol + '5/') self.assertTrue(btrfsutil.is_subvolume(subvol + '5')) btrfsutil.create_subvolume(subvol + '6//') self.assertTrue(btrfsutil.is_subvolume(subvol + '6')) transid = btrfsutil.create_subvolume(subvol + '7', async=True) self.assertTrue(btrfsutil.is_subvolume(subvol + '7')) self.assertGreater(transid, 0) # Test creating subvolumes under '/' in a chroot. pid = os.fork() if pid == 0: try: os.chroot(self.mountpoint) os.chdir('/') btrfsutil.create_subvolume('/subvol8') self.assertTrue(btrfsutil.is_subvolume('/subvol8')) with self.assertRaises(btrfsutil.BtrfsUtilError): btrfsutil.create_subvolume('/') os._exit(0) except Exception: traceback.print_exc() os._exit(1) wstatus = os.waitpid(pid, 0)[1] self.assertTrue(os.WIFEXITED(wstatus)) self.assertEqual(os.WEXITSTATUS(wstatus), 0)
def scan_dir(p: Path, filter_func=None): """Scans Path p for btrfs snapshots, returns the paths of all found snapshots as a list or None if no snapshots were found. Snapshots can be filtered by passing a function as filter_func that takes a path and returns a boolean expression. If such a function is present, only those snapshots where the filter function returns true will be returned in the snapshot list.""" snapshots = list() for f in p.iterdir(): if btrfsutil.is_subvolume(f): if filter_func is not None: if filter_func(f): snapshots.append(f) else: snapshots.append(f) if len(snapshots) < 1: return None return snapshots
def get_subvolume_from(self, filesystem_path: Path) -> Optional[Subvolume]: logger = self._logger if not filesystem_path.exists(): raise SubvolumeError( f"The '{filesystem_path}' path does not exist!") if not filesystem_path.is_dir(): raise SubvolumeError( f"The '{filesystem_path}' path does not represent a directory!" ) try: filesystem_path_str = str(filesystem_path) if btrfsutil.is_subvolume(filesystem_path_str): subvolume_id = btrfsutil.subvolume_id(filesystem_path_str) subvolume_path = btrfsutil.subvolume_path( filesystem_path_str, subvolume_id) subvolume_read_only = btrfsutil.get_subvolume_read_only( filesystem_path_str) subvolume_info = btrfsutil.subvolume_info( filesystem_path_str, subvolume_id) self_uuid = default_if_none( try_convert_bytes_to_uuid(subvolume_info.uuid), constants.EMPTY_UUID, ) parent_uuid = default_if_none( try_convert_bytes_to_uuid(subvolume_info.parent_uuid), constants.EMPTY_UUID, ) return Subvolume( filesystem_path, subvolume_path, datetime.fromtimestamp(subvolume_info.otime), UuidRelation(self_uuid, parent_uuid), NumIdRelation(subvolume_info.id, subvolume_info.parent_id), subvolume_read_only, ) except btrfsutil.BtrfsUtilError as e: logger.exception("btrfsutil call failed!") raise SubvolumeError( f"Could not initialize the subvolume for '{filesystem_path}'!" ) from e return None