コード例 #1
0
    def __init__(self, raw_mount_options: str) -> None:
        split_mount_options = [
            option.strip()
            for option in raw_mount_options.split(constants.COLUMN_SEPARATOR)
        ]
        simple_options: list[tuple[int, str]] = []
        parameterized_options: dict[str, tuple[int, str]] = {}
        parameterized_option_prefix_pattern = re.compile(
            constants.PARAMETERIZED_OPTION_PREFIX_PATTERN)

        for position, option in enumerate(split_mount_options):
            if not is_none_or_whitespace(option):
                if parameterized_option_prefix_pattern.match(option):
                    split_parameterized_option = option.split(
                        constants.PARAMETERIZED_OPTION_SEPARATOR)
                    option_name = checked_cast(str,
                                               split_parameterized_option[0])
                    option_value = checked_cast(str,
                                                split_parameterized_option[1])

                    if option_name in parameterized_options:
                        raise PartitionError(
                            f"The '{option_name}' mount option "
                            f"cannot be defined multiple times!")

                    parameterized_options[option_name] = (position,
                                                          option_value)
                else:
                    simple_options.append((position, option))

        self._simple_options = simple_options
        self._parameterized_options = parameterized_options
コード例 #2
0
    def visitBoot_stanza(
            self, ctx: RefindConfigParser.Boot_stanzaContext) -> BootStanza:
        menu_entry_context = ctx.menu_entry()
        menu_entry = menu_entry_context.accept(MenuEntryVisitor())
        main_options = OptionVisitor.map_to_options_dict(
            checked_cast(list[ParserRuleContext], ctx.main_option()))
        volume = only(always_iterable(main_options.get(RefindOption.VOLUME)))
        loader = only(always_iterable(main_options.get(RefindOption.LOADER)))
        initrd = only(always_iterable(main_options.get(RefindOption.INITRD)))
        icon = only(always_iterable(main_options.get(RefindOption.ICON)))
        os_type = only(always_iterable(main_options.get(RefindOption.OS_TYPE)))
        graphics = only(
            always_iterable(main_options.get(RefindOption.GRAPHICS)))
        boot_options = only(
            always_iterable(main_options.get(RefindOption.BOOT_OPTIONS)))
        firmware_bootnum = only(
            always_iterable(main_options.get(RefindOption.FIRMWARE_BOOTNUM)))
        disabled = only(always_iterable(main_options.get(
            RefindOption.DISABLED)),
                        default=False)
        sub_menus = always_iterable(
            main_options.get(RefindOption.SUB_MENU_ENTRY))

        return BootStanza(
            menu_entry,
            volume,
            loader,
            initrd,
            icon,
            os_type,
            graphics,
            BootOptions(boot_options),
            firmware_bootnum,
            disabled,
        ).with_sub_menus(sub_menus)
コード例 #3
0
    def visitSub_menu(self,
                      ctx: RefindConfigParser.Sub_menuContext) -> SubMenu:
        menu_entry_context = ctx.menu_entry()
        menu_entry = menu_entry_context.accept(MenuEntryVisitor())
        sub_options = OptionVisitor.map_to_options_dict(
            checked_cast(list[ParserRuleContext], ctx.sub_option()))
        loader = only(always_iterable(sub_options.get(RefindOption.LOADER)))
        initrd = only(always_iterable(sub_options.get(RefindOption.INITRD)))
        graphics = only(always_iterable(sub_options.get(
            RefindOption.GRAPHICS)))
        boot_options = only(
            always_iterable(sub_options.get(RefindOption.BOOT_OPTIONS)))
        add_boot_options = only(
            always_iterable(sub_options.get(RefindOption.ADD_BOOT_OPTIONS)))
        disabled = only(always_iterable(sub_options.get(
            RefindOption.DISABLED)),
                        default=False)

        return SubMenu(
            menu_entry,
            loader,
            initrd,
            graphics,
            BootOptions(boot_options) if boot_options is not None else None,
            BootOptions(add_boot_options),
            disabled,
        )
コード例 #4
0
    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
コード例 #5
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
コード例 #6
0
    def _block_device_partition_table(
        self, block_device: BlockDevice
    ) -> PartitionTable:
        logger = self._logger
        findmnt_columns = [
            FindmntColumn.PART_UUID,
            FindmntColumn.PART_LABEL,
            FindmntColumn.FS_UUID,
            FindmntColumn.DEVICE_NAME,
            FindmntColumn.FS_TYPE,
            FindmntColumn.FS_LABEL,
            FindmntColumn.FS_MOUNT_POINT,
            FindmntColumn.FS_MOUNT_OPTIONS,
        ]
        device_name = block_device.name
        output = constants.COLUMN_SEPARATOR.join(
            [findmnt_column_key.value.upper() for findmnt_column_key in findmnt_columns]
        )
        findmnt_command = f"findmnt --json --mtab --real --nofsroot --output {output}"

        try:
            logger.info(
                f"Initializing the live partition table for device '{device_name}' using findmnt."
            )
            logger.debug(f"Running command '{findmnt_command}'.")

            findmnt_process = subprocess.run(
                findmnt_command.split(), capture_output=True, check=True, text=True
            )
        except CalledProcessError as e:
            stderr = checked_cast(str, e.stderr)

            if is_none_or_whitespace(stderr):
                message = "findmnt execution failed!"
            else:
                message = f"findmnt execution failed: '{stderr.rstrip()}'!"

            logger.exception(message)
            raise PartitionError(
                f"Could not initialize the live partition table for '{device_name}'!"
            ) from e

        findmnt_parsed_output = json.loads(findmnt_process.stdout)
        findmnt_partitions = (
            findmnt_partition
            for findmnt_partition in always_iterable(
                findmnt_parsed_output.get(FindmntJsonKey.FILESYSTEMS.value)
            )
            if block_device.is_matched_with(
                default_if_none(
                    findmnt_partition.get(FindmntColumn.DEVICE_NAME.value),
                    constants.EMPTY_STR,
                )
            )
        )

        return PartitionTable(
            constants.EMPTY_HEX_UUID, constants.MTAB_PT_TYPE
        ).with_partitions(FindmntCommand._map_to_partitions(findmnt_partitions))
コード例 #7
0
    def with_initialization_type(
        self,
        initialization_type: ConfigInitializationType,
        derived_type: Type[TDerived],
    ) -> TDerived:
        self._initialization_type = initialization_type

        return checked_cast(derived_type, self)
コード例 #8
0
    def _get_item(self, value_key: str, local_db: Shelf) -> Optional[Any]:
        version_key = f"{value_key}_{constants.DB_ITEM_VERSION_SUFFIX}"
        default_version = Version("0.0.0")
        current_version = self._current_versions[version_key]
        actual_version = (checked_cast(Version, local_db[version_key])
                          if version_key in local_db else default_version)

        return local_db.get(
            value_key) if actual_version >= current_version else None
コード例 #9
0
    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
コード例 #10
0
    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))
コード例 #11
0
    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))
コード例 #12
0
    def get_package_config(self) -> Optional[PackageConfig]:
        db_key = LocalDbKey.PACKAGE_CONFIG.value

        with shelve.open(self._db_filename) as local_db:
            item = self._get_item(db_key, local_db)

            if item is not None:
                package_config = checked_cast(PackageConfig, item)

                if not package_config.is_modified(
                        constants.PACKAGE_CONFIG_FILE):
                    return package_config

        return None
コード例 #13
0
    def __init__(self, *args: object) -> None:
        super().__init__(args)

        if args is not None:
            self._message = checked_cast(
                str,
                first_true(
                    args,
                    pred=lambda arg: isinstance(arg, str),
                    default=constants.EMPTY_STR,
                ),
            )
        else:
            self._message = constants.EMPTY_STR
コード例 #14
0
    def on_created(self, event: FileSystemEvent) -> None:
        is_dir_created_event = (event.event_type == EVENT_TYPE_CREATED
                                and event.is_directory)

        if is_dir_created_event:
            dir_created_event = checked_cast(DirCreatedEvent, event)
            logger = self._logger
            created_directory = Path(dir_created_event.src_path)

            if self._is_snapshot_created(created_directory):
                machine = self._machine

                logger.info(
                    f"The '{created_directory}' snapshot has been created.")

                machine.run()
コード例 #15
0
    def map_to_options_dict(
        cls, option_contexts: Iterable[ParserRuleContext]
    ) -> DefaultDict[RefindOption, list[Any]]:
        option_visitor = cls()
        result = defaultdict(list)

        for option_context in option_contexts:
            option_tuple = option_context.accept(option_visitor)

            if option_tuple is not None:
                key = checked_cast(RefindOption, option_tuple[0])
                value = option_tuple[1]

                result[key].append(value)

        return result
コード例 #16
0
    def save_refind_config(self, value: RefindConfig) -> None:
        db_key = LocalDbKey.REFIND_CONFIGS.value

        with shelve.open(self._db_filename) as local_db:
            item = self._get_item(db_key, local_db)
            all_refind_configs: Optional[dict[Path, RefindConfig]] = None

            if item is not None:
                all_refind_configs = checked_cast(dict[Path, RefindConfig],
                                                  item)
            else:
                all_refind_configs = {}

            file_path = value.file_path
            all_refind_configs[file_path] = value

            self._save_item(all_refind_configs, db_key, local_db)
コード例 #17
0
    def get_refind_config(self, file_path: Path) -> Optional[RefindConfig]:
        db_key = LocalDbKey.REFIND_CONFIGS.value

        with shelve.open(self._db_filename) as local_db:
            item = self._get_item(db_key, local_db)

            if item is not None:
                all_refind_configs = checked_cast(dict[Path, RefindConfig],
                                                  item)
                refind_config = all_refind_configs.get(file_path)

                if refind_config is not None:
                    if refind_config.is_modified(file_path):
                        del all_refind_configs[file_path]

                        self._save_item(all_refind_configs, db_key, local_db)
                    else:
                        return refind_config

        return None
コード例 #18
0
    def on_deleted(self, event: FileSystemEvent) -> None:
        is_dir_deleted_event = (event.event_type == EVENT_TYPE_DELETED
                                and event.is_directory)

        if is_dir_deleted_event:
            dir_deleted_event = checked_cast(DirDeletedEvent, event)
            logger = self._logger
            deleted_directory = Path(dir_deleted_event.src_path)

            try:
                if self._is_snapshot_deleted(deleted_directory):
                    machine = self._machine

                    logger.info(
                        f"The '{deleted_directory}' snapshot has been deleted."
                    )

                    machine.run()
            except SnapshotExcludedFromDeletionError as e:
                logger.warning(e.formatted_message)
コード例 #19
0
    def get_block_devices(self) -> Iterator[BlockDevice]:
        logger = self._logger
        lsblk_columns = [
            LsblkColumn.DEVICE_NAME,
            LsblkColumn.DEVICE_TYPE,
            LsblkColumn.MAJOR_MINOR,
        ]
        output = constants.COLUMN_SEPARATOR.join(
            [lsblk_column_key.value.upper() for lsblk_column_key in lsblk_columns]
        )
        lsblk_command = f"lsblk --json --merge --paths --output {output}"

        try:
            logger.info("Initializing the block devices using lsblk.")
            logger.debug(f"Running command '{lsblk_command}'.")

            lsblk_process = subprocess.run(
                lsblk_command.split(), capture_output=True, check=True, text=True
            )
        except CalledProcessError as e:
            stderr = checked_cast(str, e.stderr)

            if is_none_or_whitespace(stderr):
                message = "lsblk execution failed!"
            else:
                message = f"lsblk execution failed: '{stderr.rstrip()}'!"

            logger.exception(message)
            raise PartitionError("Could not initialize the block devices!") from e

        lsblk_parsed_output = json.loads(lsblk_process.stdout)
        lsblk_blockdevices = always_iterable(
            lsblk_parsed_output.get(LsblkJsonKey.BLOCKDEVICES.value)
        )

        yield from LsblkCommand._map_to_block_devices(lsblk_blockdevices)
コード例 #20
0
    def _read_config_from(self, config_file_path: Path) -> RefindConfig:
        persistence_provider = self._persistence_provider
        persisted_refind_config = persistence_provider.get_refind_config(
            config_file_path)
        current_refind_config = self._refind_configs.get(config_file_path)

        if persisted_refind_config is None:
            logger = self._logger

            logger.info(f"Analyzing the '{config_file_path.name}' file.")

            try:
                input_stream = FileStream(str(config_file_path),
                                          encoding="utf-8")
                lexer = RefindConfigLexer(input_stream)
                token_stream = CommonTokenStream(lexer)
                parser = RefindConfigParser(token_stream)
                error_listener = RefindErrorListener()

                parser.removeErrorListeners()
                parser.addErrorListener(error_listener)

                refind_context = parser.refind()
            except RefindSyntaxError as e:
                logger.exception(
                    f"Error while parsing the '{config_file_path.name}' file!")
                raise RefindConfigError(
                    "Could not load rEFInd configuration from file!") from e
            else:
                config_option_contexts = checked_cast(
                    list[RefindConfigParser.Config_optionContext],
                    refind_context.config_option(),
                )
                boot_stanzas = FileRefindConfigProvider._map_to_boot_stanzas(
                    config_option_contexts)
                includes = FileRefindConfigProvider._map_to_includes(
                    config_option_contexts)
                included_configs = self._read_included_configs_from(
                    config_file_path.parent, includes)

                current_refind_config = (
                    RefindConfig(config_file_path).with_boot_stanzas(
                        boot_stanzas).with_included_configs(
                            included_configs).with_initialization_type(
                                ConfigInitializationType.PARSED, RefindConfig))

                persistence_provider.save_refind_config(current_refind_config)
        elif current_refind_config is None:
            current_refind_config = persisted_refind_config.with_initialization_type(
                ConfigInitializationType.PERSISTED, RefindConfig)

            if current_refind_config.has_included_configs():
                current_included_configs = none_throws(
                    current_refind_config.included_configs)
                actual_included_configs = [
                    self._read_config_from(included_config.file_path)
                    for included_config in current_included_configs
                    if included_config.file_path.exists()
                ]
                current_refind_config = current_refind_config.with_included_configs(
                    actual_included_configs)

        self._refind_configs[config_file_path] = current_refind_config

        return current_refind_config
コード例 #21
0
    def _block_device_partition_table(
        self, block_device: BlockDevice
    ) -> PartitionTable:
        logger = self._logger
        lsblk_columns = [
            LsblkColumn.PTABLE_UUID,
            LsblkColumn.PTABLE_TYPE,
            LsblkColumn.PART_UUID,
            LsblkColumn.PART_TYPE,
            LsblkColumn.PART_LABEL,
            LsblkColumn.FS_UUID,
            LsblkColumn.DEVICE_NAME,
            LsblkColumn.FS_TYPE,
            LsblkColumn.FS_LABEL,
            LsblkColumn.FS_MOUNT_POINT,
        ]
        device_name = block_device.name
        output = constants.COLUMN_SEPARATOR.join(
            [lsblk_column_key.value.upper() for lsblk_column_key in lsblk_columns]
        )
        lsblk_command = f"lsblk {device_name} --json --paths --tree --output {output}"

        try:
            logger.info(
                f"Initializing the physical partition table for device '{device_name}' using lsblk."
            )
            logger.debug(f"Running command '{lsblk_command}'.")

            lsblk_process = subprocess.run(
                lsblk_command.split(), check=True, capture_output=True, text=True
            )
        except CalledProcessError as e:
            stderr = checked_cast(str, e.stderr)

            if is_none_or_whitespace(stderr):
                message = "lsblk execution failed!"
            else:
                message = f"lsblk execution failed: '{stderr.rstrip()}'!"

            logger.exception(message)
            raise PartitionError(
                f"Could not initialize the physical partition table for '{device_name}'!"
            ) from e

        lsblk_parsed_output = json.loads(lsblk_process.stdout)
        lsblk_blockdevice = one(
            lsblk_parsed_output.get(LsblkJsonKey.BLOCKDEVICES.value)
        )
        lsblk_partition_table_columns = [
            default_if_none(
                lsblk_blockdevice.get(lsblk_column_key.value), constants.EMPTY_STR
            )
            for lsblk_column_key in [
                LsblkColumn.PTABLE_UUID,
                LsblkColumn.PTABLE_TYPE,
            ]
        ]
        esp_uuid = self.package_config.esp_uuid
        lsblk_partitions = always_iterable(
            lsblk_blockdevice.get(LsblkJsonKey.CHILDREN.value)
        )

        return (
            PartitionTable(*lsblk_partition_table_columns)
            .with_esp_uuid(esp_uuid)
            .with_partitions(LsblkCommand._map_to_partitions(lsblk_partitions))
        )
コード例 #22
0
    def run(self) -> int:
        logger = self._logger
        package_config_provider = self._package_config_provider
        observer = self._observer
        event_handler = self._snapshot_event_handler
        current_pid = self._current_pid
        exit_code = os.EX_OK

        try:
            with PidFile(pidname=constants.BACKGROUND_MODE_PID_NAME,
                         lock_pidfile=False) as pid_file:
                current_pid = pid_file.pid
                package_config = package_config_provider.get_config()
                directories_for_watch = [
                    str(directory) for directory in sorted(
                        package_config.directories_for_watch)
                ]

                logger.info(
                    "Scheduling watch for directories: "
                    f"{constants.DEFAULT_ITEMS_SEPARATOR.join(directories_for_watch)}."
                )

                for directory in directories_for_watch:
                    observer.schedule(
                        event_handler,
                        directory,
                        recursive=False,
                    )

                logger.info(f"Starting the observer with PID {current_pid}.")

                observer.start()
                systemd_daemon.notify(constants.NOTIFICATION_READY,
                                      pid=current_pid)

                while observer.is_alive():
                    observer.join(constants.WATCH_TIMEOUT)

                observer.join()
        except PidFileAlreadyRunningError as e:
            exit_code = constants.EX_NOT_OK
            running_pid = checked_cast(int, e.pid)

            logger.error(e.message)
            systemd_daemon.notify(
                constants.NOTIFICATION_STATUS.format(
                    f"Detected an attempt to run subsequently with PID {current_pid}."
                ),
                pid=running_pid,
            )
        else:
            try:
                observer.check()
            except SnapshotMountedAsRootError:
                pass
            except Exception:
                exit_code = constants.EX_NOT_OK

        if exit_code != os.EX_OK:
            systemd_daemon.notify(
                constants.NOTIFICATION_ERRNO.format(exit_code),
                pid=current_pid)

        return exit_code