From b2bf11382c641db944666cd25cd8f56c7c8ef518 Mon Sep 17 00:00:00 2001 From: Phil Krylov Date: Wed, 16 Jun 2021 16:12:20 +0300 Subject: [PATCH 1/6] Add --pre-snapshot-cmd and --post-snapshot-cmd options --- zfs_autobackup/ZfsAutobackup.py | 8 +++++++- zfs_autobackup/ZfsNode.py | 10 +++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/zfs_autobackup/ZfsAutobackup.py b/zfs_autobackup/ZfsAutobackup.py index 8c95f45..9e97fb1 100644 --- a/zfs_autobackup/ZfsAutobackup.py +++ b/zfs_autobackup/ZfsAutobackup.py @@ -45,6 +45,10 @@ class ZfsAutobackup: help='Target ZFS filesystem (optional: if not specified, zfs-autobackup will only operate ' 'as snapshot-tool on source)') + parser.add_argument('--pre-snapshot-cmd', metavar="COMMAND", type=str, + help='Run COMMAND before snapshotting.') + parser.add_argument('--post-snapshot-cmd', metavar="COMMAND", type=str, + help='Run COMMAND after snapshotting.') parser.add_argument('--other-snapshots', action='store_true', help='Send over other snapshots as well, not just the ones created by this tool.') parser.add_argument('--no-snapshot', action='store_true', @@ -506,7 +510,9 @@ class ZfsAutobackup: if not self.args.no_snapshot: self.set_title("Snapshotting") source_node.consistent_snapshot(source_datasets, source_node.new_snapshotname(), - min_changed_bytes=self.args.min_change) + min_changed_bytes=self.args.min_change, + pre_snapshot_cmd=self.args.pre_snapshot_cmd, + post_snapshot_cmd=self.args.post_snapshot_cmd) ################# sync # if target is specified, we sync the datasets, otherwise we just thin the source. (e.g. snapshot mode) diff --git a/zfs_autobackup/ZfsNode.py b/zfs_autobackup/ZfsNode.py index da37349..28a7f5b 100644 --- a/zfs_autobackup/ZfsNode.py +++ b/zfs_autobackup/ZfsNode.py @@ -161,7 +161,7 @@ class ZfsNode(ExecuteNode): """determine uniq new snapshotname""" return self.backup_name + "-" + time.strftime("%Y%m%d%H%M%S") - def consistent_snapshot(self, datasets, snapshot_name, min_changed_bytes): + def consistent_snapshot(self, datasets, snapshot_name, min_changed_bytes, pre_snapshot_cmd=None, post_snapshot_cmd=None): """create a consistent (atomic) snapshot of specified datasets, per pool. """ @@ -191,6 +191,10 @@ class ZfsNode(ExecuteNode): self.verbose("No changes anywhere: not creating snapshots.") return + if pre_snapshot_cmd: + self.verbose("Running pre-snapshot-cmd:\n\t{}".format(pre_snapshot_cmd)) + self.run(cmd=pre_snapshot_cmd.split(" "), readonly=False) + # create consistent snapshot per pool for (pool_name, snapshots) in pools.items(): cmd = ["zfs", "snapshot"] @@ -200,6 +204,10 @@ class ZfsNode(ExecuteNode): self.verbose("Creating snapshots {} in pool {}".format(snapshot_name, pool_name)) self.run(cmd, readonly=False) + if post_snapshot_cmd: + self.verbose("Running post-snapshot-cmd:\n\t{}".format(post_snapshot_cmd)) + self.run(cmd=post_snapshot_cmd.split(" "), readonly=False) + def selected_datasets(self, exclude_received, exclude_paths): """determine filesystems that should be backupped by looking at the special autobackup-property, systemwide From ec9459c1d217ec73665103345f6bbf89444d2746 Mon Sep 17 00:00:00 2001 From: Phil Krylov Date: Wed, 16 Jun 2021 19:35:41 +0300 Subject: [PATCH 2/6] Use shlex.split() for --pre-snapshot-cmd and --post-snapshot-cmd --- zfs_autobackup/ZfsNode.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/zfs_autobackup/ZfsNode.py b/zfs_autobackup/ZfsNode.py index 28a7f5b..e25792e 100644 --- a/zfs_autobackup/ZfsNode.py +++ b/zfs_autobackup/ZfsNode.py @@ -1,6 +1,7 @@ # python 2 compatibility from __future__ import print_function import re +import shlex import subprocess import sys import time @@ -193,7 +194,7 @@ class ZfsNode(ExecuteNode): if pre_snapshot_cmd: self.verbose("Running pre-snapshot-cmd:\n\t{}".format(pre_snapshot_cmd)) - self.run(cmd=pre_snapshot_cmd.split(" "), readonly=False) + self.run(cmd=shlex.split(pre_snapshot_cmd), readonly=False) # create consistent snapshot per pool for (pool_name, snapshots) in pools.items(): @@ -206,7 +207,7 @@ class ZfsNode(ExecuteNode): if post_snapshot_cmd: self.verbose("Running post-snapshot-cmd:\n\t{}".format(post_snapshot_cmd)) - self.run(cmd=post_snapshot_cmd.split(" "), readonly=False) + self.run(cmd=shlex.split(post_snapshot_cmd), readonly=False) def selected_datasets(self, exclude_received, exclude_paths): """determine filesystems that should be backupped by looking at the special autobackup-property, systemwide From 55ff14f1d816a32e01fd2656eb3b30c80c24f27b Mon Sep 17 00:00:00 2001 From: Phil Krylov Date: Wed, 16 Jun 2021 23:19:29 +0300 Subject: [PATCH 3/6] Allow multiple --pre/post-snapshot-cmd options. Add a usage example. --- README.md | 22 +++++++++++++++++++++- zfs_autobackup/ZfsAutobackup.py | 8 ++++---- zfs_autobackup/ZfsNode.py | 12 ++++++------ 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 182275c..0def184 100644 --- a/README.md +++ b/README.md @@ -279,6 +279,19 @@ root@ws1:~# zfs-autobackup test --verbose This also allows you to make several snapshots during the day, but only backup the data at night when the server is not busy. +## Running custom commands before and after snapshotting + +If you need, e.g. to quiesce a couple of mysql databases to make on-disk data consistent before snapshotting, try: + +```sh +zfs-autobackup \ + --pre-snapshot-cmd 'daemon -fP /tmp/mysql1_lock.pid jexec mysqljail1 mysql -s -e "set autocommit=0;flush logs;flush tables with read lock;\\! sleep 60"' \ + --pre-snapshot-cmd 'daemon -fP /tmp/mysql2_lock.pid jexec mysqljail2 mysql -s -e "set autocommit=0;flush logs;flush tables with read lock;\\! sleep 60"' \ + --post-snapshot-cmd 'pkill -F /tmp/mysql1_lock.pid' \ + --post-snapshot-cmd 'pkill -F /tmp/mysql2_lock.pid' \ + test +``` + ## Thinning out obsolete snapshots The thinner is the thing that destroys old snapshots on the source and target. @@ -493,7 +506,8 @@ Look in man ssh_config for many more options. ```console usage: zfs-autobackup [-h] [--ssh-config CONFIG-FILE] [--ssh-source USER@HOST] [--ssh-target USER@HOST] [--keep-source SCHEDULE] - [--keep-target SCHEDULE] [--other-snapshots] + [--keep-target SCHEDULE] [--pre-snapshot-cmd COMMAND] + [--post-snapshot-cmd COMMAND] [--other-snapshots] [--no-snapshot] [--no-send] [--no-thinning] [--no-holds] [--min-change BYTES] [--allow-empty] [--ignore-replicated] [--strip-path N] @@ -531,6 +545,12 @@ optional arguments: --keep-target SCHEDULE Thinning schedule for old target snapshots. Default: 10,1d1w,1w1m,1m1y + --pre-snapshot-cmd COMMAND + Run COMMAND before snapshotting (can be used multiple + times. + --post-snapshot-cmd COMMAND + Run COMMAND after snapshotting (can be used multiple + times. --other-snapshots Send over other snapshots as well, not just the ones created by this tool. --no-snapshot Don't create new snapshots (useful for finishing diff --git a/zfs_autobackup/ZfsAutobackup.py b/zfs_autobackup/ZfsAutobackup.py index 9e97fb1..37ae634 100644 --- a/zfs_autobackup/ZfsAutobackup.py +++ b/zfs_autobackup/ZfsAutobackup.py @@ -45,10 +45,10 @@ class ZfsAutobackup: help='Target ZFS filesystem (optional: if not specified, zfs-autobackup will only operate ' 'as snapshot-tool on source)') - parser.add_argument('--pre-snapshot-cmd', metavar="COMMAND", type=str, - help='Run COMMAND before snapshotting.') - parser.add_argument('--post-snapshot-cmd', metavar="COMMAND", type=str, - help='Run COMMAND after snapshotting.') + parser.add_argument('--pre-snapshot-cmd', metavar="COMMAND", default=[], action='append', + help='Run COMMAND before snapshotting (can be used multiple times.') + parser.add_argument('--post-snapshot-cmd', metavar="COMMAND", default=[], action='append', + help='Run COMMAND after snapshotting (can be used multiple times.') parser.add_argument('--other-snapshots', action='store_true', help='Send over other snapshots as well, not just the ones created by this tool.') parser.add_argument('--no-snapshot', action='store_true', diff --git a/zfs_autobackup/ZfsNode.py b/zfs_autobackup/ZfsNode.py index e25792e..08b36d3 100644 --- a/zfs_autobackup/ZfsNode.py +++ b/zfs_autobackup/ZfsNode.py @@ -192,9 +192,9 @@ class ZfsNode(ExecuteNode): self.verbose("No changes anywhere: not creating snapshots.") return - if pre_snapshot_cmd: - self.verbose("Running pre-snapshot-cmd:\n\t{}".format(pre_snapshot_cmd)) - self.run(cmd=shlex.split(pre_snapshot_cmd), readonly=False) + for cmd in pre_snapshot_cmd: + self.verbose("Running pre-snapshot-cmd") + self.run(cmd=shlex.split(cmd), readonly=False) # create consistent snapshot per pool for (pool_name, snapshots) in pools.items(): @@ -205,9 +205,9 @@ class ZfsNode(ExecuteNode): self.verbose("Creating snapshots {} in pool {}".format(snapshot_name, pool_name)) self.run(cmd, readonly=False) - if post_snapshot_cmd: - self.verbose("Running post-snapshot-cmd:\n\t{}".format(post_snapshot_cmd)) - self.run(cmd=shlex.split(post_snapshot_cmd), readonly=False) + for cmd in post_snapshot_cmd: + self.verbose("Running post-snapshot-cmd") + self.run(cmd=shlex.split(cmd), readonly=False) def selected_datasets(self, exclude_received, exclude_paths): """determine filesystems that should be backupped by looking at the special autobackup-property, systemwide From 77f5474447d68f56267fbd5988112122d2d2a384 Mon Sep 17 00:00:00 2001 From: Phil Krylov Date: Wed, 16 Jun 2021 23:37:38 +0300 Subject: [PATCH 4/6] Fix tests. --- zfs_autobackup/ZfsNode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zfs_autobackup/ZfsNode.py b/zfs_autobackup/ZfsNode.py index 08b36d3..51210ad 100644 --- a/zfs_autobackup/ZfsNode.py +++ b/zfs_autobackup/ZfsNode.py @@ -162,7 +162,7 @@ class ZfsNode(ExecuteNode): """determine uniq new snapshotname""" return self.backup_name + "-" + time.strftime("%Y%m%d%H%M%S") - def consistent_snapshot(self, datasets, snapshot_name, min_changed_bytes, pre_snapshot_cmd=None, post_snapshot_cmd=None): + def consistent_snapshot(self, datasets, snapshot_name, min_changed_bytes, pre_snapshot_cmd=[], post_snapshot_cmd=[]): """create a consistent (atomic) snapshot of specified datasets, per pool. """ From 082153e0cee1756945bd8d7505afce08c343c29c Mon Sep 17 00:00:00 2001 From: Phil Krylov Date: Thu, 17 Jun 2021 09:52:18 +0300 Subject: [PATCH 5/6] A more robust MySQL example --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0def184..b4ee22d 100644 --- a/README.md +++ b/README.md @@ -285,10 +285,10 @@ If you need, e.g. to quiesce a couple of mysql databases to make on-disk data co ```sh zfs-autobackup \ - --pre-snapshot-cmd 'daemon -fP /tmp/mysql1_lock.pid jexec mysqljail1 mysql -s -e "set autocommit=0;flush logs;flush tables with read lock;\\! sleep 60"' \ - --pre-snapshot-cmd 'daemon -fP /tmp/mysql2_lock.pid jexec mysqljail2 mysql -s -e "set autocommit=0;flush logs;flush tables with read lock;\\! sleep 60"' \ - --post-snapshot-cmd 'pkill -F /tmp/mysql1_lock.pid' \ - --post-snapshot-cmd 'pkill -F /tmp/mysql2_lock.pid' \ + --pre-snapshot-cmd 'daemon -f jexec mysqljail1 mysql -s -e "set autocommit=0;flush logs;flush tables with read lock;\\! echo \$\$ > /tmp/mysql_lock.pid && sleep 60"' \ + --pre-snapshot-cmd 'daemon -f jexec mysqljail2 mysql -s -e "set autocommit=0;flush logs;flush tables with read lock;\\! echo \$\$ > /tmp/mysql_lock.pid && sleep 60"' \ + --post-snapshot-cmd 'pkill -F /jails/mysqljail1/tmp/mysql_lock.pid' \ + --post-snapshot-cmd 'pkill -F /jails/mysqljail2/tmp/mysql_lock.pid' \ test ``` From 9fc270363866d24e8d58af4c280c2dced31675b1 Mon Sep 17 00:00:00 2001 From: Phil Krylov Date: Thu, 17 Jun 2021 12:38:16 +0300 Subject: [PATCH 6/6] Always call post-snapshot-cmd to clean up faster on snapshot failure --- zfs_autobackup/ZfsNode.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/zfs_autobackup/ZfsNode.py b/zfs_autobackup/ZfsNode.py index 51210ad..31ca4d7 100644 --- a/zfs_autobackup/ZfsNode.py +++ b/zfs_autobackup/ZfsNode.py @@ -192,22 +192,24 @@ class ZfsNode(ExecuteNode): self.verbose("No changes anywhere: not creating snapshots.") return - for cmd in pre_snapshot_cmd: - self.verbose("Running pre-snapshot-cmd") - self.run(cmd=shlex.split(cmd), readonly=False) + try: + for cmd in pre_snapshot_cmd: + self.verbose("Running pre-snapshot-cmd") + self.run(cmd=shlex.split(cmd), readonly=False) - # create consistent snapshot per pool - for (pool_name, snapshots) in pools.items(): - cmd = ["zfs", "snapshot"] + # create consistent snapshot per pool + for (pool_name, snapshots) in pools.items(): + cmd = ["zfs", "snapshot"] - cmd.extend(map(lambda snapshot_: str(snapshot_), snapshots)) + cmd.extend(map(lambda snapshot_: str(snapshot_), snapshots)) - self.verbose("Creating snapshots {} in pool {}".format(snapshot_name, pool_name)) - self.run(cmd, readonly=False) + self.verbose("Creating snapshots {} in pool {}".format(snapshot_name, pool_name)) + self.run(cmd, readonly=False) - for cmd in post_snapshot_cmd: - self.verbose("Running post-snapshot-cmd") - self.run(cmd=shlex.split(cmd), readonly=False) + finally: + for cmd in post_snapshot_cmd: + self.verbose("Running post-snapshot-cmd") + self.run(cmd=shlex.split(cmd), readonly=False, valid_exitcodes=[]) def selected_datasets(self, exclude_received, exclude_paths): """determine filesystems that should be backupped by looking at the special autobackup-property, systemwide