zfs_cli: implement set_mountpoint using pe_helper

main
svalouch 5 years ago
parent e7dd074b99
commit f93393f838

@ -3,6 +3,8 @@
Exceptions
'''
from typing import Optional
class ZFSException(Exception):
'''
@ -46,3 +48,20 @@ class ValidationError(ZFSException):
Indicates that a value failed validation.
'''
pass
class PEHelperException(ZFSException):
'''
Indicates a problem when running the PE helper.
'''
def __init__(self, message: str, returncode: Optional[int], stdout: Optional[str] = None, stderr: Optional[str] = None) -> None:
'''
:param message: The message to carry.
:param returncode: The programs return code.
:param stdout: The programs standard output, if any.
:param stderr: The programs standard error, if any.
'''
super().__init__(message)
self.returncode = returncode
self.stdout = stdout
self.stderr = stderr

@ -6,10 +6,12 @@ ZFS frontend API
import logging
import os
import stat
import subprocess
from typing import Dict, List, Optional, Union
from .exceptions import (
DatasetNotFound,
PEHelperException,
PermissionError,
PoolNotFound,
PropertyNotFound,
@ -51,9 +53,13 @@ class ZFS:
:note: Not setting a metadata namespace means that one can't set or get metadata properties, unless the overwrite
parameter for the get/set functions is used.
The parameter ``use_pe_helper`` is used to control whether the ``pe_helper`` executable will be used when
performing actions that require elevated permissions. It can be changed at anytime using the ``use_pe_helper``
property.
:param metadata_namespace: Default namespace
:param pe_helper: Privilege escalation (PE) helper to use for actions that require elevated privileges (root).
:param use_pe_helper: Whether to use the PE helper for creating and (u)mounting.#
:param use_pe_helper: Whether to use the PE helper for creating and (u)mounting.
:param kwargs: Extra arguments, ignored
'''
def __init__(self, *, metadata_namespace: Optional[str] = None, pe_helper: Optional[str] = None,
@ -163,7 +169,7 @@ class ZFS:
'''
raise NotImplementedError(f'{self} has not implemented this function')
def set_mountpoint(self, fileset: str, mountpoint: str, *, use_pe_helper: bool = False) -> None:
def set_mountpoint(self, fileset: str, mountpoint: str, *, use_pe_helper: Optional[bool] = None) -> None:
'''
Sets or changes the mountpoint property of a fileset. While this can be achieved using the generic function
:func:`~ZFS.set_property`, it allows for using the privilege escalation (PE) helper if so desired.
@ -171,6 +177,7 @@ class ZFS:
:param fileset: The fileset to modify.
:param mountpoint: The new value for the ``mountpoint`` property.
:param use_pe_helper: Overwrite the default for using the privilege escalation (PE) helper for this task.
``None`` (default) uses the default setting.
:raises DatasetNotFound: if the fileset could not be found.
:raises ValidationError: if validating the parameters failed.
'''
@ -184,6 +191,11 @@ class ZFS:
``metadata`` is set to **True**, the default metadata namespace is prepended or the one in
``overwrite_metadata_namespace`` is used if set to a valid string.
.. note::
Use :func:`set_mountpoint` to change the ``mountpoint`` property if your setup requires the use of elevated
permissions (such as Linux).
Example:
>>> z = ZFSCli(namespace='foo')
@ -327,6 +339,10 @@ class ZFS:
'''
Create a new snapshot from an existing dataset.
.. warning::
This action requires the ``pe_helper`` on Linux when not running as `root`.
:param dataset: The dataset to snapshot.
:param name: Name of the snapshot (the part after the ``@``)
:param properties: Dict of native properties to set.
@ -368,6 +384,10 @@ class ZFS:
Create a new fileset. For convenience, a ``mountpoint`` parameter can be given. If not **None**, it will
overwrite any `mountpoint` value in the ``properties`` dict.
.. warning::
This action requires the ``pe_helper`` on Linux when not running as `root`.
:param name: Name of the new fileset (complete path in the ZFS hierarchy).
:ppram mountpoint: Convenience parameter for setting/overwriting the moutpoint property
:param properties: Dict of native properties to set.
@ -589,6 +609,23 @@ class ZFS:
size: Optional[int] = None,
recursive: bool = False,
) -> Dataset:
'''
Actual implementation of :func:`create_dataset`.
:param name: The name of the new dataset. This includes the full path, e.g. ``tank/data/newdataset``.
:param dataset_type: Indicates the type of the dataset to be created.
:param properties: A dict containing the properties for this new dataset. These are the native properties.
:param metadata_properties: The metadata properties to set. To use a different namespace than the default (or
when no default is set), use the ``namespace:key`` format for the dict keys.
:param sparse: For volumes, specifies whether a sparse (thin provisioned) or normal (thick provisioned) volume
should be created.
:param size: For volumes, specifies the size in bytes.
t:param recursive: Recursively create the parent fileset. Refer to the ZFS documentation about the `-p`
parameter for ``zfs create``. This does not apply to types other than volumes or filesets.
:raises ValidationError: If validating the parameters failed.
:raises DatasetNotFound: If the dataset can't be found (snapshot, bookmark) or the parent dataset can't be
found (fileset, volume with ``recursive = False``).
'''
raise NotImplementedError(f'{self} has not implemented this function')
def destroy_dataset(self, name: str, *, recursive: bool = False) -> None:
@ -610,6 +647,101 @@ class ZFS:
'''
raise NotImplementedError(f'{self} has not implemented this function')
def handle_pe_error(self, args: List[str], proc: subprocess.CompletedProcess) -> None:
'''
Handles errors from the Privilege Escalation (PE) helper. If the returncode is ß, nothing happens, otherwise
an exception is thrown.
:param args: Arguments passed to the helper (without the helper executable itself).
:param proc: The result of subprocess.run
:raises PEHelperException: In case of error.
'''
log = logging.getLogger('simplezfs.zfs.pe_helper')
if proc.returncode == 0:
log.info(f'PE helper action {args[0]} was successful')
if len(proc.stdout) > 0:
log.debug(f'PE stdout: {proc.stdout}')
if len(proc.stderr) > 0:
log.warning(f'PE stderr: {proc.stderr}')
else:
if proc.returncode == 1:
log.error('General error in PE executable: Wrong parameters or configuration problem')
msg = 'General error'
elif proc.returncode == 2:
msg = 'Parent directory does not exist or is not a directory'
log.error(msg)
elif proc.returncode == 3:
msg = 'Parent dataset does not exist'
log.error(msg)
elif proc.returncode == 4:
msg = 'Target fileset is not a (grand)child of parent or parent does not exist'
log.error(msg)
elif proc.returncode == 5:
msg = 'Mountpoint is not inside the parent directory or otherwise invalid'
log.error(msg)
elif proc.returncode == 6:
msg = 'Calling the zfs utility failed'
log.error(msg)
else:
msg = f'Unknown / Unhandled error with returncode {proc.returncode}'
log.error(msg)
# gather output
if len(proc.stdout) > 0:
log.warning(f'PE stdout: {proc.stdout}')
if len(proc.stderr) > 0:
log.warning(f'PE stderr: {proc.stderr}')
raise PEHelperException(msg, returncode=proc.returncode, stdout=proc.stdout, stderr=proc.stderr)
def _execute_pe_helper(self, action: str, name: str, mountpoint: Optional[str] = None):
'''
Runs the specified action through the PE helper.
:param action: The action to perform. Valid are: "create", "destroy", "set_mountpoint".
:param name: The name of the dataset to operate on.
:param mountpoint: The mountpoint for create/set_mountpoint actions.
:raises ValidationError: If the parameters are invalid.
:raises PEHelperException: If the PE helper reported an error.
'''
if not self._pe_helper:
raise ValidationError('PE Helper is not set')
if action not in ('create', 'destroy', 'set_mountpoint'):
raise ValidationError(f'Invalid action')
validate_dataset_path(name)
if action == 'create':
if mountpoint is None:
raise ValidationError(f'Mountpoint has to be set for action "{action}"')
# TODO validate filesystem path
cmd = [self._pe_helper, 'create', name, mountpoint]
elif action == 'destroy':
cmd = [self._pe_helper, 'destroy', name]
elif action == 'set_mountpoint':
if mountpoint is None:
raise ValidationError(f'Mountpoint has to be set for action "{action}"')
# TODO validate filesystem path
cmd = [self._pe_helper, 'set_mountpoint', name, mountpoint]
else:
raise ValidationError('Invalid action')
print(f'PE Helper: {cmd}')
log = logging.getLogger('simplezfs.zfs.pe_helper')
log.debug(f'About to run the following command: {cmd}')
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8')
if proc.returncode != 0 or len(proc.stderr) > 0:
log.error(f'PE Helper exit code {proc.returncode}')
log.error(f'Stdout: {proc.stdout}')
log.error(f'Stderr: {proc.stderr}')
raise PEHelperException('PE Helper execution error', proc.returncode, proc.stdout, proc.stderr)
else:
log.info(f'PE Helper successful')
log.debug(f'Return code: {proc.returncode}')
log.debug(f'Stdout: {proc.stdout}')
log.debug(f'Stderr: {proc.stderr}')
def get_zfs(api: str = 'cli', metadata_namespace: Optional[str] = None, **kwargs) -> ZFS:
'''

@ -3,14 +3,14 @@
CLI-based implementation.
'''
from typing import List, Optional, Union
from typing import Dict, List, Optional, Union
import logging
import os
import shutil
import subprocess
from .exceptions import DatasetNotFound, PropertyNotFound
from .types import Dataset, Property, PropertySource
from .exceptions import DatasetNotFound, PEHelperException, PropertyNotFound, ValidationError
from .types import Dataset, DatasetType, Property, PropertySource
from .validation import (
validate_dataset_path,
validate_pool_name,
@ -120,6 +120,28 @@ class ZFSCli(ZFS):
res.append(Dataset.from_string(name.strip()))
return res
def set_mountpoint(self, fileset: str, mountpoint: str, *, use_pe_helper: Optional[bool] = False) -> None:
real_use_pe_helper = use_pe_helper if use_pe_helper is not None else self.use_pe_helper
ds_type = self.get_property(fileset, 'type')
if ds_type != 'filesystem':
raise ValidationError('Given fileset is not a filesystem, can\'t set mountpoint')
if not real_use_pe_helper:
self.set_property(fileset, 'mountpoint', mountpoint)
else:
if not self.pe_helper:
raise ValidationError('PE helper should be used, but is not defined')
args = [self.pe_helper, 'set_mountpoint', fileset, mountpoint]
log.debug(f'set_mountpoint: executing {args}')
proc = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='utf-8')
log.debug(f'set_mountpoint returncode: {proc.returncode}')
log.debug(f'set_mountpoint stdout: {proc.stdout}')
log.debug(f'set_mountpoint stderr: {proc.stderr}')
# TODO log output
if proc.returncode != 0 or len(proc.stderr) > 0:
self.handle_command_error(proc, fileset)
def handle_command_error(self, proc: subprocess.CompletedProcess, dataset: str = None) -> None:
'''
Handles errors that occured while running a command.
@ -140,6 +162,8 @@ class ZFSCli(ZFS):
raise PropertyNotFound(f'invalid property on dataset {dataset}')
else:
raise PropertyNotFound('invalid property')
elif 'permission denied' in proc.stderr:
raise PermissionError(proc.stderr)
raise Exception(f'Command execution "{" ".join(proc.args)}" failed: {proc.stderr}')
def _set_property(self, dataset: str, key: str, value: str, is_metadata: bool) -> None:
@ -209,3 +233,102 @@ class ZFSCli(ZFS):
else:
res.append(Property(key=prop_name, value=prop_value, source=property_source, namespace=None))
return res
def _create_dataset(
self,
name: str,
*,
dataset_type: DatasetType,
properties: Dict[str, str] = None,
metadata_properties: Dict[str, str] = None,
sparse: bool = False,
size: Optional[int] = None,
recursive: bool = False,
) -> Dataset:
if dataset_type == DatasetType.BOOKMARK:
raise ValidationError('Bookmarks can\'t be created by this function')
# assemble the options list for properties
prop_args: List[str] = []
if properties:
for nk, nv in properties.items():
prop_args += ['-o', f'{nk}={nv}']
if metadata_properties:
for mk, mv in metadata_properties.items():
prop_args += ['-o', f'{mk}={mv}']
if dataset_type == DatasetType.FILESET:
assert size is None, 'Filesets have no size'
assert sparse is False, 'Filesets cannot be sparse'
# try on our own first, then depending on settings use the pe helper
args = [self.__exe, 'create']
if recursive:
args += ['-p']
args += prop_args
args += [name]
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:
if 'filesystem successfully created, but it may only be mounted by root' in proc.stderr:
if self.use_pe_helper:
# We may not have a mountpoint, but tried to inherit the base from the parent.
# In this case, we need to compute it on our own, for now we simply break.
try:
mp = properties['mountpoint']
except KeyError:
msg = 'Mountpoint property not set, can\'t run pe_helper'
log.error(msg)
raise PermissionError(msg)
log.info(f'Fileset {name} was created, using pe_helper to set the mountpoint')
else:
log.info(f'Fileset {name} was created, but could not be mounted due to not running as root')
raise PermissionError(proc.stderr)
if self.use_pe_helper:
try:
mp = properties['mountpoint']
except KeyError:
raise ValidationError('Mountpoint not found in properties')
self._execute_pe_helper('create', recursive=recursive, mountpoint=properties['mountpoint'], name=name)
else:
args = [self.__exe, 'create']
if recursive:
args += ['-p']
args += [name]
print(f'Executing {args}')
elif dataset_type == DatasetType.VOLUME:
assert size is not None
args = [self.__exe, 'create']
if sparse:
args += ['-s']
if recursive:
args += ['-p']
# [-b blocksize] is set using properties
args += prop_args
args += ['-V', str(size), name]
print(f'Executing {args}')
elif dataset_type == DatasetType.SNAPSHOT:
assert size is None, 'Snapshots have no size'
assert sparse is False, 'Snapshots can\'t be sparse'
args = [self.__exe, 'snapshot', *prop_args, name]
print(f'Executing {args}')
raise NotImplementedError()
def create_bookmark(self, snapshot: str, name: str) -> Dataset:
validate_dataset_path(snapshot)
raise NotImplementedError()

Loading…
Cancel
Save