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))