diff --git a/config.toml b/config.toml new file mode 100644 index 0000000000000000000000000000000000000000..dccbb41f585423406c6f688279c907b6c86f3b0f --- /dev/null +++ b/config.toml @@ -0,0 +1,16 @@ + +[[volume]] + volume_group = "exports" + name = "pub" + + [[volume.keep]] + number = 12 + interval = "1H" + + [[volume.keep]] + number = 30 + interval = "1d" + + [[volume.keep]] + number = 6 + interval = "1m" diff --git a/lvmsnapshot.py b/lvmsnapshot.py index 6003177f660e112d6813ee0ea870faaf9b0e0288..9927dbfee24b2d4f5ef6a194bd29ef864b80a311 100644 --- a/lvmsnapshot.py +++ b/lvmsnapshot.py @@ -47,59 +47,72 @@ def freeze_xfs(mountpoint, freeze): ] sp.run(command, check=True) -class Snapshot: - def __init__(self, volume_group, parent_volume, timestamp): +class Volume: + def __init__(self, volume_group, name): self.volume_group = volume_group + self.name = name + + def get_full_name(self): + return "{}/{}".format(self.volume_group, self.name) + + def get_mapper_device(self): + return "/dev/mapper/{}-{}".format(self.volume_group, self.name) + + def get_mountpoint(self): + mapper_device = self.get_mapper_device() + command = [ + "/bin/findmnt", + "--source", mapper_device, + "--json" + ] + result = sp.check_output(command) + data = json.loads(result) + filesystems = data["filesystems"] + if len(filesystems) < 1: + raise Exception("No parent device {} found!".format(mapper_device)) + return filesystems[0]["target"] + + +class Snapshot: + def __init__(self, parent_volume, timestamp, active=False): self.parent_volume = parent_volume self.timestamp = timestamp - - def get_full_parent_volume(self): - return "{}/{}".format(self.volume_group, self.volume) + self.active = active def get_timestamp_str(self): return self.timestamp.strftime(TIMESTAMP_FORMAT) def get_name(self): return "{}-snapshot-{}".format( - self.parent_volume, + self.parent_volume.name, self.get_timestamp_str()) + @staticmethod + def parse_name(name): + parent_name, _, timestamp_str = name.split("-", 2) + timestamp = datetime.strptime(timestamp_str, TIMESTAMP_FORMAT) + return parent_name, timestamp + def get_full_volume(self): return "{}/{}".format(self.volume_group, self.get_name()) def get_mountpoint(self): return os.path.join([ - SNAPSHOT_BASE_DIR, self.parent_volume, self.get_timestamp_str() + SNAPSHOT_BASE_DIR, self.parent_volume.name, self.get_timestamp_str() ]) - def get_parent_mapper_device(self): - return "/dev/mapper/{}-{}".format(self.volume_group, self.parent_volume) - def get_mapper_device(self): return "/dev/mapper/{}-{}".format(self.volume_group, self.get_name()) - def get_parent_mountpoint(self): - command = [ - "/bin/findmnt", - "--source", self.get_parent_mapper_device(), - "--json" - ] - result = sp.check_output(command) - data = json.loads(result) - filesystems = data["filesystems"] - if len(filesystems) < 1: - raise Exception("No parent device {} found!".format(self.get_parent_mapper_device())) - return filesystems[0]["target"] - def create(self): - parent_mountpoint = self.get_parent_mountpoint() + parent_mountpoint = self.parent_volume.get_mountpoint() with xfs_freeze(mountpoint): create_command = [ "/sbin/lvcreate", "--snapshot", "--name", self.get_name(), "--permission", "r", - self.get_full_parent_volume() + self.parent_volume.get_full_name() ] sp.run(create_command, check=True) self.mount() @@ -112,6 +125,7 @@ class Snapshot: self.get_full_volume() ] sp.run(activate_command, check=True) + self.active = True mount_command = [ "/bin/mount", self.get_mapper_device(), @@ -141,4 +155,60 @@ class Snapshot: self.get_full_volume() ] sp.run(deactivate_command, check=True) - + self.active = False + + @staticmethod + def list_snapshots(): + list_command = [ + "/sbin/lvs", + "-o", "vg_name,lv_name,lv_active,origin,lv_role", + "--reportformat", "json" + ] + result = sp.check_output(list_command) + data = json.loads(result) + raw_volumes = data["report"][0]["lv"] + parent_name_map = {} + snapshots = {} + raw_snapshots = [] + for raw_volume in raw_volumes: + volume_group = raw_volume["vg_name"] + volume_name = raw_volume["lv_name"] + active = raw_volume["lv_active"] == "active" + origin = raw_volume["origin"] or None + roles = raw_volume["lv_role"].split(",") + if "snapshot" in roles: + raw_snapshots.append((volume_group, volume_name, active, origin, roles)) + elif "public" in roles: + volume = Volume(volume_group, volume_name) + snapshots[volume] = [] + parent_name_map[volume.name] = volume + else: + print("Ignoring volume {}/{}".format(volume_group, volume_name)) # todo: remove this output + for (volume_group, volume_name, active, origin, roles) in raw_snapshots: + parent_name, timestamp = Snapshot.parse_name(volume_name) + if parent_name != origin: + raise Exception("Parent volume name not matching: '{}' != '{}'".format(parent_name, origin)) + parent = parent_name_map[parent_name] + snapshot = Snapshot(parent, timestamp, active) + snapshots[parent].append(snapshot) + return snapshots + +def load_config(): + import sys + import toml + config_path = "config.toml" + ENV_VAR = "LVM_SNAPSHOT_CONFIG" + 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) + +def main(): + config = load_config() + print(config) + snapshots = Snapshot.list_snapshots() + +if __name__ == "__main__": + main()