Files
zfs-backup/zfs_backup/commands/zfs.py
T
2022-12-06 15:10:36 +01:00

110 lines
4.3 KiB
Python

from __future__ import annotations
from pathlib import Path
from subprocess import Popen
from .. import Pool
from ._core import CommandInterface
from ..dataset import Dataset
from ..messages import SAME_DATASET
from ..misc import assert_type
from ..snapshot import Snapshot
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.")
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))