add: CommandInterface

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