hivectl: add hive-wide start/stop verbs
`hivectl stop` brings the whole hive down in one operator action — all
sub-agents plus the ci/forge/gateway/matrix infra containers — and
`hivectl start` brings it back up. Scope flags (--agents, --agent <name>,
--ci, --forge, --gateway, --matrix) narrow the set; a bare invocation
targets everything. hive-c0re never stops itself.
- hive-sh4re: HostRequest::{Stop,Start} + LifecycleScope wire type;
priv_proto InfraAction + ControlInfraContainer + the
CONTROLLABLE_INFRA_CONTAINERS allowlist (adds hive-matrix, excludes
hive-c0re).
- hive-priv: control_infra_container handler (systemctl <verb>
container@<name>, allowlist-validated root-side).
- hive-c0re: handle_stop / handle_start fan out agents via lifecycle and
infra via hive-priv; per-target failures are aggregated. Infra
systemctl routes through hive-priv (the privsep boundary).
- The --graceful flag is threaded through Stop now; the per-agent quiesce
itself lands with the graceful-agent-stop work.
This commit is contained in:
parent
5b99eb1f8f
commit
fbb48ed3ce
6 changed files with 407 additions and 7 deletions
|
|
@ -19,7 +19,7 @@ use std::os::unix::process::CommandExt as _;
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use clap::{Parser, Subcommand};
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use hive_c0re::coordinator::Coordinator;
|
||||
|
||||
#[derive(Parser)]
|
||||
|
|
@ -100,6 +100,36 @@ enum Cmd {
|
|||
#[arg(long)]
|
||||
fresh: bool,
|
||||
},
|
||||
/// Stop containers hive-wide in one operator action. Bare `hivectl
|
||||
/// stop` stops **everything** — all sub-agents plus the ci, forge,
|
||||
/// gateway, and matrix infra containers. Narrow it with scope flags:
|
||||
/// `--agents` (all sub-agents), `--ci` / `--forge` / `--gateway` /
|
||||
/// `--matrix` (named infra), and `--agent <name>` (repeatable) for
|
||||
/// specific sub-agents. Flags are additive (e.g. `--agents --matrix`).
|
||||
/// Requires the hive-c0re daemon (connects to the host admin socket).
|
||||
/// hive-c0re itself is never stopped — it services the request.
|
||||
Stop {
|
||||
#[command(flatten)]
|
||||
scope: ScopeArgs,
|
||||
/// Gracefully quiesce each agent (finish the current turn, drain
|
||||
/// the inbox) before stopping, instead of a hard stop.
|
||||
#[arg(long)]
|
||||
graceful: bool,
|
||||
/// Path to the hive-c0re host admin socket.
|
||||
#[arg(long, default_value = DEFAULT_HOST_SOCKET)]
|
||||
socket: PathBuf,
|
||||
},
|
||||
/// Start containers hive-wide — the inverse of `hivectl stop`. Bare
|
||||
/// `hivectl start` starts everything back up; the same scope flags as
|
||||
/// `stop` narrow it (`--agents`, `--ci`, `--forge`, `--gateway`,
|
||||
/// `--matrix`, `--agent <name>`). Requires the hive-c0re daemon.
|
||||
Start {
|
||||
#[command(flatten)]
|
||||
scope: ScopeArgs,
|
||||
/// Path to the hive-c0re host admin socket.
|
||||
#[arg(long, default_value = DEFAULT_HOST_SOCKET)]
|
||||
socket: PathBuf,
|
||||
},
|
||||
/// Emit the full CLI reference as `CommonMark` to stdout.
|
||||
///
|
||||
/// Hidden tooling command (not part of day-to-day operator admin):
|
||||
|
|
@ -111,6 +141,49 @@ enum Cmd {
|
|||
MarkdownDocs,
|
||||
}
|
||||
|
||||
/// Shared scope flags for `hivectl stop` / `hivectl start`. With no flag
|
||||
/// set the verb targets **everything** (all sub-agents + every controllable
|
||||
/// infra container). Setting any flag restricts to the selected classes,
|
||||
/// additively.
|
||||
// One bool per selectable container class, each mapping 1:1 to a clap flag;
|
||||
// orthogonal toggles, not a state machine — hence the bools allow (mirrors
|
||||
// `hive_sh4re::LifecycleScope`).
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Args)]
|
||||
struct ScopeArgs {
|
||||
/// All sub-agent containers.
|
||||
#[arg(long)]
|
||||
agents: bool,
|
||||
/// A specific sub-agent by name. Repeatable: `--agent a --agent b`.
|
||||
#[arg(long = "agent", value_name = "NAME")]
|
||||
agent: Vec<String>,
|
||||
/// The CI runner container (`hive-ci`).
|
||||
#[arg(long)]
|
||||
ci: bool,
|
||||
/// The forge container (`hive-forge`).
|
||||
#[arg(long)]
|
||||
forge: bool,
|
||||
/// The gateway container (`hive-gateway`).
|
||||
#[arg(long)]
|
||||
gateway: bool,
|
||||
/// The matrix container (`hive-matrix`).
|
||||
#[arg(long)]
|
||||
matrix: bool,
|
||||
}
|
||||
|
||||
impl ScopeArgs {
|
||||
fn to_scope(&self) -> hive_sh4re::LifecycleScope {
|
||||
hive_sh4re::LifecycleScope {
|
||||
agents: self.agents,
|
||||
agent_names: self.agent.clone(),
|
||||
ci: self.ci,
|
||||
forge: self.forge,
|
||||
gateway: self.gateway,
|
||||
matrix: self.matrix,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum ForgeCmd {
|
||||
/// Create or refresh the Forgejo account + token for `<name>`.
|
||||
|
|
@ -348,6 +421,12 @@ async fn main() -> Result<()> {
|
|||
AgentsCmd::Restart { name, socket } => agents_restart(&socket, &name).await,
|
||||
AgentsCmd::RestartAll { socket } => agents_restart_all(&socket).await,
|
||||
},
|
||||
Cmd::Stop {
|
||||
scope,
|
||||
graceful,
|
||||
socket,
|
||||
} => stop(&socket, scope.to_scope(), graceful).await,
|
||||
Cmd::Start { scope, socket } => start(&socket, scope.to_scope()).await,
|
||||
Cmd::Choom { name, fresh } => choom(&name, fresh),
|
||||
Cmd::MarkdownDocs => {
|
||||
print!("{}", clap_markdown::help_markdown::<Cli>());
|
||||
|
|
@ -804,6 +883,43 @@ async fn agents_restart_all(socket: &Path) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn stop(socket: &Path, scope: hive_sh4re::LifecycleScope, graceful: bool) -> Result<()> {
|
||||
let resp =
|
||||
hive_c0re::client::request(socket, hive_sh4re::HostRequest::Stop { scope, graceful })
|
||||
.await
|
||||
.with_context(|| format!("connect to daemon socket {}", socket.display()))?;
|
||||
render_lifecycle(&resp, "stopped")
|
||||
}
|
||||
|
||||
async fn start(socket: &Path, scope: hive_sh4re::LifecycleScope) -> Result<()> {
|
||||
let resp = hive_c0re::client::request(socket, hive_sh4re::HostRequest::Start { scope })
|
||||
.await
|
||||
.with_context(|| format!("connect to daemon socket {}", socket.display()))?;
|
||||
render_lifecycle(&resp, "started")
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// (`stopped` / `started`).
|
||||
fn render_lifecycle(resp: &hive_sh4re::HostResponse, verb: &str) -> Result<()> {
|
||||
let items = resp.agents.as_deref().unwrap_or(&[]);
|
||||
if items.is_empty() {
|
||||
println!("{verb}: nothing matched the requested scope");
|
||||
} else {
|
||||
for item in items {
|
||||
println!("{verb}: {item}");
|
||||
}
|
||||
}
|
||||
if !resp.ok {
|
||||
bail!(
|
||||
"{verb}: {}",
|
||||
resp.error.as_deref().unwrap_or("unknown error")
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reject usernames containing `:` (field separator) or control chars
|
||||
/// that would corrupt the htpasswd file format.
|
||||
fn validate_htpasswd_username(username: &str) -> Result<()> {
|
||||
|
|
|
|||
Loading…
Reference in a new issue