* fixed weeding

* added weed_enable option and default it to unset to prevent removing snapshots of unmanaged file systems
* added locking when sending and weeding snapshots
* fixed syslog logging format
This commit is contained in:
Fredrik Eriksson 2017-05-23 20:59:40 +02:00
parent 8617e3c0c6
commit 60095c48d2
3 changed files with 149 additions and 80 deletions

View File

@ -28,6 +28,7 @@ RET_CODES = {
DEFAULT_CONFIG = { DEFAULT_CONFIG = {
'snapshot_interval': None, 'snapshot_interval': None,
'custom_keep_interval': None, 'custom_keep_interval': None,
'weed_enable': False,
'keep_yearly': 0, 'keep_yearly': 0,
'keep_monthly': 0, 'keep_monthly': 0,
'keep_weekly': 0, 'keep_weekly': 0,
@ -87,29 +88,12 @@ def get_config_for_fs(fs, config):
return fs_config return fs_config
def main():
config = configparser.SafeConfigParser() def do_snapshots(fslist, snapshots, config, sudo):
config.read('/etc/zsnapper.ini') failed_snapshots = set()
sudo = False now = datetime.datetime.now()
ret = RET_CODES['SUCCESS']
log = logging.getLogger(LOGGER) log = logging.getLogger(LOGGER)
if os.getuid() != 0:
sudo = True
try:
sudo = config.get('settings', 'sudo')
except (configparser.NoOptionError, configparser.NoSectionError):
pass
fslist = sorted(zsnaplib.get_filesystems(sudo))
snapshots = zsnaplib.get_snapshots(sudo)
now = datetime.datetime.now()
# if we fail to create or send a snapshot we do not want to remove
# the existing snapshots...
failed_snapshots = set()
# First iteration: create snapshots
for fs in fslist: for fs in fslist:
conf = get_config_for_fs(fs, config) conf = get_config_for_fs(fs, config)
if not conf['snapshot_interval']: if not conf['snapshot_interval']:
@ -126,15 +110,14 @@ def main():
log.info('{} snapshot created'.format(fs)) log.info('{} snapshot created'.format(fs))
except zsnaplib.ZFSSnapshotError as e: except zsnaplib.ZFSSnapshotError as e:
log.warning(e) log.warning(e)
ret = RET_CODES['ERROR']
failed_snapshots.add(fs) failed_snapshots.add(fs)
return failed_snapshots
# reload all snapshots so we get our new snapshots here
snapshots = zsnaplib.get_snapshots(sudo)
# Second iteration: Send snapshots def send_snapshots(fslist, snapshots, config, sudo):
failed_snapshots = set()
remote_hosts = {} remote_hosts = {}
remote_targets = {} remote_targets = {}
log = logging.getLogger(LOGGER)
for fs in fslist: for fs in fslist:
conf = get_config_for_fs(fs, config) conf = get_config_for_fs(fs, config)
remote_snapshots = None remote_snapshots = None
@ -194,7 +177,6 @@ def main():
except zsnaplib.ZFSSnapshotError as e: except zsnaplib.ZFSSnapshotError as e:
failed_snapshots.add(fs) failed_snapshots.add(fs)
log.warning(e) log.warning(e)
ret = RET_CODES['ERROR']
continue continue
remote_snapshots[remote_fs] = [base_snap] remote_snapshots[remote_fs] = [base_snap]
@ -206,8 +188,7 @@ def main():
break break
if not last_remote: if not last_remote:
failed_snapshots.add(fs) failed_snapshots.add(fs)
log.warning('No common snapshot local and remote, you need to create a new base copy!') log.warning('{}: No common snapshot local and remote, you need to create a new base copy!'.format(fs))
ret = RET_CODES['ERROR']
continue continue
last_local = snapshots[fs][0] last_local = snapshots[fs][0]
if last_remote == last_local: if last_remote == last_local:
@ -229,8 +210,10 @@ def main():
log.info('{} successfully sent to remote'.format(fs)) log.info('{} successfully sent to remote'.format(fs))
except zsnaplib.ZFSSnapshotError as e: except zsnaplib.ZFSSnapshotError as e:
log.warning(e) log.warning(e)
return failed_snapshots
# Third iteration: weed old snapshots def weed_snapshots(fslist, snapshots, config, sudo, failed_snapshots):
log = logging.getLogger(LOGGER)
for fs in fslist: for fs in fslist:
conf = get_config_for_fs(fs, config) conf = get_config_for_fs(fs, config)
if fs in failed_snapshots: if fs in failed_snapshots:
@ -238,6 +221,8 @@ def main():
continue continue
if fs not in snapshots: if fs not in snapshots:
continue continue
if not conf['weed_enable']:
continue
kwargs = {k: int(v) for k, v in conf.items() if k in [ kwargs = {k: int(v) for k, v in conf.items() if k in [
'keep_custom', 'keep_custom',
@ -261,7 +246,60 @@ def main():
**kwargs) **kwargs)
def main():
config = configparser.SafeConfigParser()
config.read('/etc/zsnapper.ini')
sudo = False
ret = RET_CODES['SUCCESS']
log = logging.getLogger(LOGGER)
if os.getuid() != 0:
sudo = True
fslist = sorted(zsnaplib.get_filesystems(sudo))
snapshots = zsnaplib.get_snapshots(sudo)
failed_snapshots = do_snapshots(fslist, snapshots, config, sudo)
if failed_snapshots:
ret = RET_CODES['ERROR']
lockfile = '/tmp/zsnapper.pid'
# This loop should run at most twice
while True:
try:
lockfd = os.open(lockfile, os.O_CREAT|os.O_EXCL|os.O_WRONLY, mode=0o640)
os.write(lockfd, "{}".format(os.getpid()).encode('utf-8'))
os.close(lockfd)
break
except OSError:
pass
# lock file exists, check if the pid seems valid
with open(lockfile, 'r') as f:
pid = f.read()
try:
pid = int(pid)
os.kill(pid, 0)
# If we got here the lock is owned by an existing pid
log.info('Previous run is not completed yet, will not send or weed snapshots')
return ret
except OSError:
# pid is not running, forcing unlock
os.remove(lockfile)
except ValueError:
log.error('lockfile {} exists but does not seem to contain a pid. Will not continue'.format(lockfile))
return RET_CODES['FAILED']
# reload all snapshots so we get our new snapshots here
snapshots = zsnaplib.get_snapshots(sudo)
failed_send = send_snapshots(fslist, snapshots, config, sudo)
if failed_send:
ret = RET_CODES['ERROR']
failed_snapshots.update(failed_send)
weed_snapshots(fslist, snapshots, config, sudo, failed_snapshots)
os.remove(lockfile)
if __name__ == '__main__': if __name__ == '__main__':
log = logging.getLogger(LOGGER) log = logging.getLogger(LOGGER)
@ -269,7 +307,10 @@ if __name__ == '__main__':
handler = logging.StreamHandler() handler = logging.StreamHandler()
handler.setLevel(logging.WARNING) handler.setLevel(logging.WARNING)
log.addHandler(handler) log.addHandler(handler)
handler = logging.handlers.SysLogHandler(address='/dev/log') handler = logging.handlers.SysLogHandler(address='/dev/log')
formatter = logging.Formatter(fmt='zsnapper %(message)s')
handler.setFormatter(formatter)
handler.setLevel(logging.INFO) handler.setLevel(logging.INFO)
log.addHandler(handler) log.addHandler(handler)
sys.exit(main()) sys.exit(main())

View File

@ -212,69 +212,69 @@ def weed_snapshots(
keep['custom'].append(date) keep['custom'].append(date)
if keep_yearly: if keep_yearly:
saved['year'] = saved['year'][-keep_yearly:] keep['year'] = keep['year'][-keep_yearly:]
else: else:
saved['year'] = [] keep['year'] = []
if keep_monthly: if keep_monthly:
saved['month'] = saved['month'][-keep_monthly:] keep['month'] = keep['month'][-keep_monthly:]
else: else:
saved['month'] = [] keep['month'] = []
if keep_weekly: if keep_weekly:
saved['week'] = saved['week'][-keep_weekly:] keep['week'] = keep['week'][-keep_weekly:]
else: else:
saved['week'] = [] keep['week'] = []
if keep_daily: if keep_daily:
saved['day'] = saved['day'][-keep_daily:] keep['day'] = keep['day'][-keep_daily:]
else: else:
saved['day'] = [] keep['day'] = []
if keep_hourly: if keep_hourly:
saved['hour'] = saved['hour'][-keep_hourly:] keep['hour'] = keep['hour'][-keep_hourly:]
else: else:
saved['hour'] = [] keep['hour'] = []
if keep_30min: if keep_30min:
saved['min30'] = saved['min30'][-keep_30min:] keep['min30'] = keep['min30'][-keep_30min:]
else: else:
saved['min30'] = [] keep['min30'] = []
if keep_15min: if keep_15min:
saved['min15'] = saved['min15'][-keep_15min:] keep['min15'] = keep['min15'][-keep_15min:]
else: else:
saved['min15'] = [] keep['min15'] = []
if keep_5min: if keep_5min:
saved['min5'] = saved['min5'][-keep_5min:] keep['min5'] = keep['min5'][-keep_5min:]
else: else:
saved['min5'] = [] keep['min5'] = []
if keep_1min: if keep_1min:
saved['min1'] = saved['min1'][-keep_1min:] keep['min1'] = keep['min1'][-keep_1min:]
else: else:
saved['min1'] = [] keep['min1'] = []
if keep_custom: if keep_custom:
saved['custom'] = saved['custom'][-keep_custom:] keep['custom'] = keep['custom'][-keep_custom:]
else: else:
saved['custom'] = [] keep['custom'] = []
all_saved = [] all_keep = []
all_saved.extend(saved['year']) all_keep.extend(keep['year'])
all_saved.extend(saved['month']) all_keep.extend(keep['month'])
all_saved.extend(saved['week']) all_keep.extend(keep['week'])
all_saved.extend(saved['day']) all_keep.extend(keep['day'])
all_saved.extend(saved['hour']) all_keep.extend(keep['hour'])
all_saved.extend(saved['min30']) all_keep.extend(keep['min30'])
all_saved.extend(saved['min15']) all_keep.extend(keep['min15'])
all_saved.extend(saved['min5']) all_keep.extend(keep['min5'])
all_saved.extend(saved['min1']) all_keep.extend(keep['min1'])
all_saved.extend(saved['custom']) all_keep.extend(keep['custom'])
all_saved = set(all_saved) all_keep = set(all_keep)
to_remove = [date for date in dates if date not in all_saved] to_remove = [date for date in dates if date not in all_keep]
for date in to_remove: for date in to_remove:
try: try:
log.info('{}: removing snapshot from {}'.format(fs, date)) log.info('{}: removing snapshot from {}'.format(fs, date))

View File

@ -1,33 +1,60 @@
[settings] # zsnapper sample configuration
# NOTE:
# --stdin is used to pass password to sudo, this does not work the first time # Each section is the name of a ZFS file system
# a user uses sudo, so make sure to run a sudo command manually first # All settings are applied recursively to all file system descendants
# #
# or preferably, leave this commented out and use NOPASSWD for sudo...
;sudo=<password>
[tank] [tank]
# frequency of snapshots # Frequency of snapshots
# Set to empty value to disable snapshoting
#
# Interval units are 'd', 'h' and 'm' for days, hours and minutes.
# they can also be combined if you for example wants a snapshot taken every 1 and a half days:
;snapshot_interval=1d 12h
snapshot_interval=1h snapshot_interval=1h
# Remote replication # Remote replication
# possible other value is 'latest' to only sync the last available snapshot # possible other value is 'latest' to only sync the latest snapshot
# Set to empty value to not send the snapshots to remote
remote_enable=all remote_enable=all
# The remote_zfs_cmd option is the command to use to execute zfs on target machine.
# remote_test_cmd, if set, is executed before trying to send any snapshot to remote.
# If remote_test_cmd returns a non-zero status the remote is considered to be unavailable
# and no snapshots are sent. (A warning is written in the log though)
#
# NOTE: # NOTE:
# The command arguments must not contain whitespace characters, since # The command arguments must not contain whitespace characters, due to implementation details.
# split() is used to create an array to subprocess.Popen() #
# Variables can be used in remote_zfs_cmd and remote_test_cmd. Any setting
# available in the section can be used as a variable
remote_zfs_cmd=/usr/bin/ssh ${remote_user}@${remote_host} /usr/bin/sudo /sbin/zfs remote_zfs_cmd=/usr/bin/ssh ${remote_user}@${remote_host} /usr/bin/sudo /sbin/zfs
remote_test_cmd=/usr/bin/ssh ${remote_user}@${remote_host} echo "success" remote_test_cmd=/usr/bin/ssh ${remote_user}@${remote_host} echo "success"
# The remote_host option is optional but recommended if you send snapshots to a remote host.
remote_host=my.backup.server.tld
# remote_user is not a actually a zsnapper option; but it's used as a variable in the remote commands.
remote_user=backup remote_user=backup
remote_host=hem.winterbird.org
remote_zfs_target=tank/backup/asuna/tank # remote_zfs_target is the file system on the remote client that should receive zfs sends
# for this file system.
# NOTE:
# Just like any other option this is inherited by file system descendants,
# but if a child has the same remote_zfs_target as the parent, the child
# will instead use this to figure out where the parent is and be sent to
# it position relative to the parent.
# For example: The local file system tank/ROOT will be sent to tank/backup/client/ROOT.
remote_zfs_target=tank/backup/client
# These can be set to use custom arguments to zfs send and zfs receive # These can be set to use custom arguments to zfs send and zfs receive
; remote_send_flags=-D -p remote_send_flags=-D -p
remote_send_flags=
remote_recv_flags= remote_recv_flags=
# snapshot weeding
# set weed_enable to an empty value to disable snapshot weeding.
# NOTE:
# If weeding is enabled but no keep_<time> setting is configured all
# your snapshots, except the latest, will be removed. Make sure to
# configure your weeding settings carefully.
weed_enable=1
keep_hourly=24 keep_hourly=24
keep_daily=7 keep_daily=7
keep_weekly=4 keep_weekly=4
@ -39,7 +66,8 @@ snapshot_interval=
remote_enable= remote_enable=
[tank/media] [tank/media]
snapshot_interval=15m snapshot_interval=
remote_enable=
[tank/tmp] [tank/tmp]
snapshot_interval= snapshot_interval=