aboutsummaryrefslogtreecommitdiff
path: root/src/zfs.rs
blob: 3800d429e66f7b3daede82a9fce07d431cc4e470 (plain)
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)
}