refactor: ZFS
This commit is contained in:
+1
@@ -1,5 +1,6 @@
|
|||||||
<component name="InspectionProjectProfileManager">
|
<component name="InspectionProjectProfileManager">
|
||||||
<settings>
|
<settings>
|
||||||
|
<option name="PROJECT_PROFILE" value="Default" />
|
||||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
<version value="1.0" />
|
<version value="1.0" />
|
||||||
</settings>
|
</settings>
|
||||||
|
|||||||
Generated
+3
@@ -1,4 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9" project-jdk-type="Python SDK" />
|
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.9" project-jdk-type="Python SDK" />
|
||||||
|
<component name="PythonCompatibilityInspectionAdvertiser">
|
||||||
|
<option name="version" value="3" />
|
||||||
|
</component>
|
||||||
</project>
|
</project>
|
||||||
Generated
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
<module type="PYTHON_MODULE" version="4">
|
<module type="PYTHON_MODULE" version="4">
|
||||||
<component name="NewModuleRootManager">
|
<component name="NewModuleRootManager">
|
||||||
<content url="file://$MODULE_DIR$" />
|
<content url="file://$MODULE_DIR$" />
|
||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="jdk" jdkName="Python 3.9" jdkType="Python SDK" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
</component>
|
</component>
|
||||||
</module>
|
</module>
|
||||||
@@ -32,7 +32,3 @@ keep_snapshots_per_interval = {
|
|||||||
"weekly": 8,
|
"weekly": 8,
|
||||||
"monthly": 12,
|
"monthly": 12,
|
||||||
}
|
}
|
||||||
|
|
||||||
ZFS = "/usr/bin/zfs"
|
|
||||||
ZPOOL = "/usr/bin/zpool"
|
|
||||||
CRYPTSETUP = "/usr/bin/cryptsetup"
|
|
||||||
|
|||||||
+72
-41
@@ -10,6 +10,7 @@ from pathlib import Path
|
|||||||
from re import search
|
from re import search
|
||||||
from subprocess import PIPE, Popen, call, check_output
|
from subprocess import PIPE, Popen, call, check_output
|
||||||
from sys import exit
|
from sys import exit
|
||||||
|
from typing import Any
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
import config
|
import config
|
||||||
@@ -191,11 +192,18 @@ class ZFSPath:
|
|||||||
|
|
||||||
|
|
||||||
class ZFS:
|
class ZFS:
|
||||||
|
# TODO introduce command constants
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_datasets(name: str) -> list[Dataset]:
|
def _assert_type(instance: Any, object_type: Any, message: str) -> None:
|
||||||
|
if not isinstance(instance, object_type):
|
||||||
|
raise TypeError(message)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_first_level_datasets(cls, pool: Pool) -> list[Dataset]:
|
||||||
|
cls._assert_type(pool, Pool, "Can only get datasets from Pool objects")
|
||||||
datasets_str = Command.get_output(
|
datasets_str = Command.get_output(
|
||||||
[
|
[
|
||||||
config.ZFS,
|
ZFS,
|
||||||
"list",
|
"list",
|
||||||
"-H",
|
"-H",
|
||||||
"-d",
|
"-d",
|
||||||
@@ -204,30 +212,35 @@ class ZFS:
|
|||||||
"name",
|
"name",
|
||||||
"-t",
|
"-t",
|
||||||
"filesystem",
|
"filesystem",
|
||||||
name,
|
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]
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def import_pool_from_directory(name: str, directory: Path) -> None:
|
def import_pool_from_directory(cls, pool: Pool, directory: Path) -> None:
|
||||||
|
cls._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([config.ZPOOL, "import", "-N", "-d", directory, name])
|
Command.run([Binaries.ZPOOL, "import", "-N", "-d", directory, pool.name])
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def export_pool(name: str) -> None:
|
def export_pool(cls, pool: Pool) -> None:
|
||||||
Command.run([config.ZPOOL, "export", name])
|
cls._assert_type(pool, Pool, "Can only export Pool objects")
|
||||||
|
Command.run([Binaries.ZPOOL, "export", pool.name])
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def get_snapshots(name: str) -> list[Snapshot]:
|
def get_snapshots(cls, dataset: Dataset) -> list[Snapshot]:
|
||||||
|
cls._assert_type(
|
||||||
|
dataset, Dataset, "Can only get snapshots from Dataset objects"
|
||||||
|
)
|
||||||
return [
|
return [
|
||||||
Snapshot(ZFSPath.from_string(name))
|
Snapshot(ZFSPath.from_string(name))
|
||||||
for name in Command.get_output(
|
for name in Command.get_output(
|
||||||
[
|
[
|
||||||
config.ZFS,
|
ZFS,
|
||||||
"list",
|
"list",
|
||||||
"-r",
|
"-r",
|
||||||
"-t",
|
"-t",
|
||||||
@@ -237,35 +250,48 @@ class ZFS:
|
|||||||
"-H",
|
"-H",
|
||||||
"-d",
|
"-d",
|
||||||
"1",
|
"1",
|
||||||
name,
|
dataset.qualified_name,
|
||||||
]
|
]
|
||||||
).split()
|
).split()
|
||||||
]
|
]
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def destroy_snapshot(snapshot: Snapshot, *, recursive=False):
|
def destroy_snapshot(cls, snapshot: Snapshot, *, recursive=False):
|
||||||
if not isinstance(snapshot, Snapshot):
|
cls._assert_type(snapshot, Snapshot, "Can only destroy snapshots.")
|
||||||
raise TypeError("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 = [config.ZFS, "destroy"]
|
cmdline = [ZFS, "destroy"]
|
||||||
if recursive:
|
if recursive:
|
||||||
cmdline.append("-r")
|
cmdline.append("-r")
|
||||||
cmdline.append(str(snapshot))
|
cmdline.append(str(snapshot))
|
||||||
Command.run(cmdline)
|
Command.run(cmdline)
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def send_incremental(old_snap: Snapshot, new_snap: Snapshot) -> Popen[str]:
|
def send_incremental(
|
||||||
if old_snap.dataset != new_snap.dataset:
|
cls, old_snapshot: Snapshot, new_snapshot: Snapshot
|
||||||
raise ValueError(SAME_DATASET)
|
) -> Popen[str]:
|
||||||
if old_snap == new_snap:
|
cls._send_sanity_checks(old_snapshot, new_snapshot)
|
||||||
raise ValueError("Cannot send stream, snapshots are identical.")
|
print(f" {old_snapshot} -> {new_snapshot}")
|
||||||
print(f" {old_snap} -> {new_snap}")
|
|
||||||
stream = Popen(
|
stream = Popen(
|
||||||
[config.ZFS, "send", "-R", "-I", str(old_snap), str(new_snap)], stdout=PIPE
|
[ZFS, "send", "-R", "-I", str(old_snapshot), str(new_snapshot)], stdout=PIPE
|
||||||
)
|
)
|
||||||
return stream
|
return stream
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _send_sanity_checks(cls, old_snapshot: Snapshot, new_snapshot: Snapshot):
|
||||||
|
for snapshot in [old_snapshot, new_snapshot]:
|
||||||
|
cls._assert_type(
|
||||||
|
snapshot,
|
||||||
|
Snapshot,
|
||||||
|
f"Cannot send stream, object is not a Snapshot: {snapshot}",
|
||||||
|
)
|
||||||
|
if old_snapshot.dataset != new_snapshot.dataset:
|
||||||
|
raise ValueError(SAME_DATASET)
|
||||||
|
if old_snapshot > new_snapshot: # chronological ordering
|
||||||
|
raise ValueError("Old snapshot is newer than new snapshot.")
|
||||||
|
if old_snapshot == new_snapshot:
|
||||||
|
raise ValueError("Cannot send stream, snapshots are identical.")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def receive(stream: Popen[str], pool: Pool):
|
def receive(stream: Popen[str], pool: Pool):
|
||||||
# -d Discard the first element of the sent snapshot's file system name
|
# -d Discard the first element of the sent snapshot's file system name
|
||||||
@@ -274,7 +300,7 @@ class ZFS:
|
|||||||
if not isinstance(pool, Pool):
|
if not isinstance(pool, Pool):
|
||||||
raise TypeError("Can only receive into pools.")
|
raise TypeError("Can only receive into pools.")
|
||||||
Command.receive_from_stream(
|
Command.receive_from_stream(
|
||||||
[config.ZFS, "receive", "-d", "-F", "-u", pool.name],
|
[ZFS, "receive", "-d", "-F", "-u", pool.name],
|
||||||
stream,
|
stream,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -290,7 +316,7 @@ class ZFS:
|
|||||||
raise TypeError("Can only scrub pools.")
|
raise TypeError("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([config.ZPOOL, "wait", "-t", "scrub", pool.name])
|
Command.run([Binaries.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.")
|
||||||
|
|
||||||
@@ -299,13 +325,13 @@ class ZFS:
|
|||||||
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([config.ZPOOL, "scrub", pool.name])
|
Command.run([Binaries.ZPOOL, "scrub", pool.name])
|
||||||
else:
|
else:
|
||||||
if not cls._recently_scrubbed(pool):
|
if not cls._recently_scrubbed(pool):
|
||||||
Command.run([config.ZPOOL, "scrub", pool.name])
|
Command.run([Binaries.ZPOOL, "scrub", pool.name])
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _recently_scrubbed(cls, pool):
|
def _recently_scrubbed(cls, pool: Pool) -> bool:
|
||||||
return datetime.now() - cls._last_scrub(pool) < config.recent_scrub_timedelta
|
return datetime.now() - cls._last_scrub(pool) < config.recent_scrub_timedelta
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -317,8 +343,8 @@ class ZFS:
|
|||||||
return "ONLINE" in cls._get_pool_status_output(pool)
|
return "ONLINE" in cls._get_pool_status_output(pool)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_pool_status_output(pool) -> str:
|
def _get_pool_status_output(pool: Pool) -> str:
|
||||||
return Command.get_output([config.ZFS, "status", pool.name])
|
return Command.get_output([ZFS, "status", pool.name])
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _last_scrub(cls, pool: Pool) -> datetime:
|
def _last_scrub(cls, pool: Pool) -> datetime:
|
||||||
@@ -384,14 +410,14 @@ class Disk:
|
|||||||
raise ValueError(NOT_DECRYPTED, self._name)
|
raise ValueError(NOT_DECRYPTED, self._name)
|
||||||
if self._was_already_decrypted:
|
if self._was_already_decrypted:
|
||||||
return
|
return
|
||||||
Command.run([config.CRYPTSETUP, "close", self._mapper_entry])
|
Command.run([Binaries.CRYPTSETUP, "close", self._mapper_entry])
|
||||||
self._decrypted = False
|
self._decrypted = False
|
||||||
|
|
||||||
|
|
||||||
class Cryptsetup:
|
class Cryptsetup:
|
||||||
@classmethod
|
@classmethod
|
||||||
def _status(cls, name: str) -> str:
|
def _status(cls, name: str) -> str:
|
||||||
return Command.get_output([config.CRYPTSETUP, "status", name])
|
return Command.get_output([Binaries.CRYPTSETUP, "status", name])
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def decrypt(cls, path: Path, mapper_entry: str) -> bool:
|
def decrypt(cls, path: Path, mapper_entry: str) -> bool:
|
||||||
@@ -400,7 +426,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([config.CRYPTSETUP, "open", str(path), mapper_entry])
|
Command.run([Binaries.CRYPTSETUP, "open", str(path), mapper_entry])
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -435,7 +461,7 @@ class Pool:
|
|||||||
@property
|
@property
|
||||||
@cache
|
@cache
|
||||||
def datasets(self) -> list[Dataset]:
|
def datasets(self) -> list[Dataset]:
|
||||||
return ZFS.get_datasets(self._name)
|
return ZFS.get_first_level_datasets(self)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
@@ -452,7 +478,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()
|
||||||
ZFS.import_pool_from_directory(self._name, MAPPER_PATH)
|
ZFS.import_pool_from_directory(self, MAPPER_PATH)
|
||||||
self._imported = True
|
self._imported = True
|
||||||
|
|
||||||
def export(self) -> None:
|
def export(self) -> None:
|
||||||
@@ -464,8 +490,7 @@ 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:
|
||||||
# TODO cancel scrub if necessary
|
ZFS.export_pool(self)
|
||||||
ZFS.export_pool(self._name)
|
|
||||||
|
|
||||||
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}")
|
||||||
@@ -514,7 +539,7 @@ class Dataset:
|
|||||||
@property
|
@property
|
||||||
def snapshots(self) -> list[Snapshot]:
|
def snapshots(self) -> list[Snapshot]:
|
||||||
"""get qualified snapshot names that are direct children of the dataset"""
|
"""get qualified snapshot names that are direct children of the dataset"""
|
||||||
return ZFS.get_snapshots(self.qualified_name)
|
return ZFS.get_snapshots(self)
|
||||||
|
|
||||||
def replace_pool(self, name: str) -> Dataset:
|
def replace_pool(self, name: str) -> Dataset:
|
||||||
return Dataset(self._path.replace_pool(name))
|
return Dataset(self._path.replace_pool(name))
|
||||||
@@ -595,5 +620,11 @@ def verify_running_as_root():
|
|||||||
exit(EXIT_ERROR)
|
exit(EXIT_ERROR)
|
||||||
|
|
||||||
|
|
||||||
|
class Binaries:
|
||||||
|
ZFS = Path("/usr/bin/zfs")
|
||||||
|
ZPOOL = Path("/usr/bin/zpool")
|
||||||
|
CRYPTSETUP = Path("/usr/bin/cryptsetup")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
Reference in New Issue
Block a user