refactor(#2352): subvolume snapshot ops via daemon wire commands
This commit is contained in:
parent
860484a193
commit
7b54e7aa50
3 changed files with 138 additions and 29 deletions
|
|
@ -740,19 +740,7 @@ async fn main() -> Result<()> {
|
|||
} => stop(&socket, scope.to_scope(), graceful, no_wait).await,
|
||||
Cmd::Start { scope, no_wait } => start(&socket, scope.to_scope(), no_wait).await,
|
||||
Cmd::Restart { scope, graceful } => restart(&socket, scope.to_scope(), graceful).await,
|
||||
Cmd::Subvol { cmd } => match cmd {
|
||||
SubvolCmd::Upgrade { name, yes } => subvol_upgrade(&socket, &name, yes).await,
|
||||
SubvolCmd::Snapshot { cmd } => match cmd {
|
||||
SnapshotCmd::Create { name, label } => subvol_snapshot_create(&name, label).await,
|
||||
SnapshotCmd::Delete { name, label } => subvol_snapshot_delete(&name, &label).await,
|
||||
SnapshotCmd::Send {
|
||||
name,
|
||||
label,
|
||||
parent,
|
||||
dest,
|
||||
} => subvol_snapshot_send(&name, &label, parent.as_deref(), &dest).await,
|
||||
},
|
||||
},
|
||||
Cmd::Subvol { cmd } => dispatch_subvol(&socket, cmd).await,
|
||||
Cmd::Choom {
|
||||
name,
|
||||
resume_session,
|
||||
|
|
@ -1714,6 +1702,29 @@ fn single_agent_scope(name: &str) -> hive_host_sock::LifecycleScope {
|
|||
/// migration via hive-priv, then restart it. The restart is attempted
|
||||
/// regardless of the migration outcome so a failed migration never leaves
|
||||
/// the agent down; the migration error (if any) is surfaced afterwards.
|
||||
/// Route a `hivectl subvol …` subcommand. Split out of `main`'s top-level
|
||||
/// match so the CLI router stays within the clippy line budget and the
|
||||
/// subvolume-op subcommands are dispatched in one place.
|
||||
async fn dispatch_subvol(socket: &Path, cmd: SubvolCmd) -> Result<()> {
|
||||
match cmd {
|
||||
SubvolCmd::Upgrade { name, yes } => subvol_upgrade(socket, &name, yes).await,
|
||||
SubvolCmd::Snapshot { cmd } => match cmd {
|
||||
SnapshotCmd::Create { name, label } => {
|
||||
subvol_snapshot_create(socket, &name, label).await
|
||||
}
|
||||
SnapshotCmd::Delete { name, label } => {
|
||||
subvol_snapshot_delete(socket, &name, &label).await
|
||||
}
|
||||
SnapshotCmd::Send {
|
||||
name,
|
||||
label,
|
||||
parent,
|
||||
dest,
|
||||
} => subvol_snapshot_send(socket, &name, &label, parent.as_deref(), &dest).await,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async fn subvol_upgrade(socket: &Path, name: &str, yes: bool) -> Result<()> {
|
||||
if !agent_exists(name)? {
|
||||
bail!("no agent named {name:?} (no state dir under the agents root)");
|
||||
|
|
@ -1749,7 +1760,14 @@ async fn subvol_upgrade(socket: &Path, name: &str, yes: bool) -> Result<()> {
|
|||
.with_context(|| format!("waiting for {name} to stop before the migration"))?;
|
||||
|
||||
println!("migrating {name} state dir to a btrfs subvolume…");
|
||||
let upgrade = hive_c0re::priv_client::upgrade_agent_subvolume(name).await;
|
||||
let upgrade = daemon_request(
|
||||
socket,
|
||||
hive_host_sock::HostRequest::UpgradeSubvolume {
|
||||
name: name.to_owned(),
|
||||
},
|
||||
"upgrade",
|
||||
)
|
||||
.await;
|
||||
|
||||
// Always attempt the restart, even if the migration failed — don't leave
|
||||
// the agent down. Capture the result rather than `?`-ing it so a
|
||||
|
|
@ -1806,7 +1824,7 @@ async fn subvol_upgrade(socket: &Path, name: &str, yes: bool) -> Result<()> {
|
|||
/// — mara: "we are making up the rules here, lets go strict"). hive-priv
|
||||
/// enforces the same rules server-side, so this check is
|
||||
/// belt-and-suspenders (fail fast client-side with a clear message).
|
||||
async fn subvol_snapshot_create(name: &str, label: String) -> Result<()> {
|
||||
async fn subvol_snapshot_create(socket: &Path, name: &str, label: String) -> Result<()> {
|
||||
if !agent_exists(name)? {
|
||||
bail!("no agent named {name:?} (no state dir under the agents root)");
|
||||
}
|
||||
|
|
@ -1821,19 +1839,31 @@ async fn subvol_snapshot_create(name: &str, label: String) -> Result<()> {
|
|||
"snapshot label {label:?} must be [A-Za-z0-9_-] only (no \".\" — hive-priv rejects it)"
|
||||
);
|
||||
}
|
||||
let path = hive_c0re::priv_client::snapshot_agent_subvolume(name, &label)
|
||||
.await
|
||||
.with_context(|| format!("snapshot {name} state subvolume (label {label:?})"))?;
|
||||
println!("{path}");
|
||||
Ok(())
|
||||
// The daemon returns the snapshot's host path as a message line, which
|
||||
// `daemon_request` prints — same bare-path output as before.
|
||||
daemon_request(
|
||||
socket,
|
||||
hive_host_sock::HostRequest::SnapshotSubvolume {
|
||||
name: name.to_owned(),
|
||||
label,
|
||||
},
|
||||
"snapshot",
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// `subvol snapshot delete <agent> <label>` — remove a snapshot created by
|
||||
/// `subvol snapshot create`.
|
||||
async fn subvol_snapshot_delete(name: &str, label: &str) -> Result<()> {
|
||||
hive_c0re::priv_client::delete_agent_snapshot(name, label)
|
||||
.await
|
||||
.with_context(|| format!("delete {name} snapshot (label {label:?})"))?;
|
||||
async fn subvol_snapshot_delete(socket: &Path, name: &str, label: &str) -> Result<()> {
|
||||
daemon_request(
|
||||
socket,
|
||||
hive_host_sock::HostRequest::DeleteSnapshot {
|
||||
name: name.to_owned(),
|
||||
label: label.to_owned(),
|
||||
},
|
||||
"snapshot delete",
|
||||
)
|
||||
.await?;
|
||||
println!("deleted snapshot {label:?} for {name}");
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1844,16 +1874,25 @@ async fn subvol_snapshot_delete(name: &str, label: &str) -> Result<()> {
|
|||
/// charset check, which excludes `/`), written under the migrate-staging
|
||||
/// dir. Prints the resulting file's host path.
|
||||
async fn subvol_snapshot_send(
|
||||
socket: &Path,
|
||||
name: &str,
|
||||
label: &str,
|
||||
parent: Option<&str>,
|
||||
dest: &str,
|
||||
) -> Result<()> {
|
||||
let path = hive_c0re::priv_client::send_agent_snapshot_to_file(name, label, parent, dest)
|
||||
.await
|
||||
.with_context(|| format!("send {name} snapshot (label {label:?}) to file {dest:?}"))?;
|
||||
println!("{path}");
|
||||
Ok(())
|
||||
// The daemon returns the written file's host path as a message line,
|
||||
// which `daemon_request` prints — same bare-path output as before.
|
||||
daemon_request(
|
||||
socket,
|
||||
hive_host_sock::HostRequest::SendSnapshot {
|
||||
name: name.to_owned(),
|
||||
label: label.to_owned(),
|
||||
parent: parent.map(str::to_owned),
|
||||
dest: dest.to_owned(),
|
||||
},
|
||||
"snapshot send",
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Render a hive-wide stop/start response: one `<verb>: <name>` line per
|
||||
|
|
|
|||
|
|
@ -202,6 +202,19 @@ async fn dispatch(req: &HostRequest, coord: Arc<Coordinator>) -> HostResponse {
|
|||
HostRequest::QuotaEnable => handle_quota_enable().await?,
|
||||
HostRequest::QuotaLimit { name, limit } => handle_quota_limit(name, *limit).await?,
|
||||
HostRequest::QuotaShow { name } => handle_quota_show(name.as_deref()).await?,
|
||||
HostRequest::UpgradeSubvolume { name } => handle_upgrade_subvolume(name).await?,
|
||||
HostRequest::SnapshotSubvolume { name, label } => {
|
||||
handle_snapshot_subvolume(name, label).await?
|
||||
}
|
||||
HostRequest::DeleteSnapshot { name, label } => {
|
||||
handle_delete_snapshot(name, label).await?
|
||||
}
|
||||
HostRequest::SendSnapshot {
|
||||
name,
|
||||
label,
|
||||
parent,
|
||||
dest,
|
||||
} => handle_send_snapshot(name, label, parent.as_deref(), dest).await?,
|
||||
})
|
||||
}
|
||||
.await;
|
||||
|
|
@ -469,6 +482,39 @@ async fn handle_quota_show(name: Option<&str>) -> Result<HostResponse> {
|
|||
Ok(HostResponse::quota(rows))
|
||||
}
|
||||
|
||||
async fn handle_upgrade_subvolume(name: &str) -> Result<HostResponse> {
|
||||
crate::priv_client::upgrade_agent_subvolume(name)
|
||||
.await
|
||||
.with_context(|| format!("upgrade {name} state subvolume"))?;
|
||||
Ok(HostResponse::success())
|
||||
}
|
||||
|
||||
async fn handle_snapshot_subvolume(name: &str, label: &str) -> Result<HostResponse> {
|
||||
let path = crate::priv_client::snapshot_agent_subvolume(name, label)
|
||||
.await
|
||||
.with_context(|| format!("snapshot {name} state subvolume (label {label:?})"))?;
|
||||
Ok(HostResponse::messages(vec![path]))
|
||||
}
|
||||
|
||||
async fn handle_delete_snapshot(name: &str, label: &str) -> Result<HostResponse> {
|
||||
crate::priv_client::delete_agent_snapshot(name, label)
|
||||
.await
|
||||
.with_context(|| format!("delete {name} snapshot (label {label:?})"))?;
|
||||
Ok(HostResponse::success())
|
||||
}
|
||||
|
||||
async fn handle_send_snapshot(
|
||||
name: &str,
|
||||
label: &str,
|
||||
parent: Option<&str>,
|
||||
dest: &str,
|
||||
) -> Result<HostResponse> {
|
||||
let path = crate::priv_client::send_agent_snapshot_to_file(name, label, parent, dest)
|
||||
.await
|
||||
.with_context(|| format!("send {name} snapshot (label {label:?}) to file {dest:?}"))?;
|
||||
Ok(HostResponse::messages(vec![path]))
|
||||
}
|
||||
|
||||
async fn handle_matrix_sync_admin() -> Result<HostResponse> {
|
||||
require_matrix_present().await?;
|
||||
let register_token =
|
||||
|
|
|
|||
|
|
@ -188,6 +188,30 @@ pub enum HostRequest {
|
|||
#[serde(default)]
|
||||
name: Option<String>,
|
||||
},
|
||||
/// Migrate an agent's plain state dir to a btrfs subvolume via the
|
||||
/// privileged helper (`hivectl subvol upgrade`). The agent MUST already
|
||||
/// be stopped — the client orchestrates stop → this → start. Returns a
|
||||
/// bare success; the client prints its own progress lines.
|
||||
UpgradeSubvolume { name: String },
|
||||
/// Create a read-only btrfs snapshot of an agent's state subvolume
|
||||
/// (`hivectl subvol snapshot create`). `label` is validated client-side
|
||||
/// AND by hive-priv. Returns the snapshot's host path in
|
||||
/// [`HostResponse::messages`].
|
||||
SnapshotSubvolume { name: String, label: String },
|
||||
/// Delete a snapshot created by `SnapshotSubvolume` (`hivectl subvol
|
||||
/// snapshot delete`). Bare success; the client prints the confirmation.
|
||||
DeleteSnapshot { name: String, label: String },
|
||||
/// Export a snapshot to a local file via `btrfs send` (`hivectl subvol
|
||||
/// snapshot send`). `dest` is a bare filename (hive-priv rejects paths);
|
||||
/// `parent` names an optional parent snapshot for an incremental send.
|
||||
/// Returns the written file's host path in [`HostResponse::messages`].
|
||||
SendSnapshot {
|
||||
name: String,
|
||||
label: String,
|
||||
#[serde(default)]
|
||||
parent: Option<String>,
|
||||
dest: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// One agent's btrfs qgroup usage row — the [`HostRequest::QuotaShow`]
|
||||
|
|
|
|||
Loading…
Reference in a new issue