diff --git a/zfs-backup.py b/zfs-backup.py index 97efb06..84057ef 100755 --- a/zfs-backup.py +++ b/zfs-backup.py @@ -9,7 +9,7 @@ from pathlib import Path from re import search from subprocess import PIPE, Popen, call, check_output from sys import exit -from typing import Optional +from typing import Optional # TODO replace with pipe notation from uuid import UUID import config @@ -42,6 +42,10 @@ def main(): manager.backup() +# TODO tests +# TODO docstrings +# TODO first backup + class Manager: def __init__(self, local_pool: Pool, external_pool: ExternalPool): self._local_pool = local_pool @@ -92,6 +96,7 @@ class Manager: ] @staticmethod + # TODO make function def _get_regex_matching_backup_tags(): return "@" + ".*".join([config.snapshot_tag, config.snapshot_interval]) @@ -230,6 +235,8 @@ class ZFS: @staticmethod def destroy(full_name, *, recursive=False): + # TODO condition must be "begins with" + # or better argument should be a ZFSPath if config.local_pool_to_backup in full_name: raise Exception(DELETE_IN_LOCAL_POOL) cmdline = [config.ZFS, "destroy"] @@ -260,8 +267,10 @@ class ZFS: @staticmethod def scrub(pool: str, *, wait_for_finish=True) -> None: + # TODO: check if scrub already in progress run_command([config.ZPOOL, "scrub", pool]) if wait_for_finish: + # TODO make Ctrl-c interrupt this run_command([config.ZPOOL, "wait", "-t", "scrub", pool]) @@ -285,6 +294,7 @@ class Disk: for item in MAPPER_PATH.iterdir(): if self._mapper_entry == item.name: print(MAPPER_ENTRY_ALREADY_EXISTS, self._mapper_entry) + # TODO exit code constants exit(1) @property @@ -298,6 +308,7 @@ class Disk: def encrypt(self): if not self._decrypted: raise ValueError(NOT_DECRYPTED, self._name) + # TODO use run instead? get_output_of_command([config.CRYPTSETUP, "close", self._name]) self._decrypted = False @@ -330,14 +341,14 @@ class ExternalPool(Pool): if self._imported: raise ValueError(ALREADY_IMPORTED, self.name) self._disk.decrypt() - self._import_from_directory(MAPPER_PATH) + self._import_from_directory(MAPPER_PATH) # TODO inline? self._imported = True def export(self) -> None: if not self._imported: raise ValueError(NOT_IMPORTED, self.name) self._export_pool() - self._disk.encrypt() + self._disk.encrypt() # TODO inline? self._imported = False print(f"Exported {self.name}. Disk {self._disk.uuid} can be removed.") @@ -345,6 +356,7 @@ class ExternalPool(Pool): ZFS.import_pool_from_directory(self._name, directory) def _export_pool(self) -> None: + # TODO cancel scrub if necessary ZFS.export_pool(self._name) def scrub(self) -> None: @@ -371,6 +383,7 @@ class ExternalPool(Pool): # return [d for d in self.datasets if d.qualified_name not in do_not_backup_correct_name] def clean_old_snapshots(self): + print("Cleaning old snapshots") for dataset in self.datasets: dataset.clean_old_snapshots() @@ -410,10 +423,12 @@ class Dataset: self._clean_old_snapshots_for_interval(interval, number) def _clean_old_snapshots_for_interval(self, interval: str, max_count: int): + # TODO this is somewhat similar to the regex method in Manager regex = ".*".join(["", config.snapshot_tag, interval, ""]) snapshots = [ snapshot for snapshot in self.snapshots if snapshot.matches_regex(regex) ] + # TODO function _get_n_oldest_snapshots if len(snapshots) > max_count: snapshots.sort() for snap in snapshots[:-max_count]: @@ -453,6 +468,7 @@ class Snapshot: return Dataset(self._path.dataset) +# TODO class CommandRunner def run_command(cmdline: list[str]): call(cmdline)