feat(#2391): btrfs SnapshotAgentSubvolume/DeleteAgentSnapshot priv ops
Adds the first missing piece from #2391's migration-gaps list: a read-only btrfs snapshot priv op so hivectl migrate can freeze a consistent point-in-time copy of an agent's state subvolume for btrfs send, without stopping the live agent. - PrivRequest::SnapshotAgentSubvolume / DeleteAgentSnapshot (hive-sh4re) - hive-priv handlers: btrfs subvolume snapshot -r / delete, sibling dot-prefixed path (<AGENT_STATE_ROOT>/.<agent>.snapshot.<label>) - hive-c0re::priv_client wrappers - hivectl subvol snapshot / delete-snapshot verbs (no agent stop needed — btrfs snapshots are atomic against a live subvolume) Does not yet wire actual btrfs send/receive or the hivectl migrate verb — those stay tracked on #2391 as separate follow-up pieces.
This commit is contained in:
parent
0bfe47458c
commit
7799e0762a
4 changed files with 219 additions and 0 deletions
|
|
@ -365,6 +365,24 @@ async fn exec(req: PrivRequest, writer: &mut OwnedWriteHalf) -> Result<(String,
|
|||
upgrade_agent_subvolume(agent_name).await
|
||||
}
|
||||
|
||||
PrivRequest::SnapshotAgentSubvolume {
|
||||
ref agent_name,
|
||||
ref snapshot_name,
|
||||
} => {
|
||||
validate_agent_name(agent_name)?;
|
||||
validate_credential_name(snapshot_name)?;
|
||||
snapshot_agent_subvolume(agent_name, snapshot_name).await
|
||||
}
|
||||
|
||||
PrivRequest::DeleteAgentSnapshot {
|
||||
ref agent_name,
|
||||
ref snapshot_name,
|
||||
} => {
|
||||
validate_agent_name(agent_name)?;
|
||||
validate_credential_name(snapshot_name)?;
|
||||
delete_agent_snapshot(agent_name, snapshot_name).await
|
||||
}
|
||||
|
||||
PrivRequest::SyncAgentTmpfiles { ref agents } => sync_agent_tmpfiles(agents).await,
|
||||
}
|
||||
}
|
||||
|
|
@ -1059,6 +1077,86 @@ async fn upgrade_agent_subvolume(agent_name: &str) -> Result<(String, String)> {
|
|||
))
|
||||
}
|
||||
|
||||
/// Derive a snapshot's path from the agent name + label: a dot-prefixed
|
||||
/// sibling of the agent's state root so it can never collide with a real
|
||||
/// agent directory (`validate_agent_name` rejects dot-prefixed names).
|
||||
fn snapshot_path(agent_name: &str, snapshot_name: &str) -> PathBuf {
|
||||
PathBuf::from(AGENT_STATE_ROOT).join(format!(".{agent_name}.snapshot.{snapshot_name}"))
|
||||
}
|
||||
|
||||
/// `SnapshotAgentSubvolume` — create a read-only btrfs snapshot of an
|
||||
/// agent's state subvolume, for `btrfs send` to stream from during
|
||||
/// inter-hive migration. See the wire doc.
|
||||
async fn snapshot_agent_subvolume(
|
||||
agent_name: &str,
|
||||
snapshot_name: &str,
|
||||
) -> Result<(String, String)> {
|
||||
let agent_root = PathBuf::from(AGENT_STATE_ROOT).join(agent_name);
|
||||
if !is_btrfs_subvolume(&agent_root) {
|
||||
bail!(
|
||||
"{} is not a btrfs subvolume — nothing to snapshot (run `hivectl subvol upgrade` first)",
|
||||
agent_root.display()
|
||||
);
|
||||
}
|
||||
let snap = snapshot_path(agent_name, snapshot_name);
|
||||
if snap.exists() {
|
||||
bail!(
|
||||
"snapshot {} already exists — delete it first or pick a different name",
|
||||
snap.display()
|
||||
);
|
||||
}
|
||||
let out = Command::new("btrfs")
|
||||
.args(["subvolume", "snapshot", "-r"])
|
||||
.arg(&agent_root)
|
||||
.arg(&snap)
|
||||
.output()
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"spawn btrfs subvolume snapshot -r {} {}",
|
||||
agent_root.display(),
|
||||
snap.display()
|
||||
)
|
||||
})?;
|
||||
if !out.status.success() {
|
||||
bail!(
|
||||
"btrfs subvolume snapshot -r {} {} failed: {}",
|
||||
agent_root.display(),
|
||||
snap.display(),
|
||||
String::from_utf8_lossy(&out.stderr).trim()
|
||||
);
|
||||
}
|
||||
tracing::info!(
|
||||
agent = %agent_name, snapshot = %snap.display(),
|
||||
"created read-only agent state snapshot"
|
||||
);
|
||||
Ok((snap.display().to_string(), String::new()))
|
||||
}
|
||||
|
||||
/// `DeleteAgentSnapshot` — delete a previously-created read-only snapshot.
|
||||
/// No-op if the path doesn't exist. See the wire doc.
|
||||
async fn delete_agent_snapshot(agent_name: &str, snapshot_name: &str) -> Result<(String, String)> {
|
||||
let snap = snapshot_path(agent_name, snapshot_name);
|
||||
if !snap.exists() {
|
||||
return Ok((String::new(), String::new()));
|
||||
}
|
||||
let out = Command::new("btrfs")
|
||||
.args(["subvolume", "delete"])
|
||||
.arg(&snap)
|
||||
.output()
|
||||
.await
|
||||
.with_context(|| format!("spawn btrfs subvolume delete {}", snap.display()))?;
|
||||
if !out.status.success() {
|
||||
bail!(
|
||||
"btrfs subvolume delete {} failed: {}",
|
||||
snap.display(),
|
||||
String::from_utf8_lossy(&out.stderr).trim()
|
||||
);
|
||||
}
|
||||
tracing::info!(agent = %agent_name, snapshot = %snap.display(), "deleted agent state snapshot");
|
||||
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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue