Beispiel #1
0
    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
Beispiel #2
0
    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))
Beispiel #3
0
    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
Beispiel #4
0
    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
Beispiel #5
0
    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
Beispiel #6
0
    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
Beispiel #7
0
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
Beispiel #8
0
    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
Beispiel #9
0
    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
Beispiel #10
0
    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
Beispiel #11
0
    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
Beispiel #13
0
    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)
Beispiel #14
0
    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
Beispiel #15
0
    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
Beispiel #16
0
    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)
Beispiel #17
0
    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
Beispiel #18
0
    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)}!"
            )
Beispiel #19
0
    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,
        )
Beispiel #21
0
    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,
                )
Beispiel #22
0
    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
Beispiel #23
0
    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
Beispiel #24
0
    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))
Beispiel #26
0
    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
Beispiel #27
0
    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,
        )
Beispiel #29
0
    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
Beispiel #30
0
    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)