ZFS Automatic Snapshot Daemon
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

129 lines
4.2 KiB

# pylint: skip-file
# type: ignore
from subprocess import run, PIPE
import asyncio
import locale
from time import time
import re
from zasd.config import config
from zasd.filesystem import FilesystemBase
#
# Constants
_DATASET_COLS = ['type', 'name', 'creation', 'mountpoint']
class _ZFSFilesystem(FilesystemBase):
_filesystems = {}
async def snapshots(self):
datasets = (ds for ds in await self._datasets('snapshot') if
_is_managed_name(ds['name']))
@classmethod
async def filesystems(cls):
datasets = (ds for ds in cls._datasets('filesystem') if
ds['name'] in cls._filesystems)
@classmethod
def _datasets(cls, dataset_type):
'''Get list of datasets'''
return _run_for_dicts(
[config['zfs_path'],
'list', '-Hp',
'-t', dataset_type,
'-o', ','.join(_DATASET_COLS)], _DATASET_COLS)
class _ZFSSnapshot():
def initialise(self, props):
name = props.get('name')
if name:
# Split name string into field list
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('ordinal'):
props['ordinal'] = fields[2]
# Assign properties to object
self._dataset = props['dataset']
self._tag = props['tag']
self._ordinal = props.get('ordinal')
def tag(self):
return self._tag
def ordinal(self):
return self._ordinal
async def take(self):
args = [config['zfs']['executable'], 'snapshot', self._zfs_name()]
await asyncio.create_subprocess_exec(*args)
# pylint: disable=attribute-defined-outside-init
self._ordinal = _generate_ordinal()
async def delete(self):
args = [config['zfs']['executable'], 'destroy', self._zfs_name()]
await asyncio.create_subprocess_exec(*args)
def _zfs_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 _ZFSFilesystem.filesystems())
def _is_snapshot_name(name):
'''
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 '@'
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 'name' as a ZFS snapshot name is not checked!
'''
sep = re.escape(config['separator'])
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
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))}