// 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" 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": fatal(orch.Init()) case "golden": fatal(orch.Golden()) case "spawn": n := 1 if len(os.Args) > 2 { fmt.Sscanf(os.Args[2], "%d", &n) } fatal(orch.Spawn(n)) case "status": orch.Status() case "kill": fatal(orch.Kill()) case "cleanup": fatal(orch.Cleanup()) 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 tap string fs.IntVar(&id, "id", 0, "clone ID") 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)) 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 Download kernel + create Alpine rootfs golden Boot golden VM → pause → snapshot spawn [N] Restore N clones from golden snapshot (default: 1) 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) } }