@ -3,15 +3,15 @@
CLI - based implementation .
CLI - based implementation .
'''
'''
from typing import Dict , List , Optional , Union
import logging
import logging
import os
import os
import shutil
import shutil
import subprocess
import subprocess
from typing import Dict , List , NoReturn , Optional , Union
from . exceptions import DatasetNotFound , PropertyNotFound , ValidationError
from . exceptions import DatasetNotFound , PropertyNotFound
from . pe_helper import PEHelperBase
from . pe_helper import PEHelperBase
from . types import Dataset , DatasetTyp e, Property , PropertySource
from . types import Dataset , PEHelperMod e, Property , PropertySource
from . validation import (
from . validation import (
validate_dataset_path ,
validate_dataset_path ,
validate_pool_name ,
validate_pool_name ,
@ -36,9 +36,9 @@ class ZFSCli(ZFS):
self . find_executable ( path = zfs_exe )
self . find_executable ( path = zfs_exe )
def __repr__ ( self ) - > str :
def __repr__ ( self ) - > str :
return f ' <ZFSCli(exe= " { self . __exe } " , pe_helper= " { self . _pe_helper } " , use_ pe_helper=" { self . _ use_ pe_helper} " )> '
return f ' <ZFSCli(exe= " { self . __exe } " , pe_helper= " { self . _pe_helper } " , pe_helper_mode =" { self . _ pe_helper_mode } " )> '
def find_executable ( self , path : str = None ) :
def find_executable ( self , path : str = None ) - > None :
'''
'''
Tries to find the executable ` ` zfs ( 8 ) ` ` . If ` ` path ` ` points to an executable , it is used instead of relying on
Tries to find the executable ` ` zfs ( 8 ) ` ` . If ` ` path ` ` points to an executable , it is used instead of relying on
the PATH to find it . It does not fall back to searching in $ PATH of ` ` path ` ` does not point to an executable .
the PATH to find it . It does not fall back to searching in $ PATH of ` ` path ` ` does not point to an executable .
@ -59,7 +59,7 @@ class ZFSCli(ZFS):
@property
@property
def executable ( self ) - > str :
def executable ( self ) - > str :
'''
'''
Returns the zfs executable that was found by find_executable
Returns the zfs executable that was found by find_executable .
'''
'''
return self . __exe
return self . __exe
@ -122,7 +122,7 @@ class ZFSCli(ZFS):
res . append ( Dataset . from_string ( name . strip ( ) ) )
res . append ( Dataset . from_string ( name . strip ( ) ) )
return res
return res
def handle_command_error ( self , proc : subprocess . CompletedProcess , dataset : str = None ) - > None :
def handle_command_error ( self , proc : subprocess . CompletedProcess , dataset : str = None ) - > NoReturn :
'''
'''
Handles errors that occured while running a command .
Handles errors that occured while running a command .
@ -155,10 +155,10 @@ class ZFSCli(ZFS):
: raises DatasetNotFound : If the dataset does not exist .
: raises DatasetNotFound : If the dataset does not exist .
'''
'''
args = [ self . __exe , ' set ' , f ' { key } = { value } ' , dataset ]
args = [ self . __exe , ' set ' , f ' { key } = { value } ' , dataset ]
log . debug ( f ' _set_property: about to run command: {args } ' )
log . debug ( ' _set_property: about to run command: %s' , args )
proc = subprocess . run ( args , stdout = subprocess . PIPE , stderr = subprocess . PIPE , encoding = ' utf-8 ' )
proc = subprocess . run ( args , stdout = subprocess . PIPE , stderr = subprocess . PIPE , encoding = ' utf-8 ' )
if proc . returncode != 0 or len ( proc . stderr ) > 0 :
if proc . returncode != 0 or len ( proc . stderr ) > 0 :
log . debug ( f ' _set_propery: command failed, code= {proc . returncode } , stderr= " { proc . stderr } " ' )
log . debug ( ' _set_propery: command failed, code= %d, stderr= " %s " ' , proc . returncode , proc . stderr . strip ( ) )
self . handle_command_error ( proc , dataset = dataset )
self . handle_command_error ( proc , dataset = dataset )
def _get_property ( self , dataset : str , key : str , is_metadata : bool ) - > Property :
def _get_property ( self , dataset : str , key : str , is_metadata : bool ) - > Property :
@ -169,10 +169,10 @@ class ZFSCli(ZFS):
: raises PropertyNotFound : If the property does not exist or is invalid ( for native ones ) .
: raises PropertyNotFound : If the property does not exist or is invalid ( for native ones ) .
'''
'''
args = [ self . __exe , ' get ' , ' -H ' , ' -p ' , key , dataset ]
args = [ self . __exe , ' get ' , ' -H ' , ' -p ' , key , dataset ]
log . debug ( f ' _get_property: about to run command: {args } ' )
log . debug ( ' _get_property: about to run command: %s' , args )
proc = subprocess . run ( args , stdout = subprocess . PIPE , stderr = subprocess . PIPE , encoding = ' utf-8 ' )
proc = subprocess . run ( args , stdout = subprocess . PIPE , stderr = subprocess . PIPE , encoding = ' utf-8 ' )
if proc . returncode != 0 or len ( proc . stderr ) > 0 :
if proc . returncode != 0 or len ( proc . stderr ) > 0 :
log . debug ( f ' _get_property: command failed, code= {proc . returncode } , stderr= " { proc . stderr . strip ( ) } " ' )
log . debug ( ' _get_property: command failed, code= %d, stderr= " %s " ' , proc . returncode , proc . stderr . strip ( ) )
self . handle_command_error ( proc , dataset = dataset )
self . handle_command_error ( proc , dataset = dataset )
name , prop_name , prop_value , prop_source = proc . stdout . strip ( ) . split ( ' \t ' )
name , prop_name , prop_value , prop_source = proc . stdout . strip ( ) . split ( ' \t ' )
if name != dataset :
if name != dataset :
@ -196,10 +196,10 @@ class ZFSCli(ZFS):
: raises DatasetNotFound : If the dataset does not exist .
: raises DatasetNotFound : If the dataset does not exist .
'''
'''
args = [ self . __exe , ' get ' , ' -H ' , ' -p ' , ' all ' , dataset ]
args = [ self . __exe , ' get ' , ' -H ' , ' -p ' , ' all ' , dataset ]
log . debug ( f ' _get_properties: about to run command: {args } ' )
log . debug ( ' _get_properties: about to run command: %s' , args )
proc = subprocess . run ( args , stdout = subprocess . PIPE , stderr = subprocess . PIPE , encoding = ' utf-8 ' )
proc = subprocess . run ( args , stdout = subprocess . PIPE , stderr = subprocess . PIPE , encoding = ' utf-8 ' )
if proc . returncode != 0 or len ( proc . stderr ) > 0 :
if proc . returncode != 0 or len ( proc . stderr ) > 0 :
log . debug ( f ' _get_properties: command faild, code= {proc . returncode } , stderr= " { proc . stderr } " ' )
log . debug ( ' _get_properties: command faild, code= %d, stderr= " %s " ' , proc . returncode , proc . stderr . strip ( ) )
self . handle_command_error ( proc , dataset = dataset )
self . handle_command_error ( proc , dataset = dataset )
res = list ( )
res = list ( )
for line in proc . stdout . split ( ' \n ' ) :
for line in proc . stdout . split ( ' \n ' ) :
@ -216,120 +216,124 @@ class ZFSCli(ZFS):
res . append ( Property ( key = prop_name , value = prop_value , source = property_source , namespace = None ) )
res . append ( Property ( key = prop_name , value = prop_value , source = property_source , namespace = None ) )
return res
return res
def _create_dataset (
def _create_fileset ( self , name : str , properties : Dict [ str , str ] = None , metadata_properties : Dict [ str , str ] = None ,
self ,
recursive : bool = False ) - > Dataset :
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 ] = [ ]
prop_args : List [ str ] = [ ]
if properties :
if properties :
for n k, n v in properties . items ( ) :
for normalkey , normalvalue in properties . items ( ) :
prop_args + = [ ' -o ' , f ' { n k} = { n v} ' ]
prop_args + = [ ' -o ' , f ' { normalkey } = { normalvalue } ' ]
if metadata_properties :
if metadata_properties :
for mk , mv in metadata_properties . items ( ) :
for metakey , metavalue in metadata_properties . items ( ) :
prop_args + = [ ' -o ' , f ' { mk } = { mv } ' ]
prop_args + = [ ' -o ' , f ' { metakey } = { metavalue } ' ]
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 } ' )
print ( 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 ' Process died with returncode { proc . returncode } and stderr: " { proc . stderr . strip ( ) } " ' )
# check if we tried something only root can do
if ' filesystem successfully created, but it may only be mounted by root ' in proc . stderr :
log . debug ( ' Command output indicates that we need to run the PE Helper ' )
if self . use_pe_helper :
# The mountpoint property may be set, in which case we can run the PE helper. If it is not
# set, we'd need to compute it based on the parent, but for now we simply error out.
if properties and ' mountpoint ' in properties :
mp = properties [ ' mountpoint ' ]
if self . pe_helper is not None :
test_prop = self . get_property ( dataset = name , key = ' mountpoint ' , metadata = False )
if test_prop . value == mp :
log . info ( f ' Fileset { name } was created with mountpoint set ' )
else :
log . info ( f ' Fileset { name } was created, using pe_helper to set the mountpoint ' )
self . pe_helper . zfs_set_mountpoint ( name , mp )
test_prop = self . get_property ( dataset = name , key = ' mounted ' , metadata = False )
if test_prop . value == ' yes ' :
log . info ( f ' Fileset { name } is mounted ' )
else :
log . info ( f ' Using pe_helper to mount fileset { name } ' )
self . pe_helper . zfs_mount ( name )
log . info ( f ' Fileset { name } created successfully (using pe_helper) ' )
return self . get_dataset_info ( name )
msg = ' Fileset created partially but no PE helper set '
log . error ( msg )
raise PermissionError ( msg )
else :
msg = ' Mountpoint property not set, can \' t run pe_helper '
log . error ( msg )
raise PermissionError ( msg )
else :
args = [ self . __exe , ' create ' ]
log . error ( f ' Fileset " { name } " was created, but could not be mounted due to lack of permissions. '
if recursive :
' Please set a PE helper and call " set_mountpoint " with an explicit mountpoint to '
args + = [ ' -p ' ]
' complete the action ' )
raise PermissionError ( proc . stderr )
args + = prop_args
else :
args + = [ name ]
try :
self . handle_command_error ( proc )
log . debug ( ' Executing: %s ' , args )
except PermissionError :
proc = subprocess . run ( args , stdout = subprocess . PIPE , stderr = subprocess . PIPE , encoding = ' utf-8 ' )
log . error ( ' Permission denied, please use " zfs allow " ' )
if proc . returncode != 0 or len ( proc . stderr ) > 0 : # pylint: disable=too-many-nested-blocks
raise
# check if we tried something only root can do
else :
if ' filesystem successfully created, but it may only be mounted by root ' in proc . stderr :
log . info ( ' Filesystem created successfully ' )
log . debug ( ' Command output indicates that we need to run the PE Helper ' )
return self . get_dataset_info ( name )
if self . pe_helper_mode != PEHelperMode . DO_NOT_USE :
# The mountpoint property may be set, in which case we can run the PE helper. if it is not set,
# we'd need to compute it based on the parent, but for now we simply error out.
if properties and ' mountpoint ' in properties :
mopo = properties [ ' mountpoint ' ]
if self . pe_helper is not None :
test_prop = self . get_property ( dataset = name , key = ' mountpoint ' , metadata = False )
if test_prop . value == mopo :
log . info ( ' Fileset " %s " was created with mountpoint set ' , name )
else :
log . info ( ' Fileset " %s " was created, using pe_helper to set the mountpoint ' , name )
self . pe_helper . zfs_set_mountpoint ( name , mopo )
test_prop = self . get_property ( dataset = name , key = ' mounted ' , metadata = False )
if test_prop . value == ' yes ' :
log . info ( ' Fileset " %s " is mounted ' , name ) # shouldn't be the case with the error above
else :
log . info ( ' Using pe_helper to mount fileset " %s " ' , name )
self . pe_helper . zfs_mount ( name )
log . info ( ' Fileset " %s " created successfully (using pe_helper) ' , name )
return self . get_dataset_info ( name )
msg = ' Fileset created partially but no PE helper set '
log . error ( msg )
raise PermissionError ( msg )
msg = ' Mountpoint property not set, can \' t run pe_helper '
log . error ( msg )
raise PermissionError ( msg )
log . error ( ' Fileset " %s " was created, but could not be mounted due to lack of permissions. Please set a '
' PE helper and set the mode accordingly, and call " set_mountpoint " with an explicit '
' mountpoint to complete the action ' , name )
raise PermissionError ( proc . stderr )
try :
self . handle_command_error ( proc )
except PermissionError :
log . error ( ' Permission denied, please use " zfs allow " and possibly set a PE Helper ' )
raise
log . info ( ' Filesystem " %s " created successfully ' , name )
return self . get_dataset_info ( name )
def _create_snapshot ( self , name : str , properties : Dict [ str , str ] = None ,
metadata_properties : Dict [ str , str ] = None , recursive : bool = False ) - > Dataset :
prop_args : List [ str ] = [ ]
if properties :
for normalkey , normalvalue in properties . items ( ) :
prop_args + = [ ' -o ' , f ' { normalkey } = { normalvalue } ' ]
if metadata_properties :
for metakey , metavalue in metadata_properties . items ( ) :
prop_args + = [ ' -o ' , f ' { metakey } = { metavalue } ' ]
args = [ self . __exe , ' create ' ]
if recursive :
args + = [ ' -r ' ]
elif dataset_type == DatasetType . VOLUME :
args + = prop_args
assert size is not None
args = [ self . __exe , ' create ' ]
log . debug ( ' Executing %s ' , args )
if sparse :
proc = subprocess . run ( args , stdout = subprocess . PIPE , stderr = subprocess . PIPE , encoding = ' utf-8 ' )
args + = [ ' -s ' ]
if proc . returncode != 0 or len ( proc . stderr ) > 0 :
if recursive :
# TODO
args + = [ ' -p ' ]
self . handle_command_error ( proc )
# [-b blocksize] is set using properties
return self . get_dataset_info ( name )
args + = prop_args
def _create_volume ( self , name : str , properties : Dict [ str , str ] = None , metadata_properties : Dict [ str , str ] = None ,
sparse : bool = False , size : Optional [ int ] = None , recursive : bool = False ) - > Dataset :
prop_args : List [ str ] = [ ]
if properties :
for normalkey , normalvalue in properties . items ( ) :
prop_args + = [ ' -o ' , f ' { normalkey } = { normalvalue } ' ]
if metadata_properties :
for metakey , metavalue in metadata_properties . items ( ) :
prop_args + = [ ' -o ' , f ' { metakey } = { metavalue } ' ]
args + = [ ' -V ' , str ( size ) , name ]
assert size is not None
print ( f ' Executing { args } ' )
args = [ self . __exe , ' create ' ]
if sparse :
args + = [ ' -s ' ]
if recursive :
args + = [ ' -p ' ]
# [-b blocksize] is set using properties
elif dataset_type == DatasetType . SNAPSHOT :
args + = prop_args
assert size is None , ' Snapshots have no size '
assert sparse is False , ' Snapshots can \' t be sparse '
args = [ self . __exe , ' snapshot ' , * prop_args , name ]
args + = [ ' -V ' , str ( size ) , name ]
print ( f ' Executing { args } ' )
raise NotImplementedError ( )
log . debug ( ' Executing %s ' , args )
proc = subprocess . run ( args , stdout = subprocess . PIPE , stderr = subprocess . PIPE , encoding = ' utf-8 ' )
if proc . returncode != 0 or len ( proc . stderr ) > 0 :
# TODO
self . handle_command_error ( proc )
return self . get_dataset_info ( name )
def create_bookmark ( self , snapshot : str , name : str ) - > Dataset :
def _ create_bookmark( self , snapshot : str , name : str ) - > Dataset :
validate_dataset_path ( snapshot )
validate_dataset_path ( snapshot )
raise NotImplementedError ( )
raise NotImplementedError ( )
@ -342,35 +346,54 @@ class ZFSCli(ZFS):
args . append ( dataset )
args . append ( dataset )
log . debug ( f ' executing: { args } ' )
log . debug ( f ' executing: { args } ' )
if self . pe_helper is not None and self . pe_helper_mode == PEHelperMode . USE_PROACTIVE :
test_prop = self . get_property ( dataset , ' mounted ' )
if test_prop . value == ' yes ' :
log . info ( ' Fileset is mounted, proactively unmounting using pe_helper ' )
self . pe_helper . zfs_umount ( dataset )
proc = subprocess . run ( args , stdout = subprocess . PIPE , stderr = subprocess . PIPE , encoding = ' utf-8 ' )
proc = subprocess . run ( args , stdout = subprocess . PIPE , stderr = subprocess . PIPE , encoding = ' utf-8 ' )
if proc . returncode != 0 or len ( proc . stderr ) > 0 :
if proc . returncode != 0 or len ( proc . stderr ) > 0 :
log . debug ( f ' destroy_dataset: command failed, code= { proc . returncode } , stderr= " { proc . stderr } " ' )
log . debug ( ' destroy_dataset: command failed, code= %d, stderr= " %s " ' , proc . returncode , proc . stderr . strip ( ) )
if ' has children ' in proc . stderr :
if ' has children ' in proc . stderr :
if recursive :
if recursive :
log . error ( f ' Dataset { dataset } has children and recursive was given, please report this ' )
log . error ( ' Dataset " %s " has children and recursive was given, please report this ' , dataset )
else :
else :
log . warning ( f ' Dataset { dataset } has children and thus cannot be destroyed without recursive=True ' )
log . warning ( ' Dataset " %s " has children and thus cannot be destroyed without recursive=True ' ,
dataset )
raise Exception
raise Exception
# t wo possible messa es: (zfs destroy -p -r [-f] $fileset_with_snapshots)
# t hree possible messag es: (zfs destroy -p -r [-f] $fileset_with_snapshots)
# * 'cannot destroy snapshots: permission denied'
# * 'cannot destroy snapshots: permission denied'
# * 'umount: only root can use "--types" option'
# * 'umount: only root can use "--types" option'
# The latter seems to originate from having `destroy` and `mount` via `zfs allow`.
# 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 \
elif ( ' cannot destroy ' in proc . stderr and ' permission denied ' in proc . stderr ) or \
' only root can ' in proc . stderr :
' only root can ' in proc . stderr :
log . debug ( ' Command output indicates that we need to run the PE Helper ' )
log . debug ( ' Command output indicates that we need to run the PE Helper ' )
if self . use_pe_helper :
if self . pe_helper_mode != PEHelperMode . DO_NOT_USE :
if self . pe_helper is not None :
if self . pe_helper is not None :
log . info ( f ' Using pe_helper to remove {dataset } ' )
log . info ( ' Using pe_helper to remove %s' , dataset )
self . pe_helper . zfs_destroy_dataset ( dataset , recursive , force_umount )
self . pe_helper . zfs_destroy_dataset ( dataset , recursive , force_umount )
log . info ( f' Dataset { dataset } destroyed (using pe_helper) ' )
log . info ( ' Dataset " %s " destroyed (using pe_helper) ' , dataset )
else :
else :
msg = ' Cannot destroy: No pe_helper set '
msg = ' Cannot destroy: No pe_helper set '
log . error ( msg )
log . error ( msg )
raise PermissionError ( msg )
raise PermissionError ( msg )
else :
else :
log . error ( f' Dataset " { dataset } " can \' t be destroyed due to lack of permissions. Please set a '
log . error ( ' Dataset " %s " can \' t be destroyed due to lack of permissions. Please set a PE helper ' ,
' PE helper ' )
dataset )
raise PermissionError ( proc . stderr )
raise PermissionError ( proc . stderr )
# Another one new with OpenZFS 2.0 that does not indicate what the problem is
# * 'cannot unmount '${fileset}': unmount failed'
elif ' cannot umount ' in proc . stderr and ' umount failed ' in proc . stderr :
if self . pe_helper is not None and self . pe_helper_mode != PEHelperMode . DO_NOT_USE :
log . info ( ' Destroy could not unmount, retrying using pe_helper ' )
self . pe_helper . zfs_umount ( dataset )
self . _destroy_dataset ( dataset , recursive = recursive , force_umount = force_umount )
else :
msg = ' Umounting failed and pe_helper is not allowed '
log . error ( msg )
raise PermissionError ( msg )
else :
else :
try :
try :
self . handle_command_error ( proc )
self . handle_command_error ( proc )
@ -379,3 +402,25 @@ class ZFSCli(ZFS):
raise
raise
else :
else :
log . info ( ' Dataset destroyed successfully ' )
log . info ( ' Dataset destroyed successfully ' )
# def _mount_umount_fileset(self, fileset: str, mount: bool) -> None:
# if '/' in fileset:
# validate_dataset_path(fileset)
# else:
# validate_pool_name(fileset)
# if not self.dataset_exists(fileset):
# raise DatasetNotFound('The fileset could not be found')
# test_prop = self.get_property(dataset=fileset, key='mounted')
# if mount:
# if test_prop.value == 'yes':
# log.warning('Fileset "%s" is already mounted', fileset)
# else:
# pass
# else:
# if test_prop.value != 'yes':
# log.warning('Fileset "%s" is not mounted', fileset)
# else:
# pass