From 6cd61f8709b0cf45e134f6c79d7b67ea066f522a Mon Sep 17 00:00:00 2001 From: timeshifter Date: Sat, 4 Dec 2021 14:43:56 +0100 Subject: [PATCH] add: CommandInterface --- zfs-backup.py | 97 ++++++++++++++++++++++++++++----------------------- 1 file changed, 54 insertions(+), 43 deletions(-) diff --git a/zfs-backup.py b/zfs-backup.py index 96fbacf..285878f 100755 --- a/zfs-backup.py +++ b/zfs-backup.py @@ -197,8 +197,28 @@ def _assert_type(instance: Any, object_type: Any, message: str) -> None: raise TypeError(message) -class ZPOOL: - _ZPOOL = Path("/usr/bin/zpool") +class CommandInterface: + _BINARY: Path + + @classmethod + def _run_command(cls, *args: str) -> None: + CommandRunner.run([str(cls._BINARY), *args]) + + @classmethod + def _get_output(cls, *args: str) -> str: + return CommandRunner.get_output([str(cls._BINARY), *args]) + + @classmethod + def _open_stream(cls, *args: str) -> Popen: + return CommandRunner.send_stream([str(cls._BINARY), *args]) + + @classmethod + def _receive_stream(cls, *args: str, stream: Popen) -> None: + CommandRunner.receive_from_stream([str(cls._BINARY), *args], stream=stream) + + +class ZPool(CommandInterface): + _BINARY = Path("/usr/bin/zpool") class _Subcommands: # -N: no mount @@ -212,14 +232,12 @@ class ZPOOL: @classmethod def import_from_directory(cls, pool: Pool, directory: Path) -> None: _assert_type(pool, Pool, "Can only import Pool objects") - CommandRunner.run( - [str(cls._ZPOOL), *cls._Subcommands.import_, directory, pool.name] - ) + cls._run_command(*cls._Subcommands.import_, str(directory), pool.name) @classmethod def export(cls, pool: Pool) -> None: _assert_type(pool, Pool, "Can only export Pool objects") - CommandRunner.run([str(cls._ZPOOL), *cls._Subcommands.export, pool.name]) + cls._run_command(*cls._Subcommands.export, pool.name) @classmethod def scrub( @@ -232,14 +250,14 @@ class ZPOOL: _assert_type(pool, Pool, "Can only scrub pools.") cls._scrub_if_necessary(pool, skip_if_recently_scrubbed) if wait_for_finish: - CommandRunner.run([str(cls._ZPOOL), *cls._Subcommands.wait, pool.name]) + cls._run_command(*cls._Subcommands.wait, pool.name) if not cls._healthy(pool): raise IOError(f"Pool {pool.name} is not healthy.") @classmethod def _scrub_if_necessary(cls, pool: Pool, skip_if_recently_scrubbed: bool) -> None: def do_scrub(): - CommandRunner.run([str(cls._ZPOOL), *cls._Subcommands.scrub, pool.name]) + cls._run_command(*cls._Subcommands.scrub, pool.name) if cls._scrub_in_progress(pool): return @@ -263,20 +281,21 @@ class ZPOOL: @classmethod def _get_pool_status_output(cls, pool: Pool) -> str: - return CommandRunner.get_output([ZFS, *cls._Subcommands.status, pool.name]) + return cls._get_output(*cls._Subcommands.status, pool.name) @classmethod def _last_scrub(cls, pool: Pool) -> datetime: return Time.get_last_scrub_from_status_output(cls._get_pool_status_output(pool)) -class ZFS: +class ZFS(CommandInterface): + """Interface of `zfs` commands""" class _Subcommands: class List: # -H Scripting mode, omit headers - # -d1 maximum of 1 + # -d1 maximum depth of 1 # -o name output column # -t object type # -r recursive @@ -293,13 +312,12 @@ class ZFS: # -u File system that is associated with the received stream is not mounted. receive = ["receive", "-d", "-F", "-u"] - _ZFS = Path("/usr/bin/zfs") + _BINARY = Path("/usr/bin/zfs") @classmethod def get_first_level_datasets(cls, pool: Pool) -> list[Dataset]: _assert_type(pool, Pool, "Can only get datasets from Pool objects") - command = [str(cls._ZFS), *cls._Subcommands.List.dataset, pool.name] - datasets_str = CommandRunner.get_output(command) + datasets_str = cls._get_output(*cls._Subcommands.List.dataset, pool.name) # we omit the first return value, which is the pool, not the dataset datasets_list = datasets_str.split("\n")[1:] return [Dataset(ZFSPath.from_string(d)) for d in datasets_list] @@ -307,12 +325,8 @@ class ZFS: @classmethod def get_snapshots(cls, dataset: Dataset) -> list[Snapshot]: _assert_type(dataset, Dataset, "Can only get snapshots from Dataset object") - command = [ - str(cls._ZFS), - *cls._Subcommands.List.snapshot, - dataset.qualified_name, - ] - snapshot_names: list[str] = CommandRunner.get_output(command).split() + command = [*cls._Subcommands.List.snapshot, dataset.qualified_name] + snapshot_names: list[str] = cls._get_output(*command).split("\n") return [Snapshot(ZFSPath.from_string(name)) for name in snapshot_names] @classmethod @@ -320,8 +334,7 @@ class ZFS: _assert_type(snapshot, Snapshot, "Can only destroy snapshots.") if config.local_pool_to_backup == snapshot.pool: raise Exception(DELETE_IN_LOCAL_POOL) - command = [str(cls._ZFS), *cls._Subcommands.destroy, str(snapshot)] - CommandRunner.run(command) + cls._run_command(*cls._Subcommands.destroy, str(snapshot)) @classmethod def send_incremental( @@ -329,13 +342,10 @@ class ZFS: ) -> Popen[str]: cls._pre_send_sanity_checks(old_snapshot, new_snapshot) print(f" {old_snapshot} -> {new_snapshot}") - return CommandRunner.send_stream( - [ - str(cls._ZFS), - *cls._Subcommands.send, - str(old_snapshot), - str(new_snapshot), - ] + return cls._open_stream( + *cls._Subcommands.send, + str(old_snapshot), + str(new_snapshot), ) @classmethod @@ -356,8 +366,7 @@ class ZFS: @classmethod def receive(cls, stream: Popen[str], pool: Pool): _assert_type(pool, Pool, "Can only receive into pools.") - command = [str(cls._ZFS), *cls._Subcommands.receive, pool.name] - CommandRunner.receive_from_stream(command, stream) + cls._receive_stream(*cls._Subcommands.receive, pool.name, stream=stream) class Time: @@ -423,33 +432,31 @@ class Disk: self._decrypted = False -class Cryptsetup: +class Cryptsetup(CommandInterface): class _Subcommands: close = "close" status = "status" open = "open" - _CRYPTSETUP = Path("/usr/bin/cryptsetup") + _BINARY = Path("/usr/bin/cryptsetup") @classmethod def encrypt(cls, mapper_entry: str) -> None: - command = [cls._CRYPTSETUP, cls._Subcommands.close, mapper_entry] - CommandRunner.run(command) + cls._run_command(cls._Subcommands.close, mapper_entry) @classmethod def _status(cls, name: str) -> str: - command = [cls._CRYPTSETUP, cls._Subcommands.status, name] - return CommandRunner.get_output(command) + return cls._get_output(cls._Subcommands.status, name) @classmethod def decrypt(cls, path: Path, mapper_entry: str) -> bool: + """Decrypt a disk, return whether it was already decrypted.""" if cls._mapper_entry_in_use(mapper_entry): if cls._already_decrypted(path, mapper_entry): return True print(MAPPER_ENTRY_ALREADY_EXISTS) exit(EXIT_ERROR) - command = [cls._CRYPTSETUP, cls._Subcommands.open, str(path), mapper_entry] - CommandRunner.run(command) + cls._run_command(cls._Subcommands.open, str(path), mapper_entry) return False @classmethod @@ -467,9 +474,11 @@ class Cryptsetup: for item in DISK_BY_UUID.iterdir(): if item.readlink() == device: result = item + break else: print(f"Could not resolve UUID of cryptsetup device {device}") exit(EXIT_ERROR) + # noinspection PyUnboundLocalVariable return result @classmethod @@ -486,7 +495,9 @@ class Cryptsetup: @classmethod def _already_decrypted(cls, path: Path, mapper_entry: str) -> bool: - decrypted_disk_path = cls._get_device_by_uuid_from_status_output(cls._status(mapper_entry)) + decrypted_disk_path = cls._get_device_by_uuid_from_status_output( + cls._status(mapper_entry) + ) return decrypted_disk_path == path @@ -514,7 +525,7 @@ class ExternalPool(Pool): if self._imported: raise ValueError(ALREADY_IMPORTED, self.name) self._disk.decrypt() - ZPOOL.import_from_directory(self, MAPPER_PATH) + ZPool.import_from_directory(self, MAPPER_PATH) self._imported = True def export(self) -> None: @@ -526,11 +537,11 @@ class ExternalPool(Pool): print(f"Exported {self.name}. Disk {self._disk.uuid} can be removed.") def _export_pool(self) -> None: - ZPOOL.export(self) + ZPool.export(self) def scrub(self, skip_if_recently_scrubbed: bool) -> None: print(f"Scrubbing {self._name}") - ZPOOL.scrub(self, skip_if_recently_scrubbed=skip_if_recently_scrubbed) + ZPool.scrub(self, skip_if_recently_scrubbed=skip_if_recently_scrubbed) @classmethod def find_from_dict(cls, backup_pools: dict[str, UUID]) -> ExternalPool: