feat: show golden VM tag in clone list, add console logging, fix ubuntu boot

- Persist golden VM tag to clones/{id}/tag at spawn time
- GET /clones now returns [{id, tag}] objects instead of plain IDs
- Web UI renders tag as a dim label next to each clone entry (clone 3 · default)
- Pre-existing fixes included in this commit:
  - console: tee all PTY output to clones/{id}/console.log for boot capture
  - network: destroy stale tap before recreating to avoid EBUSY errors
  - orchestrator: fix ubuntu systemd boot (custom fc-console.service, fstab,
    mask serial-getty udev dep, longer settle time, correct package list)
  - config: remove quiet/loglevel=0 from default boot args

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-14 22:04:11 +00:00
parent fb1db7c9ea
commit d0da012a82
6 changed files with 95 additions and 31 deletions

View File

@@ -164,6 +164,17 @@ func RunConsoleProxy(cfg Config, id int, tapName, tag string) error {
logger.Infof("clone %d: restored in %s (pid=%d, tap=%s)",
id, elapsed.Round(time.Millisecond), cmd.Process.Pid, tapName)
// --- Open console log (captures all serial output from boot) ---
consoleLogPath := filepath.Join(cloneDir, "console.log")
consoleLog, err := os.OpenFile(consoleLogPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
if err != nil {
logger.Warnf("could not open console log: %v", err)
consoleLog = nil
}
if consoleLog != nil {
defer consoleLog.Close()
}
// --- Create console socket ---
os.Remove(consoleSockPath) //nolint:errcheck
listener, err := net.Listen("unix", consoleSockPath)
@@ -191,7 +202,7 @@ func RunConsoleProxy(cfg Config, id int, tapName, tag string) error {
if resizeListener != nil {
go serveResize(resizeListener, ptm, vmDone, logger)
}
serveConsole(listener, ptm, vmDone, logger)
serveConsole(listener, ptm, consoleLog, vmDone, logger)
listener.Close()
if resizeListener != nil {
@@ -274,16 +285,20 @@ func (a *atomicWriter) Write(p []byte) (int, error) {
// A background goroutine reads from the PTY master continuously (discarding
// output when no client is connected so the VM never blocks on a full buffer).
// Only one client is served at a time; sessions are serialised.
func serveConsole(listener net.Listener, ptm *os.File, vmDone <-chan struct{}, logger *log.Entry) {
func serveConsole(listener net.Listener, ptm *os.File, logFile *os.File, vmDone <-chan struct{}, logger *log.Entry) {
aw := &atomicWriter{w: io.Discard}
// Background PTY reader — runs for the full VM lifetime.
// All output is tee'd to logFile (if set) so boot messages are never lost.
go func() {
buf := make([]byte, 4096)
for {
n, err := ptm.Read(buf)
if n > 0 {
aw.Write(buf[:n]) //nolint:errcheck
if logFile != nil {
logFile.Write(buf[:n]) //nolint:errcheck
}
}
if err != nil {
return // PTY closed (VM exited)