add: CommandInterface
This commit is contained in:
+54
-43
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user