from subprocess import run, PIPE import asyncio import locale from time import time import re from zasd.config import config from zasd.filesystem import Filesystem, Snapshot # # Constants _DATASET_COLS = ['type', 'name', 'creation', 'mountpoint'] class ZFS(Filesystem): def __init__(self, *args, **kwargs): super().__init__(*args, *kwargs) 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) @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) @classmethod def datasets(cls, dataset_type, *columns): '''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) class ZFSSnapshot(Snapshot): def initialise(self, props): # Check if 'name' property exists and looks like a snapshot name name = props.get('name') if name: # Split name string into field list fields = name.split('@' + config['separator']) # 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] # 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') 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 ''' Get mountpoint ''' def mountpoint(self): return self._mountpoint def is_snapshot(arg): ''' Check if 'arg' looks like a ZASD snapshot name, meaning a string consisting of a character sequence as follows: 1. At least one character that isn't '@' 2. Character '@' 3. At least one character that isn't a separator 4. Separator character 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! ''' sep = re.escape(config['separator']) return isinstance(arg, str) and bool( re.match('^[^@]+[^%s]+(%s[^%s]+)+$' % (sep, sep, sep), arg)) def _run_for_dicts(args, column_list): ''' Run program and convert tabulated output to list of dictionaries with given column names as keys ''' return _table_to_dicts(_run_for_table(args), column_list) def _run_for_table(args): '''Run program and convert tabulated output to nested lists''' result = run(args, check=True, stdout=PIPE, encoding=locale.getpreferredencoding()) return _str_to_table(result.stdout) def _str_to_table(string, sep='\t'): '''Convert tabulated multi-line string to nested lists''' return (line.split(sep) for line in string.splitlines()) def _table_to_dicts(table, column_list): '''Convert table to list of dictionaries with given column names as keys''' return (_row_to_dict(row, column_list) for row in table) def _row_to_dict(row, column_list): '''Convert table row to dictionary with given column names as keys''' return {column_list[i]: row[i] for i in range(len(row))}