feat(#2290): maintain /etc/tmpfiles.d/hyperhive-agents.conf for boot safety
Root cause of the boot outage: container@h-* units try to start before
hive-c0re reaches ensure_runtime, so bind-mount source dirs are missing.
Fix: hive-c0re (via hive-priv, which runs as root) writes
/etc/tmpfiles.d/hyperhive-agents.conf whenever the agent set changes.
systemd-tmpfiles-setup.service (sysinit.target) reads it at every boot
BEFORE any container units start, pre-creating:
/run/hyperhive/agents/<name> — MCP socket dir (bind -> /run/hive)
/run/hive-agent/<name> — web socket dir (bind -> /run/hive-agent)
This alone removes the outage class: even if hive-c0re is slow to start,
the bind-mount sources exist and container units can activate.
Added:
- PrivRequest::SyncAgentTmpfiles { agents } in hive-sh4re
- sync_agent_tmpfiles() in hive-priv: generates content, writes atomically,
calls systemd-tmpfiles --create to apply immediately
- priv_client::sync_agent_tmpfiles() wrapper
- lifecycle::sync_tmpfiles() best-effort helper (list + priv call)
- Call sites: hive-c0re startup, handle_spawn success, destroy success
This commit is contained in:
parent
9cd408de8b
commit
9d1f5ebe76
7 changed files with 128 additions and 0 deletions
|
|
@ -326,6 +326,10 @@ async fn exec(req: PrivRequest, writer: &mut OwnedWriteHalf) -> Result<(String,
|
|||
validate_agent_name(agent_name)?;
|
||||
upgrade_agent_subvolume(agent_name).await
|
||||
}
|
||||
|
||||
PrivRequest::SyncAgentTmpfiles { ref agents } => {
|
||||
sync_agent_tmpfiles(agents).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1493,3 +1497,64 @@ fn write_bridge_dns_marker(container: &str, isolation: Option<&NetworkIsolation>
|
|||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `SyncAgentTmpfiles` — write `/etc/tmpfiles.d/hyperhive-agents.conf` for
|
||||
/// the given agent set and immediately apply it with `systemd-tmpfiles --create`.
|
||||
///
|
||||
/// Each call atomically replaces the file with entries for all current agents,
|
||||
/// then creates any missing dirs on the running host. The file survives reboots
|
||||
/// and is read by `systemd-tmpfiles-setup.service` (runs in `sysinit.target`,
|
||||
/// before any container units can start), so bind-mount source dirs are always
|
||||
/// pre-created regardless of whether hive-c0re has reached `ensure_runtime`.
|
||||
///
|
||||
/// Directories written per agent:
|
||||
/// - `/run/hyperhive/agents/<name>` (MCP socket dir, bind-mounted into container
|
||||
/// as `/run/hive`)
|
||||
/// - `/run/hive-agent/<name>` (web socket dir, bind-mounted into container)
|
||||
const TMPFILES_PATH: &str = "/etc/tmpfiles.d/hyperhive-agents.conf";
|
||||
const AGENT_RUNTIME_ROOT: &str = "/run/hyperhive/agents";
|
||||
|
||||
async fn sync_agent_tmpfiles(agents: &[String]) -> Result<(String, String)> {
|
||||
for name in agents {
|
||||
validate_agent_name(name)?;
|
||||
}
|
||||
|
||||
// Build tmpfiles.d content. Root dirs first, then per-agent.
|
||||
let mut content = String::from(
|
||||
"# managed by hive-c0re — do not edit (regenerated on spawn/destroy)\n",
|
||||
);
|
||||
// Parent dirs — created with permissive mode so hive-c0re can make subdirs.
|
||||
// /run/hyperhive itself is also a RuntimeDirectory of hive-c0re.service; the
|
||||
// tmpfiles.d entry here ensures it exists before hive-c0re starts (boot race).
|
||||
content.push_str("d /run/hyperhive 0750 hive-core hive-core -\n");
|
||||
content.push_str(&format!("d {AGENT_RUNTIME_ROOT} 0755 hive-core hive-core -\n"));
|
||||
content.push_str(&format!("d {SOCKET_DIR_ROOT} 0755 root root -\n"));
|
||||
// Per-agent dirs.
|
||||
for name in agents {
|
||||
content.push_str(&format!("d {AGENT_RUNTIME_ROOT}/{name} 0755 hive-core hive-core -\n"));
|
||||
content.push_str(&format!("d {SOCKET_DIR_ROOT}/{name} 0755 root root -\n"));
|
||||
}
|
||||
|
||||
// Atomic write: write to a tmp file then rename so a concurrent reader
|
||||
// always sees a complete file.
|
||||
let tmp = format!("{TMPFILES_PATH}.tmp");
|
||||
std::fs::write(&tmp, &content).with_context(|| format!("write {tmp}"))?;
|
||||
std::fs::rename(&tmp, TMPFILES_PATH)
|
||||
.with_context(|| format!("rename {TMPFILES_PATH}.tmp -> {TMPFILES_PATH}"))?;
|
||||
tracing::info!(agents = agents.len(), "tmpfiles.d: wrote {TMPFILES_PATH}");
|
||||
|
||||
// Apply immediately so dirs exist on the running host, not just after next boot.
|
||||
let out = Command::new("systemd-tmpfiles")
|
||||
.args(["--create", TMPFILES_PATH])
|
||||
.output()
|
||||
.await
|
||||
.context("systemd-tmpfiles --create")?;
|
||||
if !out.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&out.stderr).trim().to_owned();
|
||||
anyhow::bail!(
|
||||
"systemd-tmpfiles --create failed ({}): {stderr}",
|
||||
out.status
|
||||
);
|
||||
}
|
||||
Ok((String::new(), String::new()))
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue