Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59de7fa3c5 | ||
|
|
36c4c61ea2 |
2 changed files with 171 additions and 7 deletions
|
|
@ -325,26 +325,34 @@ async fn systemd_daemon_reload() -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Idempotently rewrite the `EXTRA_NSPAWN_FLAGS` line in
|
/// Idempotently rewrite the lines in `/etc/nixos-containers/<container>.conf`
|
||||||
/// `/etc/nixos-containers/<container>.conf`. The start script expands this
|
/// that hive-c0re owns: `PRIVATE_NETWORK` (forced 0 so the agent's web UI port
|
||||||
/// variable unquoted into the `systemd-nspawn` command.
|
/// is reachable on the host) and `EXTRA_NSPAWN_FLAGS` (the runtime-dir bind).
|
||||||
|
/// The start script expands `$EXTRA_NSPAWN_FLAGS` unquoted into the
|
||||||
|
/// `systemd-nspawn` command.
|
||||||
fn set_nspawn_flags(container: &str, agent_dir: &Path) -> Result<()> {
|
fn set_nspawn_flags(container: &str, agent_dir: &Path) -> Result<()> {
|
||||||
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}"))?;
|
||||||
let flag = format!(
|
let bind_flag = format!(
|
||||||
"EXTRA_NSPAWN_FLAGS=\"--bind={}:{CONTAINER_RUNTIME_MOUNT}\"",
|
"EXTRA_NSPAWN_FLAGS=\"--bind={}:{CONTAINER_RUNTIME_MOUNT}\"",
|
||||||
agent_dir.display()
|
agent_dir.display()
|
||||||
);
|
);
|
||||||
let mut lines: Vec<String> = original
|
let mut lines: Vec<String> = original
|
||||||
.lines()
|
.lines()
|
||||||
.filter(|line| !line.trim_start().starts_with("EXTRA_NSPAWN_FLAGS="))
|
.filter(|line| {
|
||||||
|
let trimmed = line.trim_start();
|
||||||
|
!trimmed.starts_with("EXTRA_NSPAWN_FLAGS=")
|
||||||
|
&& !trimmed.starts_with("PRIVATE_NETWORK=")
|
||||||
|
})
|
||||||
.map(str::to_owned)
|
.map(str::to_owned)
|
||||||
.collect();
|
.collect();
|
||||||
lines.push(flag);
|
// Share host netns so per-agent web UI ports are reachable directly.
|
||||||
|
lines.push("PRIVATE_NETWORK=0".to_owned());
|
||||||
|
lines.push(bind_flag);
|
||||||
let mut content = lines.join("\n");
|
let mut content = lines.join("\n");
|
||||||
content.push('\n');
|
content.push('\n');
|
||||||
std::fs::write(&path, content).with_context(|| format!("write {path}"))?;
|
std::fs::write(&path, content).with_context(|| format!("write {path}"))?;
|
||||||
tracing::info!(%path, "set EXTRA_NSPAWN_FLAGS");
|
tracing::info!(%path, "set PRIVATE_NETWORK=0 + EXTRA_NSPAWN_FLAGS");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
156
tests/dashboard.sh
Executable file
156
tests/dashboard.sh
Executable file
|
|
@ -0,0 +1,156 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Phase 6 + 7 smoke test: dashboard HTTP, per-container web UIs,
|
||||||
|
# approve-by-POST, SSE endpoint, and orphan-approval GC.
|
||||||
|
#
|
||||||
|
# Runs as root on a host with services.hive-c0re enabled and the hm1nd
|
||||||
|
# container declared. Idempotent — wipes any prior alice state.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
AGENT=alice
|
||||||
|
PKG=htop
|
||||||
|
HOST=${HOST:-localhost}
|
||||||
|
DASH_PORT=${DASH_PORT:-7000}
|
||||||
|
MGR_PORT=${MGR_PORT:-8000}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
sudo hive-c0re kill "$AGENT" 2>/dev/null || true
|
||||||
|
sudo nixos-container destroy "h-${AGENT}" 2>/dev/null || true
|
||||||
|
sudo rm -rf \
|
||||||
|
"/var/lib/hyperhive/agents/${AGENT}" \
|
||||||
|
"/var/lib/hyperhive/applied/${AGENT}"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "=== precheck: hm1nd container is up ==="
|
||||||
|
if ! sudo machinectl status hm1nd >/dev/null 2>&1; then
|
||||||
|
echo " starting via systemd..."
|
||||||
|
sudo systemctl start container@hm1nd.service
|
||||||
|
for i in 1 2 3 4 5 6 7 8 9 10; do
|
||||||
|
sudo machinectl status hm1nd >/dev/null 2>&1 && break
|
||||||
|
sleep 0.5
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
echo " ✓ hm1nd is up"
|
||||||
|
|
||||||
|
cleanup
|
||||||
|
|
||||||
|
echo "=== dashboard reachable ==="
|
||||||
|
curl -sf "http://${HOST}:${DASH_PORT}/" | grep -q "HYPERHIVE" || {
|
||||||
|
echo "FAIL: dashboard not serving expected content"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
echo " ✓ http://${HOST}:${DASH_PORT}/ → HTML with HYPERHIVE banner"
|
||||||
|
|
||||||
|
echo "=== /messages/stream SSE endpoint ==="
|
||||||
|
curl -sI "http://${HOST}:${DASH_PORT}/messages/stream" \
|
||||||
|
| grep -qi "content-type: text/event-stream" || {
|
||||||
|
echo "FAIL: /messages/stream is not SSE"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
echo " ✓ Content-Type: text/event-stream"
|
||||||
|
|
||||||
|
echo "=== manager UI reachable ==="
|
||||||
|
curl -sf "http://${HOST}:${MGR_PORT}/" | grep -q "hm1nd" || {
|
||||||
|
echo "FAIL: manager UI not reachable at :${MGR_PORT}"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
echo " ✓ http://${HOST}:${MGR_PORT}/ → hm1nd label"
|
||||||
|
|
||||||
|
echo "=== spawn ${AGENT} ==="
|
||||||
|
sudo hive-c0re spawn "$AGENT"
|
||||||
|
|
||||||
|
echo "=== pick up ${AGENT}'s port from dashboard ==="
|
||||||
|
sleep 2
|
||||||
|
ALICE_PORT=$(curl -sf "http://${HOST}:${DASH_PORT}/" \
|
||||||
|
| sed -nE "s|.*href=\"http://[^:]+:([0-9]+)/\">${AGENT}<.*|\\1|p" \
|
||||||
|
| head -1)
|
||||||
|
if [ -z "$ALICE_PORT" ]; then
|
||||||
|
echo "FAIL: ${AGENT} port not found in dashboard"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo " ✓ ${AGENT} port: ${ALICE_PORT}"
|
||||||
|
|
||||||
|
echo "=== ${AGENT} UI reachable ==="
|
||||||
|
for i in 1 2 3 4 5 6 7 8 9 10; do
|
||||||
|
curl -sf "http://${HOST}:${ALICE_PORT}/" | grep -q "${AGENT}" && break
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
curl -sf "http://${HOST}:${ALICE_PORT}/" | grep -q "${AGENT}" || {
|
||||||
|
echo "FAIL: ${AGENT} UI not reachable at :${ALICE_PORT}"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
echo " ✓ http://${HOST}:${ALICE_PORT}/ → ${AGENT} label"
|
||||||
|
|
||||||
|
echo "=== ${PKG} not in path pre-approve ==="
|
||||||
|
if sudo nixos-container run "h-${AGENT}" -- which "$PKG" 2>/dev/null; then
|
||||||
|
echo "FAIL: ${PKG} already in path"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo " ✓"
|
||||||
|
|
||||||
|
echo "=== manager submits approval (via hm1nd) ==="
|
||||||
|
sudo nixos-container run hm1nd -- bash -c "
|
||||||
|
set -euo pipefail
|
||||||
|
cd /agents/${AGENT}/config
|
||||||
|
cat > agent.nix <<'EOF'
|
||||||
|
{ pkgs, ... }:
|
||||||
|
{
|
||||||
|
environment.systemPackages = [ pkgs.${PKG} ];
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
git commit -am 'add ${PKG}'
|
||||||
|
SHA=\$(git rev-parse HEAD)
|
||||||
|
echo \" manager commit SHA=\$SHA\"
|
||||||
|
hive-m1nd request-apply-commit ${AGENT} \$SHA
|
||||||
|
"
|
||||||
|
|
||||||
|
echo "=== pick approval id from dashboard ==="
|
||||||
|
ID=$(curl -sf "http://${HOST}:${DASH_PORT}/" \
|
||||||
|
| sed -nE 's|.*hive-c0re approve ([0-9]+).*|\1|p' \
|
||||||
|
| head -1)
|
||||||
|
if [ -z "$ID" ]; then
|
||||||
|
echo "FAIL: approval id not found in dashboard"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo " ✓ approval id: ${ID}"
|
||||||
|
|
||||||
|
echo "=== POST /approve/${ID} (browser button equivalent) ==="
|
||||||
|
HTTP_CODE=$(curl -s -o /dev/null -w '%{http_code}' \
|
||||||
|
-X POST "http://${HOST}:${DASH_PORT}/approve/${ID}")
|
||||||
|
if [ "$HTTP_CODE" != "303" ] && [ "$HTTP_CODE" != "302" ]; then
|
||||||
|
echo "FAIL: expected 303 redirect, got HTTP ${HTTP_CODE}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo " ✓ HTTP ${HTTP_CODE} (redirect)"
|
||||||
|
|
||||||
|
echo "=== verify ${PKG} now in h-${AGENT} ==="
|
||||||
|
sudo nixos-container run "h-${AGENT}" -- which "$PKG"
|
||||||
|
|
||||||
|
echo "=== orphan-GC: cleanup ${AGENT}, refresh dashboard, orphan disappears ==="
|
||||||
|
sudo nixos-container run hm1nd -- bash -c "
|
||||||
|
set -euo pipefail
|
||||||
|
cd /agents/${AGENT}/config
|
||||||
|
echo '{ ... }: { }' > agent.nix
|
||||||
|
git commit -am 'noop'
|
||||||
|
SHA=\$(git rev-parse HEAD)
|
||||||
|
hive-m1nd request-apply-commit ${AGENT} \$SHA
|
||||||
|
"
|
||||||
|
ORPHAN_ID=$(sudo hive-c0re pending \
|
||||||
|
| sed -nE 's/^[[:space:]]*"id":[[:space:]]*([0-9]+).*/\1/p' \
|
||||||
|
| tail -1)
|
||||||
|
echo " queued orphan id: ${ORPHAN_ID}"
|
||||||
|
cleanup
|
||||||
|
# First dashboard render triggers GC.
|
||||||
|
curl -sf "http://${HOST}:${DASH_PORT}/" >/dev/null
|
||||||
|
sleep 0.5
|
||||||
|
if sudo hive-c0re pending | grep -q "\"id\": ${ORPHAN_ID}"; then
|
||||||
|
echo "FAIL: orphan ${ORPHAN_ID} still pending after dashboard render"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo " ✓ orphan ${ORPHAN_ID} cleaned up"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "=== summary ==="
|
||||||
|
echo " dashboard http://${HOST}:${DASH_PORT}/"
|
||||||
|
echo " manager UI http://${HOST}:${MGR_PORT}/"
|
||||||
|
echo " (alice torn down)"
|
||||||
Loading…
Reference in a new issue