Add Alpine, Debian, and Ubuntu rootfs support to `init [distro]`. Golden snapshots are now namespaced under `golden/<tag>/` so multiple baselines can coexist. `spawn [tag] [N]` selects which snapshot to clone from. Systemd-based distros (Debian, Ubuntu) get a fc-net-init systemd unit; Alpine keeps its inittab-based init. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
169 lines
4.2 KiB
Go
169 lines
4.2 KiB
Go
// fc-orchestrator — Poor man's Firecracker snapshot orchestrator in Go.
|
|
//
|
|
// Creates a golden VM, snapshots it, then spawns N clones that share the
|
|
// base memory file via Firecracker's MAP_PRIVATE (kernel-level COW).
|
|
// Rootfs gets a filesystem-level COW copy (reflink where supported).
|
|
//
|
|
// Usage:
|
|
//
|
|
// go build -o fc-orch .
|
|
// sudo ./fc-orch init
|
|
// sudo ./fc-orch golden
|
|
// sudo ./fc-orch spawn 10
|
|
// sudo ./fc-orch status
|
|
// sudo ./fc-orch kill
|
|
// sudo ./fc-orch cleanup
|
|
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"github.com/kacerr/fc-orchestrator/orchestrator"
|
|
)
|
|
|
|
func main() {
|
|
// strip --dev flag before subcommand routing
|
|
dev := false
|
|
filtered := os.Args[:1]
|
|
for _, a := range os.Args[1:] {
|
|
if a == "--dev" {
|
|
dev = true
|
|
} else {
|
|
filtered = append(filtered, a)
|
|
}
|
|
}
|
|
os.Args = filtered
|
|
|
|
if dev {
|
|
log.SetReportCaller(true)
|
|
log.SetFormatter(&log.TextFormatter{
|
|
CallerPrettyfier: func(f *runtime.Frame) (string, string) {
|
|
return "", fmt.Sprintf("%s:%d", filepath.Base(f.File), f.Line)
|
|
},
|
|
})
|
|
}
|
|
|
|
// figure out if we are running as root
|
|
if os.Geteuid() == 0 {
|
|
fmt.Println("Running with root/sudo privileges!")
|
|
} else {
|
|
fmt.Println("Running as a normal user.")
|
|
}
|
|
|
|
if len(os.Args) < 2 {
|
|
usage()
|
|
os.Exit(1)
|
|
}
|
|
|
|
orch := orchestrator.New(orchestrator.DefaultConfig())
|
|
|
|
switch os.Args[1] {
|
|
case "init":
|
|
distro := "alpine"
|
|
if len(os.Args) > 2 {
|
|
distro = os.Args[2]
|
|
}
|
|
fatal(orch.Init(distro))
|
|
case "golden":
|
|
tag := "default"
|
|
distro := "alpine"
|
|
if len(os.Args) > 2 {
|
|
tag = os.Args[2]
|
|
}
|
|
if len(os.Args) > 3 {
|
|
distro = os.Args[3]
|
|
}
|
|
fatal(orch.Golden(tag, distro))
|
|
case "spawn":
|
|
n := 1
|
|
tag := "default"
|
|
for _, arg := range os.Args[2:] {
|
|
if parsed, err := strconv.Atoi(arg); err == nil {
|
|
n = parsed
|
|
} else {
|
|
tag = arg
|
|
}
|
|
}
|
|
fatal(orch.Spawn(n, tag))
|
|
case "status":
|
|
orch.Status()
|
|
case "kill":
|
|
fatal(orch.Kill())
|
|
case "cleanup":
|
|
fatal(orch.Cleanup())
|
|
case "serve":
|
|
addr := ":8080"
|
|
if len(os.Args) > 2 {
|
|
addr = os.Args[2]
|
|
}
|
|
fatal(orchestrator.Serve(orch, addr))
|
|
case "console":
|
|
if len(os.Args) < 3 {
|
|
fmt.Fprintf(os.Stderr, "usage: %s console <id>\n", os.Args[0])
|
|
os.Exit(1)
|
|
}
|
|
var id int
|
|
fmt.Sscanf(os.Args[2], "%d", &id)
|
|
fatal(orchestrator.ConnectConsole(orchestrator.DefaultConfig(), id))
|
|
case "_console-proxy":
|
|
// Internal subcommand: started by spawnOne, runs as a background daemon.
|
|
fs := flag.NewFlagSet("_console-proxy", flag.ContinueOnError)
|
|
var id int
|
|
var tag string
|
|
var tap string
|
|
fs.IntVar(&id, "id", 0, "clone ID")
|
|
fs.StringVar(&tag, "tag", "default", "Golden VM tag")
|
|
fs.StringVar(&tap, "tap", "", "TAP device name")
|
|
if err := fs.Parse(os.Args[2:]); err != nil {
|
|
fmt.Fprintf(os.Stderr, "console-proxy: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fatal(orchestrator.RunConsoleProxy(orchestrator.DefaultConfig(), id, tap, tag))
|
|
default:
|
|
usage()
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func usage() {
|
|
fmt.Fprintf(os.Stderr, `Usage: %s [--dev] <command> [args]
|
|
|
|
Flags:
|
|
--dev log format with source file:line (e.g. file="orchestrator.go:123")
|
|
|
|
Commands:
|
|
init [distro] Download kernel + create distro rootfs (default: alpine, options: alpine, debian, ubuntu)
|
|
golden [tag] [distro] Boot golden VM → pause → snapshot (default tag: default, default distro: alpine)
|
|
spawn [tag] [N] Restore N clones from golden snapshot (default tag: default, default N: 1)
|
|
serve [addr] Start terminal web UI (default: :8080)
|
|
console <id> Attach to the serial console of a running clone (Ctrl+] to detach)
|
|
status Show running clones
|
|
kill Kill all running VMs
|
|
cleanup Kill VMs + remove all state
|
|
|
|
Environment:
|
|
FC_BIN firecracker binary path (default: firecracker)
|
|
FC_BASE_DIR working directory (default: /tmp/fc-orch)
|
|
FC_KERNEL vmlinux path
|
|
FC_KERNEL_URL vmlinux download URL (default: pinned Firecracker CI build)
|
|
FC_ROOTFS rootfs.ext4 path
|
|
FC_VCPUS vCPUs per VM (default: 1)
|
|
FC_MEM_MIB MiB per VM (default: 128)
|
|
FC_BRIDGE bridge name or "none" (default: fcbr0)
|
|
`, os.Args[0])
|
|
}
|
|
|
|
func fatal(err error) {
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "fatal: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|