//go:build linux package vmruntime import ( "cmp" "context" "encoding/json" "errors" "fmt" "io" "os" "os/user" "path/filepath" "strconv" "strings" "syscall" "time" "github.com/johnandersen777/socialweb-computer-kcp/internal/fcapi" "github.com/johnandersen777/socialweb-computer-kcp/internal/guestdisk" "github.com/johnandersen777/socialweb-computer-kcp/internal/guestimage" "github.com/johnandersen777/socialweb-computer-kcp/internal/guestnet" "github.com/johnandersen777/socialweb-computer-kcp/internal/hostcontract" "github.com/johnandersen777/socialweb-computer-kcp/internal/hostsvc" "github.com/johnandersen777/socialweb-computer-kcp/internal/kit/fswatch" "github.com/johnandersen777/socialweb-computer-kcp/internal/kit/fsx" ) type firecracker struct { cfg Config subuid hostcontract.SubidEntry subgid hostcontract.SubidEntry slots int target string } func ForHost(cfg Config) (Runtime, error) { if cfg.Manager == nil { return nil, fmt.Errorf("vmruntime: no placement manager") } if !filepath.IsAbs(cfg.StateDir) || !filepath.IsAbs(cfg.RuntimeDir) { return nil, fmt.Errorf("vmruntime: state dir %q and runtime dir %q must be absolute", cfg.StateDir, cfg.RuntimeDir) } for name, p := range map[string]string{"swc-userns-stage": cfg.Exec.Stage, "pasta": cfg.Exec.Pasta, "socialweb-vmexec": cfg.Exec.VMExec, "firecracker": cfg.Exec.Firecracker} { if !filepath.IsAbs(p) { return nil, fmt.Errorf("vmruntime: %s path %q is not absolute", name, p) } } cfg.InnerID = cmp.Or(cfg.InnerID, DefaultInnerID) if cfg.InnerID <= 0 || cfg.InnerID >= hostcontract.VMSlotIDs { return nil, fmt.Errorf("vmruntime: inner id %d must be inside 1..%d: SELinux refuses pasta as uid 0 of the namespace", cfg.InnerID, hostcontract.VMSlotIDs-1) } cfg.BootTimeout = cmp.Or(cfg.BootTimeout, DefaultBootTime) cfg.StopGrace = cmp.Or(cfg.StopGrace, DefaultStopGrace) cfg.ConsoleCap = cmp.Or(cfg.ConsoleCap, DefaultConsoleCap) target := ImageTarget() if target == "" { return nil, ErrUnsupported } me, err := user.Current() if err != nil { return nil, fmt.Errorf("vmruntime: %w", err) } uid := os.Getuid() subuidText, err := os.ReadFile("/etc/subuid") if err != nil { return nil, fmt.Errorf("vmruntime: %w", err) } subgidText, err := os.ReadFile("/etc/subgid") if err != nil { return nil, fmt.Errorf("vmruntime: %w", err) } su, okU := hostcontract.AccountRange(string(subuidText), me.Username, uid) sg, okG := hostcontract.AccountRange(string(subgidText), me.Username, uid) if !okU || !okG { return nil, fmt.Errorf("vmruntime: %s has no subordinate id range in /etc/subuid and /etc/subgid; each VM runs in a user namespace mapped to a %d-id slot of it (socialweb-computer install bidder reserves %s::%d)", me.Username, hostcontract.VMSlotIDs, me.Username, 256*hostcontract.VMSlotIDs) } slots := int(min(su.Count, sg.Count) / hostcontract.VMSlotIDs) if slots < 1 { return nil, fmt.Errorf("vmruntime: the subordinate range of %s holds %d ids, under one %d-id slot", me.Username, min(su.Count, sg.Count), hostcontract.VMSlotIDs) } return &firecracker{cfg: cfg, subuid: su, subgid: sg, slots: slots, target: target}, nil } func (r *firecracker) Capabilities() Capabilities { c := r.cfg.Manager.Capabilities() return Capabilities{Backend: c.Backend, Hypervisor: "firecracker", EnforcesLimits: c.EnforcesLimits, GracefulStop: GracefulStop()} } func (r *firecracker) imagePin() (guestimage.Pin, error) { return guestimage.PinFor(r.target) } func (r *firecracker) imageStore() guestimage.Store { return guestimage.Store{Root: filepath.Join(r.cfg.StateDir, "images"), Out: r.cfg.ImageOut, TTY: r.cfg.ImageTTY} } func (r *firecracker) Prepare(ctx context.Context, spec VMSpec) (Prepared, error) { if err := ValidateSpec(spec); err != nil { return Prepared{}, err } pin, err := r.imagePin() if err != nil { return Prepared{}, err } store := r.imageStore() img, err := store.Ensure(ctx, pin, r.cfg.ImageSource) if err != nil { return Prepared{}, err } manifest, err := readManifest(img.Dir) if err != nil { return Prepared{}, err } p := Prepared{ Spec: spec, Unit: UnitName(spec.Handle, spec.Name), WorkDir: WorkDir(r.cfg.StateDir, spec.Handle, spec.Name), SocketDir: SocketDir(r.cfg.RuntimeDir, spec.Handle, spec.Name), VMMID: VMMID(spec.Handle, spec.Name), ImageDir: img.Dir, Vcpu: manifest.Vcpu, MemMiB: manifest.MemMiB, } p.Socket = filepath.Join(p.SocketDir, SocketFile) if len(p.Socket) >= 108 { return Prepared{}, fmt.Errorf("vmruntime: the API socket %s is %d bytes, over the 107-byte unix socket limit", p.Socket, len(p.Socket)) } if spec.Vcpu > 0 { p.Vcpu = spec.Vcpu } if spec.MemMiB > 0 { p.MemMiB = spec.MemMiB } if _, err := os.Stat(p.WorkDir); err == nil { return Prepared{}, fmt.Errorf("vmruntime: %s already has a work directory %s; clean it up first", spec.Name, p.WorkDir) } if err := os.MkdirAll(filepath.Dir(p.WorkDir), 0o700); err != nil { return Prepared{}, fmt.Errorf("vmruntime: %w", err) } if err := os.Mkdir(p.WorkDir, 0o700); err != nil { return Prepared{}, fmt.Errorf("vmruntime: %w", err) } ok := false defer func() { if !ok { os.RemoveAll(p.WorkDir) } }() rootfs := filepath.Join(p.WorkDir, RootfsFile) if err := guestdisk.Clone(filepath.Join(img.Dir, guestimage.RootfsName), rootfs); err != nil { return Prepared{}, err } if err := guestdisk.GrowTo(rootfs, spec.DiskMiB); err != nil { return Prepared{}, err } meta := spec.MetaData meta = cmp.Or(meta, "instance-id: "+spec.Name+"\nlocal-hostname: "+spec.Name+"\n") seed := filepath.Join(p.WorkDir, SeedFile) if err := guestdisk.WriteSeed(seed, []byte(spec.UserData), []byte(meta), []byte(guestnet.NetworkConfig())); err != nil { return Prepared{}, err } conf := fcapi.Config{ BootSource: fcapi.BootSource{KernelImagePath: filepath.Join(img.Dir, guestimage.KernelName), BootArgs: strings.Join(manifest.BootArgs, " ")}, Drives: []fcapi.Drive{ {DriveID: "rootfs", PathOnHost: rootfs, IsRootDevice: true}, {DriveID: "cidata", PathOnHost: seed, IsReadOnly: true}, }, MachineConfig: fcapi.MachineConfig{VcpuCount: p.Vcpu, MemSizeMiB: p.MemMiB}, NetworkInterfaces: []fcapi.NetworkInterface{{IfaceID: guestnet.Interface, HostDevName: guestnet.Tap, GuestMAC: guestnet.GuestMAC}}, } if err := fcapi.WriteConfig(filepath.Join(p.WorkDir, ConfigFile), conf); err != nil { return Prepared{}, fmt.Errorf("vmruntime: %w", err) } console := filepath.Join(p.WorkDir, ConsoleFile) hostAddrs, err := guestnet.HostAddresses() if err != nil { return Prepared{}, fmt.Errorf("vmruntime: %w", err) } if spec.WebRTCUDPPort != 0 { p.WebRTCAddresses = guestnet.LANAddresses(hostAddrs) } params := guestnet.VMExec{ Wiring: guestnet.Plan(spec.HostPorts, spec.LANUDP, spec.HostUDP, hostAddrs, spec.WebRTCUDPPort), VMM: r.cfg.Exec.Firecracker, Argv: fcapi.Argv("firecracker", p.VMMID, filepath.Join(p.WorkDir, ConfigFile), p.Socket), Console: console, } if err := fsx.WriteJSON(filepath.Join(p.WorkDir, ParamsFile), params, 0o600); err != nil { return Prepared{}, err } for _, f := range []string{console, filepath.Join(p.WorkDir, VMMLogFile)} { if err := os.WriteFile(f, nil, 0o600); err != nil { return Prepared{}, fmt.Errorf("vmruntime: %w", err) } } ok = true return p, nil } func (r *firecracker) slotIDs(k int) (uidBase, gidBase, vmmUID int) { uidBase = int(r.subuid.Start) + k*hostcontract.VMSlotIDs gidBase = int(r.subgid.Start) + k*hostcontract.VMSlotIDs return uidBase, gidBase, uidBase + r.cfg.InnerID } func (r *firecracker) UnitSpec(p Prepared, slot int) hostsvc.UnitSpec { uidBase, gidBase, _ := r.slotIDs(slot) exec := []string{r.cfg.Exec.Stage, strconv.Itoa(uidBase), strconv.Itoa(gidBase), strconv.Itoa(hostcontract.VMSlotIDs), strconv.Itoa(r.cfg.InnerID)} exec = append(exec, guestnet.PastaArgv(r.cfg.Exec.Pasta, guestnet.PastaUDPForwards(p.Spec.WebRTCUDPPort, p.WebRTCAddresses), r.cfg.Exec.VMExec, filepath.Join(p.WorkDir, ParamsFile))...) return hostsvc.UnitSpec{ Name: p.Unit, Description: fmt.Sprintf("socialweb vm %s handle %s slot=%d", p.Spec.Name, HandleID(p.Spec.Handle), slot), Exec: exec, Env: []string{"TZ=UTC"}, WorkDir: p.WorkDir, Output: hostsvc.Output{StdoutFile: filepath.Join(p.WorkDir, VMMLogFile), StderrFile: filepath.Join(p.WorkDir, VMMLogFile)}, Limits: hostsvc.Limits{ MemoryMax: uint64(p.MemMiB+VMMOverheadMiB) << 20, CPUWeight: 100, CPUQuotaPercent: 100 * p.Vcpu, TasksMax: uint64(64 + 4*p.Vcpu), }, OOMPolicy: "stop", KillSignal: syscall.SIGKILL, StopTimeout: StopTimeout, Slice: SliceName(p.Spec.Handle), Restart: hostsvc.RestartNo, } } func (r *firecracker) Start(ctx context.Context, p Prepared) (RuntimeRef, error) { slot, err := r.Reserve(ctx, p, func(ctx context.Context, _ int, spec hostsvc.UnitSpec) error { _, err := r.cfg.Manager.Start(ctx, spec) return err }) ref := RuntimeRef{} if err == nil { ref, err = r.Await(ctx, p, slot) } if err != nil { stopCtx := context.WithoutCancel(ctx) _ = r.cfg.Manager.Stop(stopCtx, p.Unit) _ = r.cfg.Manager.ResetFailed(stopCtx, p.Unit) tail := tailOf(filepath.Join(p.WorkDir, VMMLogFile), 3000) + tailOf(filepath.Join(p.WorkDir, ConsoleFile), 3000) r.Cleanup(stopCtx, p.Spec.Handle, p.Spec.Name) return RuntimeRef{}, fmt.Errorf("%w\n%s", err, tail) } return ref, nil } func (r *firecracker) Placed(handle, name string) Prepared { p := Prepared{ Spec: VMSpec{Handle: handle, Name: name}, Unit: UnitName(handle, name), WorkDir: WorkDir(r.cfg.StateDir, handle, name), SocketDir: SocketDir(r.cfg.RuntimeDir, handle, name), VMMID: VMMID(handle, name), } p.Socket = filepath.Join(p.SocketDir, SocketFile) return p } func (r *firecracker) StopGrace() time.Duration { return r.cfg.StopGrace } func (r *firecracker) awaitDown(ctx context.Context, unit string, grace time.Duration) (bool, error) { gctx, cancel := context.WithTimeout(ctx, grace) defer cancel() _, err := hostsvc.Await(gctx, r.cfg.Manager, unit, func(st hostsvc.UnitStatus) bool { return !hostsvc.Live(st) }) switch { case err == nil: return true, nil case ctx.Err() != nil: return false, ctx.Err() case gctx.Err() != nil: return false, nil } return false, err } func (r *firecracker) Shutdown(ctx context.Context, ref RuntimeRef, grace time.Duration) (bool, error) { st, err := r.cfg.Manager.Inspect(ctx, ref.Unit) if err != nil { return false, err } if !hostsvc.Live(st) { return true, nil } if !GracefulStop() || ref.Socket == "" { return false, nil } if err := fcapi.NewClient(ref.Socket, 2*time.Second).SendCtrlAltDel(ctx); err != nil { return false, nil } return r.awaitDown(ctx, ref.Unit, grace) } func (r *firecracker) Reserve(ctx context.Context, p Prepared, place Placer) (int, error) { unlock, err := r.lock() if err != nil { return 0, err } defer unlock() used, err := r.usedSlots(ctx) if err != nil { return 0, err } slot := -1 for k := 0; k < r.slots; k++ { if !used[k] { slot = k break } } if slot < 0 { return 0, fmt.Errorf("vmruntime: all %d VM slots of this account are held by loaded VM units", r.slots) } _, _, vmmUID := r.slotIDs(slot) if err := r.grant(p, vmmUID); err != nil { return 0, err } if err := place(ctx, slot, r.UnitSpec(p, slot)); err != nil { return 0, err } return slot, nil } func (r *firecracker) grant(p Prepared, vmmUID int) error { if err := os.MkdirAll(filepath.Dir(p.SocketDir), 0o700); err != nil { return fmt.Errorf("vmruntime: %w", err) } if err := os.Mkdir(p.SocketDir, 0o700); err != nil && !errors.Is(err, os.ErrExist) { return fmt.Errorf("vmruntime: %w", err) } g := newGranter(vmmUID) files := []struct { path string perm uint16 }{ {filepath.Join(p.WorkDir, RootfsFile), 6}, {filepath.Join(p.WorkDir, SeedFile), 4}, {filepath.Join(p.WorkDir, ConfigFile), 4}, {filepath.Join(p.WorkDir, ParamsFile), 4}, {filepath.Join(p.WorkDir, ConsoleFile), 6}, {p.SocketDir, 7}, } for _, f := range files { if err := g.need(f.path, f.perm, false); err != nil { return err } } if err := g.need(p.WorkDir, 1, false); err != nil { return err } if err := g.traverse(p.WorkDir, ""); err != nil { return err } if err := g.traverse(p.SocketDir, ""); err != nil { return err } for _, f := range []string{filepath.Join(p.ImageDir, guestimage.KernelName), r.cfg.Exec.Pasta, r.cfg.Exec.VMExec, r.cfg.Exec.Firecracker} { if err := g.traverse(f, ""); err != nil { return err } } for _, f := range []string{r.cfg.Exec.Pasta, r.cfg.Exec.VMExec, r.cfg.Exec.Firecracker} { if err := g.need(f, 5, true); err != nil { return err } } if err := g.need(filepath.Join(p.ImageDir, guestimage.KernelName), 4, true); err != nil { return err } var outside []Grant for _, gr := range g.grants { if !strings.HasPrefix(gr.Path, p.WorkDir+string(filepath.Separator)) && gr.Path != p.WorkDir { outside = append(outside, gr) } } return fsx.WriteJSON(filepath.Join(p.WorkDir, GrantsFile), outside, 0o600) } const ( DialRetryMin = 10 * time.Millisecond DialRetryMax = time.Second ) func (r *firecracker) awaitBoot(ctx context.Context, p Prepared) error { bctx, cancel := context.WithTimeout(ctx, r.cfg.BootTimeout) defer cancel() sock, err := fswatch.Shared().Subscribe(p.SocketDir) if err != nil { return fmt.Errorf("vmruntime: watching %s: %w", p.SocketDir, err) } defer sock.Close() units, _ := r.cfg.Manager.Watch(bctx, p.Unit) exited := func(st hostsvc.UnitStatus) error { if st.State != hostsvc.Active && st.State != hostsvc.Activating { return fmt.Errorf("vmruntime: %s went %s (result %s, status %d) before its VMM answered", p.Unit, st.State, st.Result, st.ExecMainStatus) } return nil } if st, err := r.cfg.Manager.Inspect(bctx, p.Unit); err == nil { if err := exited(st); err != nil { return err } } client := fcapi.NewClient(p.Socket, 2*time.Second) last := fmt.Errorf("the API socket %s is not there", p.Socket) delay := DialRetryMin var again <-chan time.Time check := true for { if check && fsx.Exists(p.Socket) { in, err := client.Describe(bctx) switch { case err == nil && in.ID == p.VMMID && in.State == fcapi.StateRunning: return nil case err != nil: last = err again = time.After(delay) delay = min(2*delay, DialRetryMax) default: last = fmt.Errorf("the VMM answers id %q state %q", in.ID, in.State) } } check = false select { case <-bctx.Done(): if ctx.Err() != nil { return ctx.Err() } return fmt.Errorf("vmruntime: %s: the VMM was not Running within %s: %v", p.Unit, r.cfg.BootTimeout, last) case ev, ok := <-units: if !ok { units = nil continue } if ev.Name == p.Unit { if err := exited(ev.Status); err != nil { return err } } check = true case ev, ok := <-sock.Events(): if !ok { return fmt.Errorf("vmruntime: the watch of %s ended", p.SocketDir) } check = filepath.Clean(ev.Name) == filepath.Clean(p.Socket) case err, ok := <-sock.Errors(): if !ok { return fmt.Errorf("vmruntime: the watch of %s ended", p.SocketDir) } last, check = err, true case <-again: again, check = nil, true } } } func (r *firecracker) Await(ctx context.Context, p Prepared, slot int) (RuntimeRef, error) { if err := r.awaitBoot(ctx, p); err != nil { return RuntimeRef{}, err } st, err := r.cfg.Manager.Inspect(ctx, p.Unit) if err != nil { return RuntimeRef{}, err } pid, err := vmmPID(st.MainPID) if err != nil { return RuntimeRef{}, err } id, err := r.cfg.Manager.Identify(ctx, p.Unit, pid) if err != nil { return RuntimeRef{}, err } _, _, vmmUID := r.slotIDs(slot) pin, _ := r.imagePin() ref := RuntimeRef{ Backend: id.Backend, Hypervisor: "firecracker", Handle: p.Spec.Handle, Name: p.Spec.Name, Unit: p.Unit, InvocationID: id.InvocationID, PID: id.PID, StartTicks: id.StartTicks, StartTime: time.Now().UTC(), HostBootID: id.BootID, Manager: id.Manager, VMMID: p.VMMID, Slot: slot, VMMUID: vmmUID, WorkDir: p.WorkDir, Socket: p.Socket, GuestAddress: guestnet.GuestAddress, HostAddress: guestnet.HostAddress, ImageSHA256: pin.Files[guestimage.ManifestName].SHA256, } if err := fsx.WriteJSON(filepath.Join(p.WorkDir, RefFile), ref, 0o600); err != nil { return RuntimeRef{}, err } return ref, nil } func identity(ref RuntimeRef) hostsvc.Identity { return hostsvc.Identity{Unit: ref.Unit, InvocationID: ref.InvocationID, PID: ref.PID, StartTicks: ref.StartTicks, BootID: ref.HostBootID, Backend: ref.Backend, Manager: ref.Manager} } func (r *firecracker) Inspect(ctx context.Context, ref RuntimeRef) (Observation, error) { obs, err := r.cfg.Manager.Check(ctx, identity(ref)) out := Observation{Verdict: obs.Verdict.String(), Reason: obs.Reason, Detail: obs.Detail, Stopped: obs.Stopped, State: string(obs.Status.State)} if err != nil || obs.Verdict != hostsvc.VerdictRunning { return out, err } r.rotate(ref.WorkDir) in, err := fcapi.NewClient(ref.Socket, 2*time.Second).Describe(ctx) if err != nil { return Observation{Verdict: hostsvc.VerdictUnknown.String(), Detail: err.Error(), State: out.State}, nil } if in.ID != ref.VMMID || in.State != fcapi.StateRunning { out = Observation{Verdict: hostsvc.VerdictLost.String(), Reason: hostsvc.LostIdentityMismatch, State: out.State, Detail: fmt.Sprintf("the VMM on %s answers id %q state %q, recorded %q Running", ref.Socket, in.ID, in.State, ref.VMMID)} if err := r.cfg.Manager.Stop(ctx, ref.Unit); err != nil { return out, fmt.Errorf("vmruntime: %s does not match its record and stopping it failed: %w", ref.Unit, err) } out.Stopped = true } return out, nil } func (r *firecracker) Stop(ctx context.Context, ref RuntimeRef) (StopResult, error) { start := time.Now() res := StopResult{} st, err := r.cfg.Manager.Inspect(ctx, ref.Unit) if err != nil { return res, err } if GracefulStop() && (st.State == hostsvc.Active || st.State == hostsvc.Activating) { if err := fcapi.NewClient(ref.Socket, 2*time.Second).SendCtrlAltDel(ctx); err == nil { if res.Graceful, err = r.awaitDown(ctx, ref.Unit, r.cfg.StopGrace); err != nil { return res, err } } } if err := r.cfg.Manager.Stop(ctx, ref.Unit); err != nil { return res, err } if st, err := r.cfg.Manager.Inspect(ctx, ref.Unit); err == nil { res.Result = st.Result } if err := r.cfg.Manager.ResetFailed(ctx, ref.Unit); err != nil { return res, err } res.Elapsed = time.Since(start) return res, r.Cleanup(ctx, ref.Handle, ref.Name) } func (r *firecracker) Cleanup(ctx context.Context, handle, name string) error { unit := UnitName(handle, name) st, err := r.cfg.Manager.Inspect(ctx, unit) if err != nil { return err } if st.State == hostsvc.Active || st.State == hostsvc.Activating || st.State == hostsvc.Deactivating { return fmt.Errorf("vmruntime: %s is %s; stop it before its work directory goes", unit, st.State) } work := WorkDir(r.cfg.StateDir, handle, name) unlock, err := r.lock() if err != nil { return err } defer unlock() var errs []error var grants []Grant if raw, err := os.ReadFile(filepath.Join(work, GrantsFile)); err == nil { if err := json.Unmarshal(raw, &grants); err != nil { errs = append(errs, err) } } if err := revokeAll(grants); err != nil { errs = append(errs, err) } if err := os.RemoveAll(SocketDir(r.cfg.RuntimeDir, handle, name)); err != nil { errs = append(errs, err) } if err := os.RemoveAll(work); err != nil { errs = append(errs, err) } return errors.Join(errs...) } func (r *firecracker) List(ctx context.Context, handle string) ([]Listed, error) { units, err := r.cfg.Manager.List(ctx, UnitPrefix(handle)) if err != nil { return nil, err } byName := map[string]*Listed{} var order []string for _, u := range units { name := strings.TrimSuffix(strings.TrimPrefix(u.Name, UnitPrefix(handle)), hostsvc.ServiceSuffix) byName[name] = &Listed{Name: name, Unit: u.Name, Status: u, Loaded: true} order = append(order, name) } entries, err := os.ReadDir(VMsDir(r.cfg.StateDir, handle)) if err != nil && !errors.Is(err, os.ErrNotExist) { return nil, err } for _, e := range entries { if !e.IsDir() { continue } l, ok := byName[e.Name()] if !ok { l = &Listed{Name: e.Name(), Unit: UnitName(handle, e.Name())} byName[e.Name()] = l order = append(order, e.Name()) } l.WorkDir = filepath.Join(VMsDir(r.cfg.StateDir, handle), e.Name()) } out := make([]Listed, 0, len(order)) for _, n := range order { out = append(out, *byName[n]) } return out, nil } func (r *firecracker) Watch(ctx context.Context, handle string) (<-chan hostsvc.UnitEvent, error) { return r.cfg.Manager.Watch(ctx, UnitPrefix(handle)) } func (r *firecracker) Load(handle, name string) (RuntimeRef, error) { var ref RuntimeRef raw, err := os.ReadFile(filepath.Join(WorkDir(r.cfg.StateDir, handle, name), RefFile)) if err != nil { return ref, err } return ref, json.Unmarshal(raw, &ref) } func (r *firecracker) slotLockPath() (string, error) { account := filepath.Join("/run/user", strconv.Itoa(os.Getuid())) if st, err := os.Stat(account); err == nil && st.IsDir() { return filepath.Join(account, "socialweb-vmslots.lock"), nil } if err := os.MkdirAll(r.cfg.StateDir, 0o700); err != nil { return "", fmt.Errorf("vmruntime: %w", err) } return filepath.Join(r.cfg.StateDir, "vmslots.lock"), nil } func (r *firecracker) lock() (func(), error) { path, err := r.slotLockPath() if err != nil { return nil, err } f, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0o600) if err != nil { return nil, fmt.Errorf("vmruntime: %w", err) } if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX); err != nil { f.Close() return nil, fmt.Errorf("vmruntime: locking the VM slots: %w", err) } return func() { syscall.Flock(int(f.Fd()), syscall.LOCK_UN) f.Close() }, nil } func (r *firecracker) usedSlots(ctx context.Context) (map[int]bool, error) { units, err := r.cfg.Manager.List(ctx, UnitKind) if err != nil { return nil, err } used := map[int]bool{} for _, u := range units { i := strings.LastIndex(u.Description, "slot=") if i < 0 { continue } if k, err := strconv.Atoi(u.Description[i+len("slot="):]); err == nil { used[k] = true } } return used, nil } func (r *firecracker) rotate(work string) { for _, name := range []string{ConsoleFile, VMMLogFile} { path := filepath.Join(work, name) st, err := os.Stat(path) if err != nil || st.Size() <= r.cfg.ConsoleCap { continue } in, err := os.Open(path) if err != nil { continue } if _, err := in.Seek(-r.cfg.ConsoleCap, io.SeekEnd); err == nil { if out, err := os.OpenFile(path+".1", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o600); err == nil { io.Copy(out, in) out.Close() } } in.Close() os.Truncate(path, 0) } } func vmmPID(pastaPID int) (int, error) { if pastaPID <= 0 { return 0, fmt.Errorf("vmruntime: the unit has no main pid") } tasks, _ := os.ReadDir(filepath.Join("/proc", strconv.Itoa(pastaPID), "task")) for _, t := range tasks { raw, err := os.ReadFile(filepath.Join("/proc", strconv.Itoa(pastaPID), "task", t.Name(), "children")) if err != nil { continue } for f := range strings.FieldsSeq(string(raw)) { pid, err := strconv.Atoi(f) if err != nil { continue } comm, _ := os.ReadFile(filepath.Join("/proc", f, "comm")) if strings.TrimSpace(string(comm)) == "firecracker" { return pid, nil } } } return 0, fmt.Errorf("vmruntime: no firecracker child under pasta pid %d", pastaPID) } type imageManifest struct { BootArgs []string `json:"bootArgs"` Vcpu int `json:"vcpu"` MemMiB int `json:"memMiB"` } func readManifest(dir string) (imageManifest, error) { var m imageManifest raw, err := os.ReadFile(filepath.Join(dir, guestimage.ManifestName)) if err != nil { return m, fmt.Errorf("vmruntime: %w", err) } if err := json.Unmarshal(raw, &m); err != nil { return m, fmt.Errorf("vmruntime: %s: %w", dir, err) } if len(m.BootArgs) == 0 || m.Vcpu <= 0 || m.MemMiB <= 0 { return m, fmt.Errorf("vmruntime: the image manifest in %s names no boot args or machine size", dir) } return m, nil } func tailOf(path string, n int64) string { f, err := os.Open(path) if err != nil { return "" } defer f.Close() if st, err := f.Stat(); err == nil && st.Size() > n { f.Seek(-n, io.SeekEnd) } b, _ := io.ReadAll(f) return "--- " + filepath.Base(path) + " ---\n" + string(b) }