diff --git a/src/simplezfs/pe_helper.py b/src/simplezfs/pe_helper.py index cf0ed20..2bdaf74 100644 --- a/src/simplezfs/pe_helper.py +++ b/src/simplezfs/pe_helper.py @@ -6,7 +6,7 @@ import stat import subprocess from typing import List, Optional -from .exceptions import PEHelperException, ExternalPEHelperException +from .exceptions import PEHelperException, ExternalPEHelperException, ValidationError from .validation import validate_dataset_path, validate_pool_name @@ -39,6 +39,15 @@ class PEHelperBase: ''' raise NotImplementedError(f'{self} has not implemented this function') + def zfs_destroy_dataset(self, dataset: str, recursive: bool, force_umount: bool): + ''' + Destroy the given ``dataset``. + + :raises ValidationError: If parameters do not validate. + :raises PEHelperException: If errors are encountered when running the helper. + ''' + raise NotImplementedError(f'{self} has not implemented this function') + class ExternalPEHelper(PEHelperBase): ''' @@ -156,3 +165,17 @@ class SudoPEHelper(PEHelperBase): # TODO validate mountpoint fs self._execute_cmd(['zfs', 'set', f'mountpoint={mountpoint}', fileset]) + + def zfs_destroy_dataset(self, dataset: str, recursive: bool, force_umount: bool) -> None: + if '/' not in dataset: + raise ValidationError('Can\'t remove the pool itself') + validate_dataset_path(dataset) + + args = ['zfs', 'destroy', '-p'] + if recursive: + args.append('-r') + if force_umount: + args.append('-f') + args.append(dataset) + + self._execute_cmd(args) diff --git a/src/simplezfs/zfs.py b/src/simplezfs/zfs.py index b4a3960..25efa0a 100644 --- a/src/simplezfs/zfs.py +++ b/src/simplezfs/zfs.py @@ -651,7 +651,7 @@ class ZFS: ''' raise NotImplementedError(f'{self} has not implemented this function') - def destroy_dataset(self, name: str, *, recursive: bool = False) -> None: + def destroy_dataset(self, dataset: str, *, recursive: bool = False, force_umount: bool = False) -> None: ''' Destroy a dataset. This function tries to remove a dataset, optionally removing all children recursively if ``recursive`` is **True**. This function works on all types of datasets, ``fileset``, ``volume``, ``snapshot`` @@ -666,7 +666,30 @@ class ZFS: :note: This is a destructive process that can't be undone. - :param name: Name of the dataset to remove. + :param dataset: Name of the dataset to remove. + :param recursive: Whether to recursively delete child datasets such as snapshots. + :param force_umount: Forces umounting before destroying. Refer to ``ZFS(8)`` `zfs destroy` parameter ``-f``. + :raises ValidationError: If validating the parameters failed. + :raises DatasetNotFound: If the dataset can't be found. + ''' + if '/' not in dataset: + raise ValidationError('Cannot destroy the pool using this function') + validate_dataset_path(dataset) + + if not self.dataset_exists(dataset): + raise DatasetNotFound('The dataset could not be found') + + self._destroy_dataset(dataset, recursive=recursive, force_umount=force_umount) + + def _destroy_dataset(self, dataset: str, *, recursive: bool = False, force_umount: bool = False) -> None: + ''' + Internal implementation of :func:`destroy_dataset`. + + :param dataset: The name of the dataset to remove. + :param recursive: Whether to recursively delete child datasets such as snapshots. + :param force_umount: Forces umounting before destroying. + :raises ValidationError: If validating the parameters failed. + :raises DatasetNotFound: If the dataset can't be found. ''' raise NotImplementedError(f'{self} has not implemented this function') diff --git a/src/simplezfs/zfs_cli.py b/src/simplezfs/zfs_cli.py index f9a536a..7b2452f 100644 --- a/src/simplezfs/zfs_cli.py +++ b/src/simplezfs/zfs_cli.py @@ -323,3 +323,50 @@ class ZFSCli(ZFS): def create_bookmark(self, snapshot: str, name: str) -> Dataset: validate_dataset_path(snapshot) raise NotImplementedError() + + def _destroy_dataset(self, dataset: str, *, recursive: bool = False, force_umount: bool = False) -> None: + args = [self.__exe, 'destroy', '-p'] + if recursive: + args.append('-r') + if force_umount: + args.append('-f') + args.append(dataset) + + log.debug(f'executing: {args}') + proc = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8') + if proc.returncode != 0 or len(proc.stderr) > 0: + log.debug(f'destroy_dataset: command failed, code={proc.returncode}, stderr="{proc.stderr}"') + if 'has children' in proc.stderr: + if recursive: + log.error(f'Dataset {dataset} has children and recursive was given, please report this') + else: + log.warning(f'Dataset {dataset} has children and thus cannot be destroyed without recursive=True') + raise Exception + # two possible messaes: (zfs destroy -p -r [-f] $fileset_with_snapshots) + # * 'cannot destroy snapshots: permission denied' + # * 'umount: only root can use "--types" option' + # The latter seems to originate from having `destroy` and `mount` via `zfs allow`. + elif ('cannot destroy' in proc.stderr and 'permission denied' in proc.stderr) or \ + 'only root can' in proc.stderr: + log.debug('Command output indicates that we need to run the PE Helper') + if self.use_pe_helper: + if self.pe_helper is not None: + log.info(f'Using pe_helper to remove {dataset}') + self.pe_helper.zfs_destroy_dataset(dataset, recursive, force_umount) + log.info(f'Dataset {dataset} destroyed (using pe_helper)') + else: + msg = 'Cannot destroy: No pe_helper set' + log.error(msg) + raise PermissionError(msg) + else: + log.error(f'Dataset "{dataset}" can\'t be destroyed due to lack of permissions. Please set a' + ' PE helper') + raise PermissionError(proc.stderr) + else: + try: + self.handle_command_error(proc) + except PermissionError: + log.error('Permission denied, please use "zfs allow"') + raise + else: + log.info('Dataset destroyed successfully')