Feature/converting snapshot manager to rust #95
No files matched your search
Generated
+1
-8
@@ -1179,6 +1179,7 @@ dependencies = [
|
|||||||
"signal",
|
"signal",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"toml",
|
"toml",
|
||||||
|
"zfs",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1839,14 +1840,6 @@ name = "zfs"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
|
||||||
"clap",
|
|
||||||
"env_logger",
|
|
||||||
"log",
|
|
||||||
"serde",
|
|
||||||
"signal",
|
|
||||||
"tempfile",
|
|
||||||
"toml",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ log = "0.4"
|
|||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
signal = { path = "rust/signal" }
|
signal = { path = "rust/signal" }
|
||||||
|
zfs = { path = "rust/zfs" }
|
||||||
|
|
||||||
[workspace.lints.clippy]
|
[workspace.lints.clippy]
|
||||||
all = { level = "deny", priority = -1 }
|
all = { level = "deny", priority = -1 }
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "zfs"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
description = "List ZFS datasets and manage snapshots through the zfs executable"
|
||||||
|
license = "MIT"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
//! Manage ZFS datasets and snapshots using the `zfs` executable on `PATH`.
|
||||||
|
//!
|
||||||
|
//! ```no_run
|
||||||
|
//! for snapshot in zfs::list_snapshots("pool/home")? {
|
||||||
|
//! println!("{snapshot}");
|
||||||
|
//! }
|
||||||
|
//! # Ok::<(), anyhow::Error>(())
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result, bail};
|
||||||
|
|
||||||
|
const ZFS_EXECUTABLE: &str = "zfs";
|
||||||
|
|
||||||
|
fn zfs_command(args: &[&str]) -> Result<String> {
|
||||||
|
let output = Command::new(ZFS_EXECUTABLE)
|
||||||
|
.args(args)
|
||||||
|
.env("LC_ALL", "C")
|
||||||
|
.output()
|
||||||
|
.context("Failed to execute zfs")?;
|
||||||
|
if !output.status.success() {
|
||||||
|
let message = if output.stderr.is_empty() {
|
||||||
|
&output.stdout
|
||||||
|
} else {
|
||||||
|
&output.stderr
|
||||||
|
};
|
||||||
|
bail!(
|
||||||
|
"zfs {} failed ({}): {}",
|
||||||
|
args.join(" "),
|
||||||
|
output.status,
|
||||||
|
String::from_utf8_lossy(message).trim()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
String::from_utf8(output.stdout).context("zfs returned invalid UTF-8")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List filesystem datasets, excluding pool roots.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if executing ZFS or decoding its output fails.
|
||||||
|
pub fn list_datasets() -> Result<Vec<String>> {
|
||||||
|
let output = zfs_command(&["list", "-Hp", "-t", "filesystem", "-o", "name"])?;
|
||||||
|
Ok(output
|
||||||
|
.lines()
|
||||||
|
.filter(|name| name.contains('/'))
|
||||||
|
.map(String::from)
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List snapshot names belonging directly to `dataset`, excluding child datasets.
|
||||||
|
///
|
||||||
|
/// Returned names omit the `dataset@` prefix.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if executing ZFS or decoding its output fails.
|
||||||
|
pub fn list_snapshots(dataset: &str) -> Result<Vec<String>> {
|
||||||
|
let output = zfs_command(&[
|
||||||
|
"list", "-Hp", "-t", "snapshot", "-o", "name", "-d", "1", dataset,
|
||||||
|
])?;
|
||||||
|
Ok(output
|
||||||
|
.lines()
|
||||||
|
.filter_map(|name| {
|
||||||
|
let (owner, snapshot) = name.split_once('@')?;
|
||||||
|
(owner == dataset).then(|| snapshot.to_owned())
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create `dataset@snapshot`.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if executing ZFS or decoding its output fails.
|
||||||
|
pub fn create_snapshot(dataset: &str, snapshot: &str) -> Result<()> {
|
||||||
|
zfs_command(&["snapshot", &format!("{dataset}@{snapshot}")])?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete `dataset@snapshot`.
|
||||||
|
///
|
||||||
|
/// Returns `None` on success, or `Some("snapshot has dependent clones")` when
|
||||||
|
/// dependent clones prevent deletion.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error for other ZFS failures, execution failures, or invalid UTF-8 output.
|
||||||
|
pub fn delete_snapshot(dataset: &str, snapshot: &str) -> Result<Option<&'static str>> {
|
||||||
|
if let Err(error) = zfs_command(&["destroy", &format!("{dataset}@{snapshot}")]) {
|
||||||
|
if error.to_string().contains(&format!(
|
||||||
|
"cannot destroy '{dataset}@{snapshot}': snapshot has dependent clones"
|
||||||
|
)) {
|
||||||
|
return Ok(Some("snapshot has dependent clones"));
|
||||||
|
}
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
Reference in new issue
Block a user