refactoring

This commit is contained in:
timeshifter
2021-12-03 20:10:17 +01:00
parent 61161038b2
commit fef8a3f3ea
+94 -81
View File
@@ -51,6 +51,7 @@ def main():
# TODO --skip-import # TODO --skip-import
# TODO --skip-post-backup-scrub # TODO --skip-post-backup-scrub
# TODO print time estimation while scrub is in progress # TODO print time estimation while scrub is in progress
# TODO logging
class Manager: class Manager:
@@ -125,6 +126,7 @@ class Manager:
return common_snapshots[-1] return common_snapshots[-1]
# TODO this is accessed from both Manager and Dataset. Refactor?
def _get_regex_matching_snapshots_with_tags(tags: list[str]): def _get_regex_matching_snapshots_with_tags(tags: list[str]):
return "@" + ".*".join(tags) return "@" + ".*".join(tags)
@@ -191,27 +193,25 @@ class ZFSPath:
raise ValueError(NOT_VALID_ZFS_PATH) raise ValueError(NOT_VALID_ZFS_PATH)
class ZFSBase: def _assert_type(instance: Any, object_type: Any, message: str) -> None:
@staticmethod
def _assert_type(instance: Any, object_type: Any, message: str) -> None:
if not isinstance(instance, object_type): if not isinstance(instance, object_type):
raise TypeError(message) raise TypeError(message)
class ZPOOL(ZFSBase): class ZPOOL:
_ZPOOL = Path("/usr/bin/zpool") _ZPOOL = Path("/usr/bin/zpool")
@classmethod @classmethod
def import_from_directory(cls, pool: Pool, directory: Path) -> None: def import_from_directory(cls, pool: Pool, directory: Path) -> None:
cls._assert_type(pool, Pool, "Can only import Pool objects") _assert_type(pool, Pool, "Can only import Pool objects")
# -N: no mount # -N: no mount
# -d: directory to search the pool in # -d: directory to search the pool in
Command.run([cls._ZPOOL, "import", "-N", "-d", directory, pool.name]) CommandRunner.run([cls._ZPOOL, "import", "-N", "-d", directory, pool.name])
@classmethod @classmethod
def export(cls, pool: Pool) -> None: def export(cls, pool: Pool) -> None:
cls._assert_type(pool, Pool, "Can only export Pool objects") _assert_type(pool, Pool, "Can only export Pool objects")
Command.run([cls._ZPOOL, "export", pool.name]) CommandRunner.run([cls._ZPOOL, "export", pool.name])
@classmethod @classmethod
def scrub( def scrub(
@@ -221,10 +221,10 @@ class ZPOOL(ZFSBase):
skip_if_recently_scrubbed: bool, skip_if_recently_scrubbed: bool,
wait_for_finish: bool = True, wait_for_finish: bool = True,
) -> None: ) -> None:
cls._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:
Command.run([cls._ZPOOL, "wait", "-t", "scrub", pool.name]) CommandRunner.run([cls._ZPOOL, "wait", "-t", "scrub", 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.")
@@ -233,10 +233,10 @@ class ZPOOL(ZFSBase):
if cls._scrub_in_progress(pool): if cls._scrub_in_progress(pool):
return return
if not skip_if_recently_scrubbed: if not skip_if_recently_scrubbed:
Command.run([cls._ZPOOL, "scrub", pool.name]) CommandRunner.run([cls._ZPOOL, "scrub", pool.name])
else: else:
if not cls._recently_scrubbed(pool): if not cls._recently_scrubbed(pool):
Command.run([cls._ZPOOL, "scrub", pool.name]) CommandRunner.run([cls._ZPOOL, "scrub", pool.name])
@classmethod @classmethod
def _recently_scrubbed(cls, pool: Pool) -> bool: def _recently_scrubbed(cls, pool: Pool) -> bool:
@@ -252,88 +252,80 @@ class ZPOOL(ZFSBase):
@staticmethod @staticmethod
def _get_pool_status_output(pool: Pool) -> str: def _get_pool_status_output(pool: Pool) -> str:
return Command.get_output([ZFS, "status", pool.name]) return CommandRunner.get_output([ZFS, "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(ZFSBase): class ZFS:
"""Interface of `zfs` commands"""
class _Subcommands:
class List:
# -H Scripting mode, omit headers
# -d1 maximum of 1
# -o name output column
# -t object type
# -r recursive
_base = ["list", "-H", "-d1", "-o", "name"]
dataset = _base + ["-t", "filesystem"]
snapshot = _base + ["-t", "snapshot", "-r"]
destroy = ["destroy"]
# -R Replicate filesystem
# -I send all intermediary snapshots
send = ["send", "-R", "-I"]
# -d Discard the first element of the sent snapshot's file system name
# -F Force a rollback of the file system to the most recent snapshot before performing the receive operation.
# -u File system that is associated with the received stream is not mounted.
receive = ["receive", "-d", "-F", "-u"]
_ZFS = Path("/usr/bin/zfs") _ZFS = Path("/usr/bin/zfs")
# TODO introduce command constants
@classmethod @classmethod
def get_first_level_datasets(cls, pool: Pool) -> list[Dataset]: def get_first_level_datasets(cls, pool: Pool) -> list[Dataset]:
cls._assert_type(pool, Pool, "Can only get datasets from Pool objects") _assert_type(pool, Pool, "Can only get datasets from Pool objects")
datasets_str = Command.get_output( command = [str(cls._ZFS), *cls._Subcommands.List.dataset, pool.name]
[ datasets_str = CommandRunner.get_output(command)
ZFS,
"list",
"-H",
"-d",
"1",
"-o",
"name",
"-t",
"filesystem",
pool.name,
]
)
# 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]
@classmethod @classmethod
def get_snapshots(cls, dataset: Dataset) -> list[Snapshot]: def get_snapshots(cls, dataset: Dataset) -> list[Snapshot]:
cls._assert_type( _assert_type(dataset, Dataset, "Can only get snapshots from Dataset object")
dataset, Dataset, "Can only get snapshots from Dataset objects" command = [
) str(cls._ZFS),
return [ *cls._Subcommands.List.snapshot,
Snapshot(ZFSPath.from_string(name))
for name in Command.get_output(
[
ZFS,
"list",
"-r",
"-t",
"snapshot",
"-o",
"name",
"-H",
"-d",
"1",
dataset.qualified_name, dataset.qualified_name,
] ]
).split() snapshot_names: list[str] = CommandRunner.get_output(command).split()
] return [Snapshot(ZFSPath.from_string(name)) for name in snapshot_names]
@classmethod @classmethod
def destroy_snapshot(cls, snapshot: Snapshot, *, recursive=False): def destroy_snapshot(cls, snapshot: Snapshot):
cls._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)
cmdline = [ZFS, "destroy"] command = [str(cls._ZFS), *cls._Subcommands.destroy, str(snapshot)]
if recursive: CommandRunner.run(command)
cmdline.append("-r")
cmdline.append(str(snapshot))
Command.run(cmdline)
@classmethod @classmethod
def send_incremental( def send_incremental(
cls, old_snapshot: Snapshot, new_snapshot: Snapshot cls, old_snapshot: Snapshot, new_snapshot: Snapshot
) -> Popen[str]: ) -> Popen[str]:
cls._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}")
stream = Popen( return CommandRunner.send_stream(
[ZFS, "send", "-R", "-I", str(old_snapshot), str(new_snapshot)], stdout=PIPE [cls._ZFS, *cls._Subcommands.send, str(old_snapshot), str(new_snapshot)]
) )
return stream
@classmethod @classmethod
def _send_sanity_checks(cls, old_snapshot: Snapshot, new_snapshot: Snapshot): def _pre_send_sanity_checks(cls, old_snapshot: Snapshot, new_snapshot: Snapshot):
for snapshot in [old_snapshot, new_snapshot]: for snapshot in [old_snapshot, new_snapshot]:
cls._assert_type( _assert_type(
snapshot, snapshot,
Snapshot, Snapshot,
f"Cannot send stream, object is not a Snapshot: {snapshot}", f"Cannot send stream, object is not a Snapshot: {snapshot}",
@@ -347,14 +339,9 @@ class ZFS(ZFSBase):
@classmethod @classmethod
def receive(cls, stream: Popen[str], pool: Pool): def receive(cls, stream: Popen[str], pool: Pool):
# -d Discard the first element of the sent snapshot's file system name _assert_type(pool, Pool, "Can only receive into pools.")
# -F Force a rollback of the file system to the most recent snapshot before performing the receive operation. command = [str(cls._ZFS), *cls._Subcommands.receive, pool.name]
# -u File system that is associated with the received stream is not mounted. CommandRunner.receive_from_stream(command, stream)
cls._assert_type(pool, Pool, "Can only receive into pools.")
Command.receive_from_stream(
[ZFS, "receive", "-d", "-F", "-u", pool.name],
stream,
)
class Time: class Time:
@@ -421,15 +408,19 @@ class Disk:
class Cryptsetup: class Cryptsetup:
class _Subcommands:
close = "close"
status = "status"
open = "open"
_CRYPTSETUP = Path("/usr/bin/cryptsetup") _CRYPTSETUP = Path("/usr/bin/cryptsetup")
@classmethod @classmethod
def encrypt(cls, mapper_entry: str) -> None: def encrypt(cls, mapper_entry: str) -> None:
Command.run([cls._CRYPTSETUP, "close", mapper_entry]) CommandRunner.run([cls._CRYPTSETUP, "close", mapper_entry])
@classmethod @classmethod
def _status(cls, name: str) -> str: def _status(cls, name: str) -> str:
return Command.get_output([cls._CRYPTSETUP, "status", name]) return CommandRunner.get_output([cls._CRYPTSETUP, "status", name])
@classmethod @classmethod
def decrypt(cls, path: Path, mapper_entry: str) -> bool: def decrypt(cls, path: Path, mapper_entry: str) -> bool:
@@ -438,7 +429,7 @@ class Cryptsetup:
return True return True
print(MAPPER_ENTRY_ALREADY_EXISTS) print(MAPPER_ENTRY_ALREADY_EXISTS)
exit(EXIT_ERROR) exit(EXIT_ERROR)
Command.run([cls._CRYPTSETUP, "open", str(path), mapper_entry]) CommandRunner.run([cls._CRYPTSETUP, "open", str(path), mapper_entry])
return False return False
@classmethod @classmethod
@@ -610,21 +601,43 @@ class Snapshot:
return Dataset(self._path.dataset) return Dataset(self._path.dataset)
class Command: class CommandRunner:
@staticmethod @staticmethod
def run(cmdline: list[str]) -> None: def _sanity_check(cmdline: list[str]) -> None:
_assert_type(cmdline, list, "Command line must be a list")
for item in cmdline:
_assert_type(item, str, "Only strings allowed in command line")
@staticmethod
def _to_list(command: str | list[str]) -> list[str]:
if isinstance(command, str):
return [command]
return command
@classmethod
def run(cls, cmdline: str | list[str]) -> None:
cmdline = cls._to_list(cmdline)
cls._sanity_check(cmdline)
call(cmdline) call(cmdline)
@staticmethod @classmethod
def get_output(cmdline: str | list[str]) -> str: def get_output(cls, cmdline: str | list[str]) -> str:
if isinstance(cmdline, str): cmdline = cls._to_list(cmdline)
cmdline = [cmdline] cls._sanity_check(cmdline)
return check_output(cmdline).strip().decode() return check_output(cmdline).strip().decode()
@staticmethod @classmethod
def receive_from_stream(cmdline: str | list[str], stream: Popen) -> None: def receive_from_stream(cls, cmdline: str | list[str], stream: Popen) -> None:
cmdline = cls._to_list(cmdline)
cls._sanity_check(cmdline)
check_output(cmdline, stdin=stream.stdout) check_output(cmdline, stdin=stream.stdout)
@classmethod
def send_stream(cls, cmdline: str | list[str]) -> Popen:
cmdline = cls._to_list(cmdline)
cls._sanity_check(cmdline)
return Popen(cmdline, stdout=PIPE)
def verify_running_as_root(): def verify_running_as_root():
if getuid() > 0: if getuid() > 0: