|
|
|
@ -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 |
|
|
|
|