def is_matched_with(self, block_device: BlockDevice) -> bool: if block_device.has_root(): root_location = self.root_location if root_location is not None: root_partition = none_throws(block_device.root) filesystem = none_throws(root_partition.filesystem) normalized_root_location = last( strip_quotes(root_location).split( constants.PARAMETERIZED_OPTION_SEPARATOR)) root_location_comparers = [ root_partition.label, root_partition.uuid, filesystem.label, filesystem.uuid, ] if (normalized_root_location in root_location_comparers or block_device.is_matched_with( normalized_root_location)): root_mount_options = self.root_mount_options subvolume = none_throws(filesystem.subvolume) return (root_mount_options.is_matched_with(subvolume) if root_mount_options is not None else False) return False
def _get_all_boot_file_paths( self, ) -> Iterator[tuple[BootFilePathSource, str]]: source = BootFilePathSource.BOOT_STANZA is_disabled = self.is_disabled if not is_disabled: loader_path = self.loader_path initrd_path = self.initrd_path boot_options = self.boot_options if not is_none_or_whitespace(loader_path): yield (source, none_throws(loader_path)) if not is_none_or_whitespace(initrd_path): yield (source, none_throws(initrd_path)) yield from ((source, initrd_option) for initrd_option in boot_options.initrd_options) sub_menus = self.sub_menus if has_items(sub_menus): yield from chain.from_iterable( sub_menu.all_boot_file_paths for sub_menu in none_throws(sub_menus))
def transform_fstab_line(self, fstab_line: str) -> str: if PartitionTable.is_valid_fstab_entry(fstab_line): root = none_throws(self.root) filesystem = none_throws(root.filesystem) root_mount_point = filesystem.mount_point split_fstab_entry = fstab_line.split() fstab_mount_point = split_fstab_entry[ FstabColumn.FS_MOUNT_POINT.value] if root_mount_point == fstab_mount_point: fstab_mount_options = split_fstab_entry[ FstabColumn.FS_MOUNT_OPTIONS.value] pattern = re.compile(r"(?P<whitespace_before>\s+)" f"{fstab_mount_options}" r"(?P<whitespace_after>\s+)") root_mount_options = str(filesystem.mount_options) return pattern.sub( r"\g<whitespace_before>" f"{root_mount_options}" r"\g<whitespace_after>", fstab_line, ) return fstab_line
def migrate_from_to(self, source_subvolume: Subvolume, destination_subvolume: Subvolume) -> None: root = none_throws(self.root) filesystem = none_throws(root.filesystem) mount_options = none_throws(filesystem.mount_options) destination_filesystem_path = destination_subvolume.filesystem_path mount_options.migrate_from_to(source_subvolume, destination_subvolume) self._fstab_file_path = destination_filesystem_path / constants.FSTAB_FILE
def is_matched_with(self, subvolume: Subvolume) -> bool: root = self.root if root is not None: filesystem = none_throws(root.filesystem) mount_options = none_throws(filesystem.mount_options) return mount_options.is_matched_with(subvolume) return False
def select_snapshots(self, count: int) -> Optional[list[Subvolume]]: if self.has_snapshots(): snapshots = none_throws(self.snapshots) return take(count, sorted(snapshots, reverse=True)) return None
def initialize_injector() -> Optional[Injector]: one_time_mode = RunMode.ONE_TIME.value background_mode = RunMode.BACKGROUND.value parser = ArgumentParser( prog="refind-btrfs", usage="%(prog)s [options]", description="Generate rEFInd manual boot stanzas from Btrfs snapshots", ) parser.add_argument( "-rm", "--run-mode", help="Mode of execution", choices=[one_time_mode, background_mode], type=str, nargs="?", const=one_time_mode, default=one_time_mode, ) arguments = parser.parse_args() run_mode = checked_cast(str, none_throws(arguments.run_mode)) if run_mode == one_time_mode: return Injector(CLIModule) elif run_mode == background_mode: return Injector(WatchdogModule) return None
def is_static_partition_table_matched_with(self, subvolume: Subvolume) -> bool: if self.has_static_partition_table(): static_partition_table = none_throws(self.static_partition_table) return static_partition_table.is_matched_with(subvolume) return False
def as_newly_created_from(self, other: Subvolume) -> Subvolume: self._created_from = other if other.has_static_partition_table(): self._static_partition_table = deepcopy( none_throws(other.static_partition_table)) return self
def check_filtered_block_devices(self) -> bool: logger = self._logger model = self._model esp_device = model.esp_device if esp_device is None: logger.error("Could not find the ESP!") return False esp = model.esp esp_filesystem = none_throws(esp.filesystem) logger.info( f"Found the ESP mounted at '{esp_filesystem.mount_point}' on '{esp.name}'." ) root_device = model.root_device if root_device is None: logger.error("Could not find the root partition!") return False root_partition = model.root_partition root_filesystem = none_throws(root_partition.filesystem) logger.info(f"Found the root partition on '{root_partition.name}'.") btrfs_type = constants.BTRFS_TYPE if not root_filesystem.is_of_type(btrfs_type): logger.error( f"The root partition's filesystem is not '{btrfs_type}'!") return False boot_device = model.boot_device if boot_device is not None: boot = none_throws(esp_device.boot) logger.info(f"Found a separate boot partition on '{boot.name}'.") return True
def as_located_in(self, parent_directory: Path) -> Subvolume: if not self.is_named(): raise ValueError("The '_name' attribute must be initialized!") name = none_throws(self.name) self._filesystem_path = parent_directory / name return self
def destination_loader_path(self) -> Optional[str]: current_loader_path = self._current_state.loader_path if not is_none_or_whitespace(current_loader_path): return replace_root_part_in( none_throws(current_loader_path), self._source_subvolume.logical_path, self._destination_subvolume.logical_path, ) return None
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 __lt__(self, other: object) -> bool: if isinstance(other, Subvolume): attributes_for_comparison = [ none_throws(subvolume.created_from).time_created if subvolume.is_newly_created() else subvolume.time_created for subvolume in (self, other) ] return attributes_for_comparison[0] < attributes_for_comparison[1] return False
def check_root_subvolume(self) -> bool: logger = self._logger model = self._model root_partition = model.root_partition filesystem = none_throws(root_partition.filesystem) if not filesystem.has_subvolume(): logger.error("The root partition is not mounted as a subvolume!") return False subvolume = none_throws(filesystem.subvolume) logical_path = subvolume.logical_path logger.info( f"Found subvolume '{logical_path}' mounted as the root partition.") if subvolume.is_snapshot(): package_config = model.package_config if package_config.exit_if_root_is_snapshot: parent_uuid = subvolume.parent_uuid raise SnapshotMountedAsRootError( f"Subvolume '{logical_path}' is itself a snapshot " f"(parent UUID - '{parent_uuid}'), exiting...") if not subvolume.has_snapshots(): logger.error( f"No snapshots of the '{logical_path}' subvolume were found!") return False snapshots = none_throws(subvolume.snapshots) suffix = item_count_suffix(snapshots) logger.info( f"Found {len(snapshots)} snapshot{suffix} of the '{logical_path}' subvolume." ) return True
def __init__( self, boot_stanza: BootStanza, block_device: BlockDevice, bootable_snapshots: Collection[Subvolume], ) -> None: assert has_items( bootable_snapshots ), "Parameter 'bootable_snapshots' must contain at least one item!" if not boot_stanza.is_matched_with(block_device): raise RefindConfigError( "Boot stanza is not matched with the partition!") root_partition = none_throws(block_device.root) filesystem = none_throws(root_partition.filesystem) source_subvolume = none_throws(filesystem.subvolume) self._boot_stanza = boot_stanza self._source_subvolume = source_subvolume self._bootable_snapshots = list(bootable_snapshots)
def file_name(self) -> str: if self.can_be_used_for_bootable_snapshot(): normalized_volume = self.normalized_volume dir_separator_pattern = re.compile(constants.DIR_SEPARATOR_PATTERN) split_loader_path = dir_separator_pattern.split( none_throws(self.loader_path)) loader = last(split_loader_path) extension = constants.CONFIG_FILE_EXTENSION return f"{normalized_volume}_{loader}{extension}".lower() return constants.EMPTY_STR
def validate_boot_files_check_result(self) -> None: if self.has_unmatched_boot_files(): boot_files_check_result = none_throws(self.boot_files_check_result) boot_stanza_name = boot_files_check_result.required_by_boot_stanza_name logical_path = boot_files_check_result.expected_logical_path unmatched_boot_files = boot_files_check_result.unmatched_boot_files raise RefindConfigError( f"Detected boot files required by the '{boot_stanza_name}' boot " f"stanza which are not matched with the '{logical_path}' subvolume: " f"{constants.DEFAULT_ITEMS_SEPARATOR.join(unmatched_boot_files)}!" )
def is_matched_with(self, name: str) -> bool: if self.name == name: return True else: dependencies = self.dependencies if has_items(dependencies): return any( dependency.is_matched_with(name) for dependency in none_throws(dependencies)) return False
def migrate(self) -> State: include_paths = self.include_paths is_latest = self._is_latest current_state = self._current_state inherit_from_state = self._inherit_from_state destination_loader_path = current_state.loader_path destination_initrd_path = current_state.initrd_path destination_boot_options: Optional[BootOptions] = None destination_add_boot_options = self.destination_add_boot_options if not is_latest: if include_paths: destination_loader_path = inherit_from_state.loader_path destination_initrd_path = inherit_from_state.initrd_path destination_boot_options = BootOptions.merge(( none_throws(inherit_from_state.boot_options), none_throws(destination_add_boot_options), )) destination_add_boot_options = BootOptions(constants.EMPTY_STR) if include_paths: destination_loader_path_candidate = self.destination_loader_path destination_initrd_path_candidate = self.destination_initrd_path if not is_none_or_whitespace(destination_loader_path_candidate): destination_loader_path = destination_loader_path_candidate if not is_none_or_whitespace(destination_initrd_path_candidate): destination_initrd_path = destination_initrd_path_candidate return State( self.destination_name, destination_loader_path, destination_initrd_path, None, destination_boot_options, destination_add_boot_options, )
def _migrate_sub_menus( self, refind_config_path: Path, source_subvolume: Subvolume, destination_subvolume: Subvolume, boot_stanza_result: State, boot_stanza_generation: BootStanzaGeneration, ) -> Iterator[SubMenu]: boot_stanza = self._boot_stanza if not boot_stanza.has_sub_menus(): return current_sub_menus = none_throws(boot_stanza.sub_menus) is_latest = self._is_latest_snapshot(destination_subvolume) for sub_menu in current_sub_menus: if sub_menu.can_be_used_for_bootable_snapshot(): sub_menu_migration_strategy = MainMigrationFactory.migration_strategy( sub_menu, is_latest, refind_config_path, source_subvolume, destination_subvolume, boot_stanza_generation, inherit_from_state=boot_stanza_result, ) migration_result = sub_menu_migration_strategy.migrate() yield SubMenu( migration_result.name, migration_result.loader_path, migration_result.initrd_path, sub_menu.graphics, migration_result.boot_options, none_throws(migration_result.add_boot_options), sub_menu.is_disabled, )
def is_modified(self, actual_file_path: Path) -> bool: current_file_path = self.file_path if current_file_path != actual_file_path: return True current_file_stat = none_throws(self.file_stat) if actual_file_path.exists(): actual_file_stat = actual_file_path.stat() return current_file_stat.st_mtime != actual_file_stat.st_mtime return True
def is_valid_fstab_entry(value: Optional[str]) -> bool: if is_none_or_whitespace(value): return False fstab_line = none_throws(value) comment_pattern = re.compile(r"^\s*#.*") if not comment_pattern.match(fstab_line): split_fstab_entry = fstab_line.split() return has_items(split_fstab_entry) and len( split_fstab_entry) == len(FstabColumn) return False
def search_paths_for(self, file_name: str) -> Optional[list[Path]]: if is_none_or_whitespace(file_name): raise ValueError("The 'file_name' parameter must be initialized!") filesystem = none_throws(self.filesystem) if filesystem.is_mounted(): search_directory = Path(filesystem.mount_point) all_matches = find_all_matched_files_in(search_directory, file_name) return list(all_matches) return None
def get_config(self, partition: Partition) -> RefindConfig: logger = self._logger config_file_path = FileRefindConfigProvider.all_config_file_paths.get( partition) should_begin_search = config_file_path is None or not config_file_path.exists( ) if should_begin_search: package_config_provider = self._package_config_provider package_config = package_config_provider.get_config() boot_stanza_generation = package_config.boot_stanza_generation refind_config_file = boot_stanza_generation.refind_config logger.info( f"Searching for the '{refind_config_file}' file on '{partition.name}'." ) refind_config_search_result = partition.search_paths_for( refind_config_file) if not has_items(refind_config_search_result): raise RefindConfigError( f"Could not find the '{refind_config_file}' file!") if not is_singleton(refind_config_search_result): raise RefindConfigError( f"Found multiple '{refind_config_file}' files (at most one is expected)!" ) config_file_path = one( none_throws(refind_config_search_result)).resolve() FileRefindConfigProvider.all_config_file_paths[ partition] = config_file_path return self._read_config_from(none_throws(config_file_path))
def is_located_in(self, parent_directory: Path) -> bool: if self.is_newly_created(): created_from = none_throws(self.created_from) filesystem_path = created_from.filesystem_path else: filesystem_path = self.filesystem_path path_relation = discern_path_relation_of( (parent_directory, filesystem_path)) expected_results: list[PathRelation] = [ PathRelation.SAME, PathRelation.SECOND_NESTED_IN_FIRST, ] return path_relation in expected_results
def modify_partition_table_using( self, source_subvolume: Subvolume, device_command_factory: BaseDeviceCommandFactory, ) -> None: self.initialize_partition_table_using(device_command_factory) static_partition_table = none_throws(self.static_partition_table) if not static_partition_table.is_matched_with(self): static_device_command = device_command_factory.static_device_command( ) static_partition_table.migrate_from_to(source_subvolume, self) static_device_command.save_partition_table(static_partition_table)
def migrate(self) -> State: include_paths = self.include_paths is_latest = self._is_latest current_state = self._current_state destination_loader_path = constants.EMPTY_STR destination_initrd_path = constants.EMPTY_STR if is_latest: destination_loader_path = none_throws(current_state.loader_path) destination_initrd_path = none_throws(current_state.initrd_path) if include_paths: destination_loader_path = none_throws(self.destination_loader_path) destination_initrd_path_candidate = self.destination_initrd_path if not is_none_or_whitespace(destination_initrd_path_candidate): destination_initrd_path = none_throws( destination_initrd_path_candidate) icon_migration_strategy = IconMigrationFactory.migration_strategy( self._icon_command, self._refind_config_path, default_if_none(current_state.icon_path, constants.EMPTY_STR), self.icon, ) destination_icon_path = icon_migration_strategy.migrate() return State( self.destination_name, destination_loader_path, destination_initrd_path, destination_icon_path, self.destination_boot_options, None, )
def is_matched_with(self, block_device: BlockDevice) -> bool: if self.can_be_used_for_bootable_snapshot(): boot_options = self.boot_options if boot_options.is_matched_with(block_device): return True else: sub_menus = self.sub_menus if has_items(sub_menus): return any( sub_menu.is_matched_with(block_device) for sub_menu in none_throws(sub_menus)) return False
def _search_for_snapshots_in( self, directory: Path, max_depth: int, current_depth: int = 0, parent: Optional[Subvolume] = None, ) -> Iterator[Subvolume]: if current_depth > max_depth: return logger = self._logger is_initial_call = not bool(current_depth) resolved_path = directory.resolve() if is_initial_call: if parent is None: logger.info( f"Getting all snapshots in the '{directory}' directory.") else: logical_path = parent.logical_path logger.info(f"Searching for snapshots of the '{logical_path}' " f"subvolume in the '{directory}' directory.") searched_directories = self._searched_directories if resolved_path not in searched_directories: subvolume = self.get_subvolume_from(resolved_path) can_subvolume_be_yielded = False if subvolume is not None: can_subvolume_be_yielded = (subvolume.is_snapshot_of(parent) if parent is not None else subvolume.is_snapshot()) searched_directories.add(resolved_path) if can_subvolume_be_yielded: yield none_throws(subvolume) else: subdirectories = (child for child in directory.iterdir() if child.is_dir()) for subdirectory in subdirectories: yield from self._search_for_snapshots_in( subdirectory, max_depth, current_depth + 1, parent)