feat: ZFS trait abstraction with SystemZfs and MockZfs
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
08b7f94e4c
commit
965eef4db5
@@ -0,0 +1,159 @@
|
|||||||
|
use anyhow::{bail, Context, Result};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
pub fn snapshot_name(dataset: &str, kind: &str, timestamp: &str) -> String {
|
||||||
|
format!("{}@{}-{}", dataset, kind, timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ZfsOps: Send + Sync {
|
||||||
|
fn snapshot(&self, dataset: &str, tag: &str) -> Result<()>;
|
||||||
|
fn list_snapshots(&self, dataset: &str) -> Result<Vec<String>>;
|
||||||
|
fn delete_snapshot(&self, full_name: &str) -> Result<()>;
|
||||||
|
fn send_to_remote(&self, snapshot: &str, incremental_from: Option<&str>,
|
||||||
|
remote_user: &str, remote_host: &str,
|
||||||
|
remote_dataset: &str) -> Result<()>;
|
||||||
|
fn clone_snapshot(&self, snapshot: &str, dest_dataset: &str) -> Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SystemZfs;
|
||||||
|
|
||||||
|
impl ZfsOps for SystemZfs {
|
||||||
|
fn snapshot(&self, dataset: &str, tag: &str) -> Result<()> {
|
||||||
|
let name = format!("{}@{}", dataset, tag);
|
||||||
|
let out = std::process::Command::new("zfs")
|
||||||
|
.args(["snapshot", &name])
|
||||||
|
.output()
|
||||||
|
.context("running zfs snapshot")?;
|
||||||
|
if !out.status.success() {
|
||||||
|
bail!("zfs snapshot failed: {}", String::from_utf8_lossy(&out.stderr));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_snapshots(&self, dataset: &str) -> Result<Vec<String>> {
|
||||||
|
let out = std::process::Command::new("zfs")
|
||||||
|
.args(["list", "-H", "-t", "snapshot", "-o", "name", "-r", dataset])
|
||||||
|
.output()
|
||||||
|
.context("running zfs list")?;
|
||||||
|
if !out.status.success() {
|
||||||
|
bail!("zfs list failed: {}", String::from_utf8_lossy(&out.stderr));
|
||||||
|
}
|
||||||
|
Ok(String::from_utf8_lossy(&out.stdout)
|
||||||
|
.lines()
|
||||||
|
.filter(|l| !l.is_empty())
|
||||||
|
.map(String::from)
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_snapshot(&self, full_name: &str) -> Result<()> {
|
||||||
|
let out = std::process::Command::new("zfs")
|
||||||
|
.args(["destroy", full_name])
|
||||||
|
.output()
|
||||||
|
.context("running zfs destroy")?;
|
||||||
|
if !out.status.success() {
|
||||||
|
bail!("zfs destroy failed: {}", String::from_utf8_lossy(&out.stderr));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_to_remote(&self, snapshot: &str, incremental_from: Option<&str>,
|
||||||
|
remote_user: &str, remote_host: &str,
|
||||||
|
remote_dataset: &str) -> Result<()> {
|
||||||
|
let mut send_args = vec!["send".to_string()];
|
||||||
|
if let Some(prev) = incremental_from {
|
||||||
|
send_args.push("-i".to_string());
|
||||||
|
send_args.push(prev.to_string());
|
||||||
|
}
|
||||||
|
send_args.push(snapshot.to_string());
|
||||||
|
let cmd = format!(
|
||||||
|
"zfs {} | ssh {}@{} zfs receive -F {}",
|
||||||
|
send_args.join(" "), remote_user, remote_host, remote_dataset
|
||||||
|
);
|
||||||
|
let out = std::process::Command::new("sh")
|
||||||
|
.args(["-c", &cmd])
|
||||||
|
.output()
|
||||||
|
.context("running zfs send | ssh")?;
|
||||||
|
if !out.status.success() {
|
||||||
|
bail!("zfs send failed: {}", String::from_utf8_lossy(&out.stderr));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clone_snapshot(&self, snapshot: &str, dest_dataset: &str) -> Result<()> {
|
||||||
|
let out = std::process::Command::new("zfs")
|
||||||
|
.args(["clone", snapshot, dest_dataset])
|
||||||
|
.output()
|
||||||
|
.context("running zfs clone")?;
|
||||||
|
if !out.status.success() {
|
||||||
|
bail!("zfs clone failed: {}", String::from_utf8_lossy(&out.stderr));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
pub struct MockZfs {
|
||||||
|
snapshots: Arc<Mutex<Vec<String>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ZfsOps for MockZfs {
|
||||||
|
fn snapshot(&self, dataset: &str, tag: &str) -> Result<()> {
|
||||||
|
self.snapshots.lock().unwrap().push(format!("{}@{}", dataset, tag));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_snapshots(&self, dataset: &str) -> Result<Vec<String>> {
|
||||||
|
Ok(self.snapshots.lock().unwrap()
|
||||||
|
.iter()
|
||||||
|
.filter(|s| s.starts_with(&format!("{}@", dataset)))
|
||||||
|
.cloned()
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_snapshot(&self, full_name: &str) -> Result<()> {
|
||||||
|
self.snapshots.lock().unwrap().retain(|s| s != full_name);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_to_remote(&self, _snap: &str, _incr: Option<&str>,
|
||||||
|
_user: &str, _host: &str, _dest: &str) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clone_snapshot(&self, snapshot: &str, dest_dataset: &str) -> Result<()> {
|
||||||
|
self.snapshots.lock().unwrap()
|
||||||
|
.push(format!("{} -> {}", snapshot, dest_dataset));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_snapshot_name_format() {
|
||||||
|
let name = snapshot_name("slab/projects", "hourly", "2026-06-16-0300");
|
||||||
|
assert_eq!(name, "slab/projects@hourly-2026-06-16-0300");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mock_zfs_snapshot() {
|
||||||
|
let zfs = MockZfs::default();
|
||||||
|
zfs.snapshot("slab/projects", "test-snap").unwrap();
|
||||||
|
let snaps = zfs.list_snapshots("slab/projects").unwrap();
|
||||||
|
assert_eq!(snaps.len(), 1);
|
||||||
|
assert_eq!(snaps[0], "slab/projects@test-snap");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mock_zfs_delete() {
|
||||||
|
let zfs = MockZfs::default();
|
||||||
|
zfs.snapshot("slab/projects", "s1").unwrap();
|
||||||
|
zfs.snapshot("slab/projects", "s2").unwrap();
|
||||||
|
zfs.delete_snapshot("slab/projects@s1").unwrap();
|
||||||
|
let snaps = zfs.list_snapshots("slab/projects").unwrap();
|
||||||
|
assert_eq!(snaps.len(), 1);
|
||||||
|
assert_eq!(snaps[0], "slab/projects@s2");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user