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
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)
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, )
def delete_snapshot(self, snapshot: Subvolume) -> None: logger = self._logger filesystem_path = snapshot.filesystem_path logical_path = snapshot.logical_path try: filesystem_path_str = str(filesystem_path) is_subvolume = filesystem_path.exists() and btrfsutil.is_subvolume( filesystem_path_str) if is_subvolume: root_dir_str = str(constants.ROOT_DIR) num_id = snapshot.num_id deleted_subvolumes = checked_cast( list[int], btrfsutil.deleted_subvolumes(root_dir_str)) if num_id not in deleted_subvolumes: logger.info(f"Deleting the '{logical_path}' snapshot.") btrfsutil.delete_subvolume(filesystem_path_str) else: logger.warning( f"The '{logical_path}' snapshot has already " "been deleted but not yet cleaned up.") else: logger.warning( f"The '{filesystem_path}' directory is not a subvolume.") except btrfsutil.BtrfsUtilError as e: logger.exception("btrfsutil call failed!") raise SubvolumeError( f"Could not delete the '{logical_path}' snapshot!") from e
def 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 _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))
def with_initialization_type( self, initialization_type: ConfigInitializationType, derived_type: Type[TDerived], ) -> TDerived: self._initialization_type = initialization_type return checked_cast(derived_type, self)
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
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 _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 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
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
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()
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
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)
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
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)
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)
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
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)) )
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