Further Filesystem API revisions; ZFS implementation

oop-refactor
Thor 4 years ago
parent e3738245c3
commit d6ea42dff3
  1. 30
      src/zasd/filesystem.py
  2. 124
      src/zasd/filesystem/zfs.py
  3. 2
      src/zasd/pathwatcher.py

@ -5,7 +5,7 @@
class Filesystem:
''' Base filsystem class inherited by each filesystem implementation '''
def __init__(self, name, props, **kwprops):
def __init__(self, name, props=None, **kwprops):
# Set common properties
self._name = name
@ -22,12 +22,23 @@ class Filesystem:
transforming property values and assigning them to private
attributes on the Filesystem object. '''
def name(self):
''' Retrieve filesystem name '''
return self._name
async def snapshots(self):
''' Retrieve an {ordinal: snapshot} dictionary of the snapshots that are
managed by this Filesystem object. '''
raise NotImplementedError()
async def mountpoint(self):
''' Retrieve the mountpoint path this Filesystem object is managing. '''
raise NotImplementedError()
@classmethod
def key(cls):
''' Retrieve the registry key of this Filesystem class. '''
raise NotImplementedError()
@classmethod
async def filesystems(cls):
@ -43,16 +54,16 @@ class Filesystem:
variables and methods that the user will need in order to configure
the filesystem. The method is called whenever the configuration
file is reloaded. '''
return {}
class Snapshot:
''' Base snapshot class inherited by each filesystem implementation '''
def __init__(self, filesystem, tag, props, **kwprops):
def __init__(self, filesystem, props=None, **kwprops):
# pylint: disable=unused-argument
# Set common properties
self._filesystem = filesystem
self._tag = tag
# Process arguments to extract all properties
props = _process_args(props, kwprops)
@ -77,7 +88,7 @@ class Snapshot:
name. The tag name is passed in the constructor of the snapshot. A
tag name paired with an ordinal uniquely identifies a snapshot. This
method need not be overridden in subclasses. '''
return self._tag
raise NotImplementedError()
def ordinal(self):
''' Get ordinal. Ordinals are sortable, hashable timestamps that are
@ -90,13 +101,16 @@ class Snapshot:
async def take(self):
''' Take snapshot and return the ordinal. This method is never called
more than once per snapshot object. '''
raise NotImplementedError()
def taken(self):
''' This method returns True if the snapshot has been taken. '''
raise NotImplementedError()
async def delete(self):
''' Delete this snapshot. No further method calls are made on the
snapshot once it has been destroyed. '''
raise NotImplementedError()
class FilesystemRegistry:
''' Class that keeps a global registry of available Filesystem classes '''
@ -104,11 +118,11 @@ class FilesystemRegistry:
_filesystems = {}
@classmethod
def register(cls, name, klass):
''' Register a filesystem. If a filesystem needs to be configured by the
user, it must retrieve its configuration from a dictionary at
def register(cls, klass):
''' Register a Filesystem class. If a filesystem needs to be configured
by the user, it must retrieve its configuration from a dictionary at
config['filesystems'][name]. '''
cls._filesystems[name] = klass
cls._filesystems[klass.key()] = klass
@classmethod
def all(cls):

@ -12,49 +12,39 @@ from zasd.filesystem import Filesystem, Snapshot
_DATASET_COLS = ['type', 'name', 'creation', 'mountpoint']
class ZFS(Filesystem):
def __init__(self, *args, **kwargs):
super().__init__(*args, *kwargs)
class ZFSFilesystem(Filesystem):
_filesystems = {}
def snapshots(self, name=None):
datasets = self.datasets('snapshot')
if name is None:
return datasets
return next(ds for ds in datasets if ds['name'] == name)
def initialise(self, props):
self._filesystems[self.name()] = self
self._mountpoint = props['mountpoint']
async def snapshots(self):
datasets = (ds for ds in self._datasets('snapshot') if
_is_managed_name(ds['name']))
return (ZFSSnapshot(self, ds, taken=True) for ds in datasets)
async def mountpoint(self):
return self._mountpoint
@classmethod
def key(cls):
return 'zfs'
@classmethod
def filesystems(cls, name=None):
datasets = cls.datasets('filesystem')
if name is None:
return datasets
return next(ds for ds in datasets if ds['name'] == name)
async def filesystems(cls):
datasets = (ds for ds in cls._datasets('filesystem') if
ds['name'] in cls._filesystems)
return (ZFSFilesystem(ds['name'], ds) for ds in datasets)
@classmethod
def datasets(cls, dataset_type, *columns):
def _datasets(cls, dataset_type):
'''Get list of datasets'''
return _run_for_dicts(
[config['zfs_path'],
'list', '-Hp',
'-t', dataset_type,
'-o', ','.join(columns)], columns)
async def take_snapshot(self, name, recursive=False):
'''Create ZFS snapshot and, optionally, include the children'''
args = [config['zfs']['executable'], 'snapshot']
if recursive:
args.append('-r')
args.append(name)
return await asyncio.create_subprocess_exec(*args)
async def destroy_snapshot(self, name, recursive=False):
'''Destroy ZFS snapshot and, optionally, include the children'''
args = [config['zfs']['executable'], 'destroy']
if recursive:
args.append('-r')
args.append(name)
return await asyncio.create_subprocess_exec(*args)
'-o', ','.join(_DATASET_COLS)], _DATASET_COLS)
class ZFSSnapshot(Snapshot):
def initialise(self, props):
@ -63,46 +53,54 @@ class ZFSSnapshot(Snapshot):
if name:
# Split name string into field list
fields = name.split('@' + config['separator'])
fields = _split_snapshot_name(name)
# Get any missing properties from field list
if not props.get('dataset'):
props['dataset'] = fields[0]
if not props.get('tag'):
props['tag'] = fields[1]
if not props.get('serial'):
props['serial'] = fields[2]
if not props.get('ordinal'):
props['ordinal'] = fields[2]
# Assign properties to object
self._dataset = props.get('dataset')
self._tag = props.get('tag')
self._serial = props.get('serial', ('%x' % int(time()))[-8:])
self._mountpoint = props.get('mountpoint')
self._dataset = props.get('dataset')
self._tag = props.get('tag')
self._ordinal = props.get('ordinal')
self._mountpoint = props.get('mountpoint')
self._taken = props.get('taken', False)
def dataset(self):
return self._dataset
''' Get ZFS name of snapshot '''
def name(self):
return '{}@{}{}{}'.format(
self._dataset, self._tag, config['separator'], self._serial)
''' Get tag name'''
def tag(self):
return self._tag
''' Get serial '''
def serial(self):
return self._serial
def ordinal(self):
return self._ordinal
''' Get mountpoint '''
def mountpoint(self):
return self._mountpoint
async def take(self):
args = [config['zfs']['executable'], 'snapshot', self._name()]
await asyncio.create_subprocess_exec(*args)
# pylint: disable=attribute-defined-outside-init
self._ordinal = _generate_ordinal()
def taken(self):
return self._taken
async def delete(self):
args = [config['zfs']['executable'], 'destroy', self._name()]
await asyncio.create_subprocess_exec(*args)
def is_snapshot(arg):
def _name(self):
''' Get ZFS name of snapshot '''
return '{}@{}{}{}'.format(
self._dataset, self._tag, config['separator'], self._ordinal)
def _is_managed_name(name):
return (_is_snapshot_name(name) and
_split_snapshot_name(name)[0] in Filesystem.filesystems())
def _is_snapshot_name(name):
'''
Check if 'arg' looks like a ZASD snapshot name, meaning a string
Check if 'name' looks like a managed snapshot name, meaning a string
consisting of a character sequence as follows:
1. At least one character that isn't '@'
@ -112,11 +110,17 @@ def is_snapshot(arg):
5. At least one character that isn't a separator
6. Go to #4 and repeat until the string ends
NOTE: The validity of 'arg' as a ZFS snapshot name is not checked!
NOTE: The validity of 'name' as a ZFS snapshot name is not checked!
'''
sep = re.escape(config['separator'])
return isinstance(arg, str) and bool(
re.match('^[^@]+[^%s]+(%s[^%s]+)+$' % (sep, sep, sep), arg))
return isinstance(name, str) and bool(
re.match('^[^@]+[^%s]+(%s[^%s]+)+$' % (sep, sep, sep), name))
def _split_snapshot_name(name):
return name.split('@' + config['separator'])
def _generate_ordinal():
return ('%x' % int(time()))[-8:]
def _run_for_dicts(args, column_list):
''' Run program and convert tabulated output to list

@ -28,4 +28,4 @@ class PathWatcherProtocol(LineBufferedProtocol):
return
log.info('Detected change on filesystem %s', self._name)
self._callback(self._name)
self._callback()

Loading…
Cancel
Save