// 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 \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] [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 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) } }