feat(#1763): local-file half of btrfs send/receive migration transport
SendAgentSnapshotToFile priv op: btrfs send [-p <parent>] <snapshot> to a file under MIGRATE_STAGING_ROOT. Standalone-useful as a point-in-time snapshot export/backup today; the cross-hive ssh-piped leg (auth/trust design posted on #1763, awaiting mara/damocles steer) is a later, separate piece this doesn't block on. - hive-sh4re: PrivRequest::SendAgentSnapshotToFile + MIGRATE_STAGING_ROOT - hive-priv: validates names, refuses to overwrite an existing export, cleans up a partial file on btrfs send failure - hive-c0re: priv_client::send_agent_snapshot_to_file - hivectl: `hivectl subvol snapshot send <agent> <label> [--parent <label>] --dest <file>`
This commit is contained in:
parent
f5c8a6a8db
commit
90af0edab0
4 changed files with 201 additions and 2 deletions
|
|
@ -22,8 +22,8 @@ use std::path::{Path, PathBuf};
|
|||
use anyhow::{Context as _, Result, bail};
|
||||
use hive_sh4re::priv_proto::{
|
||||
AGENT_PREFIX, AGENT_RUNTIME_ROOT, AGENT_STATE_ROOT, BindMount, CredentialMount, InfraAction,
|
||||
InfraContainer, JournalQuery, META_DIR, NetworkIsolation, PRIV_SOCK, PrivEvent, PrivRequest,
|
||||
PrivResponse, PrivStream, PrivStreamLine, SIBLING_CONTAINERS,
|
||||
InfraContainer, JournalQuery, META_DIR, MIGRATE_STAGING_ROOT, NetworkIsolation, PRIV_SOCK,
|
||||
PrivEvent, PrivRequest, PrivResponse, PrivStream, PrivStreamLine, SIBLING_CONTAINERS,
|
||||
};
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
use tokio::net::unix::OwnedWriteHalf;
|
||||
|
|
@ -383,6 +383,27 @@ async fn exec(req: PrivRequest, writer: &mut OwnedWriteHalf) -> Result<(String,
|
|||
delete_agent_snapshot(agent_name, snapshot_name).await
|
||||
}
|
||||
|
||||
PrivRequest::SendAgentSnapshotToFile {
|
||||
ref agent_name,
|
||||
ref snapshot_name,
|
||||
ref parent_snapshot_name,
|
||||
ref dest_file_name,
|
||||
} => {
|
||||
validate_agent_name(agent_name)?;
|
||||
validate_snapshot_name(snapshot_name)?;
|
||||
if let Some(parent) = parent_snapshot_name {
|
||||
validate_snapshot_name(parent)?;
|
||||
}
|
||||
validate_credential_name(dest_file_name)?;
|
||||
send_agent_snapshot_to_file(
|
||||
agent_name,
|
||||
snapshot_name,
|
||||
parent_snapshot_name.as_deref(),
|
||||
dest_file_name,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
PrivRequest::SyncAgentTmpfiles { ref agents } => sync_agent_tmpfiles(agents).await,
|
||||
}
|
||||
}
|
||||
|
|
@ -1177,6 +1198,77 @@ async fn delete_agent_snapshot(agent_name: &str, snapshot_name: &str) -> Result<
|
|||
Ok((String::new(), String::new()))
|
||||
}
|
||||
|
||||
/// `SendAgentSnapshotToFile` — stream a read-only snapshot (optionally
|
||||
/// incremental against `parent_name`) to a file under
|
||||
/// `MIGRATE_STAGING_ROOT` via `btrfs send`. Local-file half of the
|
||||
/// inter-hive migration transport; see `PrivRequest::SendAgentSnapshotToFile`
|
||||
/// for the cross-hive follow-up.
|
||||
async fn send_agent_snapshot_to_file(
|
||||
agent_name: &str,
|
||||
snapshot_name: &str,
|
||||
parent_name: Option<&str>,
|
||||
dest_file_name: &str,
|
||||
) -> Result<(String, String)> {
|
||||
let snap = snapshot_path(agent_name, snapshot_name);
|
||||
if !snap.exists() {
|
||||
bail!(
|
||||
"snapshot {} does not exist — create it with `subvol snapshot create` first",
|
||||
snap.display()
|
||||
);
|
||||
}
|
||||
|
||||
std::fs::create_dir_all(MIGRATE_STAGING_ROOT)
|
||||
.with_context(|| format!("create {MIGRATE_STAGING_ROOT}"))?;
|
||||
let dest = Path::new(MIGRATE_STAGING_ROOT).join(dest_file_name);
|
||||
if dest.exists() {
|
||||
bail!(
|
||||
"{} already exists — pick a different destination or remove it first \
|
||||
(send never overwrites an existing export)",
|
||||
dest.display()
|
||||
);
|
||||
}
|
||||
let dest_file =
|
||||
std::fs::File::create(&dest).with_context(|| format!("create {}", dest.display()))?;
|
||||
|
||||
let mut cmd = Command::new("btrfs");
|
||||
cmd.arg("send");
|
||||
if let Some(parent) = parent_name {
|
||||
let parent_path = snapshot_path(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_file));
|
||||
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() {
|
||||
// Clean up a partial/failed export so a retry doesn't trip the
|
||||
// "already exists" guard on garbage.
|
||||
let _ = std::fs::remove_file(&dest);
|
||||
bail!(
|
||||
"btrfs send {} failed: {}",
|
||||
snap.display(),
|
||||
String::from_utf8_lossy(&out.stderr).trim()
|
||||
);
|
||||
}
|
||||
tracing::info!(
|
||||
agent = %agent_name, snapshot = %snap.display(), dest = %dest.display(),
|
||||
parent = ?parent_name, "exported agent snapshot to file"
|
||||
);
|
||||
Ok((dest.display().to_string(), 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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue