1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
|
use std::process::Command;
use eyre::{Context, Result};
const PROPERTY_TAG: &str = "zsnap:tag";
#[derive(Debug)]
struct ZfsSnapshot {
name: String,
dataset: String,
time: chrono::DateTime<chrono::Utc>,
tag: String,
}
pub fn snapshot_create(tag: &str, dataset: &str, recursive: bool) -> Result<()> {
let time = chrono::Utc::now();
let name = format!("{}@{}", dataset, time.format("zsnap_%Y-%m-%d_%H-%M-%S"));
let rec = if recursive { "-r" } else { "" };
let cmd = format!("zfs snapshot -o {PROPERTY_TAG}={tag} {rec} {name}");
let _output = run_shell_command(&cmd)?;
Ok(())
}
pub fn snapshot_prune(tag: &str, dataset: &str, recursive: bool, keep: usize) -> Result<()> {
let mut snapshots = zfs_snapshot_list()?
.into_iter()
.filter(|s| s.dataset == dataset && s.tag == tag)
.collect::<Vec<_>>();
snapshots.sort_by_key(|s| s.time);
snapshots.reverse();
tracing::trace!("snapshots for dataset '{dataset}': {snapshots:?}");
tracing::trace!("skipping {keep} snapshots");
for snapshot in snapshots.into_iter().skip(keep) {
tracing::debug!(name = snapshot.name.as_str(), "destroying snapshot");
let rec = if recursive { "-r" } else { "" };
assert!(snapshot.name.contains('@')); // safe-guard to prevent accidental deletion of non-snapshots
let cmd = format!("zfs destroy {} {}", rec, snapshot.name);
let _output = run_shell_command(&cmd)?;
}
Ok(())
}
fn zfs_snapshot_list() -> Result<Vec<ZfsSnapshot>> {
fn parse_line(line: &str) -> Option<ZfsSnapshot> {
let mut parts = line.split('\t');
let name = parts.next()?;
let tag = parts.next()?;
let mut parts = name.split('@');
let dataset = parts.next()?;
let time = parts.next()?;
let time = chrono::NaiveDateTime::parse_from_str(time, "zsnap_%Y-%m-%d_%H-%M-%S").ok()?;
if tag == "-" {
return None;
}
Some(ZfsSnapshot {
name: name.to_string(),
dataset: dataset.to_string(),
time: time.and_utc(),
tag: tag.to_string(),
})
}
let output = run_shell_command(&format!("zfs list -H -t snapshot -o name,{PROPERTY_TAG}"))?;
Ok(output.lines().filter_map(parse_line).collect())
}
fn run_shell_command(cmd: &str) -> Result<String> {
tracing::debug!("running shell command '{cmd}'");
let output = Command::new("/bin/sh")
.arg("-c")
.arg(cmd)
.output()
.with_context(|| format!("running shell command '{cmd}'"))?;
if !output.status.success() {
let stderr = String::from_utf8(output.stderr).context("converting shell output to utf8")?;
let stderr = stderr.trim();
tracing::error!("shell command failed: '{stderr}'");
return Err(eyre::eyre!("shell command failed: '{stderr}'"));
}
let output = String::from_utf8(output.stdout).context("converting shell output to utf8")?;
tracing::trace!("shell command output: '{output}'");
Ok(output)
}
|