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:
@@ -80,7 +80,7 @@ func (o *Orchestrator) Init(distro string) error {
|
||||
|
||||
func (o *Orchestrator) buildRootfs(distro, rootfsPath string) error {
|
||||
sizeMB := 512
|
||||
if distro == "debian" {
|
||||
if distro == "debian" || distro == "ubuntu" {
|
||||
sizeMB = 2048
|
||||
}
|
||||
mnt := filepath.Join(o.cfg.BaseDir, "mnt")
|
||||
@@ -161,6 +161,8 @@ func (o *Orchestrator) buildRootfs(distro, rootfsPath string) error {
|
||||
netInitScript := `#!/bin/sh
|
||||
# Poll Firecracker MMDS for network config, apply it, then exit.
|
||||
# Runs in background; loops until MMDS responds (survives snapshot resume).
|
||||
ip link set eth0 up 2>/dev/null
|
||||
ip route add 169.254.169.254 dev eth0 2>/dev/null
|
||||
ip addr add 169.254.169.2/32 dev eth0 2>/dev/null
|
||||
while true; do
|
||||
ip=$(wget -q -T1 -O- http://169.254.169.254/ip 2>/dev/null | tr -d '"')
|
||||
@@ -179,12 +181,6 @@ done
|
||||
return err
|
||||
}
|
||||
|
||||
// write fc-net-init
|
||||
os.MkdirAll(filepath.Join(mnt, "sbin"), 0o755)
|
||||
if err := os.WriteFile(filepath.Join(mnt, "sbin", "fc-net-init"), []byte(netInitScript), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if distro == "alpine" {
|
||||
// write init script
|
||||
initScript := `#!/bin/sh
|
||||
@@ -208,10 +204,10 @@ ip route add 169.254.169.254 dev eth0 2>/dev/null
|
||||
// systemd-based distributions (Debian, Ubuntu)
|
||||
svc := `[Unit]
|
||||
Description=Firecracker Network Init
|
||||
After=network.target
|
||||
After=basic.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
Type=simple
|
||||
ExecStart=/sbin/fc-net-init
|
||||
RemainAfterExit=yes
|
||||
|
||||
@@ -229,10 +225,33 @@ WantedBy=multi-user.target
|
||||
os.MkdirAll(wantsDir, 0o755)
|
||||
os.Symlink("/etc/systemd/system/fc-net-init.service", filepath.Join(wantsDir, "fc-net-init.service")) //nolint:errcheck
|
||||
|
||||
// Ensure serial console is active
|
||||
gettyWantsDir := filepath.Join(mnt, "etc", "systemd", "system", "getty.target.wants")
|
||||
os.MkdirAll(gettyWantsDir, 0o755)
|
||||
os.Symlink("/lib/systemd/system/serial-getty@.service", filepath.Join(gettyWantsDir, "serial-getty@ttyS0.service")) //nolint:errcheck
|
||||
// Mask serial-getty@ttyS0.service and the udev device unit it depends on.
|
||||
// In Firecracker, udev never runs so dev-ttyS0.device never activates,
|
||||
// causing a 90-second systemd timeout. We replace it entirely with a
|
||||
// custom service that uses ConditionPathExists (filesystem check) instead.
|
||||
systemdDir := filepath.Join(mnt, "etc", "systemd", "system")
|
||||
os.Symlink("/dev/null", filepath.Join(systemdDir, "serial-getty@ttyS0.service")) //nolint:errcheck
|
||||
os.Symlink("/dev/null", filepath.Join(systemdDir, "dev-ttyS0.device")) //nolint:errcheck
|
||||
|
||||
// Custom console service: no udev dependency, autologin as root.
|
||||
consoleSvc := `[Unit]
|
||||
Description=Serial Console (ttyS0)
|
||||
After=basic.target
|
||||
ConditionPathExists=/dev/ttyS0
|
||||
|
||||
[Service]
|
||||
ExecStart=/sbin/agetty --autologin root --noclear ttyS0 vt220
|
||||
Restart=always
|
||||
RestartSec=1
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
`
|
||||
consoleSvcPath := filepath.Join(systemdDir, "fc-console.service")
|
||||
os.WriteFile(consoleSvcPath, []byte(consoleSvc), 0o644) //nolint:errcheck
|
||||
wantsDir2 := filepath.Join(systemdDir, "multi-user.target.wants")
|
||||
os.MkdirAll(wantsDir2, 0o755)
|
||||
os.Symlink("/etc/systemd/system/fc-console.service", filepath.Join(wantsDir2, "fc-console.service")) //nolint:errcheck
|
||||
|
||||
// Clear root password for auto-login on console
|
||||
shadowPath := filepath.Join(mnt, "etc", "shadow")
|
||||
@@ -249,6 +268,11 @@ WantedBy=multi-user.target
|
||||
}
|
||||
os.WriteFile(shadowPath, []byte(strings.Join(lines, "\n")), 0o640) //nolint:errcheck
|
||||
}
|
||||
|
||||
// Write fstab so systemd mounts virtual filesystems at boot.
|
||||
// Minimal tarball rootfs has no fstab; without it /proc, /sys, /dev are not mounted.
|
||||
fstab := "proc\t/proc\tproc\tdefaults\t0 0\nsysfs\t/sys\tsysfs\tdefaults\t0 0\ndevtmpfs\t/dev\tdevtmpfs\tdefaults\t0 0\n"
|
||||
os.WriteFile(filepath.Join(mnt, "etc", "fstab"), []byte(fstab), 0o644) //nolint:errcheck
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -363,8 +387,15 @@ func (o *Orchestrator) Golden(tag string, distro string) error {
|
||||
[]byte(fmt.Sprintf("%d", cmd.Process.Pid)), 0o644)
|
||||
}
|
||||
|
||||
o.log.Info("golden VM booted, letting it settle ...")
|
||||
time.Sleep(3 * time.Second)
|
||||
settleTime := 3 * time.Second
|
||||
if distro == "debian" || distro == "ubuntu" {
|
||||
// systemd takes significantly longer to reach multi-user.target than
|
||||
// Alpine's busybox init. Snapshot too early and serial-getty@ttyS0
|
||||
// won't have started yet, leaving the console unresponsive on resume.
|
||||
settleTime = 20 * time.Second
|
||||
}
|
||||
o.log.Infof("golden VM booted, letting it settle (%s) ...", settleTime)
|
||||
time.Sleep(settleTime)
|
||||
|
||||
// pause
|
||||
o.log.Info("pausing golden VM ...")
|
||||
@@ -494,6 +525,7 @@ func (o *Orchestrator) spawnOne(id int, net bool, tag string) error {
|
||||
goldenDir := o.goldenDir(tag)
|
||||
cloneDir := filepath.Join(o.clonesDir(), strconv.Itoa(id))
|
||||
os.MkdirAll(cloneDir, 0o755)
|
||||
os.WriteFile(filepath.Join(cloneDir, "tag"), []byte(tag), 0o644) //nolint:errcheck
|
||||
|
||||
sockPath := filepath.Join(cloneDir, "api.sock")
|
||||
os.Remove(sockPath)
|
||||
@@ -707,7 +739,7 @@ func installUbuntuPackages(mnt string, logger *log.Entry) error {
|
||||
os.WriteFile(filepath.Join(mnt, "etc/resolv.conf"), data, 0o644) //nolint:errcheck
|
||||
}
|
||||
|
||||
pkgs := "bash curl iproute2 wget ca-certificates"
|
||||
pkgs := "bash curl iproute2 wget ca-certificates systemd systemd-sysv util-linux"
|
||||
script := "DEBIAN_FRONTEND=noninteractive apt-get update -q && " +
|
||||
"DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends " + pkgs + " && " +
|
||||
"apt-get clean && rm -rf /var/lib/apt/lists/*"
|
||||
|
||||
Reference in New Issue
Block a user