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
5 changed files with 51 additions and 521 deletions

View file

@ -1,7 +1,7 @@
//! `hivectl agent <name>` — everything scoped to ONE managed agent:
//! container lifecycle over the host admin socket (restart/pause/resume/
//! spawn/kill/destroy/rebuild/choom/watch), plus the
//! `quota` and `subvol` groups, whose handlers live in their own modules.
//! `subvol` group, whose handlers live in their own module.
//! `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
@ -12,7 +12,7 @@ use std::path::Path;
use anyhow::{Context as _, Result, bail};
use hive_host_sock::HostRequest;
use crate::cli::{AgentCmd, AgentQuotaCmd};
use crate::cli::AgentCmd;
use crate::dag_progress::wait_for_nodes;
use crate::util::render;
@ -282,13 +282,8 @@ pub(crate) async fn run_agent(socket: &Path, name: &str, cmd: AgentCmd) -> Resul
crate::choom::choom(socket, name, resume_session.as_deref()).await
}
AgentCmd::Watch => crate::watch::watch(socket, name).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,
},
// `subvol` keeps its own module — this arm is just the reparenting
// glue that hoists `name` in from the parent `agent <name>` command.
AgentCmd::Subvol { cmd } => crate::subvol::dispatch_subvol(socket, name, cmd).await,
}
}

View file

@ -33,7 +33,7 @@ pub enum Cmd {
/// 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`).
/// `restart`/`stop`/`start` with a scope).
Agent {
/// Agent name (for example `agent1`).
name: String,
@ -51,12 +51,6 @@ pub enum Cmd {
#[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,
/// WireGuard inter-hive mesh setup helpers.
///
/// Generate this hive's mesh key and print the nix to enable the mesh,
@ -277,39 +271,20 @@ pub enum AgentCmd {
/// ends the watch with exit 0. `Ctrl-C` to stop.
/// Requires the agent to be running.
Watch,
/// 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: AgentQuotaCmd,
},
/// 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.
/// so it gains snapshots.
Subvol {
#[command(subcommand)]
cmd: SubvolCmd,
},
}
#[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,
},
}
#[derive(Subcommand)]
pub enum SubvolCmd {
/// Convert a plain-dir agent state root into a btrfs subvolume in
/// place, so it gains snapshots and per-subvol usage/quota.
/// place, so it gains snapshots.
///
/// Bounces the agent to migrate its state, so it requires `--yes`.
Upgrade {

View file

@ -28,7 +28,6 @@ mod client;
mod dag_progress;
use cli::{Cli, Cmd, WgCmd};
mod completions;
mod quota;
mod util;
use completions::generate_completions;
mod wg;
@ -54,7 +53,6 @@ async fn main() -> Result<()> {
match cli.cmd {
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::Wg { cmd } => match cmd {
WgCmd::Init { address } => wg_init(&socket, address.as_deref()).await,
WgCmd::Peer {

View file

@ -1,113 +0,0 @@
//! `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;
use anyhow::{Context as _, Result, bail};
use crate::util::daemon_request;
/// `quota enable` — turn on btrfs qgroup accounting (operator opt-in).
pub(crate) async fn quota_enable(socket: &Path) -> Result<()> {
daemon_request(socket, hive_host_sock::HostRequest::QuotaEnable, "quota").await
}
/// `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: 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 quota-enable`
// first" hint when qgroups are off.
bail!("{}", resp.error.as_deref().unwrap_or("quota show failed"));
}
let rows = resp.quota.unwrap_or_default();
if rows.is_empty() {
println!("no agents with a state dir found");
return Ok(());
}
for row in &rows {
match (row.referenced, row.exclusive) {
(Some(rfer), Some(excl)) => println!(
"{:<12} referenced {:>10} exclusive {:>10}",
row.agent,
human_bytes(rfer),
human_bytes(excl)
),
// Plain-dir agent (no qgroup) — the daemon set an explanatory note.
_ => println!(
"{:<12} ({})",
row.agent,
row.note.as_deref().unwrap_or("no qgroup data")
),
}
}
Ok(())
}
/// `quota limit <name> <size>` — set or clear an agent's disk quota.
pub(crate) async fn quota_limit(socket: &Path, name: &str, size: &str) -> Result<()> {
let limit = parse_quota_size(size)?;
daemon_request(
socket,
hive_host_sock::HostRequest::QuotaLimit {
name: crate::util::parse_ident(name)?,
limit,
},
"quota",
)
.await?;
match limit {
Some(n) => println!("set {name} quota to {} ({n} bytes)", human_bytes(n)),
None => println!("cleared {name} quota"),
}
Ok(())
}
/// Parse a quota size: a byte count, a `K`/`M`/`G`/`T`-suffixed value
/// (powers of 1024), or `none` to clear. Rejects overflow + bad input.
fn parse_quota_size(s: &str) -> Result<Option<u64>> {
let t = s.trim();
if t.eq_ignore_ascii_case("none") {
return Ok(None);
}
let (num, mult) = match t.chars().last() {
Some('K' | 'k') => (&t[..t.len() - 1], 1024u64),
Some('M' | 'm') => (&t[..t.len() - 1], 1024 * 1024),
Some('G' | 'g') => (&t[..t.len() - 1], 1024 * 1024 * 1024),
Some('T' | 't') => (&t[..t.len() - 1], 1024u64 * 1024 * 1024 * 1024),
_ => (t, 1),
};
let n: u64 = num.trim().parse().with_context(|| {
format!("invalid size {s:?} — use a byte count, a K/M/G/T suffix (e.g. 5G), or `none`")
})?;
n.checked_mul(mult)
.map(Some)
.with_context(|| format!("size {s:?} overflows u64"))
}
/// Format a byte count as a human-friendly KiB/MiB/GiB/TiB string.
#[allow(clippy::cast_precision_loss)] // display-only; precision loss is cosmetic
fn human_bytes(n: u64) -> String {
const UNITS: [&str; 5] = ["B", "KiB", "MiB", "GiB", "TiB"];
if n < 1024 {
return format!("{n} B");
}
let mut v = n as f64;
let mut i = 0;
while v >= 1024.0 && i < UNITS.len() - 1 {
v /= 1024.0;
i += 1;
}
format!("{v:.1} {}", UNITS[i])
}

View file

@ -18,7 +18,7 @@ pub(crate) fn parse_ident(name: &str) -> Result<hive_types::Ident> {
/// Send a provisioning request to the daemon and print its result lines.
/// The daemon owns the provisioning logic; hivectl just relays the outcome,
/// prefixing any error with `label` (e.g. `forge` / `quota`).
/// prefixing any error with `label` (e.g. `forge`).
pub(crate) async fn daemon_request(
socket: &Path,
req: hive_host_sock::HostRequest,