fix(#977): add MANAGER_CONTAINER=h-root, migrate root container name
This commit is contained in:
parent
286a7c8fd3
commit
68cc433ac9
5 changed files with 115 additions and 29 deletions
|
|
@ -104,13 +104,15 @@ which axis the post-milestone version reads each special-case along:
|
||||||
|
|
||||||
### A — naming + bootstrap
|
### A — naming + bootstrap
|
||||||
|
|
||||||
- `MANAGER_AGENT = "root"` (broker recipient name) and
|
- `MANAGER_AGENT = "root"` (broker recipient name),
|
||||||
`MANAGER_NAME = "root"` (container name). Both now `"root"`;
|
`MANAGER_NAME = "root"` (logical name, state-dir key), and
|
||||||
renamed from `"manager"`/`"hm1nd"` via the migration script in
|
`MANAGER_CONTAINER = "h-root"` (nixos-container name). The `h-`
|
||||||
`migrate.rs` (idempotent, marker-guarded).
|
prefix makes the manager consistent with all sub-agents and lets
|
||||||
|
`lifecycle::list()` use a single `starts_with("h-")` filter.
|
||||||
|
Existing `root` containers are renamed to `h-root` by migration
|
||||||
|
phase 5 in `migrate.rs` (idempotent, marker-guarded).
|
||||||
- `auto_update::ensure_manager` runs at hive-c0re boot and spawns
|
- `auto_update::ensure_manager` runs at hive-c0re boot and spawns
|
||||||
`root` if missing. Becomes "ensure the root agent exists" once any
|
`h-root` if missing. **Topology**: root has no parent, so
|
||||||
agent can be at the root. **Topology**: root has no parent, so
|
|
||||||
hive-c0re itself owns its lifecycle (no parent to delegate to).
|
hive-c0re itself owns its lifecycle (no parent to delegate to).
|
||||||
|
|
||||||
### B — wire-protocol privileges
|
### B — wire-protocol privileges
|
||||||
|
|
@ -236,8 +238,9 @@ repos they're working on.
|
||||||
meta-flake's per-agent flake.nix wrapper). For the manager unit
|
meta-flake's per-agent flake.nix wrapper). For the manager unit
|
||||||
that means a hardcoded `HIVE_LABEL` env value:
|
that means a hardcoded `HIVE_LABEL` env value:
|
||||||
|
|
||||||
- `HIVE_LABEL = "root"` — container name; matches what `meta.rs`
|
- `HIVE_LABEL = "root"` — logical agent name; matches what `meta.rs`
|
||||||
injects at deploy time.
|
injects at deploy time. (The nixos-container is `h-root`, but the
|
||||||
|
harness identifies itself by logical name.)
|
||||||
|
|
||||||
Real deploys never read these — `meta::render_flake` overrides them
|
Real deploys never read these — `meta::render_flake` overrides them
|
||||||
via the generated wrapper. They exist so the manager
|
via the generated wrapper. They exist so the manager
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,6 @@ use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
|
||||||
use crate::lifecycle::MANAGER_NAME;
|
|
||||||
|
|
||||||
const HOST_SOCKETS_PATH: &str = "/var/lib/hyperhive/agent-sockets.json";
|
const HOST_SOCKETS_PATH: &str = "/var/lib/hyperhive/agent-sockets.json";
|
||||||
|
|
||||||
/// Host-side parent directory holding per-agent socket subdirs. The
|
/// Host-side parent directory holding per-agent socket subdirs. The
|
||||||
|
|
@ -217,6 +215,7 @@ pub fn spawn_poll() {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::lifecycle::MANAGER_NAME;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn socket_path_for_uses_subdir_layout() {
|
fn socket_path_for_uses_subdir_layout() {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ use rusqlite::Connection;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::coordinator::Coordinator;
|
use crate::coordinator::Coordinator;
|
||||||
use crate::lifecycle::{self, AGENT_PREFIX, MANAGER_NAME};
|
use crate::lifecycle::{self, AGENT_PREFIX, MANAGER_CONTAINER, MANAGER_NAME};
|
||||||
|
|
||||||
/// An agent-declared extra navigation link surfaced on the dashboard card.
|
/// An agent-declared extra navigation link surfaced on the dashboard card.
|
||||||
/// Written by the `hive-dashboard-links` NixOS oneshot into
|
/// Written by the `hive-dashboard-links` NixOS oneshot into
|
||||||
|
|
@ -108,7 +108,7 @@ pub async fn build_all(coord: &Coordinator) -> Vec<ContainerView> {
|
||||||
let topology = crate::topology::read();
|
let topology = crate::topology::read();
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
for c in &raw {
|
for c in &raw {
|
||||||
let (logical, is_manager) = if c == MANAGER_NAME {
|
let (logical, is_manager) = if c == MANAGER_CONTAINER {
|
||||||
(MANAGER_NAME.to_owned(), true)
|
(MANAGER_NAME.to_owned(), true)
|
||||||
} else if let Some(n) = c.strip_prefix(AGENT_PREFIX) {
|
} else if let Some(n) = c.strip_prefix(AGENT_PREFIX) {
|
||||||
(n.to_owned(), false)
|
(n.to_owned(), false)
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,14 @@ use tokio::process::Command;
|
||||||
/// name itself can be at most `MAX_AGENT_NAME` chars.
|
/// name itself can be at most `MAX_AGENT_NAME` chars.
|
||||||
pub const AGENT_PREFIX: &str = "h-";
|
pub const AGENT_PREFIX: &str = "h-";
|
||||||
pub const MAX_AGENT_NAME: usize = 9;
|
pub const MAX_AGENT_NAME: usize = 9;
|
||||||
/// Container name of the manager. Lives in the same path scheme as sub-agents
|
/// Logical name of the manager agent (broker recipient, state-dir key,
|
||||||
/// (`/var/lib/hyperhive/agents/root/`, `/var/lib/hyperhive/applied/root/`),
|
/// meta flake attribute). All persistent state lives under `root/`.
|
||||||
/// but its container has no `h-` prefix and extends a different
|
|
||||||
/// nixosConfiguration (`root`, not `agent-base`).
|
|
||||||
pub const MANAGER_NAME: &str = "root";
|
pub const MANAGER_NAME: &str = "root";
|
||||||
|
/// Container name of the manager. Uses the same `h-` prefix as sub-agents
|
||||||
|
/// so `nixos-container list` output is uniform and the list filter is
|
||||||
|
/// a single `starts_with(AGENT_PREFIX)` check. Logical name → container
|
||||||
|
/// name: `root` → `h-root`.
|
||||||
|
pub const MANAGER_CONTAINER: &str = "h-root";
|
||||||
|
|
||||||
/// Mount point of the per-agent runtime directory inside the container.
|
/// Mount point of the per-agent runtime directory inside the container.
|
||||||
pub const CONTAINER_RUNTIME_MOUNT: &str = "/run/hive";
|
pub const CONTAINER_RUNTIME_MOUNT: &str = "/run/hive";
|
||||||
|
|
@ -72,7 +75,7 @@ pub fn agent_web_port(name: &str) -> u16 {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn container_name(name: &str) -> String {
|
pub fn container_name(name: &str) -> String {
|
||||||
if name == MANAGER_NAME {
|
if name == MANAGER_NAME {
|
||||||
MANAGER_NAME.to_owned()
|
MANAGER_CONTAINER.to_owned()
|
||||||
} else {
|
} else {
|
||||||
format!("{AGENT_PREFIX}{name}")
|
format!("{AGENT_PREFIX}{name}")
|
||||||
}
|
}
|
||||||
|
|
@ -161,7 +164,7 @@ async fn port_collision(self_name: &str) -> Option<String> {
|
||||||
let port = agent_web_port(self_name);
|
let port = agent_web_port(self_name);
|
||||||
let raw = list().await.unwrap_or_default();
|
let raw = list().await.unwrap_or_default();
|
||||||
for c in raw {
|
for c in raw {
|
||||||
let other = if c == MANAGER_NAME {
|
let other = if c == MANAGER_CONTAINER {
|
||||||
MANAGER_NAME.to_owned()
|
MANAGER_NAME.to_owned()
|
||||||
} else if let Some(n) = c.strip_prefix(AGENT_PREFIX) {
|
} else if let Some(n) = c.strip_prefix(AGENT_PREFIX) {
|
||||||
n.to_owned()
|
n.to_owned()
|
||||||
|
|
@ -233,7 +236,7 @@ async fn agents_for_meta(name_to_add: Option<&str>) -> Result<Vec<crate::meta::A
|
||||||
let mut out: Vec<crate::meta::AgentSpec> = containers
|
let mut out: Vec<crate::meta::AgentSpec> = containers
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|c| {
|
.filter_map(|c| {
|
||||||
let (name, is_manager) = if c == MANAGER_NAME {
|
let (name, is_manager) = if c == MANAGER_CONTAINER {
|
||||||
(MANAGER_NAME.to_owned(), true)
|
(MANAGER_NAME.to_owned(), true)
|
||||||
} else if let Some(n) = c.strip_prefix(AGENT_PREFIX) {
|
} else if let Some(n) = c.strip_prefix(AGENT_PREFIX) {
|
||||||
(n.to_owned(), false)
|
(n.to_owned(), false)
|
||||||
|
|
@ -598,7 +601,7 @@ pub async fn list() -> Result<Vec<String>> {
|
||||||
Ok(String::from_utf8_lossy(&out.stdout)
|
Ok(String::from_utf8_lossy(&out.stdout)
|
||||||
.lines()
|
.lines()
|
||||||
.map(str::trim)
|
.map(str::trim)
|
||||||
.filter(|line| line.starts_with(AGENT_PREFIX) || *line == MANAGER_NAME)
|
.filter(|line| line.starts_with(AGENT_PREFIX))
|
||||||
.map(str::to_owned)
|
.map(str::to_owned)
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
@ -1076,9 +1079,8 @@ fn set_nspawn_flags(
|
||||||
let path = format!("/etc/nixos-containers/{container}.conf");
|
let path = format!("/etc/nixos-containers/{container}.conf");
|
||||||
let original = std::fs::read_to_string(&path).with_context(|| format!("read {path}"))?;
|
let original = std::fs::read_to_string(&path).with_context(|| format!("read {path}"))?;
|
||||||
|
|
||||||
// Logical agent name (container name minus the sub-agent prefix).
|
// Logical agent name — strip the `h-` prefix.
|
||||||
// For the manager the strip is a no-op — harmless, manager paths
|
// For the manager: `h-root` → `root`. For sub-agents: `h-iris` → `iris`.
|
||||||
// below are gated on `container == MANAGER_NAME` anyway.
|
|
||||||
let agent_name = container.strip_prefix(AGENT_PREFIX).unwrap_or(container);
|
let agent_name = container.strip_prefix(AGENT_PREFIX).unwrap_or(container);
|
||||||
|
|
||||||
// Claude credentials land at `/home/<agent>/.claude` so the
|
// Claude credentials land at `/home/<agent>/.claude` so the
|
||||||
|
|
@ -1099,7 +1101,7 @@ fn set_nspawn_flags(
|
||||||
// the `/agents` bind below already exposes both (along with
|
// the `/agents` bind below already exposes both (along with
|
||||||
// every sub-agent's). For regular agents the harness dir is
|
// every sub-agent's). For regular agents the harness dir is
|
||||||
// the sibling of notes_dir (same parent, "harness" subdir).
|
// the sibling of notes_dir (same parent, "harness" subdir).
|
||||||
if container != MANAGER_NAME {
|
if container != MANAGER_CONTAINER {
|
||||||
let _ = write!(
|
let _ = write!(
|
||||||
binds,
|
binds,
|
||||||
" --bind={notes}:/agents/{agent_name}/state",
|
" --bind={notes}:/agents/{agent_name}/state",
|
||||||
|
|
@ -1120,7 +1122,7 @@ fn set_nspawn_flags(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if container == MANAGER_NAME {
|
if container == MANAGER_CONTAINER {
|
||||||
// systemd-nspawn refuses to start a container whose bind
|
// systemd-nspawn refuses to start a container whose bind
|
||||||
// source doesn't exist. The meta repo is created by the
|
// source doesn't exist. The meta repo is created by the
|
||||||
// startup migration, but make sure the directory is there
|
// startup migration, but make sure the directory is there
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
//! Startup auto-migration from the pre-meta layout. Four idempotent
|
//! Startup auto-migration. Five idempotent phases: applied repo,
|
||||||
//! phases: applied repo, proposed repo, meta repo, container repoint.
|
//! proposed repo, meta repo, container repoint, root→h-root rename.
|
||||||
//! Kill-switch: `HIVE_SKIP_META_MIGRATION=1`. Full migration sequence
|
//! Kill-switch: `HIVE_SKIP_META_MIGRATION=1`. Full migration sequence
|
||||||
//! and phase details: `docs/approvals.md::Migration from the pre-tag`.
|
//! and phase details: `docs/approvals.md::Migration from the pre-tag`.
|
||||||
|
|
||||||
|
|
@ -10,7 +10,7 @@ use anyhow::{Context, Result};
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
|
|
||||||
use crate::coordinator::Coordinator;
|
use crate::coordinator::Coordinator;
|
||||||
use crate::lifecycle::{self, AGENT_PREFIX, MANAGER_NAME};
|
use crate::lifecycle::{self, AGENT_PREFIX, MANAGER_CONTAINER, MANAGER_NAME};
|
||||||
use crate::meta;
|
use crate::meta;
|
||||||
|
|
||||||
const KILL_SWITCH: &str = "HIVE_SKIP_META_MIGRATION";
|
const KILL_SWITCH: &str = "HIVE_SKIP_META_MIGRATION";
|
||||||
|
|
@ -21,6 +21,12 @@ fn repoint_marker() -> PathBuf {
|
||||||
PathBuf::from("/var/lib/hyperhive/.meta-migration-done")
|
PathBuf::from("/var/lib/hyperhive/.meta-migration-done")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Marker for phase 5. Once present, root→h-root container rename is
|
||||||
|
/// skipped on future restarts.
|
||||||
|
fn hroot_rename_marker() -> PathBuf {
|
||||||
|
PathBuf::from("/var/lib/hyperhive/.hroot-rename-done")
|
||||||
|
}
|
||||||
|
|
||||||
/// Substring that identifies the *current* agent flake boilerplate.
|
/// Substring that identifies the *current* agent flake boilerplate.
|
||||||
/// Bumped whenever the template changes so the startup migration
|
/// Bumped whenever the template changes so the startup migration
|
||||||
/// re-renders existing agents onto the new shape. Today the marker
|
/// re-renders existing agents onto the new shape. Today the marker
|
||||||
|
|
@ -108,6 +114,12 @@ pub async fn run(coord: &Arc<Coordinator>) -> Result<()> {
|
||||||
{
|
{
|
||||||
tracing::warn!(error = ?e, "migration: write repoint marker failed");
|
tracing::warn!(error = ?e, "migration: write repoint marker failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 5: rename `root` nixos-container to `h-root` for naming
|
||||||
|
// consistency with sub-agents. Guarded by marker; skipped on
|
||||||
|
// fresh installs (conf file absent) and after first successful run.
|
||||||
|
rename_manager_container(coord).await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -140,12 +152,82 @@ fn migrate_harness_files(name: &str) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Phase 5: rename the `root` nixos-container to `h-root` so the
|
||||||
|
/// manager container name is consistent with the `h-` prefix used by
|
||||||
|
/// all sub-agents. Idempotent and marker-guarded. Steps:
|
||||||
|
///
|
||||||
|
/// 1. Check `/etc/nixos-containers/root.conf` exists (old name present).
|
||||||
|
/// 2. Stop the `root` container.
|
||||||
|
/// 3. Copy `root.conf` → `h-root.conf`.
|
||||||
|
/// 4. Move `/var/lib/nixos-containers/root/` → `h-root/` (if present).
|
||||||
|
/// 5. `systemctl daemon-reload` so systemd sees the new unit name.
|
||||||
|
/// 6. `nixos-container start h-root`.
|
||||||
|
/// 7. Write the done marker.
|
||||||
|
///
|
||||||
|
/// Best-effort: logs warnings on failure. A failed rename leaves both
|
||||||
|
/// conf files present; on the next hive-c0re start the marker is
|
||||||
|
/// absent so the phase retries.
|
||||||
|
async fn rename_manager_container(coord: &Arc<Coordinator>) {
|
||||||
|
if hroot_rename_marker().exists() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let old_conf = std::path::PathBuf::from("/etc/nixos-containers/root.conf");
|
||||||
|
let new_conf = std::path::PathBuf::from("/etc/nixos-containers/h-root.conf");
|
||||||
|
if !old_conf.exists() {
|
||||||
|
// Fresh install — root container was never created under the old name.
|
||||||
|
let _ = std::fs::write(hroot_rename_marker(), b"done\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if new_conf.exists() {
|
||||||
|
// Already renamed (but marker was lost — write it and return).
|
||||||
|
tracing::info!("migration phase 5: h-root.conf already present, marking done");
|
||||||
|
let _ = std::fs::write(hroot_rename_marker(), b"done\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
tracing::info!("migration phase 5: renaming root container to h-root");
|
||||||
|
let _guard = coord.transient_guard(MANAGER_NAME, crate::coordinator::TransientKind::Rebuilding);
|
||||||
|
|
||||||
|
// Stop the old container.
|
||||||
|
if let Err(e) = Command::new("nixos-container").args(["stop", "root"]).status().await {
|
||||||
|
tracing::warn!(error = ?e, "migration phase 5: nixos-container stop root failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy conf file.
|
||||||
|
if let Err(e) = std::fs::copy(&old_conf, &new_conf) {
|
||||||
|
tracing::warn!(error = ?e, "migration phase 5: copy root.conf failed — aborting");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move rootfs if it exists (may be absent for ephemeral containers).
|
||||||
|
let old_rootfs = std::path::PathBuf::from("/var/lib/nixos-containers/root");
|
||||||
|
let new_rootfs = std::path::PathBuf::from("/var/lib/nixos-containers/h-root");
|
||||||
|
if old_rootfs.exists() && !new_rootfs.exists() {
|
||||||
|
if let Err(e) = std::fs::rename(&old_rootfs, &new_rootfs) {
|
||||||
|
tracing::warn!(error = ?e, "migration phase 5: rename rootfs failed (non-fatal)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Daemon reload so systemd picks up the new container@h-root unit.
|
||||||
|
if let Err(e) = Command::new("systemctl").args(["daemon-reload"]).status().await {
|
||||||
|
tracing::warn!(error = ?e, "migration phase 5: systemctl daemon-reload failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the renamed container.
|
||||||
|
if let Err(e) = Command::new("nixos-container").args(["start", "h-root"]).status().await {
|
||||||
|
tracing::warn!(error = ?e, "migration phase 5: nixos-container start h-root failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::info!("migration phase 5: root container renamed to h-root");
|
||||||
|
let _ = std::fs::write(hroot_rename_marker(), b"done\n");
|
||||||
|
}
|
||||||
|
|
||||||
async fn enumerate_agents() -> Vec<String> {
|
async fn enumerate_agents() -> Vec<String> {
|
||||||
let containers = lifecycle::list().await.unwrap_or_default();
|
let containers = lifecycle::list().await.unwrap_or_default();
|
||||||
containers
|
containers
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|c| {
|
.filter_map(|c| {
|
||||||
if c == MANAGER_NAME {
|
if c == MANAGER_CONTAINER {
|
||||||
Some(MANAGER_NAME.to_owned())
|
Some(MANAGER_NAME.to_owned())
|
||||||
} else {
|
} else {
|
||||||
c.strip_prefix(AGENT_PREFIX).map(str::to_owned)
|
c.strip_prefix(AGENT_PREFIX).map(str::to_owned)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue