def check_prepared_snapshots(self) -> bool: logger = self._logger model = self._model prepared_snapshots = model.prepared_snapshots snapshots_for_addition = prepared_snapshots.snapshots_for_addition snapshots_for_removal = prepared_snapshots.snapshots_for_removal if has_items(snapshots_for_addition): subvolume = model.root_subvolume suffix = item_count_suffix(snapshots_for_addition) logger.info( f"Found {len(snapshots_for_addition)} snapshot{suffix} for addition." ) for snapshot in snapshots_for_addition: try: snapshot.validate_static_partition_table(subvolume) except SubvolumeError as e: logger.warning(e.formatted_message) usable_snapshots_for_addition = model.usable_snapshots_for_addition if not has_items(usable_snapshots_for_addition): logger.warning( "None of the snapshots for addition are usable!") if has_items(snapshots_for_removal): suffix = item_count_suffix(snapshots_for_removal) logger.info( f"Found {len(snapshots_for_removal)} snapshot{suffix} for removal." ) return True
def __str__(self) -> str: root_location = self._root_location root_mount_options = self._root_mount_options initrd_options = self._initrd_options other_options = self._other_options result: list[str] = [constants.EMPTY_STR ] * (sum( (len(initrd_options), len(other_options))) + (1 if root_location is not None else 0) + (1 if root_mount_options is not None else 0)) if root_location is not None: result[root_location[0]] = constants.ROOT_PREFIX + root_location[1] if root_mount_options is not None: result[root_mount_options[0]] = constants.ROOTFLAGS_PREFIX + str( root_mount_options[1]) if has_items(initrd_options): for initrd_option in initrd_options: result[initrd_option[ 0]] = constants.INITRD_PREFIX + initrd_option[1] if has_items(other_options): for other_option in other_options: result[other_option[0]] = other_option[1] if has_items(result): joined_options = constants.BOOT_OPTION_SEPARATOR.join(result) return constants.DOUBLE_QUOTE + joined_options + constants.DOUBLE_QUOTE return constants.EMPTY_STR
def check_matched_boot_stanzas(self) -> bool: logger = self._logger model = self._model matched_boot_stanzas = model.matched_boot_stanzas if not has_items(matched_boot_stanzas): logger.error( "Could not find a boot stanza matched with the root partition!" ) return False suffix = item_count_suffix(matched_boot_stanzas) logger.info(f"Found {len(matched_boot_stanzas)} boot " f"stanza{suffix} matched with the root partition.") grouping_result = groupby(matched_boot_stanzas) for key, grouper in grouping_result: grouped_boot_stanzas = list(grouper) if not is_singleton(grouped_boot_stanzas): volume = key.volume loader_path = key.loader_path logger.error( f"Found {len(grouped_boot_stanzas)} boot stanzas defined with " f"the same volume ('{volume}') and loader ('{loader_path}') options!" ) return False package_config = model.package_config boot_stanza_generation = package_config.boot_stanza_generation icon_generation_mode = boot_stanza_generation.icon.mode for boot_stanza in matched_boot_stanzas: try: boot_stanza.validate_boot_files_check_result() except RefindConfigError as e: logger.warning(e.formatted_message) boot_stanza.validate_icon_path(icon_generation_mode) usable_boot_stanzas = model.usable_boot_stanzas if not has_items(usable_boot_stanzas): logger.error("None of the matched boot stanzas are usable!") return False return True
def actual_bootable_snapshots(self) -> list[Subvolume]: persistence_provider = self._persistence_provider prepared_snapshots = self.prepared_snapshots usable_snapshots_for_addition = self.usable_snapshots_for_addition previous_run_result = persistence_provider.get_previous_run_result() snapshots_for_removal = prepared_snapshots.snapshots_for_removal bootable_snapshots = set(previous_run_result.bootable_snapshots) if has_items(usable_snapshots_for_addition): bootable_snapshots |= set(usable_snapshots_for_addition) if has_items(snapshots_for_removal): bootable_snapshots -= set(snapshots_for_removal) return list(bootable_snapshots)
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 initialize_block_devices(self) -> None: device_command_factory = self._device_command_factory physical_device_command = device_command_factory.physical_device_command( ) all_block_devices = list(physical_device_command.get_block_devices()) if has_items(all_block_devices): for block_device in all_block_devices: block_device.initialize_partition_tables_using( device_command_factory) def block_device_filter( filter_func: Callable[[BlockDevice], bool], ) -> Optional[BlockDevice]: return only(block_device for block_device in all_block_devices if filter_func(block_device)) filtered_block_devices = BlockDevices( block_device_filter(BlockDevice.has_esp), block_device_filter(BlockDevice.has_root), block_device_filter(BlockDevice.has_boot), ) else: filtered_block_devices = BlockDevices.none() self._filtered_block_devices = filtered_block_devices
def migrate_from_to( self, source_subvolume: Subvolume, destination_subvolume: Subvolume, include_paths: bool, ) -> None: root_mount_options = self.root_mount_options if root_mount_options is not None: root_mount_options.migrate_from_to(source_subvolume, destination_subvolume) if include_paths: initrd_options = self._initrd_options if has_items(initrd_options): source_logical_path = source_subvolume.logical_path destination_logical_path = destination_subvolume.logical_path self._initrd_options = [( initrd_option[0], replace_root_part_in( initrd_option[1], source_logical_path, destination_logical_path, ( constants.FORWARD_SLASH, constants.BACKSLASH, ), ), ) for initrd_option in initrd_options]
def _is_snapshot_deleted(self, deleted_directory: Path) -> bool: persistence_provider = self._persistence_provider previous_run_result = persistence_provider.get_previous_run_result() bootable_snapshots = previous_run_result.bootable_snapshots if has_items(bootable_snapshots): deleted_snapshot = only( snapshot for snapshot in bootable_snapshots if snapshot.is_located_in(deleted_directory)) if deleted_snapshot is not None: deleted_snapshots = self._deleted_snapshots deletion_lock = self._deletion_lock with deletion_lock: if deleted_snapshot not in deleted_snapshots: snapshot_manipulation = ( self.package_config.snapshot_manipulation) cleanup_exclusion = snapshot_manipulation.cleanup_exclusion deleted_snapshots.add(deleted_snapshot) if deleted_snapshot in cleanup_exclusion: raise SnapshotExcludedFromDeletionError( f"The deleted snapshot ('{deleted_directory}') " "is explicitly excluded from cleanup!") return True return False
def _get_directories_for_watch(self) -> Iterator[Path]: snapshot_searches = self.snapshot_searches if has_items(snapshot_searches): for snapshot_search in snapshot_searches: directory = snapshot_search.directory max_depth = snapshot_search.max_depth - 1 yield from find_all_directories_in(directory, max_depth)
def __str__(self) -> str: simple_options = self._simple_options parameterized_options = self._parameterized_options result: list[str] = [constants.EMPTY_STR] * sum( (len(simple_options), len(parameterized_options))) if has_items(simple_options): for simple_option in simple_options: result[simple_option[0]] = simple_option[1] if has_items(parameterized_options): for option_name, option_value in parameterized_options.items(): result[option_value[ 0]] = constants.PARAMETERIZED_OPTION_SEPARATOR.join( (option_name, option_value[1])) if has_items(result): return constants.COLUMN_SEPARATOR.join(result) return constants.EMPTY_STR
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 _map_to_includes( config_option_contexts: list[RefindConfigParser.Config_optionContext], ) -> Iterator[str]: if has_items(config_option_contexts): include_visitor = IncludeVisitor() for config_option_context in config_option_contexts: include_context = config_option_context.include() if include_context is not None: yield checked_cast(str, include_context.accept(include_visitor))
def _map_to_boot_stanzas( config_option_contexts: list[RefindConfigParser.Config_optionContext], ) -> Iterator[BootStanza]: if has_items(config_option_contexts): boot_stanza_visitor = BootStanzaVisitor() for config_option_context in config_option_contexts: boot_stanza_context = config_option_context.boot_stanza() if boot_stanza_context is not None: yield checked_cast( BootStanza, boot_stanza_context.accept(boot_stanza_visitor))
def _process_snapshots(self) -> list[Subvolume]: subvolume_command_factory = self._subvolume_command_factory actual_bootable_snapshots = self.actual_bootable_snapshots usable_snapshots_for_addition = self.usable_snapshots_for_addition subvolume_command = subvolume_command_factory.subvolume_command() if has_items(usable_snapshots_for_addition): device_command_factory = self._device_command_factory subvolume = self.root_subvolume boot_stanzas_with_snapshots = self.boot_stanzas_with_snapshots all_usable_snapshots = set( chain.from_iterable( self.usable_boot_stanzas_with_snapshots.values())) for addition in usable_snapshots_for_addition: if addition in all_usable_snapshots: bootable_snapshot = subvolume_command.get_bootable_snapshot_from( addition) bootable_snapshot.modify_partition_table_using( subvolume, device_command_factory) replace_item_in(actual_bootable_snapshots, addition, bootable_snapshot) for item in boot_stanzas_with_snapshots: item.replace_matched_snapshot(addition, bootable_snapshot) else: actual_bootable_snapshots.remove(addition) prepared_snapshots = self.prepared_snapshots snapshots_for_removal = prepared_snapshots.snapshots_for_removal if has_items(snapshots_for_removal): for removal in snapshots_for_removal: subvolume_command.delete_snapshot(removal) return actual_bootable_snapshots
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 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 check_boot_stanzas_with_snapshots(self) -> bool: logger = self._logger model = self._model boot_stanzas_with_snapshots = model.boot_stanzas_with_snapshots for item in boot_stanzas_with_snapshots: if item.has_unmatched_snapshots(): unmatched_snapshots = item.unmatched_snapshots for snapshot in unmatched_snapshots: try: snapshot.validate_boot_files_check_result() except SubvolumeError as e: logger.warning(e.formatted_message) if not item.has_matched_snapshots(): boot_stanza = item.boot_stanza normalized_name = boot_stanza.normalized_name logger.warning("None of the prepared snapshots are matched " f"with the '{normalized_name}' boot stanza!") usable_boot_stanzas_with_snapshots = model.usable_boot_stanzas_with_snapshots if not has_items(usable_boot_stanzas_with_snapshots): logger.error("None of the matched boot stanzas can be " "combined with any of the prepared snapshots!") return False package_config = model.package_config if package_config.exit_if_no_changes_are_detected: refind_config = model.refind_config prepared_snapshots = model.prepared_snapshots has_changes = (package_config.is_of_initialization_type( ConfigInitializationType.PARSED) or refind_config.is_of_initialization_type( ConfigInitializationType.PARSED) or prepared_snapshots.has_changes()) if not has_changes: raise NoChangesDetectedError( "No changes were detected, aborting...") return True
def __init__( self, logger_factory: BaseLoggerFactory, model: Model, states: States, ): self._logger = logger_factory.logger(__name__) if not has_items(states) or is_singleton(states): raise ValueError( "The 'states' collection must be initialized and contain at least two items!" ) initial = checked_cast(State, first(states)) expected_initial_name = StateNames.INITIAL.value if initial.name != expected_initial_name: raise ValueError("The first item of the 'states' collection must " f"be a state named '{expected_initial_name}'!") final = checked_cast(State, last(states)) expected_final_name = StateNames.FINAL.value if final.name != expected_final_name: raise ValueError("The last item of the 'states' collection must " f"be a state named '{expected_final_name}'!") conditions = model.conditions super().__init__( model=model, states=list(states), initial=initial, auto_transitions=False, name=__name__, ) self.add_ordered_transitions( loop=False, conditions=conditions, ) self._initial_state = initial
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 save_config(self, config: RefindConfig) -> None: logger = self._logger persistence_provider = self._persistence_provider boot_stanzas = config.boot_stanzas if has_items(boot_stanzas): config_file_path = config.file_path destination_directory = config_file_path.parent refind_directory = destination_directory.parent if not destination_directory.exists(): logger.info( "Creating the " f"'{destination_directory.relative_to(refind_directory)}' " "destination directory.") destination_directory.mkdir() try: logger.info( f"Writing to the '{config_file_path.relative_to(refind_directory)}' file." ) with config_file_path.open("w") as config_file: lines_for_writing: list[str] = [] lines_for_writing.append( constants.NEWLINE.join( str(boot_stanza) for boot_stanza in none_throws(boot_stanzas))) lines_for_writing.append(constants.NEWLINE) config_file.writelines(lines_for_writing) except OSError as e: logger.exception("Path.open('w') call failed!") raise RefindConfigError( f"Could not write to the '{config_file_path.name}' file!" ) from e config.refresh_file_stat() persistence_provider.save_refind_config(config)
def initialize_prepared_snapshots(self) -> None: persistence_provider = self._persistence_provider snapshot_manipulation = self.package_config.snapshot_manipulation subvolume = self.root_subvolume previous_run_result = persistence_provider.get_previous_run_result() selected_snapshots = none_throws( subvolume.select_snapshots(snapshot_manipulation.selection_count)) destination_directory = snapshot_manipulation.destination_directory snapshots_union = snapshot_manipulation.cleanup_exclusion.union( selected_snapshots) if previous_run_result.has_bootable_snapshots(): bootable_snapshots = previous_run_result.bootable_snapshots snapshots_for_addition = [ snapshot for snapshot in selected_snapshots if snapshot.can_be_added(bootable_snapshots) ] snapshots_for_removal = [ snapshot for snapshot in bootable_snapshots if snapshot.can_be_removed( destination_directory, snapshots_union) ] else: destination_snapshots = self.destination_snapshots snapshots_for_addition = selected_snapshots snapshots_for_removal = [ snapshot for snapshot in destination_snapshots.difference( snapshots_union) if snapshot.can_be_removed( destination_directory, selected_snapshots) ] if has_items(snapshots_for_addition): device_command_factory = self._device_command_factory for snapshot in snapshots_for_addition: snapshot.initialize_partition_table_using( device_command_factory) self._prepared_snapshots = PreparedSnapshots(snapshots_for_addition, snapshots_for_removal)
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 has_unmatched_snapshots(self) -> bool: return has_items(self.unmatched_snapshots)
def has_sub_menus(self) -> bool: return has_items(self.sub_menus)
def has_changes(self) -> bool: return has_items(self.snapshots_for_addition) or has_items( self.snapshots_for_removal)
def append_to_config(self, config: RefindConfig) -> None: logger = self._logger persistence_provider = self._persistence_provider config_file_path = config.file_path actual_config = persistence_provider.get_refind_config( config_file_path) if actual_config is not None: new_included_configs = config.get_included_configs_difference_from( actual_config) if has_items(new_included_configs): included_configs_for_appending = none_throws( new_included_configs) try: with config_file_path.open("r") as config_file: all_lines = config_file.readlines() last_line = last(all_lines) except OSError as e: logger.exception("Path.open('r') call failed!") raise RefindConfigError( f"Could not read from the '{config_file_path}' file!" ) from e else: include_option = RefindOption.INCLUDE.value suffix = item_count_suffix(included_configs_for_appending) try: logger.info( f"Appending {len(included_configs_for_appending)} '{include_option}' " f"directive{suffix} to the '{config_file_path.name}' file." ) with config_file_path.open("a") as config_file: lines_for_appending: list[str] = [] should_prepend_newline = False if not is_none_or_whitespace(last_line): include_option_pattern = re.compile( constants.INCLUDE_OPTION_PATTERN, re.DOTALL) should_prepend_newline = ( not include_option_pattern.match(last_line) ) if should_prepend_newline: lines_for_appending.append(constants.NEWLINE) destination_directory = config_file_path.parent for included_config in included_configs_for_appending: included_config_relative_file_path = ( included_config.file_path.relative_to( destination_directory)) lines_for_appending.append( f"{include_option} {included_config_relative_file_path}" f"{constants.NEWLINE}") config_file.writelines(lines_for_appending) except OSError as e: logger.exception("Path.open('a') call failed!") raise RefindConfigError( f"Could not append to the '{config_file_path.name}' file!" ) from e config.refresh_file_stat() persistence_provider.save_refind_config(config)
def __str__(self) -> str: result: list[str] = [] main_indent = constants.EMPTY_STR option_indent = constants.TAB name = self.name result.append( f"{main_indent}{RefindOption.MENU_ENTRY.value} {name} {{") icon_path = self.icon_path if not is_none_or_whitespace(icon_path): result.append( f"{option_indent}{RefindOption.ICON.value} {icon_path}") volume = self.volume if not is_none_or_whitespace(volume): result.append( f"{option_indent}{RefindOption.VOLUME.value} {volume}") loader_path = self.loader_path if not is_none_or_whitespace(loader_path): result.append( f"{option_indent}{RefindOption.LOADER.value} {loader_path}") initrd_path = self.initrd_path if not is_none_or_whitespace(initrd_path): result.append( f"{option_indent}{RefindOption.INITRD.value} {initrd_path}") os_type = self.os_type if not is_none_or_whitespace(os_type): result.append( f"{option_indent}{RefindOption.OS_TYPE.value} {os_type}") graphics = self.graphics if graphics is not None: graphics_parameter = (GraphicsParameter.ON if graphics else GraphicsParameter.OFF) result.append( f"{option_indent}{RefindOption.GRAPHICS.value} {graphics_parameter.value}" ) boot_options_str = str(self.boot_options) if not is_none_or_whitespace(boot_options_str): result.append( f"{option_indent}{RefindOption.BOOT_OPTIONS.value} {boot_options_str}" ) firmware_bootnum = self.firmware_bootnum if firmware_bootnum is not None: result.append( f"{option_indent}{RefindOption.FIRMWARE_BOOTNUM.value} {firmware_bootnum:04x}" ) sub_menus = self.sub_menus if has_items(sub_menus): result.extend(str(sub_menu) for sub_menu in none_throws(sub_menus)) is_disabled = self.is_disabled if is_disabled: result.append(f"{option_indent}{RefindOption.DISABLED.value}") result.append(f"{main_indent}}}") return constants.NEWLINE.join(result)
def has_snapshots(self) -> bool: return has_items(self.snapshots)
def has_bootable_snapshots(self) -> bool: return has_items(self.bootable_snapshots)
def has_partitions(self) -> bool: return has_items(self.partitions)