def mid_activate(self, be_mountpoint: str): ZELogger.verbose_log( { "level": "INFO", "message": f"Running {self.bootloader} mid activate.\n" }, self.verbose) self.modify_fstab(be_mountpoint)
def check_zedenv_properties(self): """ Get zedenv properties in format: {"property": <property val>} If prop unset, leave default """ for prop in self.zedenv_properties: ZELogger.verbose_log( { "level": "INFO", "message": f"Checking prop: 'org.zedenv.{self.bootloader}:{prop}'" }, self.verbose) # Use the properties from the old boot environment # as we can not be sure if the new boot environment exists at this point. prop_val = zedenv.lib.be.get_property( "/".join([self.be_root, self.old_boot_environment]), f"org.zedenv.{self.bootloader}:{prop}") if prop_val is not None and prop_val != "-": self.zedenv_properties[prop] = prop_val ZELogger.verbose_log( { "level": "INFO", "message": (f"org.zedenv.{self.bootloader}:{prop}=" f"{self.zedenv_properties[prop]}.\n") }, self.verbose)
def apply_settings_to_child_datasets(be_child_datasets_list, be_requested, verbose): canmount_setting = "canmount=noauto" for ds in be_child_datasets_list: if be_requested == ds: try: pyzfscmds.cmd.zfs_set(ds, canmount_setting) except RuntimeError: ZELogger.log( { "level": "EXCEPTION", "message": f"Failed to set {canmount_setting} for {ds}\n{e}\n" }, exit_on_error=True) if pyzfscmds.utility.is_clone(ds): try: pyzfscmds.cmd.zfs_promote(ds) except RuntimeError: ZELogger.log( { "level": "EXCEPTION", "message": f"Failed to promote BE {ds}\n{e}\n" }, exit_on_error=True) ZELogger.verbose_log( { "level": "INFO", "message": f"Promoted {ds}.\n" }, verbose)
def __init__(self, zedenv_data: dict): super().__init__(zedenv_data) self.env_dir = "env" self.boot_mountpoint = "/boot" self.entry_prefix = "zedenv" self.old_entry = f"{self.entry_prefix}-{self.old_boot_environment}" self.new_entry = f"{self.entry_prefix}-{self.boot_environment}" # Set defaults for pr in self.allowed_properties: self.zedenv_properties[pr["property"]] = pr["default"] self.check_zedenv_properties() ZELogger.verbose_log( { "level": "INFO", "message": f"esp set to {self.zedenv_properties['esp']}\n" }, self.verbose) if not os.path.isdir(self.zedenv_properties["esp"]): self.plugin_property_error(self.zedenv_properties)
def post_activate(self): ZELogger.verbose_log( { "level": "INFO", "message": (f"Creating Temporary working directory. " "No changes will be made until the end of " "the systemd-boot configuration.\n") }, self.verbose) with tempfile.TemporaryDirectory(prefix="zedenv", suffix=self.bootloader) as t_esp: ZELogger.verbose_log( { "level": "INFO", "message": f"Created {t_esp}.\n" }, self.verbose) self.modify_bootloader(t_esp) self.edit_bootloader_entry(t_esp) self.recurse_move(t_esp, self.esp) self.edit_bootloader_default(t_esp, overwrite=True)
def recurse_move(self, source, dest, overwrite=False): for tf in os.listdir(source): tf_path_src = os.path.join(source, tf) tf_path_dst = os.path.join(dest, tf) if os.path.isfile(tf_path_src): if os.path.isfile(tf_path_dst) and not overwrite: ZELogger.verbose_log({ "level": "INFO", "message": f"File {tf_path_dst} already exists, will not modify.\n" }, self.verbose) else: try: shutil.copy(tf_path_src, tf_path_dst) except PermissionError: ZELogger.log({ "level": "EXCEPTION", "message": f"Require Privileges to write to '{tf_path_dst}.'\n" }, exit_on_error=True) ZELogger.verbose_log({ "level": "INFO", "message": f"Copied file {tf_path_src} -> {tf_path_dst}\n" }, self.verbose) elif os.path.isdir(tf_path_src): if os.path.isdir(tf_path_dst) and not overwrite: ZELogger.verbose_log({ "level": "INFO", "message": f"Directory {tf_path_dst} already exists, will not modify.\n" }, self.verbose) # Call again, may be empty self.recurse_move(tf_path_src, tf_path_dst, overwrite=overwrite) else: if os.path.isdir(tf_path_dst): shutil.move(tf_path_dst, f"{tf_path_dst}.bak") ZELogger.verbose_log({ "level": "INFO", "message": (f"Directory {tf_path_dst} already exists, " f"creating backup {tf_path_dst}.bak.\n") }, self.verbose) try: shutil.copytree(tf_path_src, tf_path_dst) except PermissionError as e: ZELogger.log({ "level": "EXCEPTION", "message": f"Require Privileges to write to {tf_path_dst}\n{e}" }, exit_on_error=True) except IOError as e: ZELogger.log({ "level": "EXCEPTION", "message": f"IOError writing to {tf_path_dst}\n{e}" }, exit_on_error=True) ZELogger.verbose_log({ "level": "INFO", "message": f"Copied dir {tf_path_src} -> {tf_path_dst}\n" }, self.verbose)
def _loader_replace(self, configs: list): be_dataset = f"{self.be_root}/{self.boot_environment}" target = re.compile(r'^vfs.root.mountfrom=.*$') for c in configs: with open(c, "r") as loader_conf: conf_list = loader_conf.readlines() line_nums = [ l for l, val in enumerate(conf_list) if target.search(val) ] for lnum in line_nums: conf_list[lnum] = f"vfs.root.mountfrom={be_dataset}\n" if not self.noop: if os.path.isfile(c): ZELogger.verbose_log( { "level": "INFO", "message": (f"File {c} already exists, backed up to " f"'{c}.bak' and replaced.\n") }, self.verbose) if os.path.isfile(f"{c}.bak"): try: os.remove(f"{c}.bak") except PermissionError: ZELogger.log( { "level": "EXCEPTION", "message": (f"Require Privileges to remove " f"'{c}.bak'\n") }, exit_on_error=True) try: shutil.move(c, f"{c}.bak") except PermissionError: ZELogger.log( { "level": "EXCEPTION", "message": (f"Require Privileges to write to " f"'{c}.bak'\n") }, exit_on_error=True) with open(c, "w") as loader_conf: loader_conf.writelines(conf_list)
def mid_activate(self, be_mountpoint: str): ZELogger.verbose_log( { "level": "INFO", "message": f"Running {self.bootloader} mid activate.\n" }, self.verbose) replace_pattern = r'(^{esp}/{env}/?)(.*)(\s.*{boot}\s.*$)'.format( esp=self.zedenv_properties["esp"], env=self.env_dir, boot=self.boot_mountpoint) self.modify_fstab(be_mountpoint, replace_pattern, self.new_entry)
def zedenv_set(verbose: Optional[bool], zedenv_properties: Optional[list], be_root: str): for prop in zedenv_properties: try: pyzfscmds.cmd.zfs_set(be_root, prop) except RuntimeError: ZELogger.log({ "level": "EXCEPTION", "message": f"Failed to set zedenv property '{prop}'\n" }, exit_on_error=True) if verbose: ZELogger.verbose_log({ "level": "INFO", "message": f"Set '{prop}' successfully" }, verbose)
def __init__(self, zedenv_data: dict): for k in zedenv_data: if k not in plugin_config.allowed_keys: raise ValueError(f"Type {k} is not in allowed keys") self.boot_environment = zedenv_data['boot_environment'] self.old_boot_environment = zedenv_data['old_boot_environment'] self.bootloader = zedenv_data['bootloader'] self.verbose = zedenv_data['verbose'] self.noconfirm = zedenv_data['noconfirm'] self.noop = zedenv_data['noop'] self.be_root = zedenv_data['boot_environment_root'] self.env_dir = "env" self.boot_mountpoint = "/boot" self.entry_prefix = "zedenv" self.old_entry = f"{self.entry_prefix}-{self.old_boot_environment}" self.new_entry = f"{self.entry_prefix}-{self.boot_environment}" esp = zedenv.lib.be.get_property( "/".join([self.be_root, self.boot_environment]), "org.zedenv:esp") if esp is None or esp == "-": self.esp = "/mnt/efi" else: self.esp = esp ZELogger.verbose_log( { "level": "INFO", "message": f"esp set to {esp}\n" }, self.verbose) if not os.path.isdir(self.esp): ZELogger.log( { "level": "EXCEPTION", "message": ("To use the systemdboot plugin, an 'esp' must be mounted at the " "default location of `/mnt/esp`, or at another location, with the " "property 'org.zedenv:esp' set on the dataset. To set it use the " "command (replacing with your pool and dataset)\n'" "zfs set org.zedenv:esp='/mnt/efi' zpool/ROOT/default\n") }, exit_on_error=True)
def zedenv_umount(boot_environment: str, verbose: bool, be_root: str): boot_environment_dataset = f"{be_root}/{boot_environment}" child_datasets_unformatted = None try: child_datasets_unformatted = pyzfscmds.cmd.zfs_list( boot_environment_dataset, sort_properties_descending=['name'], recursive=True, columns=['name']) except RuntimeError: ZELogger.log( { "level": "EXCEPTION", "message": f"Failed to get list of datasets for '{boot_environment}'.\n{e}" }, exit_on_error=True) for d in zedenv.lib.be.split_zfs_output(child_datasets_unformatted): mountpoint = pyzfscmds.system.agnostic.dataset_mountpoint(d[0]) if mountpoint: try: zedenv.lib.system.umount(mountpoint) except RuntimeError as e: ZELogger.log( { "level": "EXCEPTION", "message": f"Failed Un-mounting child dataset from '{mountpoint}'.\n{e}" }, exit_on_error=True) ZELogger.verbose_log( { "level": "INFO", "message": f"Unmounted {d[0]} from {mountpoint}.\n" }, verbose) else: ZELogger.verbose_log( { "level": "INFO", "message": f"Child dataset {d[0]} wasn't mounted, won't unmount.\n" }, verbose)
def activate_boot_environment(be_requested: str, dataset_mountpoint: Optional[str], verbose: Optional[bool], noop: Optional[bool], bootloader_plugin): if dataset_mountpoint != "/": if dataset_mountpoint: ZELogger.verbose_log( { "level": "INFO", "message": f"Unmounting {dataset_mountpoint}.\n" }, verbose) if not noop: try: zedenv.lib.system.umount(dataset_mountpoint) except RuntimeError as e: ZELogger.log( { "level": "EXCEPTION", "message": f"Failed unmounting dataset {be_requested}\n{e}\n" }, exit_on_error=True) mount_and_modify_dataset(be_requested, pre_mount_properties=["canmount=noauto"], post_mount_properties=["mountpoint=/"], verbose=verbose, noop=noop, plugin=bootloader_plugin) if not noop: try: pyzfscmds.cmd.zpool_set(zedenv.lib.be.dataset_pool(be_requested), f"bootfs={be_requested}") except RuntimeError as e: ZELogger.log( { "level": "EXCEPTION", "message": f"Failed to set bootfs to {be_requested}\n{e}\n" }, exit_on_error=True)
def setup_boot_env_tree(self): mount_root = os.path.join(self.zedenv_properties["boot"], self.zfs_env_dir) if not os.path.exists(mount_root): os.mkdir(mount_root) be_list = None be_list = zedenv.lib.be.list_boot_environments(self.be_root, ['name']) ZELogger.verbose_log( { "level": "INFO", "message": f"Going over list {be_list}.\n" }, self.verbose) for b in be_list: if not pyzfscmds.utility.is_snapshot(b['name']): be_name = pyzfscmds.utility.dataset_child_name( b['name'], False) if pyzfscmds.system.agnostic.dataset_mountpoint( b['name']) == "/": ZELogger.verbose_log( { "level": "INFO", "message": f"Dataset {b['name']} is root, skipping.\n" }, self.verbose) else: be_boot_mount = os.path.join(mount_root, f"zedenv-{be_name}") ZELogger.verbose_log( { "level": "INFO", "message": f"Setting up {b['name']}.\n" }, self.verbose) if not os.path.exists(be_boot_mount): os.mkdir(be_boot_mount) if not os.listdir(be_boot_mount): zedenv.cli.mount.zedenv_mount(be_name, be_boot_mount, self.verbose, self.be_root) else: ZELogger.verbose_log( { "level": "WARNING", "message": f"Mount directory {be_boot_mount} wasn't empty, skipping.\n" }, self.verbose)
def grub_mkconfig(self, location: str): env = dict(os.environ, ZPOOL_VDEV_NAME_PATH='1') ZELogger.verbose_log( { "level": "INFO", "message": (f"Generating " "the GRUB configuration.\n") }, self.verbose) grub_call = ["grub-mkconfig", "-o", location] try: grub_output = subprocess.check_call(grub_call, env=env, universal_newlines=True, stderr=subprocess.PIPE) except subprocess.CalledProcessError as e: raise RuntimeError(f"Failed to generate GRUB config.\n{e}\n.") return grub_output
def mount_children(child_datasets: list, mountpoint: str, verbose: bool): for cd in child_datasets: if cd['mountpoint'] == "none" or cd['mountpoint'] == "legacy": ZELogger.verbose_log( { "level": "INFO", "message": (f"Skipped mounting dataset {cd['name']} " f"since mountpoint is {cd['mountpoint']}.\n") }, verbose) else: if cd['source'] == 'local': child = pyzfscmds.utility.dataset_child_name( cd['name'], check_exists=False) new_mount = os.path.join(mountpoint, child.lstrip('/')) else: new_mount = os.path.join(mountpoint, cd['mountpoint'].lstrip('/')) if not os.path.exists(new_mount): os.makedirs(new_mount) try: zedenv.lib.system.zfs_manual_mount(cd['name'], new_mount) except RuntimeError as e: ZELogger.log( { "level": "EXCEPTION", "message": f"Failed mounting child dataset to '{new_mount}'.\n{e}" }, exit_on_error=True) ZELogger.verbose_log( { "level": "INFO", "message": f"Mounted dataset {cd['name']} to '{new_mount}'.\n" }, verbose)
def destroy_origin_snapshots(destroy_dataset, be_pool, origin_snaps, noop, verbose): # destroy origin snapshots used by destroy_dataset snapshots = None try: snapshots = pyzfscmds.cmd.zfs_list(be_pool, recursive=True, columns=['name'], zfs_types=['snapshot']) except RuntimeError: ZELogger.log( { "level": "EXCEPTION", "message": f"Failed to list origins snapshots of '{destroy_dataset}'.\n" }, exit_on_error=True) snapshots_list = zedenv.lib.be.split_zfs_output(snapshots) for ors in origin_snaps: for ol in snapshots_list: snap = ol[0].rstrip() if ors == snap: if not noop: try: pyzfscmds.cmd.zfs_destroy_snapshot(snap) except RuntimeError: ZELogger.log( { "level": "EXCEPTION", "message": f"Failed to destroy {snap}\n" }, exit_on_error=True) ZELogger.verbose_log( { "level": "INFO", "message": f"Destroyed {snap}.\n" }, verbose)
def post_activate(self): ZELogger.verbose_log( { "level": "INFO", "message": (f"Creating Temporary working directory. " "No changes will be made until the end of " "the GRUB configuration.\n") }, self.verbose) if not self.bootonzfs: with tempfile.TemporaryDirectory(prefix="zedenv", suffix=self.bootloader) as t_grub: ZELogger.verbose_log( { "level": "INFO", "message": f"Created {t_grub}.\n" }, self.verbose) self.modify_bootloader(t_grub) self.recurse_move(t_grub, self.zedenv_properties["boot"], overwrite=False) if self.bootonzfs: self.setup_boot_env_tree() if not self.skip_update_grub: try: self.grub_mkconfig(self.grub_cfg_path) except RuntimeError as e: ZELogger.verbose_log( { "level": "INFO", "message": f"During 'post activate', 'grub-mkconfig' failed with:\n{e}.\n" }, self.verbose) else: ZELogger.verbose_log( { "level": "INFO", "message": f"Generated GRUB menu successfully at {self.grub_cfg_path}.\n" }, self.verbose) if self.bootonzfs and not self.skip_cleanup: self.teardown_boot_env_tree()
def zedenv_list( verbose: Optional[bool], # alldatasets: Optional[bool], spaceused: Optional[bool], scripting: Optional[bool], # snapshots: Optional[bool], origin: Optional[bool], be_root: str): """ Main list command. Separate for testing. """ ZELogger.verbose_log( { "level": "INFO", "message": "Listing Boot Environments:\n" }, verbose) columns = ["name"] # TODO: Complete # if spaceused: # columns.extend(["used", "usedds", "usedbysnapshots", "usedrefreserv", "refer"]) """ TODO: if all_datasets: if snapshots: """ if origin: columns.append("origin") columns.append("creation") boot_environments = configure_boot_environment_list( be_root, columns, scripting) for list_output in boot_environments: ZELogger.log({"level": "INFO", "message": list_output})
def disable_children_automount(be_child_datasets: List[str], be_requested: str, boot_environment_root: str, verbose: Optional[bool]): """ Dont run if noop """ for ds in be_child_datasets: if not (be_requested in ds) and not (boot_environment_root == ds): try: pyzfscmds.cmd.zfs_set(ds, "canmount=noauto") except RuntimeError as e: ZELogger.log( { "level": "EXCEPTION", "message": f"Failed to set canmount=noauto on {ds}\n{e}\n" }, exit_on_error=True) ZELogger.verbose_log( { "level": "INFO", "message": f"Disabled automount for {ds}\n" }, verbose)
def post_destroy(self, target): real_kernel_dir = os.path.join(self.zedenv_properties["esp"], self.env_dir) dataset_kernels = os.path.join(real_kernel_dir, f"{self.entry_prefix}-{target}") # if not self.noop: if os.path.exists(dataset_kernels): shutil.rmtree(dataset_kernels) ZELogger.verbose_log( { "level": "INFO", "message": f"Removed {dataset_kernels}.\n" }, self.verbose) real_entries_dir = os.path.join(self.zedenv_properties["esp"], "loader/entries") real_bootloader_file = os.path.join(real_entries_dir, f"zedenv-{target}.conf") if os.path.isfile(real_bootloader_file): try: os.remove(real_bootloader_file) except PermissionError: ZELogger.log( { "level": "EXCEPTION", "message": (f"Require Privileges to remove " f"'{real_bootloader_file}'\n") }, exit_on_error=True) ZELogger.verbose_log( { "level": "INFO", "message": f"Removed {real_bootloader_file}.\n" }, self.verbose)
def promote_origins(destroy_dataset, be_pool, origin_snaps, noop, verbose): # promote dependents of origins used by destroy_dataset origins = None try: origins = pyzfscmds.cmd.zfs_list( be_pool, recursive=True, columns=['name', 'origin'], zfs_types=['filesystem', 'snapshot', 'volume']) except RuntimeError: ZELogger.log( { "level": "EXCEPTION", "message": f"Failed to list origins of '{destroy_dataset}'.\n" }, exit_on_error=True) origins_list = zedenv.lib.be.split_zfs_output(origins) for ors in origin_snaps: for ol in origins_list: if ors == ol[1].rstrip(): if not noop: try: pyzfscmds.cmd.zfs_promote(ol[0].rstrip()) except RuntimeError: ZELogger.log( { "level": "EXCEPTION", "message": f"Failed to promote {ol[0].rstrip()}\n" }, exit_on_error=True) ZELogger.verbose_log( { "level": "INFO", "message": f"Promoted {ol[0].rstrip()}.\n" }, verbose)
def edit_bootloader_default(self, temp_esp: str, overwrite: bool): real_loader_dir_path = os.path.join(self.esp, "loader") temp_loader_dir_path = os.path.join(temp_esp, "loader") real_loader_conf_path = os.path.join(real_loader_dir_path, "loader.conf") temp_loader_conf_path = os.path.join(temp_loader_dir_path, "loader.conf") ZELogger.verbose_log( { "level": "INFO", "message": f"Updating {real_loader_conf_path}\n" }, self.verbose) if not os.path.isdir(temp_loader_dir_path): try: os.makedirs(temp_loader_dir_path) except PermissionError as e: ZELogger.log( { "level": "EXCEPTION", "message": f"Require Privileges to write to {temp_loader_dir_path}\n{e}" }, exit_on_error=True) except OSError as os_err: ZELogger.log({ "level": "EXCEPTION", "message": os_err }, exit_on_error=True) if not os.path.isfile(real_loader_conf_path): ZELogger.log( { "level": "EXCEPTION", "message": f"Missing file: {real_loader_conf_path}\n" }, exit_on_error=True) try: shutil.copy(real_loader_conf_path, temp_loader_conf_path) except PermissionError as e: ZELogger.log( { "level": "EXCEPTION", "message": f"Require Privileges to write to '{temp_loader_conf_path}'\n{e}" }, exit_on_error=True) except IOError as e: ZELogger.log( { "level": "EXCEPTION", "message": f"IOError writing to '{temp_loader_conf_path}'\n{e}" }, exit_on_error=True) with open(temp_loader_conf_path, "r") as loader_conf: conf_list = loader_conf.readlines() line_num = next((l for l, val in enumerate(conf_list) if val.split(' ', 1)[0] == "default"), None) if line_num: conf_list[line_num] = f"default {self.new_entry}\n" if not self.noop: if os.path.isfile(real_loader_conf_path): ZELogger.verbose_log( { "level": "INFO", "message": (f"File {real_loader_conf_path} already exists, backed up to " f"'{real_loader_conf_path}.bak' and replaced.\n") }, self.verbose) if os.path.isfile(f"{real_loader_conf_path}.bak"): try: os.remove(f"{real_loader_conf_path}.bak") except PermissionError: ZELogger.log( { "level": "EXCEPTION", "message": (f"Require Privileges to remove " f"'{real_loader_conf_path}.bak'\n") }, exit_on_error=True) try: shutil.move(real_loader_conf_path, f"{real_loader_conf_path}.bak") except PermissionError: ZELogger.log( { "level": "EXCEPTION", "message": (f"Require Privileges to write to " f"'{real_loader_conf_path}.bak'\n") }, exit_on_error=True) with open(real_loader_conf_path, "w") as loader_conf: loader_conf.writelines(conf_list) if not self.noconfirm: if click.confirm( "Would you like to edit the generated 'loader.conf'?", default=True): click.edit(filename=real_loader_conf_path)
def zedenv_mount(boot_environment: str, mountpoint: Optional[str], verbose: bool, be_root: str): """ Create a temporary directory and mount a boot environment. If an extra argument is given, mount the boot environment at the given mountpoint. """ ZELogger.verbose_log( { "level": "INFO", "message": f"Mounting boot environment '{boot_environment}'.\n" }, verbose) if not mountpoint: mountpoint_used = tempfile.mkdtemp(suffix=f"-{boot_environment}", prefix="zedenv-") ZELogger.verbose_log( { "level": "INFO", "message": f"No mountpoint given, using a temporary directory {mountpoint_used}.\n" }, verbose) else: mountpoint_used = mountpoint[0] if os.path.ismount(mountpoint_used): ZELogger.log( { "level": "EXCEPTION", "message": f"There is already a file system mounted at {mountpoint_used}" }, exit_on_error=True) if not os.path.isdir(mountpoint_used): ZELogger.log( { "level": "EXCEPTION", "message": (f"The path'{mountpoint_used}' is not a directory, " "cannot be used as mountpoint.\n") }, exit_on_error=True) ZELogger.verbose_log( { "level": "INFO", "message": f"Mountpoint '{mountpoint_used}' given, using as mountpoint.\n" }, verbose) be_dataset = f"{be_root}/{boot_environment}" try: zedenv.lib.system.zfs_manual_mount(be_dataset, mountpoint_used) except RuntimeError as e: ZELogger.log( { "level": "EXCEPTION", "message": f"Failed mounting dataset to '{mountpoint_used}'.\n{e}" }, exit_on_error=True) ZELogger.verbose_log( { "level": "INFO", "message": f"Mounted dataset to '{mountpoint_used}'.\n" }, verbose) if not verbose: ZELogger.log({"level": "INFO", "message": mountpoint_used}) child_datasets = zedenv.lib.be.list_child_mountpoints(be_dataset) if child_datasets: ZELogger.verbose_log( { "level": "INFO", "message": f"Mounting children of '{boot_environment}'.\n" }, verbose) mount_children(child_datasets, mountpoint_used, verbose)
def zedenv_create(parent_dataset: str, root_dataset: str, boot_environment: str, verbose: bool, existing: Optional[str], bootloader: Optional[str]): """ :Parameters: parent_dataset : str Dataset boot environment root, commonly 'zpool/ROOT'. root_dataset : str Current boot dataset. boot_environment : str Name of new boot environment, e.g. default-02 verbose : bool Print information verbosely. existing : bool Create boot environment from certain dataset. :return: """ ZELogger.verbose_log( { "level": "INFO", "message": "Creating Boot Environment:\n" }, verbose) # Remove the final part of the data set after the last / and add new name boot_environment_dataset = f"{parent_dataset}/{boot_environment}" zpool = zedenv.lib.be.dataset_pool(boot_environment_dataset) current_be = None try: current_be = pyzfscmds.utility.dataset_child_name( zedenv.lib.be.bootfs_for_pool(zpool)) except RuntimeError: ZELogger.log( { "level": "EXCEPTION", "message": f"Failed to get active boot environment'\n" }, exit_on_error=True) bootloader_plugin = None if bootloader: bootloader_plugin = zedenv.lib.configure.get_bootloader( boot_environment, current_be, bootloader, verbose, False, False, parent_dataset) ZELogger.verbose_log( { "level": "INFO", "message": f"Using plugin {bootloader}\n" }, verbose) if zfs_utility.dataset_exists(boot_environment_dataset): ZELogger.log( { "level": "EXCEPTION", "message": f"Failed to create {boot_environment_dataset}, already exists." }, exit_on_error=True) # Getting snapshot for clone clone_sources = get_clones(root_dataset, existing) ZELogger.verbose_log( { "level": "INFO", "message": f"Getting properties of {boot_environment_dataset} for clones {clone_sources}\n" }, verbose) for source in clone_sources: if source['datasetchild'] == '': be_clone = f"{boot_environment_dataset}" else: be_clone = f"{boot_environment_dataset}/{source['datasetchild']}" try: pyzfscmds.cmd.zfs_clone(source['snapshot'], be_clone, properties=source['properties']) except RuntimeError as e: ZELogger.log( { "level": "EXCEPTION", "message": (f"Failed to create {boot_environment_dataset}", f" from {clone_sources['snapshot']}") }, exit_on_error=True) if bootloader_plugin: try: bootloader_plugin.post_create() except RuntimeWarning as err: ZELogger.verbose_log( { "level": "WARNING", "message": f"During {bootloader_plugin.bootloader} post create the following" f" occurred:\n\n{err}\nContinuing creation.\n" }, verbose) except RuntimeError as err: ZELogger.log( { "level": "EXCEPTION", "message": f"During {bootloader_plugin.bootloader} post create the following " f"occurred:\n\n{err}\nStopping creation.\n" }, exit_on_error=True) except AttributeError: ZELogger.verbose_log( { "level": "INFO", "message": f"Tried to run {bootloader_plugin.bootloader} 'post create', " f"not implemented.\n" }, verbose)
def show_source_properties(property_list, verbose): ZELogger.verbose_log({"level": "INFO", "message": "PROPERTIES"}, verbose) for p in property_list: ZELogger.verbose_log({"level": "INFO", "message": p}, verbose) ZELogger.verbose_log({"level": "INFO", "message": ""}, verbose)
def __init__(self, zedenv_data: dict, skip_update: bool = False, skip_cleanup: bool = False): super().__init__(zedenv_data) self.entry_prefix = "zedenv" self.old_entry = f"{self.entry_prefix}-{self.old_boot_environment}" self.new_entry = f"{self.entry_prefix}-{self.boot_environment}" self.boot_mountpoint = "/boot" self.env_dir = "env" self.zfs_env_dir = "zfsenv" if not os.path.isdir(self.boot_mountpoint): ZELogger.log( { "level": "EXCEPTION", "message": f"Boot mountpoint {self.boot_mountpoint} does not exist. Exiting.\n" }, exit_on_error=True) self.skip_update_grub = skip_update self.skip_cleanup = skip_cleanup # Set defaults for pr in self.allowed_properties: self.zedenv_properties[pr["property"]] = pr["default"] self.check_zedenv_properties() if self.zedenv_properties["bootonzfs"] in ("yes", "1"): self.bootonzfs = True elif self.zedenv_properties["bootonzfs"] in ("no", "0"): self.bootonzfs = False else: ZELogger.log( { "level": "EXCEPTION", "message": (f"Property 'bootonzfs' is set to invalid value " f"{self.zedenv_properties['bootonzfs']}, should be " "'yes', 'no', '0', or '1'. Exiting.\n") }, exit_on_error=True) if self.bootonzfs: if not self.noop: if not os.path.isdir(self.zedenv_properties["boot"]): try: os.makedirs(self.zedenv_properties["boot"]) except PermissionError as e: ZELogger.log( { "level": "EXCEPTION", "message": ("Require Privileges to write to " f"{self.zedenv_properties['boot']}\n{e}") }, exit_on_error=True) except OSError as os_err: ZELogger.log({ "level": "EXCEPTION", "message": os_err }, exit_on_error=True) ZELogger.verbose_log( { "level": "INFO", "message": ("Created mount directory " f"{self.zedenv_properties['boot']}\n") }, self.verbose) zfs_env_dir_path = os.path.join(self.zedenv_properties["boot"], self.zfs_env_dir) if not os.path.isdir(zfs_env_dir_path): try: os.makedirs(zfs_env_dir_path) except PermissionError as e: ZELogger.log( { "level": "EXCEPTION", "message": (f"Require Privileges to write to " f"{zfs_env_dir_path}\n{e}") }, exit_on_error=True) except OSError as os_err: ZELogger.log({ "level": "EXCEPTION", "message": os_err }, exit_on_error=True) else: if not os.path.isdir(self.zedenv_properties["boot"]): self.plugin_property_error("boot") self.grub_boot_dir = os.path.join(self.boot_mountpoint, self.zedenv_properties["grubsubdir"]) if not os.path.isdir(self.grub_boot_dir): ZELogger.log( { "level": "EXCEPTION", "message": (f"Directory {self.grub_boot_dir} does not exist. " "Check 'grubsubdir' property is set correctly") }, exit_on_error=True) self.grub_cfg = "grub.cfg" self.grub_cfg_path = os.path.join(self.grub_boot_dir, self.grub_cfg)
def teardown_boot_env_tree(self): def ismount(path, boot): if not os.path.ismount(path): """ This is required because `os.path.ismount()` returns False if a ZFS dataset is being mounted again to a subfolder of itself. E.g. bpool/boot/env/zedenv-default is mounted to - `/boot` and - `/boot/zfsenv/zedenv-default` """ s1 = os.lstat(path) s2 = os.lstat(boot) return s1.st_ino == s2.st_ino else: return True mount_root = os.path.join(self.zedenv_properties["boot"], self.zfs_env_dir) cleanup = True if not os.path.exists(mount_root): ZELogger.verbose_log( { "level": "INFO", "message": f"Mount root: '{mount_root}' doesnt exist.\n" }, self.verbose) else: for m in os.listdir(mount_root): mount_path = os.path.join(mount_root, m) ZELogger.verbose_log( { "level": "INFO", "message": f"Unmounting {m}\n" }, self.verbose) if ismount(mount_path, self.boot_mountpoint): try: zedenv.lib.system.umount(mount_path) except RuntimeError as e: ZELogger.log( { "level": "WARNING", "message": f"Failed Un-mountingdataset from '{m}'.\n{e}" }, exit_on_error=True) cleanup = False else: ZELogger.verbose_log( { "level": "INFO", "message": f"Unmounted {m} from {mount_path}.\n" }, self.verbose) try: os.rmdir(mount_path) except OSError as ex: ZELogger.verbose_log( { "level": "WARNING", "message": f"Couldn't remove directory {mount_path}.\n{ex}\n" }, self.verbose) cleanup = False else: ZELogger.verbose_log( { "level": "INFO", "message": f"Removed directory {mount_path}.\n" }, self.verbose) if cleanup and os.path.exists(mount_root): try: os.rmdir(mount_root) except OSError as ex: ZELogger.verbose_log( { "level": "WARNING", "message": f"Couldn't remove directory {mount_root}.\n{ex}\n" }, self.verbose)
def zedenv_rename(be_root: str, boot_environment: str, new_boot_environment: str, bootloader: Optional[str], verbose: Optional[bool]): old_be_dataset = f"{be_root}/{boot_environment}" new_be_dataset = f"{be_root}/{new_boot_environment}" zpool = zedenv.lib.be.dataset_pool(old_be_dataset) current_be = None try: current_be = pyzfscmds.utility.dataset_child_name( zedenv.lib.be.bootfs_for_pool(zpool)) except RuntimeError: ZELogger.log( { "level": "EXCEPTION", "message": f"Failed to get active boot environment'\n" }, exit_on_error=True) bootloader_plugin = None if bootloader: bootloader_plugin = zedenv.lib.configure.get_bootloader( boot_environment, current_be, bootloader, verbose, False, False, be_root) ZELogger.verbose_log( { "level": "INFO", "message": f"Using plugin {bootloader}\n" }, verbose) dataset_mountpoint = pyzfscmds.system.agnostic.dataset_mountpoint( old_be_dataset) if pyzfscmds.utility.dataset_exists(new_be_dataset): ZELogger.log( { "level": "EXCEPTION", "message": f"Boot environment '{new_boot_environment}' already exists.\n" }, exit_on_error=True) if zedenv.lib.be.is_current_boot_environment(boot_environment): ZELogger.log( { "level": "EXCEPTION", "message": f"Cannot rename current boot environment '{boot_environment}.\n" }, exit_on_error=True) if zedenv.lib.be.is_active_boot_environment( old_be_dataset, zedenv.lib.be.dataset_pool(old_be_dataset)): ZELogger.log( { "level": "EXCEPTION", "message": f"Cannot rename active boot environment '{boot_environment}.\n" }, exit_on_error=True) if dataset_mountpoint: ZELogger.log( { "level": "EXCEPTION", "message": f"Dataset is mounted to '{dataset_mountpoint}', unmount and try again\n" }, exit_on_error=True) try: pyzfscmds.cmd.zfs_rename(old_be_dataset, new_be_dataset) except RuntimeError as err: ZELogger.log({ "level": "EXCEPTION", "message": err }, exit_on_error=True) # Rename the boot dataset if a separate ZFS boot pool is used if zedenv.lib.be.extra_bpool(): be_boot = zedenv.lib.be.root('/boot') old_be_boot_dataset = f"{be_boot}/zedenv-{boot_environment}" new_be_boot_dataset = f"{be_boot}/zedenv-{new_boot_environment}" try: pyzfscmds.cmd.zfs_rename(old_be_boot_dataset, new_be_boot_dataset) except RuntimeError as e: ZELogger.log( { "level": "EXCEPTION", "message": (f"Failed to rename the boot dataset '{old_be_boot_dataset}' to" f" '{new_be_boot_dataset}'. The following error occured:\n\n{e}" "\nStopping rename.\n") }, exit_on_error=True) if bootloader_plugin: try: bootloader_plugin.post_rename() except RuntimeWarning as err: ZELogger.verbose_log( { "level": "WARNING", "message": f"During {bootloader_plugin.bootloader} post rename the following" f" occurred:\n\n{err}\nContinuing rename.\n" }, verbose) except RuntimeError as err: ZELogger.log( { "level": "EXCEPTION", "message": f"During {bootloader_plugin.bootloader} post rename the following " f"occurred:\n\n{err}\nStopping rename.\n" }, exit_on_error=True) except AttributeError: ZELogger.verbose_log( { "level": "INFO", "message": f"Tried to run {bootloader_plugin.bootloader} 'post rename', " f"not implemented.\n" }, verbose)
def zedenv_activate(boot_environment: str, boot_environment_root: str, verbose: Optional[bool], bootloader: Optional[str], noconfirm: Optional[bool], noop: Optional[bool]): """ If a plugin is found that can be run on the system, run the activate command from the plugin. """ ZELogger.verbose_log( { "level": "INFO", "message": f"Activating Boot Environment: {boot_environment}\n" }, verbose) be_requested = f"{boot_environment_root}/{boot_environment}" zpool = zedenv.lib.be.dataset_pool(be_requested) current_be = None try: current_be = pyzfscmds.utility.dataset_child_name( zedenv.lib.be.bootfs_for_pool(zpool)) except RuntimeError: ZELogger.log( { "level": "EXCEPTION", "message": f"Failed to get active boot environment'\n" }, exit_on_error=True) bootloader_set = zedenv.lib.be.get_property(be_requested, "org.zedenv:bootloader") if not bootloader and bootloader_set: bootloader = bootloader_set if bootloader_set != '-' else None bootloader_plugin = None if bootloader: bootloader_plugin = zedenv.lib.configure.get_bootloader( boot_environment, current_be, bootloader, verbose, noconfirm, noop, boot_environment_root) ZELogger.verbose_log( { "level": "INFO", "message": f"Using plugin {bootloader}\n" }, verbose) if bootloader_plugin: try: bootloader_plugin.pre_activate() except RuntimeWarning as err: ZELogger.verbose_log( { "level": "WARNING", "message": f"During {plugin.bootloader} mid activate the following occurred:\n" f"\n{err}\nContinuing activation.\n" }, verbose) except RuntimeError as err: ZELogger.log( { "level": "EXCEPTION", "message": f"During {plugin.bootloader} mid activate the following occurred:\n" f"\n{err}\nStopping activation.\n" }, exit_on_error=True) if not pyzfscmds.utility.dataset_exists(be_requested): ds_is_clone = None try: ds_is_clone = pyzfscmds.utility.is_clone(be_requested) except RuntimeError: ZELogger.log( { "level": "EXCEPTION", "message": f"Boot environment {boot_environment} doesn't exist'\n" }, exit_on_error=True) if not ds_is_clone: ZELogger.log( { "level": "EXCEPTION", "message": f"Boot environment {boot_environment} doesn't exist'\n" }, exit_on_error=True) ZELogger.verbose_log( { "level": "INFO", "message": f"Boot environment {boot_environment} exists'\n" }, verbose) if current_be == be_requested: ZELogger.verbose_log( { "level": "INFO", "message": f"Boot Environment {boot_environment} is already active.\n" }, verbose) else: # Set bootfs on dataset dataset_mountpoint = pyzfscmds.system.agnostic.dataset_mountpoint( be_requested) activate_boot_environment(be_requested, dataset_mountpoint, verbose, noop, bootloader_plugin) be_child_datasets = None try: be_child_datasets = pyzfscmds.cmd.zfs_list(boot_environment_root, recursive=True, columns=["name"], zfs_types=["filesystem"]) except RuntimeError as e: ZELogger.log( { "level": "EXCEPTION", "message": f"Failed to list datasets under {boot_environment_root}\n{e}\n" }, exit_on_error=True) be_child_datasets_list = [line for line in be_child_datasets.splitlines()] if not noop: disable_children_automount(be_child_datasets_list, be_requested, boot_environment_root, verbose) apply_settings_to_child_datasets(be_child_datasets_list, be_requested, verbose) if bootloader_plugin: try: bootloader_plugin.post_activate() except RuntimeWarning as err: ZELogger.verbose_log( { "level": "WARNING", "message": f"During {plugin.bootloader} mid activate the following occurred:\n" f"\n{err}\nContinuing activation.\n" }, verbose) except RuntimeError as err: ZELogger.log( { "level": "EXCEPTION", "message": f"During {plugin.bootloader} mid activate the following occurred:\n" f"\n{err}\nStopping activation.\n" }, exit_on_error=True)
def mount_and_modify_dataset(dataset: str, verbose: bool = False, noop: bool = False, pre_mount_properties: List[str] = None, post_mount_properties: List[str] = None, plugin=None): ZELogger.verbose_log( { "level": "INFO", "message": f"Mount dataset for customization\n" }, verbose) if not noop: if pre_mount_properties: for pre_prop in pre_mount_properties: try: pyzfscmds.cmd.zfs_set(dataset, pre_prop) except RuntimeError as e: ZELogger.log( { "level": "EXCEPTION", "message": f"Failed to set {pre_prop} on {dataset}\n{e}\n" }, exit_on_error=True) # Allow even with noop, just mounts and runs plugin with tempfile.TemporaryDirectory() as tmpdir: try: pyzfscmds.cmd.zfs_set(dataset, f"mountpoint={tmpdir}") except RuntimeError as e: ZELogger.log( { "level": "EXCEPTION", "message": f"Failed to set mountpoint={tmpdir}\n{e}\n" }, exit_on_error=True) try: pyzfscmds.cmd.zfs_mount(dataset) except RuntimeError as e: ZELogger.log( { "level": "EXCEPTION", "message": f"Failed to mount mountpoint={tmpdir}\n{e}\n" }, exit_on_error=True) ZELogger.verbose_log( { "level": "INFO", "message": f"Mounted {dataset} to {tmpdir}\n" }, verbose) # Do stuff while mounted if plugin is not None: ZELogger.verbose_log( { "level": "INFO", "message": f"Running plugin: '{plugin.bootloader}' - mid_activate\n" }, verbose) try: plugin.mid_activate(tmpdir) except RuntimeWarning as err: ZELogger.verbose_log( { "level": "WARNING", "message": f"During {plugin.bootloader} mid activate the following occurred:\n" f"\n{err}\nContinuing activation.\n" }, verbose) except RuntimeError as err: ZELogger.log( { "level": "EXCEPTION", "message": f"During {plugin.bootloader} mid activate the following occurred:\n" f"\n{err}\nStopping activation.\n" }, exit_on_error=True) try: pyzfscmds.cmd.zfs_unmount(dataset) except RuntimeError as e: ZELogger.log( { "level": "EXCEPTION", "message": f"Failed to unmount {dataset} from {tmpdir}\n{e}\n" }, exit_on_error=True) ZELogger.verbose_log( { "level": "INFO", "message": f"Unmounted {dataset} from {tmpdir}\n" }, verbose) if not noop: if post_mount_properties: for post_prop in post_mount_properties: try: pyzfscmds.cmd.zfs_set(dataset, post_prop) except RuntimeError as e: ZELogger.log( { "level": "EXCEPTION", "message": f"Failed to set {post_prop} on {dataset}\n{e}\n" }, exit_on_error=True)