113 lines
4.4 KiB
Python
113 lines
4.4 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from subprocess import Popen
|
|
|
|
import config
|
|
from .messages import DELETE_IN_LOCAL_POOL, SAME_DATASET
|
|
from .misc import assert_type
|
|
from .commands import CommandInterface
|
|
from .snapshot import Snapshot
|
|
from .dataset import Dataset
|
|
from .pool import Pool
|
|
from .zfs_path import ZFSPath
|
|
|
|
|
|
class ZFS(CommandInterface):
|
|
|
|
"""Interface of `zfs` commands"""
|
|
|
|
class _Subcommands:
|
|
class List:
|
|
# -H Scripting mode, omit headers
|
|
# -d1 maximum depth 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"]
|
|
|
|
class Send:
|
|
# -R Replicate filesystem
|
|
# -I send all intermediary snapshots
|
|
absolute = ["send", "-R"]
|
|
incremental = [*absolute, "-I"]
|
|
|
|
destroy = ["destroy"]
|
|
# -d Discard the first element of the "send" snapshot's file system name
|
|
# -F Force a rollback of the file system to the most recent snapshot before performing the "receive".
|
|
# -u File system that is associated with the received stream is not mounted.
|
|
receive = ["receive", "-d", "-F", "-u"]
|
|
|
|
_BINARY = Path("/usr/bin/zfs")
|
|
|
|
@classmethod
|
|
def get_first_level_datasets(cls, pool: Pool) -> list[Dataset]:
|
|
"""Get the first level datasets of a pool.
|
|
|
|
If a pool `p` has the datasets `p/a`, `p/a/b` and `p/c`, return `[p/a, p/c]`."""
|
|
assert_type(pool, Pool, "Can only get datasets from Pool objects")
|
|
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]
|
|
|
|
@classmethod
|
|
def get_snapshots(cls, dataset: Dataset) -> list[Snapshot]:
|
|
"""Get list of snapshots that are associated with a dataset."""
|
|
assert_type(dataset, Dataset, "Can only get snapshots from Dataset object")
|
|
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
|
|
def destroy_snapshot(cls, snapshot: Snapshot) -> None:
|
|
"""Non-recursively destroy a snapshot."""
|
|
assert_type(snapshot, Snapshot, "Can only destroy snapshots.")
|
|
if config.local_pool_to_backup == snapshot.pool:
|
|
raise Exception(DELETE_IN_LOCAL_POOL)
|
|
cls._run_command(*cls._Subcommands.destroy, str(snapshot))
|
|
|
|
@classmethod
|
|
def send_incremental(
|
|
cls, old_snapshot: Snapshot, new_snapshot: Snapshot
|
|
) -> Popen[str]:
|
|
"""Send the incremental replicating stream between two snapshots."""
|
|
cls._pre_send_sanity_checks(old_snapshot, new_snapshot)
|
|
print(f" {old_snapshot} -> {new_snapshot}")
|
|
return cls._open_stream(
|
|
*cls._Subcommands.Send.incremental,
|
|
str(old_snapshot),
|
|
str(new_snapshot),
|
|
)
|
|
|
|
@classmethod
|
|
def _pre_send_sanity_checks(
|
|
cls, old_snapshot: Snapshot, new_snapshot: Snapshot
|
|
) -> None:
|
|
for snapshot in [old_snapshot, new_snapshot]:
|
|
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.newer_than(new_snapshot):
|
|
raise ValueError("Old snapshot is newer than new snapshot.")
|
|
if old_snapshot == new_snapshot:
|
|
raise ValueError("Cannot send stream, snapshots are identical.")
|
|
|
|
@classmethod
|
|
def receive(cls, stream: Popen[str], pool: Pool):
|
|
"""Receive a stream generated with `zfs send` into a pool."""
|
|
assert_type(pool, Pool, "Can only receive into pools.")
|
|
cls._receive_stream(*cls._Subcommands.receive, pool.name, stream=stream)
|
|
|
|
@classmethod
|
|
def send_absolute(cls, snapshot: Snapshot) -> Popen:
|
|
"""Send the non-incremental replicating stream of the snapshot."""
|
|
print(f" {snapshot} -> [new]")
|
|
return cls._open_stream(*cls._Subcommands.Send.absolute, str(snapshot))
|