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.
 
 

142 lines
4.6 KiB

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))}