diff --git a/lvmsnapshot.py b/lvmsnapshot.py index 088b34fac19780a2546d7474b8e738304bc8d7f3..74d3dc941ae8a42bd4d2cb73ebb4609cf65a58e5 100644 --- a/lvmsnapshot.py +++ b/lvmsnapshot.py @@ -22,6 +22,7 @@ # mark snapshots to keep, from oldest within period to new # delete unmarked snapshots +import logging import os import re from datetime import datetime, timedelta @@ -40,6 +41,14 @@ PERIOD_KEYS = OrderedDict([ ("M", 60), ]) +def run_process(command, check=True): + result = sp.run(command, check=check, stdout=sp.PIPE, stderr=sp.PIPE) + if result.stdout: + logging.info(result.stdout.decode("utf-8").strip()) + if result.stderr: + logging.warn(result.stderr.decode("utf-8").strip()) + return result + @contextmanager def xfs_freeze(mountpoint): freeze_xfs(mountpoint, True) @@ -52,7 +61,7 @@ def freeze_xfs(mountpoint, freeze): "-f" if freeze else "-u", mountpoint ] - sp.run(command, check=True) + run_process(command, check=True) class Volume: def __init__(self, volume_group, name): @@ -149,17 +158,19 @@ class Snapshot: "--permission", "r", self.parent_volume.get_full_name() ] - sp.run(create_command, check=True) + run_process(create_command, check=True) self.mount() def mount(self): + if self.check_mount(): + return activate_command = [ "/sbin/lvchange", "--activate", "y", "--ignoreactivationskip", self.get_full_volume() ] - sp.run(activate_command, check=True) + run_process(activate_command, check=True) self.active = True self.create_mountpoint() mount_command = [ @@ -168,7 +179,7 @@ class Snapshot: self.get_mountpoint(), "-o", self.get_mount_options() ] - sp.run(mount_command, check=True) + run_process(mount_command, check=True) def check_mount(self): check_command = [ @@ -176,7 +187,7 @@ class Snapshot: "--source", self.get_mapper_device() ] try: - sp.check_call(check_command) + sp.check_call(check_command, stdout=sp.DEVNULL, stderr=sp.DEVNULL) return True except sp.CalledProcessError: return False @@ -187,7 +198,7 @@ class Snapshot: "/sbin/lvremove", self.get_full_volume() ] - sp.run(remove_command, check=True) + run_process(remove_command, check=True) def unmount(self): if self.check_mount(): @@ -195,14 +206,14 @@ class Snapshot: "/bin/umount", self.get_mountpoint() ] - sp.run(unmount_command, check=True) + run_process(unmount_command, check=True) deactivate_command = [ "/sbin/lvchange", "--activate", "n", "--ignoreactivationskip", self.get_full_volume() ] - sp.run(deactivate_command, check=True) + run_process(deactivate_command, check=True) self.active = False os.rmdir(self.get_mountpoint()) @@ -287,11 +298,12 @@ def load_config(): import sys import toml config_path = "config.toml" + global_config_path = "/etc/lvm-snapshot.toml" ENV_VAR = "LVM_SNAPSHOT_CONFIG" + if not os.path.isfile(config_path) and os.path.isfile(global_config_path): + config_path = global_config_path if ENV_VAR in os.environ: config_path = os.environ[ENV_VAR] - if len(sys.argv) > 1: - config_path = sys.argv[1] with open(config_path, "r") as config_file: return toml.load(config_file) @@ -330,23 +342,59 @@ def mark_snapshots(snapshots, periods, min_interval): unmarked_snapshots = all_snapshots - marked_snapshots return unmarked_snapshots -def main(): +def list_snapshots(**kwargs): + snapshots = Snapshot.list_snapshots() + for volume in snapshots: + print("volume: {}".format(str(volume))) + for snapshot in snapshots[volume]: + print(" {}".format(str(snapshot))) + +def mount_snapshots(**kwargs): + snapshots = Snapshot.list_snapshots() + for volume in snapshots: + for snapshot in snapshots[volume]: + snapshot.mount() + +def unmount_snapshots(**kwargs): + snapshots = Snapshot.list_snapshots() + for volume in snapshots: + for snapshot in snapshots[volume]: + snapshot.unmount() + +def update_snapshots(): config = load_config() snapshots = Snapshot.list_snapshots() periods, min_interval = parse_config(config) for volume in set(periods.keys()) - set(snapshots.keys()): - print("Warning: Volume {} is configured but does not exist or has no snapshots.".format(volume)) + logging.warn("Warning: Volume {} is configured but does not exist or has no snapshots.".format(volume)) for volume in set(snapshots.keys()) - set(periods.keys()): - print("Warning: Volume {} does exist but is not configured.".format(volume)) + logging.warn("Warning: Volume {} does exist but is not configured.".format(volume)) snapshots.pop(volume) for volume in snapshots: unmarked_snapshots = mark_snapshots(snapshots[volume], periods[volume], min_interval) - print("removing", unmarked_snapshots) for snapshot in unmarked_snapshots: snapshot.remove() new_snapshot = Snapshot(volume, datetime.now()) new_snapshot.create() +operations = { + "list": list_snapshots, + "update": update_snapshots, + "mount": mount_snapshots, + "unmount": unmount_snapshots, +} +def main(): + import argparse + parser = argparse.ArgumentParser() + parser.add_argument("command", help="list|update|mount|unmount") + parser.add_argument("--verbose", action="store_true", help="do not redirect the command output to /dev/null") + args = parser.parse_args() + loglevel = logging.ERROR + if args.verbose: + loglevel = logging.INFO + logging.basicConfig(level=loglevel) + operations[args.command]() + if __name__ == "__main__": main()