refactoring
This commit is contained in:
+93
-80
@@ -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:
|
|
||||||
@staticmethod
|
|
||||||
def _assert_type(instance: Any, object_type: Any, message: str) -> None:
|
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:
|
||||||
|
|||||||
Reference in New Issue
Block a user