diff --git a/hive-c0re/src/priv_client.rs b/hive-c0re/src/priv_client.rs new file mode 100644 index 00000000..357d8723 --- /dev/null +++ b/hive-c0re/src/priv_client.rs @@ -0,0 +1,133 @@ +//! Async client for the `hive-priv` privileged-helper socket. +//! +//! Exposes a standalone async function per operation that callers in +//! `lifecycle`, `forge`, `matrix`, and `gateway_nginx` can call +//! without carrying any client state. Each call opens a fresh connection +//! to `/run/hive/priv.sock`, sends one JSON line, reads the response, +//! and closes the connection. +//! +//! Connection-per-call is intentional: priv calls are infrequent (one +//! per rebuild step), so the simplicity is worth more than a persistent +//! connection. + +use std::path::{Path, PathBuf}; + +use anyhow::{Context as _, Result, bail}; +use hive_sh4re::priv_proto::{PRIV_SOCK, PrivRequest, PrivResponse}; +use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; +use tokio::net::UnixStream; + +/// Send a single request to `hive-priv` and return the response. +pub async fn call(req: &PrivRequest) -> Result { + let mut stream = UnixStream::connect(PRIV_SOCK) + .await + .context("connect to hive-priv socket")?; + let line = serde_json::to_string(req).context("serialise PrivRequest")? + "\n"; + stream + .write_all(line.as_bytes()) + .await + .context("send request to hive-priv")?; + stream.shutdown().await.context("shutdown write half")?; + let mut resp_line = String::new(); + BufReader::new(stream) + .read_line(&mut resp_line) + .await + .context("read response from hive-priv")?; + serde_json::from_str(&resp_line).context("parse PrivResponse") +} + +/// Run `nixos-container `. +/// +/// On success returns `(stdout, stderr)`. +pub async fn container_run(args: Vec) -> Result<(String, String)> { + let resp = call(&PrivRequest::ContainerRun { args }).await?; + check(resp) +} + +/// Run `systemctl daemon-reload`. +pub async fn daemon_reload() -> Result<()> { + let resp = call(&PrivRequest::DaemonReload).await?; + check(resp)?; + Ok(()) +} + +/// Overwrite `/etc/nixos-containers/.conf`. +pub async fn write_nspawn_conf(container: &str, content: &str) -> Result<()> { + let resp = call(&PrivRequest::WriteNspawnConf { + container: container.to_owned(), + content: content.to_owned(), + }) + .await?; + check(resp)?; + Ok(()) +} + +/// Write a systemd drop-in file for `container@.service`. +pub async fn write_systemd_dropin(container: &str, filename: &str, content: &str) -> Result<()> { + let resp = call(&PrivRequest::WriteSystemdDropin { + container: container.to_owned(), + filename: filename.to_owned(), + content: content.to_owned(), + }) + .await?; + check(resp)?; + Ok(()) +} + +/// Remove the systemd drop-in dir for `container@.service`. +pub async fn remove_systemd_dropin(container: &str) -> Result<()> { + let resp = call(&PrivRequest::RemoveSystemdDropin { + container: container.to_owned(), + }) + .await?; + check(resp)?; + Ok(()) +} + +/// `chown(path, uid, gid)` via hive-priv. +/// +/// `path` must be under `/run/hive-agent/` or `/var/lib/hyperhive/`. +pub async fn chown(path: &Path, uid: u32, gid: u32) -> Result<()> { + let resp = call(&PrivRequest::Chown { + path: PathBuf::from(path), + uid, + gid, + }) + .await?; + check(resp)?; + Ok(()) +} + +/// `chmod(path, mode)` via hive-priv. +/// +/// `path` must be under `/run/hive-agent/` or `/var/lib/hyperhive/`. +pub async fn chmod(path: &Path, mode: u32) -> Result<()> { + let resp = call(&PrivRequest::Chmod { + path: PathBuf::from(path), + mode, + }) + .await?; + check(resp)?; + Ok(()) +} + +/// Run a command inside a machine container via `systemd-run --machine`. +pub async fn systemd_run_machine(machine: &str, cmd: Vec) -> Result<(String, String)> { + let resp = call(&PrivRequest::SystemdRunMachine { + machine: machine.to_owned(), + cmd, + }) + .await?; + check(resp) +} + +fn check(resp: PrivResponse) -> Result<(String, String)> { + if resp.ok { + Ok((resp.stdout, resp.stderr)) + } else { + bail!( + "{}", + resp.error.as_deref().unwrap_or("hive-priv returned error") + ) + } +}