hivectl: rename hivectl agents to hivectl agent <name> <verb>
This commit is contained in:
parent
f48ba3ca0d
commit
03afbd1316
19 changed files with 367 additions and 525 deletions
|
|
@ -1,15 +1,18 @@
|
|||
//! `hivectl agents` — everything scoped to a managed agent: container
|
||||
//! lifecycle over the host admin socket
|
||||
//! (list/restart/restart-all/pause/resume/spawn/kill/destroy/rebuild/
|
||||
//! set-parent/set-limits), plus the `quota` and `subvol` groups, whose
|
||||
//! handlers live in their own modules.
|
||||
//! `hivectl agent <name>` — everything scoped to ONE managed agent:
|
||||
//! container lifecycle over the host admin socket (restart/pause/resume/
|
||||
//! spawn/kill/destroy/rebuild/set-parent/set-limits/choom), plus the
|
||||
//! `quota` and `subvol` groups, whose handlers live in their own modules.
|
||||
//! `agents_list` (`hivectl list-agents`) is the one genuinely hive-wide
|
||||
//! read that lives in this module too since it shares the same daemon
|
||||
//! request as everything else here, even though it's dispatched from a
|
||||
//! top-level `Cmd` variant, not `AgentCmd`.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use hive_host_sock::HostRequest;
|
||||
|
||||
use crate::cli::{AgentsCmd, QuotaCmd};
|
||||
use crate::cli::{AgentCmd, AgentQuotaCmd};
|
||||
use crate::dag_progress::wait_for_dags;
|
||||
use crate::util::render;
|
||||
|
||||
|
|
@ -33,11 +36,11 @@ async fn agents_restart(socket: &Path, name: &str, no_wait: bool) -> Result<()>
|
|||
}
|
||||
}
|
||||
|
||||
/// `hivectl agents list` — fetch the per-agent status roster from the
|
||||
/// `hivectl list-agents` — fetch the per-agent status roster from the
|
||||
/// daemon (`HostRequest::AgentStatus`) and render it as a padded table,
|
||||
/// or the raw JSON rows with `--json`. Reuses the dashboard's
|
||||
/// `ContainerView` aggregation, so the CLI and the web UI never drift.
|
||||
async fn agents_list(socket: &Path, json: bool) -> Result<()> {
|
||||
pub(crate) async fn agents_list(socket: &Path, json: bool) -> Result<()> {
|
||||
let resp = crate::client::request(socket, hive_host_sock::HostRequest::AgentStatus)
|
||||
.await
|
||||
.with_context(|| format!("connect to daemon socket {}", socket.display()))?;
|
||||
|
|
@ -112,28 +115,7 @@ async fn agents_list(socket: &Path, json: bool) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn agents_restart_all(socket: &Path, no_wait: bool) -> Result<()> {
|
||||
let resp = crate::client::request(socket, hive_host_sock::HostRequest::RestartAll)
|
||||
.await
|
||||
.with_context(|| format!("connect to daemon socket {}", socket.display()))?;
|
||||
let agents = resp.agents.as_deref().unwrap_or(&[]);
|
||||
if agents.is_empty() {
|
||||
println!("restart-all: no managed containers found");
|
||||
} else {
|
||||
for a in agents {
|
||||
println!("restart queued: {a}");
|
||||
}
|
||||
}
|
||||
if !resp.ok {
|
||||
bail!(
|
||||
"restart-all: {}",
|
||||
resp.error.as_deref().unwrap_or("unknown error")
|
||||
);
|
||||
}
|
||||
wait_for_dags(socket, resp.queued_dags.unwrap_or_default(), no_wait).await
|
||||
}
|
||||
|
||||
/// `hivectl agents pause|resume` — flip the agent's pause marker. Not a
|
||||
/// `hivectl agent <name> pause|resume` — flip the agent's pause marker. Not a
|
||||
/// DAG, so there's nothing to wait on: the daemon writes the marker and
|
||||
/// the harness picks it up on its next poll.
|
||||
async fn set_paused(socket: &Path, name: &str, paused: bool) -> Result<()> {
|
||||
|
|
@ -159,41 +141,36 @@ async fn set_paused(socket: &Path, name: &str, paused: bool) -> Result<()> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Dispatch `hivectl agents <verb>` — container lifecycle over the host
|
||||
/// admin socket.
|
||||
pub(crate) async fn run_agents(socket: &Path, cmd: AgentsCmd) -> Result<()> {
|
||||
/// Dispatch `hivectl agent <name> <verb>` — container lifecycle over the
|
||||
/// host admin socket, all scoped to the single `name` hoisted from the
|
||||
/// parent command.
|
||||
pub(crate) async fn run_agent(socket: &Path, name: &str, cmd: AgentCmd) -> Result<()> {
|
||||
match cmd {
|
||||
AgentsCmd::List { json } => agents_list(socket, json).await,
|
||||
AgentsCmd::Restart { name, no_wait } => agents_restart(socket, &name, no_wait).await,
|
||||
AgentsCmd::RestartAll { no_wait } => agents_restart_all(socket, no_wait).await,
|
||||
AgentsCmd::Pause { name } => set_paused(socket, &name, true).await,
|
||||
AgentsCmd::Resume { name } => set_paused(socket, &name, false).await,
|
||||
AgentsCmd::Spawn { name } => {
|
||||
let name = crate::util::parse_ident(&name)?;
|
||||
AgentCmd::Restart { no_wait } => agents_restart(socket, name, no_wait).await,
|
||||
AgentCmd::Pause => set_paused(socket, name, true).await,
|
||||
AgentCmd::Resume => set_paused(socket, name, false).await,
|
||||
AgentCmd::Spawn => {
|
||||
let name = crate::util::parse_ident(name)?;
|
||||
render(crate::client::request(socket, HostRequest::Spawn { name }).await?)
|
||||
}
|
||||
AgentsCmd::RequestSpawn { name } => {
|
||||
let name = crate::util::parse_ident(&name)?;
|
||||
AgentCmd::RequestSpawn => {
|
||||
let name = crate::util::parse_ident(name)?;
|
||||
render(crate::client::request(socket, HostRequest::RequestSpawn { name }).await?)
|
||||
}
|
||||
AgentsCmd::Kill { name } => {
|
||||
let name = crate::util::parse_ident(&name)?;
|
||||
AgentCmd::Kill => {
|
||||
let name = crate::util::parse_ident(name)?;
|
||||
render(crate::client::request(socket, HostRequest::Kill { name }).await?)
|
||||
}
|
||||
AgentsCmd::Destroy { name, purge } => {
|
||||
let name = crate::util::parse_ident(&name)?;
|
||||
AgentCmd::Destroy { purge } => {
|
||||
let name = crate::util::parse_ident(name)?;
|
||||
render(crate::client::request(socket, HostRequest::Destroy { name, purge }).await?)
|
||||
}
|
||||
AgentsCmd::Rebuild { name } => {
|
||||
let name = crate::util::parse_ident(&name)?;
|
||||
AgentCmd::Rebuild => {
|
||||
let name = crate::util::parse_ident(name)?;
|
||||
render(crate::client::request(socket, HostRequest::Rebuild { name }).await?)
|
||||
}
|
||||
AgentsCmd::SetParent {
|
||||
child,
|
||||
parent,
|
||||
root,
|
||||
} => {
|
||||
let child = crate::util::parse_ident(&child)?;
|
||||
AgentCmd::SetParent { parent, root } => {
|
||||
let child = crate::util::parse_ident(name)?;
|
||||
let new_parent = if root {
|
||||
None
|
||||
} else {
|
||||
|
|
@ -204,16 +181,15 @@ pub(crate) async fn run_agents(socket: &Path, cmd: AgentsCmd) -> Result<()> {
|
|||
.await?,
|
||||
)
|
||||
}
|
||||
AgentsCmd::SetLimits {
|
||||
name,
|
||||
AgentCmd::SetLimits {
|
||||
cpu_quota,
|
||||
memory_max,
|
||||
reset,
|
||||
} => {
|
||||
let name = crate::util::parse_ident(&name)?;
|
||||
let name = crate::util::parse_ident(name)?;
|
||||
// `--reset` is the only way to reach an all-`None` request;
|
||||
// clap rejects a bare `set-limits <name>` with neither flag,
|
||||
// so a forgotten value can't silently clear the overrides.
|
||||
// clap rejects a bare `set-limits` with neither flag, so a
|
||||
// forgotten value can't silently clear the overrides.
|
||||
let (cpu_quota, memory_max) = if reset {
|
||||
(None, None)
|
||||
} else {
|
||||
|
|
@ -231,13 +207,16 @@ pub(crate) async fn run_agents(socket: &Path, cmd: AgentsCmd) -> Result<()> {
|
|||
.await?,
|
||||
)
|
||||
}
|
||||
// `quota` and `subvol` keep their own modules — this arm is just
|
||||
// the reparenting glue that moved them under `agents`.
|
||||
AgentsCmd::Quota { cmd } => match cmd {
|
||||
QuotaCmd::Enable => crate::quota::quota_enable(socket).await,
|
||||
QuotaCmd::Show { name } => crate::quota::quota_show(socket, name.as_deref()).await,
|
||||
QuotaCmd::Set { name, size } => crate::quota::quota_limit(socket, &name, &size).await,
|
||||
AgentCmd::Choom { resume_session } => {
|
||||
crate::choom::choom(socket, name, resume_session.as_deref()).await
|
||||
}
|
||||
// `quota` and `subvol` keep their own modules — these arms are
|
||||
// just the reparenting glue that hoists `name` in from the
|
||||
// parent `agent <name>` command.
|
||||
AgentCmd::Quota { cmd } => match cmd {
|
||||
AgentQuotaCmd::Show => crate::quota::quota_show(socket, name).await,
|
||||
AgentQuotaCmd::Set { size } => crate::quota::quota_limit(socket, name, &size).await,
|
||||
},
|
||||
AgentsCmd::Subvol { cmd } => crate::subvol::dispatch_subvol(socket, cmd).await,
|
||||
AgentCmd::Subvol { cmd } => crate::subvol::dispatch_subvol(socket, name, cmd).await,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
//! `hivectl choom <agent>` — drop into an interactive Claude session inside
|
||||
//! `hivectl agent <name> choom` — drop into an interactive Claude session inside
|
||||
//! an agent container by exec-ing `machinectl shell` running claude as the
|
||||
//! agent user, mirroring the harness's per-turn claude invocation.
|
||||
|
||||
|
|
|
|||
|
|
@ -17,8 +17,9 @@ recovery / debugging verbs.\
|
|||
)]
|
||||
pub struct Cli {
|
||||
/// Path to the hive-c0re host admin socket, used by the daemon-assisted
|
||||
/// verbs (`agents`, `stop`, `start`). Global: accepted before or after
|
||||
/// the subcommand. Verbs that don't talk to the daemon ignore it.
|
||||
/// verbs (`agent`, `list-agents`, `stop`, `start`). Global: accepted
|
||||
/// before or after the subcommand. Verbs that don't talk to the daemon
|
||||
/// ignore it.
|
||||
#[arg(long, global = true, default_value = DEFAULT_HOST_SOCKET)]
|
||||
pub(crate) socket: PathBuf,
|
||||
#[command(subcommand)]
|
||||
|
|
@ -61,14 +62,36 @@ pub enum Cmd {
|
|||
#[command(subcommand)]
|
||||
cmd: GatewayCmd,
|
||||
},
|
||||
/// Agent container management.
|
||||
/// Lifecycle actions on ONE managed agent container. Needs the
|
||||
/// hive-c0re daemon running.
|
||||
///
|
||||
/// Lifecycle actions on managed agent containers. Needs the hive-c0re
|
||||
/// daemon running.
|
||||
Agents {
|
||||
/// Everything here targets a single named agent (`hivectl agent foo
|
||||
/// restart`, `hivectl agent foo choom`, …) — anything that acts
|
||||
/// hive-wide lives at the top level instead (`list-agents`,
|
||||
/// `restart`/`stop`/`start` with a scope, `quota-enable`).
|
||||
Agent {
|
||||
/// Agent name (e.g. `damocles`, `iris`).
|
||||
name: String,
|
||||
#[command(subcommand)]
|
||||
cmd: AgentsCmd,
|
||||
cmd: AgentCmd,
|
||||
},
|
||||
/// Show all managed agents with their status and technical state.
|
||||
///
|
||||
/// Global — not scoped to one agent, so it lives at the top level
|
||||
/// rather than under `hivectl agent <name>`. Needs the hive-c0re
|
||||
/// daemon running.
|
||||
ListAgents {
|
||||
/// Emit the raw JSON rows instead of the padded table (for
|
||||
/// scripting). The table is the default human-readable shape.
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
},
|
||||
/// Enable btrfs qgroup accounting on the agent-state filesystem.
|
||||
///
|
||||
/// Global one-shot toggle (not per-agent), hence top-level rather than
|
||||
/// under `hivectl agent <name>`. Idempotent — safe to re-run. Once
|
||||
/// enabled, `hivectl agent <name> quota show`/`quota set` work.
|
||||
QuotaEnable,
|
||||
/// Operator approval queue: list, approve, or deny pending requests.
|
||||
///
|
||||
/// Needs the hive-c0re daemon running.
|
||||
|
|
@ -98,22 +121,6 @@ pub enum Cmd {
|
|||
#[arg(long)]
|
||||
wg_endpoint: Option<String>,
|
||||
},
|
||||
/// Open an interactive Claude session inside an agent container.
|
||||
///
|
||||
/// A fresh session by default, or resume a prior one. Requires root
|
||||
/// and a running container.
|
||||
Choom {
|
||||
/// Agent name (e.g. `damocles`, `iris`).
|
||||
name: String,
|
||||
/// Resume a prior claude session by its session id, passed
|
||||
/// through as `claude --resume <value>` (claude's `--continue`
|
||||
/// takes no value — it resumes the cwd's latest session, which
|
||||
/// is the harness's, so choom never uses it; this flag matches
|
||||
/// the claude flag it maps to). Omit for a fresh blank session.
|
||||
/// A value is required when the flag is given.
|
||||
#[arg(long = "resume", value_name = "SESSION")]
|
||||
resume_session: Option<String>,
|
||||
},
|
||||
/// Stop containers hive-wide in one operator action.
|
||||
///
|
||||
/// Bare `hivectl stop` stops everything; scope flags narrow it to
|
||||
|
|
@ -435,128 +442,68 @@ pub enum WgCmd {
|
|||
Status,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum QuotaCmd {
|
||||
/// Enable btrfs qgroup accounting on the agent-state filesystem.
|
||||
///
|
||||
/// Run once before `show` / `limit`. No-op on non-btrfs hosts.
|
||||
Enable,
|
||||
/// Report per-agent disk usage from btrfs qgroups (all agents, or one
|
||||
/// by name).
|
||||
Show {
|
||||
/// Agent to show (omit for all agents with a state subvolume).
|
||||
name: Option<String>,
|
||||
},
|
||||
/// Set or clear an agent's disk-usage quota.
|
||||
///
|
||||
/// Named `set` rather than `set-quota` because the enclosing `quota`
|
||||
/// group already carries the noun — `agents quota set iris 5G`. The
|
||||
/// `set-<noun>` spelling stays for the flat verbs (`set-parent`,
|
||||
/// `set-limits`), which have no group to inherit it from.
|
||||
Set {
|
||||
/// Agent whose state subvolume to limit.
|
||||
name: String,
|
||||
/// Size cap (`5G`, `500M`, `1073741824`) or `none` to clear.
|
||||
size: String,
|
||||
},
|
||||
}
|
||||
|
||||
// Default host admin socket path. Shared with `hive-c0re`'s `main.rs`
|
||||
// default via `hive_host_sock::HOST_SOCKET` — the daemon binds there
|
||||
// and `hivectl agents` connects to it.
|
||||
// and `hivectl` connects to it.
|
||||
pub(crate) use hive_host_sock::HOST_SOCKET as DEFAULT_HOST_SOCKET;
|
||||
|
||||
/// Verbs under `hivectl agent <name> <verb>` — every one of these targets
|
||||
/// the single agent named on the parent command, so none of them carry
|
||||
/// their own `name` field.
|
||||
#[derive(Subcommand)]
|
||||
pub enum AgentsCmd {
|
||||
/// Show all managed agents with their status and technical state.
|
||||
List {
|
||||
/// Emit the raw JSON rows instead of the padded table (for
|
||||
/// scripting). The table is the default human-readable shape.
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
},
|
||||
/// Stop and start a single agent container without rebuilding config.
|
||||
pub enum AgentCmd {
|
||||
/// Stop and start this agent container without rebuilding config.
|
||||
Restart {
|
||||
/// Agent name (e.g. `damocles`, `ruth`).
|
||||
name: String,
|
||||
/// Return immediately after the restart DAG is queued.
|
||||
#[arg(long)]
|
||||
no_wait: bool,
|
||||
},
|
||||
/// Restart all managed agent containers.
|
||||
RestartAll {
|
||||
/// Return immediately after the restart DAGs are queued.
|
||||
#[arg(long)]
|
||||
no_wait: bool,
|
||||
},
|
||||
/// Park an agent's turn loop, leaving the container running.
|
||||
/// Park this agent's turn loop, leaving the container running.
|
||||
///
|
||||
/// The harness stops driving turns but keeps serving its web UI and
|
||||
/// MCP daemons, so the container, its mounts and its warm caches stay
|
||||
/// up while it burns no tokens. Inbox messages queue unacked and the
|
||||
/// backlog drains on `resume`. Sticky: it survives a restart, and
|
||||
/// pausing a stopped agent makes it come up paused.
|
||||
Pause {
|
||||
/// Agent name.
|
||||
name: String,
|
||||
},
|
||||
/// Resume a paused agent — it drains whatever queued up while parked.
|
||||
Resume {
|
||||
/// Agent name.
|
||||
name: String,
|
||||
},
|
||||
/// Spawn a new agent container directly, bypassing the approval queue.
|
||||
Pause,
|
||||
/// Resume this paused agent — it drains whatever queued up while parked.
|
||||
Resume,
|
||||
/// Spawn this agent container directly, bypassing the approval queue.
|
||||
///
|
||||
/// Operator-on-the-host only; use `request-spawn` for an approval-gated
|
||||
/// spawn.
|
||||
Spawn {
|
||||
/// Agent name (e.g. `iris`).
|
||||
name: String,
|
||||
},
|
||||
Spawn,
|
||||
/// Queue a spawn request for operator approval.
|
||||
RequestSpawn {
|
||||
/// Agent name.
|
||||
name: String,
|
||||
},
|
||||
/// Stop a managed container (graceful).
|
||||
Kill {
|
||||
/// Agent name.
|
||||
name: String,
|
||||
},
|
||||
/// Tear down a sub-agent container, keeping its state by default. No
|
||||
/// undo.
|
||||
RequestSpawn,
|
||||
/// Stop this managed container (graceful).
|
||||
Kill,
|
||||
/// Tear down this sub-agent container, keeping its state by default.
|
||||
/// No undo.
|
||||
Destroy {
|
||||
/// Agent name.
|
||||
name: String,
|
||||
/// Also wipe the agent's state dirs (config + creds + notes).
|
||||
#[arg(long)]
|
||||
purge: bool,
|
||||
},
|
||||
/// Apply pending config to a managed container.
|
||||
Rebuild {
|
||||
/// Agent name.
|
||||
name: String,
|
||||
},
|
||||
/// Move an agent in the topology tree — under a new parent, or to root.
|
||||
/// Apply pending config to this managed container.
|
||||
Rebuild,
|
||||
/// Move this agent in the topology tree — under a new parent, or to
|
||||
/// root.
|
||||
SetParent {
|
||||
/// Agent to move.
|
||||
child: String,
|
||||
/// New parent agent name. Mutually exclusive with `--root`.
|
||||
#[arg(long, conflicts_with = "root", required_unless_present = "root")]
|
||||
parent: Option<String>,
|
||||
/// Promote `child` to root (no parent).
|
||||
/// Promote this agent to root (no parent).
|
||||
#[arg(long)]
|
||||
root: bool,
|
||||
},
|
||||
/// Declare an agent's CPU/memory limits, overriding the hive-wide defaults.
|
||||
/// Declare this agent's CPU/memory limits, overriding the hive-wide
|
||||
/// defaults.
|
||||
///
|
||||
/// Replaces the agent's whole override entry rather than merging into
|
||||
/// it: any limit you don't pass returns to the hive-wide default. To
|
||||
/// change one and keep the other, pass both. Disk is a separate
|
||||
/// resource with its own group — see `agents quota`.
|
||||
/// resource with its own group — see `quota`.
|
||||
SetLimits {
|
||||
/// Agent name.
|
||||
name: String,
|
||||
/// systemd `CPUQuota=` value, e.g. `400%` (100% = one full core).
|
||||
#[arg(long, conflicts_with = "reset")]
|
||||
cpu_quota: Option<String>,
|
||||
|
|
@ -573,15 +520,29 @@ pub enum AgentsCmd {
|
|||
)]
|
||||
reset: bool,
|
||||
},
|
||||
/// Per-agent disk accounting + optional quotas via btrfs qgroups.
|
||||
/// Open an interactive Claude session inside this agent's container.
|
||||
///
|
||||
/// Opt-in: enable qgroup accounting, then report per-agent usage or
|
||||
/// cap an agent. No-op on non-btrfs hosts.
|
||||
/// A fresh session by default, or resume a prior one. Requires root
|
||||
/// and a running container.
|
||||
Choom {
|
||||
/// Resume a prior claude session by its session id, passed
|
||||
/// through as `claude --resume <value>` (claude's `--continue`
|
||||
/// takes no value — it resumes the cwd's latest session, which
|
||||
/// is the harness's, so choom never uses it; this flag matches
|
||||
/// the claude flag it maps to). Omit for a fresh blank session.
|
||||
/// A value is required when the flag is given.
|
||||
#[arg(long = "resume", value_name = "SESSION")]
|
||||
resume_session: Option<String>,
|
||||
},
|
||||
/// This agent's disk accounting + optional quota via btrfs qgroups.
|
||||
///
|
||||
/// Needs `hivectl quota-enable` run once hive-wide first. No-op on
|
||||
/// non-btrfs hosts.
|
||||
Quota {
|
||||
#[command(subcommand)]
|
||||
cmd: QuotaCmd,
|
||||
cmd: AgentQuotaCmd,
|
||||
},
|
||||
/// btrfs subvolume management for agent state dirs.
|
||||
/// btrfs subvolume management for this agent's state dir.
|
||||
///
|
||||
/// Upgrade an existing plain-dir agent's state into a btrfs subvolume
|
||||
/// so it gains snapshots and per-subvol usage/quota.
|
||||
|
|
@ -591,6 +552,17 @@ pub enum AgentsCmd {
|
|||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
pub enum AgentQuotaCmd {
|
||||
/// Report this agent's disk usage from btrfs qgroups.
|
||||
Show,
|
||||
/// Set or clear this agent's disk-usage quota.
|
||||
Set {
|
||||
/// Size cap (`5G`, `500M`, `1073741824`) or `none` to clear.
|
||||
size: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Operator approval queue: list, approve, or deny pending requests.
|
||||
#[derive(Subcommand)]
|
||||
pub enum ApprovalsCmd {
|
||||
|
|
@ -615,14 +587,12 @@ pub enum SubvolCmd {
|
|||
///
|
||||
/// Bounces the agent to migrate its state, so it requires `--yes`.
|
||||
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,
|
||||
},
|
||||
/// Read-only snapshots of an agent's state subvolume.
|
||||
/// Read-only snapshots of this agent's state subvolume.
|
||||
Snapshot {
|
||||
#[command(subcommand)]
|
||||
cmd: SnapshotCmd,
|
||||
|
|
@ -633,8 +603,6 @@ pub enum SubvolCmd {
|
|||
pub enum SnapshotCmd {
|
||||
/// Create a read-only snapshot (agent must already be a subvolume).
|
||||
Create {
|
||||
/// Agent name (e.g. `damocles`, `iris`).
|
||||
name: String,
|
||||
/// Snapshot label. Mandatory, and must start with `hive-` — the
|
||||
/// prefix doubles as an allow-list hive-priv checks so only
|
||||
/// hivectl-issued snapshot names can reach the `btrfs subvolume
|
||||
|
|
@ -644,8 +612,6 @@ pub enum SnapshotCmd {
|
|||
},
|
||||
/// Delete a snapshot created by `subvol snapshot create`.
|
||||
Delete {
|
||||
/// Agent name the snapshot belongs to.
|
||||
name: String,
|
||||
/// Snapshot label passed to `subvol snapshot create --label`.
|
||||
label: String,
|
||||
},
|
||||
|
|
@ -655,8 +621,6 @@ pub enum SnapshotCmd {
|
|||
/// point-in-time backup: a full send with no `--parent` produces a
|
||||
/// self-contained archive of the snapshot.
|
||||
Send {
|
||||
/// Agent name the snapshot belongs to.
|
||||
name: String,
|
||||
/// Snapshot label passed to `subvol snapshot create --label`.
|
||||
label: String,
|
||||
/// Optional parent snapshot label for an incremental send
|
||||
|
|
|
|||
|
|
@ -2,15 +2,16 @@
|
|||
//!
|
||||
//! A thin client for the `hive-c0re` daemon: it speaks the host admin
|
||||
//! socket protocol (`hive-host-sock`) and does NOT link the daemon crate.
|
||||
//! Container lifecycle + the approval queue (`agents <spawn|kill|rebuild|
|
||||
//! restart|…>`, `approvals <pending|approve|deny>`, `stop` / `start`) and
|
||||
//! provisioning (`forge` / `matrix` / `github` / `gateway`) all forward to
|
||||
//! the daemon, which owns the broker, the credentials, and the provisioning
|
||||
//! logic — a running daemon is required for those. A couple of verbs work
|
||||
//! off local host state directly instead (`wg` / `peer-config` read the mesh
|
||||
//! key + TLS CA), so they don't need the socket. `choom` execs into a
|
||||
//! container rather than asking the daemon to do anything, but still uses the
|
||||
//! socket for its "is this an agent?" pre-flight — that answer lives in a
|
||||
//! Container lifecycle + the approval queue (`agent <name> <spawn|kill|
|
||||
//! rebuild|restart|choom|…>`, `list-agents`, `approvals <pending|approve|
|
||||
//! deny>`, `stop` / `start`) and provisioning (`forge` / `matrix` /
|
||||
//! `github` / `gateway`) all forward to the daemon, which owns the broker,
|
||||
//! the credentials, and the provisioning logic — a running daemon is
|
||||
//! required for those. A couple of verbs work off local host state
|
||||
//! directly instead (`wg` / `peer-config` read the mesh key + TLS CA), so
|
||||
//! they don't need the socket. `agent <name> choom` execs into a container
|
||||
//! rather than asking the daemon to do anything, but still uses the socket
|
||||
//! for its "is this an agent?" pre-flight — that answer lives in a
|
||||
//! directory only the daemon's user can read.
|
||||
//!
|
||||
//! One module per subcommand family (see the `mod` list below); `main` is
|
||||
|
|
@ -40,13 +41,12 @@ use open::open_url;
|
|||
mod wg;
|
||||
use wg::{peer_config, require_hive_domain, wg_init, wg_peer, wg_status};
|
||||
mod choom;
|
||||
use choom::choom;
|
||||
mod github;
|
||||
use github::github_set_token;
|
||||
mod forge;
|
||||
use forge::{forge_create_user, forge_reconcile_config};
|
||||
mod agents;
|
||||
use agents::run_agents;
|
||||
use agents::{agents_list, run_agent};
|
||||
mod power;
|
||||
use power::{restart, start, stop};
|
||||
mod approvals;
|
||||
|
|
@ -93,7 +93,9 @@ async fn main() -> Result<()> {
|
|||
GatewayCmd::DeleteUser { username } => gateway_delete_user(&socket, &username).await,
|
||||
GatewayCmd::ListUsers => gateway_list_users(&socket).await,
|
||||
},
|
||||
Cmd::Agents { cmd } => run_agents(&socket, cmd).await,
|
||||
Cmd::Agent { name, cmd } => run_agent(&socket, &name, cmd).await,
|
||||
Cmd::ListAgents { json } => agents_list(&socket, json).await,
|
||||
Cmd::QuotaEnable => quota::quota_enable(&socket).await,
|
||||
Cmd::Approvals { cmd } => run_approvals(&socket, cmd).await,
|
||||
Cmd::Wg { cmd } => match cmd {
|
||||
WgCmd::Init { address } => wg_init(&socket, address.as_deref()).await,
|
||||
|
|
@ -123,10 +125,6 @@ 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::Choom {
|
||||
name,
|
||||
resume_session,
|
||||
} => choom(&socket, &name, resume_session.as_deref()).await,
|
||||
Cmd::MarkdownDocs => {
|
||||
print!("{}", clap_markdown::help_markdown::<Cli>());
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
//! `hivectl agents quota` — per-agent disk accounting + optional quotas via
|
||||
//! btrfs qgroups. The daemon holds the privileged helper that reads /
|
||||
//! sets qgroups; hivectl relays the request and formats the reply.
|
||||
//! `hivectl quota-enable` + `hivectl agent <name> quota` — per-agent disk
|
||||
//! accounting + optional quotas via btrfs qgroups. The daemon holds the
|
||||
//! privileged helper that reads / sets qgroups; hivectl relays the
|
||||
//! request and formats the reply.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
|
|
@ -13,20 +14,20 @@ pub(crate) async fn quota_enable(socket: &Path) -> Result<()> {
|
|||
daemon_request(socket, hive_host_sock::HostRequest::QuotaEnable, "quota").await
|
||||
}
|
||||
|
||||
/// `quota show [name]` — report per-agent disk usage from btrfs qgroups.
|
||||
/// The daemon resolves the agent set + reads each subvolume's usage (it
|
||||
/// holds the privileged helper); the client just formats the returned rows.
|
||||
pub(crate) async fn quota_show(socket: &Path, name: Option<&str>) -> Result<()> {
|
||||
/// `agent <name> quota show` — report one agent's disk usage from btrfs
|
||||
/// qgroups. The daemon reads that subvolume's usage (it holds the
|
||||
/// privileged helper); the client just formats the returned row(s).
|
||||
pub(crate) async fn quota_show(socket: &Path, name: &str) -> Result<()> {
|
||||
let resp = crate::client::request(
|
||||
socket,
|
||||
hive_host_sock::HostRequest::QuotaShow {
|
||||
name: name.map(crate::util::parse_ident).transpose()?,
|
||||
name: Some(crate::util::parse_ident(name)?),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("connect to daemon socket {}", socket.display()))?;
|
||||
if !resp.ok {
|
||||
// Carries the "btrfs quota not enabled — run `hivectl agents quota enable`
|
||||
// Carries the "btrfs quota not enabled — run `hivectl quota-enable`
|
||||
// first" hint when qgroups are off.
|
||||
bail!("{}", resp.error.as_deref().unwrap_or("quota show failed"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
//! `hivectl agents subvol` — btrfs state-subvolume ops: migrate a plain-dir agent
|
||||
//! state root to a subvolume (`upgrade`), and snapshot create/delete/send.
|
||||
//! `hivectl agent <name> subvol` — btrfs state-subvolume ops: migrate a
|
||||
//! plain-dir agent state root to a subvolume (`upgrade`), and snapshot
|
||||
//! create/delete/send.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
|
|
@ -28,25 +29,21 @@ 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 agents 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.
|
||||
pub(crate) async fn dispatch_subvol(socket: &Path, cmd: SubvolCmd) -> Result<()> {
|
||||
/// Route a `hivectl agent <name> 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. `name` is hoisted in from the parent `agent <name>` command.
|
||||
pub(crate) async fn dispatch_subvol(socket: &Path, name: &str, cmd: SubvolCmd) -> Result<()> {
|
||||
match cmd {
|
||||
SubvolCmd::Upgrade { name, yes } => subvol_upgrade(socket, &name, yes).await,
|
||||
SubvolCmd::Upgrade { 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::Create { label } => subvol_snapshot_create(socket, name, label).await,
|
||||
SnapshotCmd::Delete { 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,
|
||||
} => subvol_snapshot_send(socket, name, &label, parent.as_deref(), &dest).await,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue