diff --git a/lvmsnapshot.py b/lvmsnapshot.py index 345fb8e4b9993d149c6419ebee1260ee4e884147..82f70f58589716293e299c15ed73009df44d919d 100644 --- a/lvmsnapshot.py +++ b/lvmsnapshot.py @@ -42,9 +42,9 @@ PERIOD_KEYS = OrderedDict([ @contextmanager def xfs_freeze(mountpoint): - freeze_xfs(True) + #freeze_xfs(mountpoint, True) yield - freeze_xfs(False) + #freeze_xfs(mountpoint, False) def freeze_xfs(mountpoint, freeze): command = [ @@ -113,19 +113,24 @@ class Snapshot: return parent_name, timestamp def get_full_volume(self): - return "{}/{}".format(self.volume_group, self.get_name()) + return "{}/{}".format(self.parent_volume.volume_group, self.get_name()) def get_mountpoint(self): - return os.path.join([ + return os.path.join( SNAPSHOT_BASE_DIR, self.parent_volume.name, self.get_timestamp_str() - ]) + ) + + def create_mountpoint(self): + os.makedirs(self.get_mountpoint(), mode=0o755, exist_ok=True) def get_mapper_device(self): - return "/dev/mapper/{}-{}".format(self.volume_group, self.get_name()) + return "/dev/mapper/{}-{}".format( + self.parent_volume.volume_group, + self.get_name().replace("-", "--")) def create(self): parent_mountpoint = self.parent_volume.get_mountpoint() - with xfs_freeze(mountpoint): + with xfs_freeze(parent_mountpoint): create_command = [ "/sbin/lvcreate", "--snapshot", @@ -145,11 +150,12 @@ class Snapshot: ] sp.run(activate_command, check=True) self.active = True + self.create_mountpoint() mount_command = [ "/bin/mount", self.get_mapper_device(), self.get_mountpoint(), - "-onouuid,ro" # nouuid is necessary for xfs snapshots + "-onouuid,ro,norecovery" # nouuid is necessary for xfs snapshots ] sp.run(mount_command, check=True) @@ -175,6 +181,7 @@ class Snapshot: ] sp.run(deactivate_command, check=True) self.active = False + os.rmdir(self.get_mountpoint()) @staticmethod def list_snapshots(): @@ -267,6 +274,7 @@ def load_config(): def parse_config(config): periods = {} + min_interval = None for volume_conf in config["volume"]: name = volume_conf["name"] volume_group = volume_conf["volume_group"] @@ -274,36 +282,48 @@ def parse_config(config): periods[volume] = [] for raw_period in volume_conf["keep"]: interval = Period.parse_interval(raw_period["interval"]) + if min_interval is None or interval < min_interval: + min_interval = interval number = int(raw_period["number"]) periods[volume].append( Period(target_number=number, interval=interval)) - return periods + return periods, min_interval -def mark_snapshots(snapshots, periods): +def mark_snapshots(snapshots, periods, min_interval): all_snapshots = set(snapshots) marked_snapshots = set() budgets = {period: period.target_number for period in periods} + last_snapshot = {period: None for period in periods} for snapshot in sorted(snapshots, key=lambda s: s.timestamp): - for period in sorted(periods, key=Period.interval): + for period in sorted(periods, key=lambda p: p.interval): + required_distance = period.interval - min_interval/2 if (budgets[period] > 0 and period.get_start() < snapshot.timestamp): - marked_snapshots.add(snapshot) - budgets[period] -= 1 + if (last_snapshot[period] is None + or abs(snapshot.timestamp - last_snapshot[period].timestamp) > required_distance): + marked_snapshots.add(snapshot) + last_snapshot[period] = snapshot + budgets[period] -= 1 unmarked_snapshots = all_snapshots - marked_snapshots return unmarked_snapshots def main(): config = load_config() snapshots = Snapshot.list_snapshots() - periods = parse_config(config) + 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)) for volume in set(snapshots.keys()) - set(periods.keys()): print("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]) - print(unmarked_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() + if __name__ == "__main__": main()