def __enter__(self): if os.path.exists(self.pidfile): pid = None with open(self.pidfile) as f: pid = self._check() if pid: self.pidfd = None raise ProcessRunningException( f'process already running in {self.pidfile} as {pid}') else: os.remove(self.pidfile) self.pidfd = f if pid: ProcessRunningException( f'process already running in {self.pidfile} as {pid}') try: with open(self.pidfile, 'w+') as f: f.write(str(os.getpid())) except OSError: ZELogger.log( { "level": "EXCEPTION", "message": f"Cannot write to pidfile {self.pidfile}" }, exit_on_error=True) return self
def get_promote_snapshots(be_pool: str, destroy_dataset: str) -> list: """ Look for clone we need to promote because they're dependent on snapshots """ promote_snaps = None try: promote_snaps = pyzfscmds.cmd.zfs_list( be_pool, recursive=True, columns=['name', 'origin'], zfs_types=['filesystem', 'snapshot', 'volume']) except RuntimeError as e: ZELogger.log( { "level": "EXCEPTION", "message": f"Failed to list snapshots for promote in '{be_pool}'.\n{e}" }, exit_on_error=True) split_promote_snaps = zedenv.lib.be.split_zfs_output(promote_snaps) target = re.compile(r'\b' + destroy_dataset + r'(@|/.*@).*' + r'\b') return [ds[0] for ds in split_promote_snaps if target.match(ds[1])]
def plugin_property_error(self, prop): ZELogger.log({ "level": "EXCEPTION", "message": (f"To use the {self.bootloader} plugin, use default{prop}, or set props\n" f"To set it use the command (replacing with your pool and dataset)\n'" f"zfs set org.zedenv:{prop}='<new mount>' zpool/ROOT/default\n") }, exit_on_error=True)
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) prop_val = zedenv.lib.be.get_property( "/".join([self.be_root, self.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 properties(dataset, appended_properties: Optional[list]) -> list: dataset_properties = None try: dataset_properties = pyzfscmds.cmd.zfs_get( dataset, columns=["property", "value"], source=["local", "received"], properties=["all"]) except RuntimeError: ZELogger.log( { "level": "EXCEPTION", "message": f"Failed to get properties of '{dataset}'" }, exit_on_error=True) """ Take each line of output containing properties and convert it to a list of property=value strings """ dp = [line.split() for line in dataset_properties.splitlines()] remove_props = [rp[0] for rp in appended_properties] used_props = ["=".join(p) for p in dp if p[0] not in remove_props] used_props.extend(["=".join(pa) for pa in appended_properties]) return used_props
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.zedenv_properties["esp"]) self.edit_bootloader_default(t_esp, overwrite=True)
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 modify_bootloader(self, temp_boot: str): real_kernel_dir = os.path.join(self.zedenv_properties["boot"], "env") temp_kernel_dir = os.path.join(temp_boot, "env") real_old_dataset_kernel = os.path.join(real_kernel_dir, self.old_entry) temp_new_dataset_kernel = os.path.join(temp_kernel_dir, self.new_entry) if not os.path.isdir(real_old_dataset_kernel): ZELogger.log({ "level": "INFO", "message": (f"No directory for Boot environments kernels found at " f"'{real_old_dataset_kernel}', creating empty directory." f"Don't forget to add your kernel to " f"{real_kernel_dir}/zedenv-{self.boot_environment}.") }) if not self.noop: try: os.makedirs(temp_new_dataset_kernel) except PermissionError as e: ZELogger.log( { "level": "EXCEPTION", "message": f"Require Privileges to write to {temp_new_dataset_kernel}\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 self.noop: try: shutil.copytree(real_old_dataset_kernel, temp_new_dataset_kernel) except PermissionError as e: ZELogger.log( { "level": "EXCEPTION", "message": f"Require Privileges to write to {temp_new_dataset_kernel}\n{e}" }, exit_on_error=True) except IOError as e: ZELogger.log( { "level": "EXCEPTION", "message": f"IOError writing to {temp_new_dataset_kernel}\n{e}" }, exit_on_error=True)
def plugin_property_error(self, prop): ZELogger.log({ "level": "EXCEPTION", "message": (f"To use the {self.bootloader} plugin, use the default setting '{prop}', " f"or set a different value\n. To set it use the command (replacing with " f"your pool and dataset)\n'zedenv set " f"org.zedenv.{self.bootloader}:{prop}='<new mount>'\n") }, exit_on_error=True)
def cli(verbose: Optional[bool], zedenv_properties: Optional[list]): try: zedenv.lib.check.startup_check() except RuntimeError as err: ZELogger.log({"level": "EXCEPTION", "message": err}, exit_on_error=True) zedenv_set(verbose, zedenv_properties, zedenv.lib.be.root())
def properties(dataset, appended_properties: Optional[list]) -> list: dataset_properties = None try: dataset_properties = pyzfscmds.cmd.zfs_get( dataset, columns=["property", "value"], source=["local", "received"], properties=["all"]) except RuntimeError: ZELogger.log( { "level": "EXCEPTION", "message": f"Failed to get properties of '{dataset}'" }, exit_on_error=True) """ Take each line of output containing properties and convert it to a list of property=value strings """ dp = [line.split() for line in dataset_properties.splitlines()] remove_props = [rp[0] for rp in appended_properties] used_props = ["=".join(p) for p in dp if p[0] not in remove_props] used_props.extend(["=".join(pa) for pa in appended_properties]) # Make sure that the mountpoint is correct even if we are in a chroot environment. # In this case, the ZFS pool is mounted with an alternative root (e.g. to `/mnt`). rpool = zedenv.lib.be.dataset_pool(dataset) altroot = None try: altroot = pyzfscmds.cmd.zpool_get(rpool, columns=["value"], properties=["altroot"]) except RuntimeError: ZELogger.log( { "level": "EXCEPTION", "message": f"Failed to get properties of '{dataset}'" }, exit_on_error=True) if altroot.strip() != '-': # Search and remove the alternative root at the beginning of the mountpoint for i, p in enumerate(used_props): prop, val = p.split("=") if prop != "mountpoint": continue alt_len, mp_len = len(altroot), len(val) if (mp_len >= alt_len) and (val[:alt_len] == altroot): mountpoint = "mountpoint=/" if mp_len != alt_len: mountpoint = mountpoint + val[alt_len:] used_props[i] = mountpoint return used_props
def post_activate(self): canmount_setting = "canmount=noauto" if self.zfs_be else "canmount=on" try: pyzfscmds.cmd.zfs_set(f"{self.be_root}/{self.boot_environment}", canmount_setting) except RuntimeError: ZELogger.log({ "level": "EXCEPTION", "message": f"Failed to set {canmount_setting} for {ds}\n{e}\n" }, exit_on_error=True)
def cli(boot_environment: str, mountpoint: Optional[list], verbose: Optional[bool]): try: zedenv.lib.check.startup_check() except RuntimeError as err: ZELogger.log({ "level": "EXCEPTION", "message": err }, exit_on_error=True) be_root = zedenv.lib.be.root() dataset_mountpoint = pyzfscmds.system.agnostic.dataset_mountpoint( f"{be_root}/{boot_environment}") if not pyzfscmds.utility.dataset_exists(f"{be_root}/{boot_environment}"): ZELogger.log( { "level": "EXCEPTION", "message": f"Boot environment doesn't exist {boot_environment}.\n" }, exit_on_error=True) if dataset_mountpoint: if dataset_mountpoint == "/": ZELogger.log( { "level": "EXCEPTION", "message": f"Cannot Mount root dataset.\n" }, exit_on_error=True) ZELogger.log( { "level": "EXCEPTION", "message": f"Dataset already mounted to {dataset_mountpoint}\n" }, exit_on_error=True) real_mountpoint = None if mountpoint: if len(mountpoint) > 1: ZELogger.log( { "level": "EXCEPTION", "message": f"Boot environments can only view mounted to one location at once.\n" }, exit_on_error=True) real_mountpoint = mountpoint[0] zedenv_mount(boot_environment, real_mountpoint, verbose, be_root)
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 cli(boot_environment: str, verbose: Optional[bool]): try: zedenv.lib.check.startup_check() except RuntimeError as err: ZELogger.log({"level": "EXCEPTION", "message": err}, exit_on_error=True) be_root = zedenv.lib.be.root() dataset_mountpoint = pyzfscmds.system.agnostic.dataset_mountpoint( f"{be_root}/{boot_environment}") if not pyzfscmds.utility.dataset_exists(f"{be_root}/{boot_environment}"): ZELogger.log({ "level": "EXCEPTION", "message": f"Boot environment doesn't exist {boot_environment}.\n" }, exit_on_error=True) if dataset_mountpoint == "/": ZELogger.log({ "level": "EXCEPTION", "message": f"Cannot Unmount root dataset.\n" }, exit_on_error=True) if not dataset_mountpoint: ZELogger.log({ "level": "EXCEPTION", "message": f"Boot environment already un-mounted\n" }, exit_on_error=True) zedenv_umount(boot_environment, verbose, be_root)
def check_zedenv_properties(self): """ Get zedenv properties in format: {"property": <property val>} If prop unset, leave default """ for prop in self.zedenv_properties: prop_val = zedenv.lib.be.get_property( "/".join([self.be_root, self.boot_environment]), f"org.zedenv:{prop}") if prop_val is not None and prop_val != "-": self.zedenv_properties[prop] = prop_val ZELogger.log({"level": "INFO", "message": f"Found: {prop}"})
def cli(boot_environment: str, verbose: Optional[bool], bootloader: Optional[str], noconfirm: Optional[bool], noop: Optional[bool]): try: zedenv.lib.check.startup_check() except RuntimeError as err: ZELogger.log({ "level": "EXCEPTION", "message": err }, exit_on_error=True) try: with zedenv.lib.check.Pidfile(): boot_environment_root = zedenv.lib.be.root() bootloader_set = zedenv.lib.be.get_property( boot_environment_root, "org.zedenv:bootloader") if not bootloader and bootloader_set: if bootloader_set != '-': bootloader = bootloader_set if not bootloader: ZELogger.log({ "level": "WARNING", "message": ("WARNING: Running activate without a bootloader. " "Re-run with a default bootloader, or with the " "'--bootloader/-b' flag. If you plan to manually edit your " "bootloader config this message can safely be ignored.\n") }) if noconfirm: sys.exit( "The '--noconfirm/-y' flag requires the bootloader option " "'--bootloader/-b'.") zedenv_activate(boot_environment, boot_environment_root, verbose, bootloader, noconfirm, noop) except IOError as e: if e[0] == errno.EPERM: ZELogger.log( { "level": "EXCEPTION", "message": "You need root permissions to activate" }, exit_on_error=True) except zedenv.lib.check.ProcessRunningException as pr: ZELogger.log( { "level": "EXCEPTION", "message": f"Already running activate.\n {pr}" }, exit_on_error=True)
def cli(boot_environment: str, verbose: Optional[bool], existing: Optional[str]): try: zedenv.lib.check.startup_check() except RuntimeError as err: ZELogger.log({ "level": "EXCEPTION", "message": err }, exit_on_error=True) parent_dataset = zedenv.lib.be.root() root_dataset = pyzfscmds.system.agnostic.mountpoint_dataset("/") zedenv_create(parent_dataset, root_dataset, boot_environment, verbose, existing)
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 as e: ZELogger.log({ "level": "EXCEPTION", "message": f"Failed to get list of datasets for '{boot_environment}'.\n{e}" }, exit_on_error=True) mountpoint = pyzfscmds.system.agnostic.dataset_mountpoint(boot_environment_dataset) if mountpoint: # If a separate ZFS boot pool is used, start with unmounting the corresponding boot dataset if zedenv.lib.be.extra_bpool(): try: zedenv.lib.system.umount(f"{mountpoint}/boot") except RuntimeError as e: ZELogger.log({ "level": "EXCEPTION", "message": f"Failed Un-mounting boot dataset from '{mountpoint}/boot'.\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 cli(zedenv_properties: Optional[list], scripting: Optional[bool], recursive: Optional[bool], defaults: Optional[bool]): try: zedenv.lib.check.startup_check() except RuntimeError as err: ZELogger.log({"level": "EXCEPTION", "message": err}, exit_on_error=True) formatted_list_entries = zedenv_get(zedenv_properties, scripting, recursive, defaults, zedenv.lib.be.root()) for k in formatted_list_entries: ZELogger.log({"level": "INFO", "message": k})
def cli( boot_environment: str, verbose: Optional[bool], # unmount: Optional[bool], noconfirm: Optional[bool], noop: Optional[bool]): try: zedenv.lib.check.startup_check() except RuntimeError as err: ZELogger.log({ "level": "EXCEPTION", "message": err }, exit_on_error=True) zedenv_destroy(boot_environment, zedenv.lib.be.root(), pyzfscmds.system.agnostic.mountpoint_dataset("/"), verbose, noconfirm, noop)
def snapshot(boot_environment_name, boot_environment_root, snap_prefix: Optional[str] = None, snap_suffix_time_format: str = "%Y-%m-%d-%H-%f") -> str: """ Recursively Snapshot BE :param boot_environment_name: Name of BE to snapshot. :param boot_environment_root: Root dataset for BEs. :param snap_prefix: Prefix on snapshot names. :param snap_suffix_time_format: Suffix on snapshot names. :return: Name of snapshot without dataset. """ if "/" in boot_environment_name: ZELogger.log( { "level": "EXCEPTION", "message": ("Failed to get snapshot.\n", "Existing boot environment name ", f"{boot_environment_name} should not contain '/'") }, exit_on_error=True) dataset_name = f"{boot_environment_root}/{boot_environment_name}" with zedenv.lib.system.setlocale(): suffix_time = datetime.datetime.now().strftime(snap_suffix_time_format) full_snap_suffix = f"{snap_prefix}-{suffix_time}" if snap_prefix else suffix_time try: pyzfscmds.cmd.zfs_snapshot(dataset_name, full_snap_suffix, recursive=True) except RuntimeError: ZELogger.log( { "level": "EXCEPTION", "message": f"Failed to create snapshot: '{dataset_name}@{full_snap_suffix}'" }, exit_on_error=True) return full_snap_suffix
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 cli(boot_environment: str, verbose: Optional[bool], bootloader: Optional[str], noconfirm: Optional[bool], noop: Optional[bool]): if noconfirm and not bootloader: sys.exit( "The '--noconfirm/-y' flag requires the bootloader option '--bootloader/-b'." ) try: zedenv.lib.check.startup_check() except RuntimeError as err: ZELogger.log({ "level": "EXCEPTION", "message": err }, exit_on_error=True) zedenv_activate(boot_environment, zedenv.lib.be.root(), verbose, bootloader, noconfirm, noop)
def get_origin_snapshots(destroy_dataset: str) -> list: origin_all_snaps = None try: origin_all_snaps = pyzfscmds.cmd.zfs_list( destroy_dataset, recursive=True, columns=['origin'], zfs_types=['filesystem', 'snapshot', 'volume']) except RuntimeError as e: ZELogger.log( { "level": "EXCEPTION", "message": f"Failed to list origin snapshots for '{destroy_dataset}'.\n{e}" }, exit_on_error=True) split_snaps = zedenv.lib.be.split_zfs_output(origin_all_snaps) return [ds[0].rstrip() for ds in split_snaps if ds[0].rstrip() != '-']
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 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()