Skip to content

Commit

Permalink
New pyznap fix comand for rename snapshots #16
Browse files Browse the repository at this point in the history
  • Loading branch information
orgoj committed Mar 29, 2021
1 parent 4f6a552 commit 5a993ac
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 3 deletions.
133 changes: 133 additions & 0 deletions pyznap/fix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
"""
pyznap.status
~~~~~~~~~~~~~~
Fix filesystem snapshots.
:copyright: (c) 2021 by Michael Heca.
:license: GPLv3, see LICENSE for more details.
"""

import sys
from pprint import pprint

import logging
import re
from datetime import datetime
from fnmatch import fnmatch
from subprocess import CalledProcessError
from .utils import parse_name
import pyznap.pyzfs as zfs
from .process import DatasetBusyError, DatasetNotFoundError

FORMATS={
'@zfs-auto-snap': 'zfs-auto-snap_(?P<type>[a-z]+)-(?P<year>\d{2,4})-(?P<month>\d{2})-(?P<day>\d{2})-(?P<hour>\d{2})(?P<minute>\d{2})',
'@zfsnap': '(?P<year>\d{2,4})-(?P<month>\d{2})-(?P<day>\d{2})_(?P<hour>\d{2}).(?P<minute>\d{2}).(?P<second>\d{2})--(?P<type>\d+[a-z])',
}

MAPS={
'@zfsnap': {
'4d': 'frequent',
'10d': 'hourly',
'14d': 'hourly',
'2w': 'hourly',
'3w': 'daily',
'5w': 'daily',
'8w': 'daily',
'2m': 'daily',
'90d': 'weekly',
'7m': 'weekly',
'12m': 'monthly',
'18m': 'monthly',
'24m': 'monthly',
'4y': 'yearly',
}
}

def re_get_group(r, group, default=0):
try:
result = r[group]
except IndexError:
result = default
return result

def re_get_group_int(r, group, default=0):
return int(re_get_group(r, group, default=default))

def fix_snapshots(filesystems, format=None, type=None, type_map=None, recurse=False):
"""Fix snapshots name
Parameters:
----------
filesystems : [strings]
Filesystems to fix
"""

logger = logging.getLogger(__name__)

if format.startswith('@'):
if not type_map and format in MAPS:
type_map=MAPS[format]
if format in FORMATS:
format=FORMATS[format]
else:
logger.error('Unknown format {}.'.format(format))
sys.exit(1)

logger.debug('FORMAT: '+str(format))
logger.debug('MAP: '+str(type_map))

rp = re.compile(format)
now = datetime.now()
cur_century = int(now.year/100)*100

# for all specified filesystems
for fsname in filesystems:
logger.info('Checking snapshots on {}...'.format(fsname))
try:
parent = zfs.open(fsname)
except DatasetNotFoundError:
logger.error('Filesystem not exists {}'.format(fsname))
continue

if recurse:
# get all childs filesystem
fstree = zfs.find(fsname, types=['filesystem', 'volume'])
else:
# only scan specified filesystem
fstree = [parent]

for filesystem in fstree:

logger.info('Fixing {}...'.format(filesystem.name))
snapshots = filesystem.snapshots()
for snapshot in snapshots:
snapname = snapshot.snapname()
try:
r=rp.match(snapname)
except:
r=False
if r:
# guess yeaar
year = re_get_group_int(r, 'year', default=now.year)
if year < 100:
year += +cur_century
# get type from snap, with optinal map or default type if specified
snaptype = r.group('type')
if type_map:
if snaptype in type_map:
snaptype = type_map[snaptype]
if not snaptype and type:
snaptype = type
if not snaptype:
logger.error('Unknown snap type {} for snapshot {}'.format(snaptype, snapname))
continue
new_snapname = 'pyznap_'+datetime(year,
re_get_group_int(r, 'month', default=now.month),
re_get_group_int(r, 'day', default=now.day),
hour=re_get_group_int(r, 'hour', default=now.hour),
minute=re_get_group_int(r, 'minute', default=now.minute),
second=re_get_group_int(r, 'second', default=now.second)
).strftime('%Y-%m-%d_%H:%M:%S')+'_'+snaptype
logger.debug('Renaming {} -> {}'.format(snapname, new_snapname))
snapshot.rename(snapshot.fsname()+'@'+new_snapname)
20 changes: 20 additions & 0 deletions pyznap/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from .take import take_config
from .send import send_config
from .status import status_config
from .fix import fix_snapshots
from .process import set_dry_run
import pyznap.pyzfs as zfs
from . import __version__
Expand Down Expand Up @@ -121,6 +122,19 @@ def _main():
dest='retry_interval', default=10,
help='interval in seconds between retries. default is 10')

parser_fix = subparsers.add_parser('fix', help='fix zfs snaphot from other format to pyznap')
parser_fix.add_argument('-t', '--type', action="store",
dest='type', help='snaphot type name')
parser_fix.add_argument('-f', '--format', action="store", required=True,
dest='format', help='snaphot format specification (regexp/@predefined[@zfs-auto-snap,@zfsnap])')
parser_fix.add_argument('-m', '--map', action="store",
dest='map', help='optional type mapping (old=new:...)')
parser_fix.add_argument('-r', '--recurse', action="store_true",
dest='recurse', help='recurse in child filesystems')
# TODO: time shift
parser_fix.add_argument('filesystem', nargs='+', help='filesystems to fix')


subparsers.add_parser('full', help='full cycle: snap --take / send / snap --clean')
subparsers.add_parser('status', help='check filesystem snapshots status')

Expand Down Expand Up @@ -252,6 +266,12 @@ def _main():
else:
send_config(config)

elif args.command == 'fix':
tmap = args.map
if tmap:
tmap = dict(kw.split('=') for kw in args.map.split(':'))
fix_snapshots(args.filesystem, format=args.format, type=args.type, recurse=args.recurse, type_map=tmap)

elif args.command == 'status':
status_config(config)

Expand Down
24 changes: 21 additions & 3 deletions pyznap/pyzfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,9 +306,23 @@ def rollback(self, snapname, force=False):
def promote(self):
raise NotImplementedError()

# TODO: split force to allow -f and -p to be specified individually
def rename(self, name, recursive=False, force=False):
raise NotImplementedError()
def rename(self, name, recursive=False, force=False, create_parents=False):
cmd = ['zfs', 'rename']

if force:
cmd.append('-f')

if create_parents:
cmd.append('-p')

if recursive:
cmd.append('-r')

cmd.append(self.name)
cmd.append(name)

STATS.add('zfs_rename')
check_output_dry(cmd, ssh=self.ssh)

def getprops(self):
return findprops(self.name, ssh=self.ssh, max_depth=0)[self.name]
Expand Down Expand Up @@ -381,6 +395,10 @@ def snapname(self):
snapname = self.name.split('@')[-1]
return snapname

def fsname(self):
fsname = self.name.split('@')[0]
return fsname

def parent(self):
parent_path = self.name.split('@')[0]
return open(name=parent_path, ssh=self.ssh)
Expand Down

0 comments on commit 5a993ac

Please sign in to comment.