add: CommandInterface
This commit is contained in:
+51
-40
@@ -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(
|
||||||
[
|
|
||||||
str(cls._ZFS),
|
|
||||||
*cls._Subcommands.send,
|
*cls._Subcommands.send,
|
||||||
str(old_snapshot),
|
str(old_snapshot),
|
||||||
str(new_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:
|
||||||
|
|||||||
Reference in New Issue
Block a user