* 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:
parent
8617e3c0c6
commit
60095c48d2
101
bin/zsnapper
101
bin/zsnapper
@ -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())
|
||||||
|
@ -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))
|
||||||
|
@ -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=
|
||||||
|
Loading…
Reference in New Issue
Block a user