hive-c0re: hivectl subvol upgrade — migrate an agent state dir to a btrfs subvolume
New agents get a btrfs subvolume state root automatically when the host FS is btrfs, but agents that predate that migration are left on plain dirs and miss the subvolume feature set (snapshots, per-subvol usage/quota, send/receive migration). Add an opt-in operator verb to convert an existing plain-dir agent in place. btrfs cannot promote a directory to a subvolume in place, so the new privileged op stages a sibling subvolume mirroring the dir (create + `cp -a --reflink=auto` preserving ownership/permissions/xattrs + match the root's owner and mode), then atomically renames the original aside and the subvolume into place, then removes the original. Any failure before the swap leaves the original untouched; idempotent (no-op if already a subvolume) and btrfs-gated. The `hivectl subvol upgrade <agent> --yes` verb composes it client-side like `restart`: stop the agent so its state bind-mount is released, run the 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. - hive-sh4re: UpgradeAgentSubvolume priv request variant. - hive-priv: the migration handler plus stage/cleanup helpers. - hive-c0re: priv_client wrapper and the hivectl verb; regen CLI docs.
This commit is contained in:
parent
2966f682ce
commit
6b1dbebe5a
5 changed files with 351 additions and 0 deletions
|
|
@ -173,6 +173,18 @@ enum Cmd {
|
|||
#[command(subcommand)]
|
||||
cmd: QuotaCmd,
|
||||
},
|
||||
/// btrfs subvolume management for agent state dirs.
|
||||
///
|
||||
/// New agents get a btrfs subvolume state root automatically (when the
|
||||
/// host FS is btrfs); agents that predate that are left on plain dirs.
|
||||
/// `subvol upgrade <agent>` opts an existing plain-dir agent into the
|
||||
/// subvolume feature set (snapshots, per-subvol usage/quota, migration)
|
||||
/// by migrating its state dir in place. Requires the hive-c0re daemon
|
||||
/// (for the stop/start) and root (for the privileged migration).
|
||||
Subvol {
|
||||
#[command(subcommand)]
|
||||
cmd: SubvolCmd,
|
||||
},
|
||||
/// Emit the full CLI reference as `CommonMark` to stdout.
|
||||
///
|
||||
/// Hidden tooling command (not part of day-to-day operator admin):
|
||||
|
|
@ -487,6 +499,25 @@ enum AgentsCmd {
|
|||
RestartAll,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum SubvolCmd {
|
||||
/// Convert an existing plain-dir agent state root into a btrfs subvolume
|
||||
/// in place. Stops the agent (so its state bind-mount is released),
|
||||
/// migrates `…/agents/<name>/` to a subvolume preserving
|
||||
/// ownership/permissions/xattrs, then restarts it. Idempotent (no-op if
|
||||
/// already a subvolume) and safe (the original dir is left untouched on
|
||||
/// any failure before the final swap). Requires `--yes` since it bounces
|
||||
/// the agent and moves its state.
|
||||
Upgrade {
|
||||
/// Agent name (e.g. `damocles`, `iris`).
|
||||
name: String,
|
||||
/// Confirm: this stops the agent, migrates its state dir, and
|
||||
/// restarts it. Required — the command refuses without it.
|
||||
#[arg(long)]
|
||||
yes: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt()
|
||||
|
|
@ -546,6 +577,9 @@ async fn main() -> Result<()> {
|
|||
Cmd::Stop { scope, graceful } => stop(&socket, scope.to_scope(), graceful).await,
|
||||
Cmd::Start { scope } => start(&socket, scope.to_scope()).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,
|
||||
},
|
||||
Cmd::Choom { name, fresh } => choom(&name, fresh),
|
||||
Cmd::Quota { cmd } => match cmd {
|
||||
QuotaCmd::Enable => quota_enable().await,
|
||||
|
|
@ -1255,6 +1289,80 @@ async fn restart(socket: &Path, scope: hive_sh4re::LifecycleScope, graceful: boo
|
|||
start(socket, scope).await
|
||||
}
|
||||
|
||||
/// A [`LifecycleScope`](hive_sh4re::LifecycleScope) targeting exactly one
|
||||
/// agent by name (no infra containers, no all-agents flag).
|
||||
fn single_agent_scope(name: &str) -> hive_sh4re::LifecycleScope {
|
||||
hive_sh4re::LifecycleScope {
|
||||
agents: false,
|
||||
agent_names: vec![name.to_owned()],
|
||||
ci: false,
|
||||
forge: false,
|
||||
gateway: false,
|
||||
matrix: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// `subvol upgrade <agent>` — migrate an existing plain-dir agent state root
|
||||
/// to a btrfs subvolume. Composed client-side (like `restart`): stop the
|
||||
/// agent so its state bind-mount is released, run the privileged in-place
|
||||
/// 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.
|
||||
async fn subvol_upgrade(socket: &Path, name: &str, yes: bool) -> Result<()> {
|
||||
if !is_agent(name) {
|
||||
bail!("no agent named {name:?} (no state dir under the agents root)");
|
||||
}
|
||||
if !yes {
|
||||
bail!(
|
||||
"`subvol upgrade {name}` stops the agent, migrates its state dir to a btrfs \
|
||||
subvolume, then restarts it. Re-run with --yes to proceed."
|
||||
);
|
||||
}
|
||||
|
||||
println!("stopping {name} (releasing its state bind-mount)…");
|
||||
let stop_resp = hive_c0re::client::request(
|
||||
socket,
|
||||
hive_sh4re::HostRequest::Stop {
|
||||
scope: single_agent_scope(name),
|
||||
graceful: false,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("connect to daemon socket {}", socket.display()))?;
|
||||
if !stop_resp.ok {
|
||||
bail!(
|
||||
"stop {name}: {}",
|
||||
stop_resp.error.as_deref().unwrap_or("unknown error")
|
||||
);
|
||||
}
|
||||
|
||||
println!("migrating {name} state dir to a btrfs subvolume…");
|
||||
let upgrade = hive_c0re::priv_client::upgrade_agent_subvolume(name).await;
|
||||
|
||||
// Always restart, even if the migration failed — don't leave the agent down.
|
||||
println!("starting {name}…");
|
||||
let start_resp = hive_c0re::client::request(
|
||||
socket,
|
||||
hive_sh4re::HostRequest::Start {
|
||||
scope: single_agent_scope(name),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("connect to daemon socket {}", socket.display()))?;
|
||||
|
||||
// Surface the migration error first — it's the meaningful one — but only
|
||||
// after the restart attempt above.
|
||||
upgrade.with_context(|| format!("upgrade {name} state subvolume"))?;
|
||||
if !start_resp.ok {
|
||||
bail!(
|
||||
"migration succeeded but restarting {name} failed: {}",
|
||||
start_resp.error.as_deref().unwrap_or("unknown error")
|
||||
);
|
||||
}
|
||||
println!("upgraded {name} to a btrfs subvolume and restarted it");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Render a hive-wide stop/start response: one `<verb>: <name>` line per
|
||||
/// touched container, then surface any aggregated per-target failure as a
|
||||
/// non-zero exit. `verb` is the past-tense word printed per item
|
||||
|
|
|
|||
Loading…
Reference in a new issue