hive-priv: drop the descriptor-taking snapshot send and its fd-receiving loop
This commit is contained in:
parent
20d286f06a
commit
418b50980c
1 changed files with 39 additions and 750 deletions
|
|
@ -2,13 +2,12 @@
|
|||
//! `DeleteAgentSubvolume`, `UpgradeAgentSubvolume`), qgroup accounting and
|
||||
//! quotas (`EnsureBtrfsQuota`, `ReadSubvolumeUsage`, `SetSubvolumeQuota`),
|
||||
//! read-only snapshots and their export (`SnapshotAgentSubvolume`,
|
||||
//! `DeleteAgentSnapshot`, `SendAgentSnapshotToFile`, `SendAgentSnapshotToFd`)
|
||||
//! `DeleteAgentSnapshot`, `SendAgentSnapshotToFile`)
|
||||
//! and the `du`-based `MeasureAgentDisk`.
|
||||
//!
|
||||
//! Every path is derived from a validated agent name (and, for snapshots, a
|
||||
//! `hive-`-prefixed label) under `AGENT_STATE_ROOT`.
|
||||
|
||||
use std::os::fd::OwnedFd;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
|
|
@ -101,29 +100,6 @@ pub(crate) async fn exec_send_agent_snapshot_to_file(
|
|||
.await
|
||||
}
|
||||
|
||||
/// `SendAgentSnapshotToFd`.
|
||||
pub(crate) async fn exec_send_agent_snapshot_to_fd(
|
||||
agent_name: &str,
|
||||
snapshot_name: &str,
|
||||
parent_snapshot_name: Option<&str>,
|
||||
fd: Option<OwnedFd>,
|
||||
) -> Result<(String, String)> {
|
||||
validate_agent_name(agent_name)?;
|
||||
validate_snapshot_name(snapshot_name)?;
|
||||
if let Some(parent) = parent_snapshot_name {
|
||||
validate_snapshot_name(parent)?;
|
||||
}
|
||||
let dest = fd.context("no descriptor to stream into")?;
|
||||
send_agent_snapshot_to_fd(
|
||||
&Env::real(),
|
||||
agent_name,
|
||||
snapshot_name,
|
||||
parent_snapshot_name,
|
||||
dest,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// A btrfs snapshot label must start with `hive-` — this doubles as an
|
||||
/// allow-list: only names hivectl itself constructs (or an operator who
|
||||
/// knows the convention) can reach the `btrfs subvolume snapshot`/`delete`
|
||||
|
|
@ -953,70 +929,6 @@ async fn send_agent_snapshot_to_file(
|
|||
Ok((dest.display().to_string(), String::new()))
|
||||
}
|
||||
|
||||
/// `SendAgentSnapshotToFd` — stream a read-only snapshot (optionally
|
||||
/// incremental against `parent_name`) straight into a descriptor the
|
||||
/// caller passed us.
|
||||
///
|
||||
/// Arranged so this helper never learns there *is* a network: the
|
||||
/// caller connects to the destination, writes any header itself, and
|
||||
/// hands the connected socket over. We only ever see "a thing to write bytes into",
|
||||
/// which keeps a root process out of any address, protocol or trust
|
||||
/// decision — and keeps everyone out of the data path once `btrfs send`
|
||||
/// starts, which matters at multi-gigabyte sizes.
|
||||
async fn send_agent_snapshot_to_fd(
|
||||
env: &Env<'_, impl Spawn, impl Probes>,
|
||||
agent_name: &str,
|
||||
snapshot_name: &str,
|
||||
parent_name: Option<&str>,
|
||||
dest: OwnedFd,
|
||||
) -> Result<(String, String)> {
|
||||
let snap = snapshot_path(env.state_root, agent_name, snapshot_name);
|
||||
if !snap.exists() {
|
||||
bail!(
|
||||
"snapshot {} does not exist — create it with `subvol snapshot create` first",
|
||||
snap.display()
|
||||
);
|
||||
}
|
||||
|
||||
let mut cmd = env.btrfs();
|
||||
cmd.arg("send");
|
||||
if let Some(parent) = parent_name {
|
||||
let parent_path = snapshot_path(env.state_root, agent_name, parent);
|
||||
if !parent_path.exists() {
|
||||
bail!(
|
||||
"parent snapshot {} does not exist — pick an existing parent or omit it for a full send",
|
||||
parent_path.display()
|
||||
);
|
||||
}
|
||||
cmd.arg("-p").arg(&parent_path);
|
||||
}
|
||||
cmd.arg(&snap);
|
||||
cmd.stdout(std::process::Stdio::from(dest));
|
||||
cmd.stderr(std::process::Stdio::piped());
|
||||
|
||||
let out = cmd
|
||||
.spawn()
|
||||
.with_context(|| format!("spawn btrfs send {}", snap.display()))?
|
||||
.wait_with_output()
|
||||
.await
|
||||
.with_context(|| format!("wait on btrfs send {}", snap.display()))?;
|
||||
if !out.status.success() {
|
||||
// Nothing to clean up: the destination isn't ours. A partial
|
||||
// stream is the receiving end's problem, and `btrfs receive`
|
||||
// refuses to commit an incomplete subvolume anyway.
|
||||
bail!(
|
||||
"btrfs send {} failed: {}",
|
||||
snap.display(),
|
||||
String::from_utf8_lossy(&out.stderr).trim()
|
||||
);
|
||||
}
|
||||
tracing::info!(
|
||||
agent = %agent_name, snapshot = %snap.display(), parent = ?parent_name,
|
||||
"streamed agent snapshot into a passed descriptor"
|
||||
);
|
||||
Ok((String::new(), String::new()))
|
||||
}
|
||||
|
||||
/// `SetSubvolumeQuota` — set or clear a qgroup size limit on an agent
|
||||
/// subvolume (`btrfs qgroup limit <bytes|none> <…/agent_name>`). See the
|
||||
/// wire doc.
|
||||
|
|
@ -1051,12 +963,11 @@ mod tests {
|
|||
delete_agent_subvolume, du_command, du_total, ensure_agent_subvolume,
|
||||
ensure_btrfs_quota_in, exec_delete_agent_snapshot, exec_delete_agent_subvolume,
|
||||
exec_ensure_agent_subvolume, exec_measure_agent_disk, exec_read_subvolume_usage,
|
||||
exec_send_agent_snapshot_to_fd, exec_send_agent_snapshot_to_file, exec_set_subvolume_quota,
|
||||
exec_snapshot_agent_subvolume, exec_upgrade_agent_subvolume, existing_paths,
|
||||
is_btrfs_subvolume, is_on_btrfs, is_subvolume_root, open_export_dest, parse_du_total,
|
||||
read_subvolume_usage, send_agent_snapshot_to_fd, send_agent_snapshot_to_file,
|
||||
set_subvolume_quota, snapshot_agent_subvolume, snapshot_path, upgrade_agent_subvolume,
|
||||
validate_snapshot_name,
|
||||
exec_send_agent_snapshot_to_file, exec_set_subvolume_quota, exec_snapshot_agent_subvolume,
|
||||
exec_upgrade_agent_subvolume, existing_paths, is_btrfs_subvolume, is_on_btrfs,
|
||||
is_subvolume_root, open_export_dest, parse_du_total, read_subvolume_usage,
|
||||
send_agent_snapshot_to_file, set_subvolume_quota, snapshot_agent_subvolume, snapshot_path,
|
||||
upgrade_agent_subvolume, validate_snapshot_name,
|
||||
};
|
||||
use crate::shared::Spawn;
|
||||
use crate::shared::test_support::{MissingTool, ScriptSpawn, scratch};
|
||||
|
|
@ -1343,15 +1254,7 @@ mod tests {
|
|||
exec_send_agent_snapshot_to_file(bad, "hive-x", None, "out").await,
|
||||
"send-file",
|
||||
);
|
||||
want(
|
||||
exec_send_agent_snapshot_to_fd(bad, "hive-x", None, None).await,
|
||||
"send-fd",
|
||||
);
|
||||
}
|
||||
let err = exec_send_agent_snapshot_to_fd("iris", "hive-x", None, None)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(err.to_string().contains("no descriptor"), "{err}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -1379,14 +1282,6 @@ mod tests {
|
|||
exec_send_agent_snapshot_to_file("iris", "hive-x", Some(bad), "out").await,
|
||||
"send-file parent",
|
||||
);
|
||||
want(
|
||||
exec_send_agent_snapshot_to_fd("iris", bad, None, None).await,
|
||||
"send-fd snapshot",
|
||||
);
|
||||
want(
|
||||
exec_send_agent_snapshot_to_fd("iris", "hive-x", Some(bad), None).await,
|
||||
"send-fd parent",
|
||||
);
|
||||
}
|
||||
for bad in ["", ".", "..", "../x", "a/b", "a.b", "/etc/passwd"] {
|
||||
let err = exec_send_agent_snapshot_to_file("iris", "hive-x", None, bad)
|
||||
|
|
@ -2052,61 +1947,4 @@ esac"#;
|
|||
assert!(err.to_string().contains("does not exist"), "{err}");
|
||||
assert!(!fx.staging.join("none").exists());
|
||||
}
|
||||
|
||||
fn fd_for(path: &Path) -> std::os::fd::OwnedFd {
|
||||
std::os::fd::OwnedFd::from(std::fs::File::create(path).unwrap())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn export_to_fd_streams_into_the_descriptor() {
|
||||
let fx = Fx::new();
|
||||
snapshots(&fx, &["hive-a", "hive-p"]);
|
||||
let btrfs = fx.btrfs(LIKE_BTRFS);
|
||||
let env = fx.env(&btrfs, on_btrfs, not_subvol);
|
||||
let sink = fx.state.join("sink");
|
||||
|
||||
let (out, err) =
|
||||
send_agent_snapshot_to_fd(&env, "iris", "hive-a", Some("hive-p"), fd_for(&sink))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(out.is_empty() && err.is_empty());
|
||||
assert_eq!(std::fs::read(&sink).unwrap(), b"STREAM");
|
||||
send_agent_snapshot_to_fd(&env, "iris", "hive-a", None, fd_for(&sink))
|
||||
.await
|
||||
.unwrap();
|
||||
let (a, p) = (fx.snap("iris", "hive-a"), fx.snap("iris", "hive-p"));
|
||||
assert_eq!(
|
||||
fx.calls(),
|
||||
[
|
||||
format!("send -p {} {}", p.display(), a.display()),
|
||||
format!("send {}", a.display()),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn export_to_fd_needs_both_snapshots_and_reports_a_failed_send() {
|
||||
let fx = Fx::new();
|
||||
snapshots(&fx, &["hive-a"]);
|
||||
let sink = fx.state.join("sink");
|
||||
let btrfs = fx.btrfs(LIKE_BTRFS);
|
||||
let env = fx.env(&btrfs, on_btrfs, not_subvol);
|
||||
|
||||
let err = send_agent_snapshot_to_fd(&env, "iris", "hive-no", None, fd_for(&sink))
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(err.to_string().contains("does not exist"), "{err}");
|
||||
let err = send_agent_snapshot_to_fd(&env, "iris", "hive-a", Some("hive-no"), fd_for(&sink))
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(err.to_string().contains("parent snapshot"), "{err}");
|
||||
assert!(fx.calls().is_empty());
|
||||
|
||||
let failing = fx.btrfs(FAIL);
|
||||
let env = fx.env(&failing, on_btrfs, not_subvol);
|
||||
let err = send_agent_snapshot_to_fd(&env, "iris", "hive-a", None, fd_for(&sink))
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(err.to_string().contains("boom"), "{err}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue