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