Watch
0
0
Fork
You've already forked hyperhive
0

remove the hivectl-only quota verbs and the trollshell status feed

This commit is contained in:
atlas 2026-11-13 16:00:30 +01:00
commit cf360b74b9

View file

@ -93,21 +93,12 @@ async fn handle_with(
continue;
}
};
// The one verb that answers with more than one response — see its
// doc comment. Hands `write` over and stops reading further
// requests on this connection; a client wanting anything else
// opens a fresh one.
if matches!(req, HostRequest::SubscribeAgentStatus) {
return stream_agent_status(write, coord).await;
}
let resp = dispatch(&req, coord.clone()).await;
write_response(&mut write, &resp).await?;
}
}
/// Serialize one `HostResponse` as a JSON line and flush it. Shared by the
/// ordinary one-response-per-request path and `stream_agent_status`'s
/// multi-response one, so the two can't quietly drift on framing.
/// Serialize one `HostResponse` as a JSON line and flush it.
async fn write_response(
write: &mut tokio::net::unix::OwnedWriteHalf,
resp: &HostResponse,
@ -167,25 +158,11 @@ async fn dispatch(req: &HostRequest, coord: Arc<Coordinator>) -> HostResponse {
// see the request's doc comment for why the client asks.
HostRequest::AgentExists { name } => HostResponse::agent_exists(agent_exists(name)?),
HostRequest::AgentStatus => handle_agent_status().await,
// Intercepted in `handle()` before a request ever reaches
// `dispatch` — see `stream_agent_status`'s doc comment. This
// arm exists only so the match stays exhaustive; reaching it
// would mean a future caller invoked `dispatch` directly.
HostRequest::SubscribeAgentStatus => {
HostResponse::error("SubscribeAgentStatus is handled by the connection loop")
}
// The hive domain + per-surface public URLs are injected into
// c0re's service env by the hyperhive module; surface them so the
// operator CLI can fill in this hive's own identity (the
// federation peer-config block).
HostRequest::Urls => HostResponse::urls(hive_urls()),
HostRequest::QuotaEnable => handle_quota_enable().await?,
HostRequest::QuotaLimit { name, limit } => {
handle_quota_limit(name.as_str(), *limit).await?
}
HostRequest::QuotaShow { name } => {
handle_quota_show(name.as_ref().map(hive_types::Ident::as_str)).await?
}
HostRequest::UpgradeSubvolume { name } => {
handle_upgrade_subvolume(name.as_str()).await?
}
@ -264,49 +241,6 @@ async fn handle_agent_status() -> HostResponse {
}
}
/// `SubscribeAgentStatus` — ack once, then push one single-row
/// [`HostResponse::agent_statuses`] per agent-status change for as long as
/// the client stays connected. Consumes `write` (rather than borrowing it,
/// like every other handler here) because this is the one verb that keeps
/// writing after its own response, so `handle()` hands over ownership and
/// stops reading further requests on this connection once it calls this.
///
/// Rides the same `Coordinator.dashboard_events` broadcast channel the
/// dashboard's own SSE route reads (`dashboard/state_snapshot.rs`) —
/// `SetStatus` already triggers a `rescan_containers_and_emit` on every
/// status change (`socket_server/mod.rs::handle_set_status`), so that
/// channel already carries every event this needs; no separate plumbing.
async fn stream_agent_status(
mut write: tokio::net::unix::OwnedWriteHalf,
coord: Arc<Coordinator>,
) -> Result<()> {
let mut events = coord.dashboard_subscribe();
write_response(&mut write, &HostResponse::success()).await?;
loop {
match events.recv().await {
Ok(crate::dashboard_events::DashboardEvent::ContainerStateChanged {
container,
..
}) => {
let row = hive_sh4re::container::AgentStatusRow::from(container);
write_response(&mut write, &HostResponse::agent_statuses(vec![row])).await?;
}
// Every other event kind on this channel is dashboard-only
// (broker traffic, the queue, …) — nothing this
// subscriber asked for.
Ok(_) => {}
// Best-effort, same contract the dashboard's own live channel
// already has (see `HostRequest::SubscribeAgentStatus`'s doc
// comment) — a slow reader drops updates rather than stalling
// the broadcaster for everyone else on the channel.
Err(tokio::sync::broadcast::error::RecvError::Lagged(skipped)) => {
tracing::warn!(skipped, "agent-status subscribe: receiver lagged");
}
Err(tokio::sync::broadcast::error::RecvError::Closed) => return Ok(()),
}
}
}
/// True when `name` has a state dir under the agents root, i.e. it's a
/// managed agent rather than a bare (operator/human) account.
///
@ -320,67 +254,6 @@ fn agent_exists(name: &hive_types::Ident) -> Result<bool> {
.with_context(|| format!("check agent state dir for {name}"))
}
async fn handle_quota_enable() -> Result<HostResponse> {
crate::priv_client::ensure_btrfs_quota()
.await
.context("enable btrfs qgroup accounting")?;
Ok(HostResponse::messages(vec![
"btrfs qgroup accounting enabled on the agent-state filesystem.".to_owned(),
"(usage may read 0 until btrfs finishes its background rescan)".to_owned(),
]))
}
async fn handle_quota_limit(name: &str, limit: Option<u64>) -> Result<HostResponse> {
crate::priv_client::set_subvolume_quota(name, limit)
.await
.with_context(|| format!("set quota for {name}"))?;
// Bare success: the client prints the human-readable confirmation from
// the value it sent (it holds the `human_bytes` formatter).
Ok(HostResponse::success())
}
async fn handle_quota_show(name: Option<&str>) -> Result<HostResponse> {
let agents: Vec<String> = match name {
Some(n) => vec![n.to_owned()],
None => Coordinator::kept_state_names()
.into_iter()
.map(hive_types::Ident::into_string)
.collect(),
};
let mut rows = Vec::with_capacity(agents.len());
for agent in &agents {
match crate::priv_client::read_subvolume_usage(agent).await {
Ok((referenced, exclusive)) => rows.push(hive_host_sock::QuotaRow {
agent: agent.clone(),
referenced: Some(referenced),
exclusive: Some(exclusive),
note: None,
}),
Err(e) => {
let msg = format!("{e:#}");
// btrfs-progs prints "ERROR: ... quota not enabled" to stderr
// when qgroups are off; short-circuit the whole sweep with the
// enable hint (case-insensitive fragment match — the wording
// varies across btrfs-progs versions).
if msg.to_ascii_lowercase().contains("quota not enabled") {
return Ok(HostResponse::error(
"btrfs quota not enabled — run `hivectl quota-enable` first",
));
}
// A plain-dir agent (no subvolume) has no qgroup; note it
// inline and keep going rather than aborting the whole sweep.
rows.push(hive_host_sock::QuotaRow {
agent: agent.clone(),
referenced: None,
exclusive: None,
note: Some(format!("no qgroup data — plain dir or: {msg}")),
});
}
}
}
Ok(HostResponse::quota(rows))
}
async fn handle_upgrade_subvolume(name: &str) -> Result<HostResponse> {
crate::priv_client::upgrade_agent_subvolume(name)
.await
@ -748,12 +621,34 @@ mod tests {
/// `parse error` by the connection loop) while a served verb parses.
#[test]
fn a_removed_verb_is_a_parse_error_and_a_served_one_parses() {
let err =
serde_json::from_slice::<HostRequest>(br#"{"cmd":"list"}"#).expect_err("list is gone");
assert!(err.to_string().contains("unknown variant `list`"), "{err}");
assert!(
serde_json::from_slice::<HostRequest>(br#"{"cmd":"subscribe_agent_status"}"#).is_ok()
);
for gone in [
"list",
"subscribe_agent_status",
"quota_enable",
"quota_limit",
"quota_show",
] {
let line = format!(r#"{{"cmd":"{gone}"}}"#);
let err = serde_json::from_slice::<HostRequest>(line.as_bytes()).expect_err(gone);
assert!(
err.to_string()
.contains(&format!("unknown variant `{gone}`")),
"{gone}: {err}"
);
}
for served in [
br#"{"cmd":"agent_status"}"#.as_slice(),
br#"{"cmd":"upgrade_subvolume","name":"atlas"}"#,
br#"{"cmd":"snapshot_subvolume","name":"atlas","label":"hive-x"}"#,
br#"{"cmd":"delete_snapshot","name":"atlas","label":"hive-x"}"#,
br#"{"cmd":"send_snapshot","name":"atlas","label":"hive-x","dest":"out"}"#,
] {
assert!(
serde_json::from_slice::<HostRequest>(served).is_ok(),
"{}",
String::from_utf8_lossy(served)
);
}
}
/// A real `Coordinator` over a throwaway sqlite dir; `handle_with` takes