diff --git a/cmd/core/helpers.go b/cmd/core/helpers.go index f9ba1e91..a8214804 100644 --- a/cmd/core/helpers.go +++ b/cmd/core/helpers.go @@ -277,33 +277,6 @@ func ResolveImageOwner(ctx context.Context, backends []imagebackend.Images, ref ) } -// resolveOwner returns the unique backend where found==true; notFound on zero, ambiguous wrapped on multi-match (lists matched types). -func resolveOwner[T interface{ Type() string }](backends []T, ref string, found func(T) (bool, error), notFound, ambiguous error) (T, error) { - var matches []T - var zero T - for _, b := range backends { - ok, err := found(b) - if err != nil { - return zero, fmt.Errorf("inspect %s in %s: %w", ref, b.Type(), err) - } - if ok { - matches = append(matches, b) - } - } - switch len(matches) { - case 0: - return zero, notFound - case 1: - return matches[0], nil - default: - names := make([]string, len(matches)) - for i, b := range matches { - names[i] = b.Type() - } - return zero, fmt.Errorf("%w (backends: %s)", ambiguous, strings.Join(names, ", ")) - } -} - func VMConfigFromFlags(cmd *cobra.Command, image string) (*types.VMConfig, error) { vmName, _ := cmd.Flags().GetString("name") cpu, _ := cmd.Flags().GetInt("cpu") @@ -398,8 +371,7 @@ func CloneVMConfigFromFlags(cmd *cobra.Command, snapCfg types.SnapshotConfig) (* }, nil } -// RestoreVMConfigFromFlags builds VMConfig for restore: resources from the -// snapshot, Name/Network from the VM (CNI namespace survives restore). +// RestoreVMConfigFromFlags builds VMConfig for restore: resources from the snapshot, Name/Network from the VM (CNI namespace survives restore). func RestoreVMConfigFromFlags(cmd *cobra.Command, vm *types.VM, snapCfg types.SnapshotConfig) (*types.VMConfig, error) { if snapCfg.NICs != len(vm.NetworkConfigs) { return nil, fmt.Errorf("nic count mismatch: vm has %d, snapshot has %d", @@ -444,8 +416,7 @@ func AddFormatFlag(cmd *cobra.Command) { cmd.Flags().StringP("format", "o", "table", `output format: "table" or "json"`) } -// AddOutputFlag adds --output/-o for lifecycle commands. Empty default keeps -// the human-readable log output; "json" emits a parseable result on stdout. +// AddOutputFlag adds --output/-o for lifecycle commands. Empty default keeps the human-readable log output; "json" emits a parseable result on stdout. func AddOutputFlag(cmd *cobra.Command) { cmd.Flags().StringP("output", "o", "", `emit "json" for machine-readable output`) } @@ -483,6 +454,33 @@ func IsURL(ref string) bool { return strings.HasPrefix(ref, "http://") || strings.HasPrefix(ref, "https://") } +// resolveOwner returns the unique backend where found==true; notFound on zero, ambiguous wrapped on multi-match (lists matched types). +func resolveOwner[T interface{ Type() string }](backends []T, ref string, found func(T) (bool, error), notFound, ambiguous error) (T, error) { + var matches []T + var zero T + for _, b := range backends { + ok, err := found(b) + if err != nil { + return zero, fmt.Errorf("inspect %s in %s: %w", ref, b.Type(), err) + } + if ok { + matches = append(matches, b) + } + } + switch len(matches) { + case 0: + return zero, notFound + case 1: + return matches[0], nil + default: + names := make([]string, len(matches)) + for i, b := range matches { + names[i] = b.Type() + } + return zero, fmt.Errorf("%w (backends: %s)", ambiguous, strings.Join(names, ", ")) + } +} + // validateRefShape rejects URL/OCI ref mismatches early so backends don't surface misleading downstream errors. func validateRefShape(ref, imageType string) error { switch imageType { @@ -572,8 +570,7 @@ func sanitizeVMName(image string) string { return n } -// parseDataDiskFlags parses --data-disk values, normalizes defaults, and -// returns the spec list ready for hypervisor.PrepareDataDisks. +// parseDataDiskFlags parses --data-disk values, normalizes defaults, and returns the spec list ready for hypervisor.PrepareDataDisks. func parseDataDiskFlags(raw []string) ([]types.DataDiskSpec, error) { specs := make([]types.DataDiskSpec, 0, len(raw)) for _, s := range raw { diff --git a/cmd/vm/lifecycle.go b/cmd/vm/lifecycle.go index c37474bf..86715b79 100644 --- a/cmd/vm/lifecycle.go +++ b/cmd/vm/lifecycle.go @@ -199,95 +199,6 @@ func (h Handler) Logs(cmd *cobra.Command, args []string) error { return streamLog(ctx, path, follow, tail) } -func streamLog(ctx context.Context, path string, follow bool, tail int) error { - f, err := os.Open(path) //nolint:gosec - if err != nil { - if os.IsNotExist(err) { - return fmt.Errorf("open log %s: VM may not have been started yet", path) - } - return fmt.Errorf("open log: %w", err) - } - defer f.Close() //nolint:errcheck - - if tail > 0 { - if seekErr := seekToLastNLines(f, tail); seekErr != nil { - return fmt.Errorf("seek tail: %w", seekErr) - } - } - - if !follow { - if _, copyErr := io.Copy(os.Stdout, f); copyErr != nil { - return fmt.Errorf("read log: %w", copyErr) - } - return nil - } - - events, err := utils.WatchFile(ctx, path, logFollowDebounce) - if err != nil { - return fmt.Errorf("watch log: %w", err) - } - sig, _ := utils.FileHead(f, logHeadSigLen) - if _, err := io.Copy(os.Stdout, f); err != nil { - return fmt.Errorf("read log: %w", err) - } - for { - select { - case <-ctx.Done(): - return nil - case _, ok := <-events: - if !ok { - return nil - } - // Stop/start re-opens O_TRUNC; head bytes shift because CH/FC stamp a unique boot timestamp on line 1, so sig mismatch catches new generations even at the same length. - newSig, _ := utils.FileHead(f, logHeadSigLen) - if !bytes.Equal(newSig, sig) { - if _, err := f.Seek(0, io.SeekStart); err != nil { - return fmt.Errorf("rewind log: %w", err) - } - sig = newSig - } - if _, err := io.Copy(os.Stdout, f); err != nil { - return fmt.Errorf("read log: %w", err) - } - } - } -} - -// seekToLastNLines positions f so a subsequent read returns the last n lines. -// A final trailing '\n' is not counted as a line separator. -func seekToLastNLines(f *os.File, n int) error { - info, err := f.Stat() - if err != nil { - return err - } - size := info.Size() - if size == 0 { - return nil - } - const chunk = 4096 - buf := make([]byte, chunk) - pos, found := size, 0 - for pos > 0 { - readSize := min(int64(chunk), pos) - pos -= readSize - if _, readErr := f.ReadAt(buf[:readSize], pos); readErr != nil { - return readErr - } - for i := readSize - 1; i >= 0; i-- { - if buf[i] != '\n' || pos+i == size-1 { - continue - } - found++ - if found == n { - _, seekErr := f.Seek(pos+i+1, io.SeekStart) - return seekErr - } - } - } - _, err = f.Seek(0, io.SeekStart) - return err -} - func (h Handler) RM(cmd *cobra.Command, args []string) error { ctx, conf, err := h.Init(cmd) if err != nil { @@ -483,3 +394,91 @@ func collectAttachedDevices(ctx context.Context, hyper hypervisor.Hypervisor, re } return out } + +func streamLog(ctx context.Context, path string, follow bool, tail int) error { + f, err := os.Open(path) //nolint:gosec + if err != nil { + if os.IsNotExist(err) { + return fmt.Errorf("open log %s: VM may not have been started yet", path) + } + return fmt.Errorf("open log: %w", err) + } + defer f.Close() //nolint:errcheck + + if tail > 0 { + if seekErr := seekToLastNLines(f, tail); seekErr != nil { + return fmt.Errorf("seek tail: %w", seekErr) + } + } + + if !follow { + if _, copyErr := io.Copy(os.Stdout, f); copyErr != nil { + return fmt.Errorf("read log: %w", copyErr) + } + return nil + } + + events, err := utils.WatchFile(ctx, path, logFollowDebounce) + if err != nil { + return fmt.Errorf("watch log: %w", err) + } + sig, _ := utils.FileHead(f, logHeadSigLen) + if _, err := io.Copy(os.Stdout, f); err != nil { + return fmt.Errorf("read log: %w", err) + } + for { + select { + case <-ctx.Done(): + return nil + case _, ok := <-events: + if !ok { + return nil + } + // Stop/start re-opens O_TRUNC; head bytes shift because CH/FC stamp a unique boot timestamp on line 1, so sig mismatch catches new generations even at the same length. + newSig, _ := utils.FileHead(f, logHeadSigLen) + if !bytes.Equal(newSig, sig) { + if _, err := f.Seek(0, io.SeekStart); err != nil { + return fmt.Errorf("rewind log: %w", err) + } + sig = newSig + } + if _, err := io.Copy(os.Stdout, f); err != nil { + return fmt.Errorf("read log: %w", err) + } + } + } +} + +// seekToLastNLines positions f so a subsequent read returns the last n lines. A final trailing '\n' is not counted as a line separator. +func seekToLastNLines(f *os.File, n int) error { + info, err := f.Stat() + if err != nil { + return err + } + size := info.Size() + if size == 0 { + return nil + } + const chunk = 4096 + buf := make([]byte, chunk) + pos, found := size, 0 + for pos > 0 { + readSize := min(int64(chunk), pos) + pos -= readSize + if _, readErr := f.ReadAt(buf[:readSize], pos); readErr != nil { + return readErr + } + for i := readSize - 1; i >= 0; i-- { + if buf[i] != '\n' || pos+i == size-1 { + continue + } + found++ + if found == n { + _, seekErr := f.Seek(pos+i+1, io.SeekStart) + return seekErr + } + } + } + _, err = f.Seek(0, io.SeekStart) + return err +} diff --git a/cmd/vm/netresize.go b/cmd/vm/netresize.go index 7e0805a4..dd046249 100644 --- a/cmd/vm/netresize.go +++ b/cmd/vm/netresize.go @@ -50,7 +50,7 @@ func plumbingForVM(conf *config.Config, vm *types.VM) (network.Network, error) { return nil, fmt.Errorf("no network backend on VM; cannot resize") } if backend == types.BackendCNI && vm.ResolvedNetnsPath() == "" { - return nil, fmt.Errorf("CNI backend but no netns; resize would target host netns") + return nil, fmt.Errorf("cni backend but no netns; resize would target host netns") } return providerForVM(conf, nil, map[string]network.Network{}, vm) } diff --git a/cmd/vm/run.go b/cmd/vm/run.go index b8494c3e..9ebf84c4 100644 --- a/cmd/vm/run.go +++ b/cmd/vm/run.go @@ -255,8 +255,7 @@ func (h Handler) cloneDirect(ctx context.Context, cmd *cobra.Command, conf *conf fmt.Sprintf("snapshot %s (direct)", snapRef), logger) } -// cloneFromDir runs DirectClone over an envelope-bearing dir. The dir stays -// read-only across the call so concurrent clones of a golden image are safe. +// cloneFromDir runs DirectClone over an envelope-bearing dir. The dir stays read-only across the call so concurrent clones of a golden image are safe. func (h Handler) cloneFromDir(ctx context.Context, cmd *cobra.Command, conf *config.Config, dir string, logger *log.Fields) error { cfg, err := snapshot.ReadSnapshotEnvelope(dir) if err != nil { @@ -375,8 +374,7 @@ func (h Handler) restoreDirect(ctx context.Context, cmd *cobra.Command, snapRef, fmt.Sprintf("snapshot %s", snapRef), logger) } -// runDirectRestore is the shared tail for the snapshot-DB and --from-dir -// restore paths: log, DirectRestore, output. +// runDirectRestore is the shared tail for the snapshot-DB and --from-dir restore paths: log, DirectRestore, output. func (h Handler) runDirectRestore(ctx context.Context, cmd *cobra.Command, dcr hypervisor.Direct, vmRef string, vmCfg *types.VMConfig, srcDir, sourceLabel string, logger *log.Fields) error { wantJSON := cmdcore.WantJSON(cmd) if !wantJSON { diff --git a/cmd/vm/status.go b/cmd/vm/status.go index 24e54ac7..074418ea 100644 --- a/cmd/vm/status.go +++ b/cmd/vm/status.go @@ -21,8 +21,7 @@ import ( "github.com/cocoonstack/cocoon/utils" ) -// statusWatchDebounce coalesces fsnotify events on the per-backend index file -// during `vm status` polling. +// statusWatchDebounce coalesces fsnotify events on the per-backend index file during `vm status` polling. const statusWatchDebounce = 200 * time.Millisecond type vmEvent struct { diff --git a/extend/fs/fs.go b/extend/fs/fs.go index aff6412e..87b9d7e7 100644 --- a/extend/fs/fs.go +++ b/extend/fs/fs.go @@ -34,8 +34,7 @@ type Spec struct { QueueSize int } -// Attached is the inspect-time view of one fs device read from the -// running VM's CH config. +// Attached is the inspect-time view of one fs device read from the running VM's CH config. type Attached struct { ID string `json:"id"` Tag string `json:"tag"` diff --git a/extend/vfio/vfio.go b/extend/vfio/vfio.go index f6b5a164..a3b8598a 100644 --- a/extend/vfio/vfio.go +++ b/extend/vfio/vfio.go @@ -29,8 +29,7 @@ var ( ErrUnsupportedBackend = errors.New("backend does not support device attach") ) -// Spec is one attach request. PCI may be a short BDF, full BDF, or a sysfs -// path; NormalizePath canonicalizes it. +// Spec is one attach request. PCI may be a short BDF, full BDF, or a sysfs path; NormalizePath canonicalizes it. type Spec struct { PCI string ID string diff --git a/hypervisor/clone.go b/hypervisor/clone.go index 99884b54..354209f5 100644 --- a/hypervisor/clone.go +++ b/hypervisor/clone.go @@ -10,8 +10,7 @@ import ( "github.com/cocoonstack/cocoon/utils" ) -// CloneSetup is the shared pre-clone sequence: validate CPU, reserve a -// placeholder, ensure dirs, return a cleanup that rolls back both. +// CloneSetup is the shared pre-clone sequence: validate CPU, reserve a placeholder, ensure dirs, return a cleanup that rolls back both. func (b *Backend) CloneSetup(ctx context.Context, vmID string, vmCfg *types.VMConfig, snapshotConfig *types.SnapshotConfig) (runDir, logDir string, now time.Time, cleanup func(), err error) { if err = ValidateHostCPU(vmCfg.CPU); err != nil { return "", "", time.Time{}, nil, err @@ -35,8 +34,7 @@ func (b *Backend) CloneSetup(ctx context.Context, vmID string, vmCfg *types.VMCo return runDir, logDir, now, cleanup, nil } -// DirectCloneBase clones from a local snapshot directory. Used when the -// snapshot lives on the same host (no tar streaming needed). +// DirectCloneBase clones from a local snapshot directory. Used when the snapshot lives on the same host (no tar streaming needed). func (b *Backend) DirectCloneBase( ctx context.Context, vmID string, vmCfg *types.VMConfig, net types.NetSetup, snapshotConfig *types.SnapshotConfig, srcDir string, @@ -60,8 +58,7 @@ func (b *Backend) DirectCloneBase( return afterExtract(ctx, vmID, vmCfg, net, runDir, logDir, now) } -// CloneFromStream clones from a tar stream into a fresh runDir. Used when -// the snapshot arrives over the network (cross-node clone). +// CloneFromStream clones from a tar stream into a fresh runDir. Used when the snapshot arrives over the network (cross-node clone). func (b *Backend) CloneFromStream( ctx context.Context, vmID string, vmCfg *types.VMConfig, net types.NetSetup, snapshotConfig *types.SnapshotConfig, snapshot io.Reader, diff --git a/hypervisor/cloudhypervisor/api.go b/hypervisor/cloudhypervisor/api.go index a03dee2e..766371cd 100644 --- a/hypervisor/cloudhypervisor/api.go +++ b/hypervisor/cloudhypervisor/api.go @@ -104,8 +104,7 @@ type chDevice struct { Path string `json:"path"` } -// chPciDeviceInfo is the response body from vm.add-fs / vm.add-device / -// vm.add-disk / vm.add-net (HTTP 200). +// chPciDeviceInfo is the response body from vm.add-fs / vm.add-device / vm.add-disk / vm.add-net (HTTP 200). type chPciDeviceInfo struct { ID string `json:"id"` BDF string `json:"bdf"` diff --git a/hypervisor/cloudhypervisor/clone.go b/hypervisor/cloudhypervisor/clone.go index 4dcf5c1f..4e6c8e4a 100644 --- a/hypervisor/cloudhypervisor/clone.go +++ b/hypervisor/cloudhypervisor/clone.go @@ -34,7 +34,7 @@ func (ch *CloudHypervisor) cloneAfterExtract(ctx context.Context, vmID string, v networkConfigs := net.NetworkConfigs logger := log.WithFunc("cloudhypervisor.Clone") - chConfigPath := filepath.Join(runDir, "config.json") + chConfigPath := filepath.Join(runDir, configJSONName) chCfg, err := parseCHConfig(chConfigPath) if err != nil { return nil, fmt.Errorf("parse CH config: %w", err) @@ -89,7 +89,7 @@ func (ch *CloudHypervisor) cloneAfterExtract(ctx context.Context, vmID string, v return nil, fmt.Errorf("patch CH config: %w", err) } - stateJSONPath := filepath.Join(runDir, "state.json") + stateJSONPath := filepath.Join(runDir, stateJSONName) if err = patchStateJSON(stateJSONPath, stateReplacements); err != nil { return nil, fmt.Errorf("patch state.json: %w", err) } @@ -239,8 +239,7 @@ func hasCidataRole(sc *types.StorageConfig) bool { return sc.Role == types.StorageRoleCidata } -// restorePatchStorageConfigs strips ensureCloneCidata's appended cidata when -// the snapshot lacked one, so patchCHConfig matches chCfg.Disks; cidata gets hot-plugged. +// restorePatchStorageConfigs strips ensureCloneCidata's appended cidata when the snapshot lacked one, so patchCHConfig matches chCfg.Disks; cidata gets hot-plugged. func restorePatchStorageConfigs(storageConfigs []*types.StorageConfig, directBoot, windows, hadCidataInSnapshot bool) []*types.StorageConfig { if directBoot || windows || hadCidataInSnapshot { return storageConfigs @@ -278,18 +277,12 @@ func updateCOWPath(configs []*types.StorageConfig, newCOWPath string) error { } func buildCmdline(storageConfigs []*types.StorageConfig, networkConfigs []*types.NetworkConfig, vmName string, dnsServers []string) string { - var cmdline strings.Builder - fmt.Fprintf(&cmdline, - "console=hvc0 loglevel=3 boot=cocoon-overlay cocoon.layers=%s cocoon.cow=%s clocksource=kvm-clock rw", - strings.Join(ReverseLayerSerials(storageConfigs), ","), CowSerial, + return hypervisor.BuildBaseCmdline( + "console=hvc0 loglevel=3", + strings.Join(ReverseLayerSerials(storageConfigs), ","), + CowSerial, + networkConfigs, vmName, dnsServers, ) - - if len(networkConfigs) > 0 { - cmdline.WriteString(" net.ifnames=0") - cmdline.WriteString(hypervisor.BuildIPParams(networkConfigs, vmName, dnsServers)) - } - - return cmdline.String() } // buildStateReplacements maps source disk paths → clone paths for state.json patching; slices to min length so an appended cidata doesn't desync (MACs go via NIC hot-swap). @@ -304,8 +297,7 @@ func buildStateReplacements(chCfg *chVMConfig, storageConfigs []*types.StorageCo return m } -// hotSwapNets removes NICs with stale MAC (from snapshot binary state) and -// adds fresh ones. Must run between vm.restore and vm.resume (VM paused). +// hotSwapNets removes NICs with stale MAC (from snapshot binary state) and adds fresh ones. Must run between vm.restore and vm.resume (VM paused). func hotSwapNets(ctx context.Context, hc *http.Client, oldNets []chNet, networkConfigs []*types.NetworkConfig) error { logger := log.WithFunc("cloudhypervisor.hotSwapNets") for _, oldNet := range oldNets { diff --git a/hypervisor/cloudhypervisor/create.go b/hypervisor/cloudhypervisor/create.go index c9554316..e7974201 100644 --- a/hypervisor/cloudhypervisor/create.go +++ b/hypervisor/cloudhypervisor/create.go @@ -88,8 +88,7 @@ func (ch *CloudHypervisor) prepareCloudimg(ctx context.Context, vmID string, vmC return configs, nil } -// generateCidata writes the NoCloud cidata image. storageConfigs lets cidata -// pick up Role==Data disks for auto-mount via /dev/disk/by-id/virtio-. +// generateCidata writes the NoCloud cidata image. storageConfigs lets cidata pick up Role==Data disks for auto-mount via /dev/disk/by-id/virtio-. func (ch *CloudHypervisor) generateCidata(vmID string, vmCfg *types.VMConfig, networkConfigs []*types.NetworkConfig, storageConfigs []*types.StorageConfig) error { dns, err := ch.conf.DNSServers() if err != nil { diff --git a/hypervisor/cloudhypervisor/direct.go b/hypervisor/cloudhypervisor/direct.go index 85b5c516..0b8b9934 100644 --- a/hypervisor/cloudhypervisor/direct.go +++ b/hypervisor/cloudhypervisor/direct.go @@ -10,8 +10,7 @@ import ( "github.com/cocoonstack/cocoon/types" ) -// DirectClone clones from a local snapshot dir. Per-type: hardlink memory-range-*, -// reflink/copy COW, plain copy metadata; cidata is regenerated. +// DirectClone clones from a local snapshot dir. Per-type: hardlink memory-range-*, reflink/copy COW, plain copy metadata; cidata is regenerated. func (ch *CloudHypervisor) DirectClone(ctx context.Context, vmID string, vmCfg *types.VMConfig, net types.NetSetup, snapshotConfig *types.SnapshotConfig, srcDir string) (*types.VM, error) { return ch.DirectCloneBase(ctx, vmID, vmCfg, net, snapshotConfig, srcDir, cloneSnapshotFiles, ch.cloneAfterExtract) } @@ -33,14 +32,14 @@ func (ch *CloudHypervisor) DirectRestore(ctx context.Context, vmRef string, vmCf } func cloneSnapshotFiles(dstDir, srcDir string) error { - chCfg, err := parseCHConfig(filepath.Join(srcDir, "config.json")) + chCfg, err := parseCHConfig(filepath.Join(srcDir, configJSONName)) if err != nil { return fmt.Errorf("parse source config: %w", err) } cowFiles := identifyCOWFiles(chCfg) return hypervisor.CloneSnapshotFiles(dstDir, srcDir, func(name string) hypervisor.SnapshotFileKind { switch { - case strings.HasPrefix(name, "memory-range"): + case strings.HasPrefix(name, memoryRangeFile): return hypervisor.SnapshotFileMemory case cowFiles[name]: return hypervisor.SnapshotFileCOW @@ -50,14 +49,13 @@ func cloneSnapshotFiles(dstDir, srcDir string) error { }) } -// cleanSnapshotFiles enumerates by name so stale data-*.raw and cocoon.json -// from a previous incarnation don't linger; COW files are overwritten anyway. +// cleanSnapshotFiles enumerates by name so stale data-*.raw and cocoon.json from a previous incarnation don't linger; COW files are overwritten anyway. func cleanSnapshotFiles(runDir string) error { return hypervisor.CleanSnapshotFiles(runDir, func(name string) bool { switch { - case strings.HasPrefix(name, "memory-range"): + case strings.HasPrefix(name, memoryRangeFile): return true - case name == "config.json" || name == "state.json": + case name == configJSONName || name == stateJSONName: return true case name == hypervisor.SnapshotMetaFile: return true diff --git a/hypervisor/cloudhypervisor/extend.go b/hypervisor/cloudhypervisor/extend.go index 615650d9..2a33e5d6 100644 --- a/hypervisor/cloudhypervisor/extend.go +++ b/hypervisor/cloudhypervisor/extend.go @@ -248,8 +248,7 @@ func listWith[A any]( return extract(info), nil } -// bdfFromSysfsPath returns the BDF suffix when path is under the canonical -// sysfs PCI prefix; empty otherwise (CH may report a non-PCI host path). +// bdfFromSysfsPath returns the BDF suffix when path is under the canonical sysfs PCI prefix; empty otherwise (CH may report a non-PCI host path). func bdfFromSysfsPath(p string) string { bdf, ok := strings.CutPrefix(p, vfio.SysfsPCIPrefix) if !ok { diff --git a/hypervisor/cloudhypervisor/helper.go b/hypervisor/cloudhypervisor/helper.go index 9270387f..e6bb6993 100644 --- a/hypervisor/cloudhypervisor/helper.go +++ b/hypervisor/cloudhypervisor/helper.go @@ -24,6 +24,9 @@ type chMemoryRestoreMode string const ( pidFileName = "ch.pid" cmdlineFileName = "cmdline" + configJSONName = "config.json" + stateJSONName = "state.json" + memoryRangeFile = "memory-range" // prefix shared by all per-region memory-range-* files in a CH snapshot // chMemoryRestoreOnDemand uses userfaultfd (UFFD) to lazily page in // guest memory from the snapshot file, avoiding a full upfront copy. @@ -47,7 +50,7 @@ func validateSnapshotIntegrity(srcDir string, sidecar []*types.StorageConfig) er if err := hypervisor.ValidateSnapshotIntegrity(srcDir, sidecar); err != nil { return err } - chCfg, err := parseCHConfig(filepath.Join(srcDir, "config.json")) + chCfg, err := parseCHConfig(filepath.Join(srcDir, configJSONName)) if err != nil { return fmt.Errorf("parse snapshot config: %w", err) } @@ -63,7 +66,7 @@ func validateSnapshotIntegrity(srcDir string, sidecar []*types.StorageConfig) er return fmt.Errorf("sidecar/config.json disk[%d] readonly mismatch: sidecar=%v config=%v", i, sc.RO, chCfg.Disks[i].ReadOnly) } } - if _, statErr := os.Stat(filepath.Join(srcDir, "state.json")); statErr != nil { + if _, statErr := os.Stat(filepath.Join(srcDir, stateJSONName)); statErr != nil { return fmt.Errorf("state.json missing: %w", statErr) } hasMemory, memErr := hasMemoryRangeFile(srcDir) @@ -76,15 +79,14 @@ func validateSnapshotIntegrity(srcDir string, sidecar []*types.StorageConfig) er return nil } -// hasMemoryRangeFile reports whether srcDir has at least one CH -// memory-range-* file. A missing prefix is enough to fail vm.restore. +// hasMemoryRangeFile reports whether srcDir has at least one CH memory-range-* file. A missing prefix is enough to fail vm.restore. func hasMemoryRangeFile(srcDir string) (bool, error) { entries, err := os.ReadDir(srcDir) if err != nil { return false, err } for _, e := range entries { - if strings.HasPrefix(e.Name(), "memory-range") { + if strings.HasPrefix(e.Name(), memoryRangeFile) { return true, nil } } @@ -96,8 +98,7 @@ func vmAPIOnce(ctx context.Context, hc *http.Client, endpoint string, body []byt return utils.DoAPIOnce(ctx, hc, http.MethodPut, "http://localhost/api/v1/"+endpoint, body, successCodes...) } -// vmPutJSON marshals payload and PUTs to endpoint via vmAPIOnce. Mirrors -// firecracker.putJSON so per-endpoint helpers stay one-line wrappers. +// vmPutJSON marshals payload and PUTs to endpoint via vmAPIOnce. Mirrors firecracker.putJSON so per-endpoint helpers stay one-line wrappers. func vmPutJSON[T any](ctx context.Context, hc *http.Client, endpoint, kind string, payload T, successCodes ...int) error { body, err := json.Marshal(payload) if err != nil { @@ -140,8 +141,7 @@ func isAlreadyInStateError(err error, state string) bool { return strings.Contains(ae.Message, fmt.Sprintf("Invalid transition: InvalidStateTransition(%s, %s)", state, state)) } -// snapshotVM and restoreVM temporarily extend the client timeout for -// long-running memory transfers, then restore it for subsequent calls. +// snapshotVM and restoreVM temporarily extend the client timeout for long-running memory transfers, then restore it for subsequent calls. func snapshotVM(ctx context.Context, hc *http.Client, destDir string) error { hc.Timeout = hypervisor.VMMemTransferTimeout defer func() { hc.Timeout = utils.HTTPTimeout }() @@ -213,8 +213,7 @@ func addCocoonNIC(ctx context.Context, hc *http.Client, nc *types.NetworkConfig) return chN.ID, nil } -// getVMInfo fetches vm.info; cocoon uses it to detect tag/id conflicts -// before hot-add and to surface attached devices through inspect. +// getVMInfo fetches vm.info; cocoon uses it to detect tag/id conflicts before hot-add and to surface attached devices through inspect. func getVMInfo(ctx context.Context, hc *http.Client) (*chVMInfoResponse, error) { body, err := utils.DoAPI(ctx, hc, http.MethodGet, "http://localhost/api/v1/vm.info", nil, http.StatusOK) if err != nil { diff --git a/hypervisor/cloudhypervisor/patch.go b/hypervisor/cloudhypervisor/patch.go index 18270f65..ab27d6aa 100644 --- a/hypervisor/cloudhypervisor/patch.go +++ b/hypervisor/cloudhypervisor/patch.go @@ -19,8 +19,7 @@ type patchOptions struct { noDirectIO bool } -// patchCHConfig patches specific fields in config.json while preserving all -// unknown fields that CH adds internally (platform, cpus.topology, etc.). +// patchCHConfig patches specific fields in config.json while preserving all unknown fields that CH adds internally (platform, cpus.topology, etc.). func patchCHConfig(path string, opts *patchOptions) error { rawData, err := os.ReadFile(path) //nolint:gosec if err != nil { @@ -130,8 +129,7 @@ func isDiskPathKey(key string) bool { return key == "disk_path" || key == "path" } -// walkAndReplace recursively traverses a parsed JSON value and replaces string -// values that are under a disk-path key and exactly match a replacement entry. +// walkAndReplace recursively traverses a parsed JSON value and replaces string values that are under a disk-path key and exactly match a replacement entry. func walkAndReplace(v any, key string, replacements map[string]string) any { switch val := v.(type) { case map[string]any: @@ -166,8 +164,7 @@ func setField(obj map[string]json.RawMessage, key string, value any) error { return nil } -// patchRawArray unmarshals a JSON array, applies fn to each element's raw map, -// and returns the patched array. Validates array length == count. +// patchRawArray unmarshals a JSON array, applies fn to each element's raw map, and returns the patched array. Validates array length == count. func patchRawArray(raw json.RawMessage, count int, fn func(int, map[string]json.RawMessage) error) (json.RawMessage, error) { var arr []json.RawMessage if err := json.Unmarshal(raw, &arr); err != nil { diff --git a/hypervisor/cloudhypervisor/restore.go b/hypervisor/cloudhypervisor/restore.go index 0d164cb3..24631b7c 100644 --- a/hypervisor/cloudhypervisor/restore.go +++ b/hypervisor/cloudhypervisor/restore.go @@ -46,7 +46,7 @@ func (ch *CloudHypervisor) restoreAfterExtract(ctx context.Context, vmID string, } }() - chConfigPath := filepath.Join(rec.RunDir, "config.json") + chConfigPath := filepath.Join(rec.RunDir, configJSONName) // rec may have trailing cidata absent from the snapshot (cloudimg post-first-boot); slice to sidecar length. meta, metaErr := hypervisor.LoadAndValidateMeta(rec.RunDir, ch.conf.RootDir, ch.conf.Config.RunDir) if metaErr != nil { diff --git a/hypervisor/cloudhypervisor/snapshot.go b/hypervisor/cloudhypervisor/snapshot.go index 0796070e..1e2a6bc8 100644 --- a/hypervisor/cloudhypervisor/snapshot.go +++ b/hypervisor/cloudhypervisor/snapshot.go @@ -48,7 +48,7 @@ func (ch *CloudHypervisor) Snapshot(ctx context.Context, ref string) (*types.Sna // buildSnapshotMeta mirrors config.json's disk shape (not activeDisks — diverges for cloudimg post-FirstBooted pre-restart where CH still holds cidata). func buildSnapshotMeta(rec *hypervisor.VMRecord, tmpDir string) (*hypervisor.SnapshotMeta, error) { - chCfg, err := parseCHConfig(filepath.Join(tmpDir, "config.json")) + chCfg, err := parseCHConfig(filepath.Join(tmpDir, configJSONName)) if err != nil { return nil, fmt.Errorf("parse snapshot config: %w", err) } diff --git a/hypervisor/cloudhypervisor/stop.go b/hypervisor/cloudhypervisor/stop.go index 71f8b552..c779ed3e 100644 --- a/hypervisor/cloudhypervisor/stop.go +++ b/hypervisor/cloudhypervisor/stop.go @@ -37,8 +37,7 @@ func (ch *CloudHypervisor) stopOne(ctx context.Context, id string) error { return ch.HandleStopResult(ctx, id, rec.RunDir, runtimeFiles, shutdownErr) } -// shutdownUEFI shuts down a UEFI-boot VM via ACPI power-button with -// poll-and-escalate handled by the shared GracefulStop helper. +// shutdownUEFI shuts down a UEFI-boot VM via ACPI power-button with poll-and-escalate handled by the shared GracefulStop helper. func (ch *CloudHypervisor) shutdownUEFI(ctx context.Context, hc *http.Client, vmID, socketPath string, pid int, timeout time.Duration) error { return ch.GracefulStop(ctx, vmID, pid, timeout, func() error { return powerButton(ctx, hc) }, @@ -54,8 +53,7 @@ func (ch *CloudHypervisor) forceTerminate(ctx context.Context, hc *http.Client, return utils.TerminateProcess(ctx, pid, ch.conf.BinaryName(), socketPath, ch.conf.TerminateGracePeriod()) } -// isDirectBoot returns true when the VM was started with a direct kernel boot -// (OCI images). False means UEFI boot (cloudimg). +// isDirectBoot returns true when the VM was started with a direct kernel boot (OCI images). False means UEFI boot (cloudimg). func isDirectBoot(boot *types.BootConfig) bool { return boot != nil && boot.KernelPath != "" } diff --git a/hypervisor/firecracker/api.go b/hypervisor/firecracker/api.go index 73a1a81a..37536fb0 100644 --- a/hypervisor/firecracker/api.go +++ b/hypervisor/firecracker/api.go @@ -87,8 +87,7 @@ type fcSnapshotLoad struct { VsockOverride *fcVsockOverride `json:"vsock_override,omitempty"` } -// fcNetworkOverride overrides a network interface from the snapshot -// with a new TAP device (FC v1.14+, PR #4731). +// fcNetworkOverride overrides a network interface from the snapshot with a new TAP device (FC v1.14+, PR #4731). type fcNetworkOverride struct { IfaceID string `json:"iface_id"` HostDevName string `json:"host_dev_name"` diff --git a/hypervisor/firecracker/create.go b/hypervisor/firecracker/create.go index 86684438..b4b55720 100644 --- a/hypervisor/firecracker/create.go +++ b/hypervisor/firecracker/create.go @@ -154,23 +154,14 @@ func decompressGzip(data []byte) ([]byte, error) { } func buildCmdline(storageConfigs []*types.StorageConfig, networkConfigs []*types.NetworkConfig, vmName string, dnsServers []string) string { - // Layer device paths reversed (top layer first for overlayfs lowerdir). + // Top layer first for overlayfs lowerdir; FC quirks (reboot=k, pci=off, i8042.noaux, 8250.nr_uarts=1) skip absent-hardware probes. layerDevs := hypervisor.ReverseLayers(storageConfigs, func(idx int, _ *types.StorageConfig) string { return DevPath(idx) }) - cowDev := DevPath(len(layerDevs)) - - var cmdline strings.Builder - // FC quirks: ttyS0 + reboot=k (i8042 reset, no ACPI), pci=off + i8042.noaux + 8250.nr_uarts=1 skip absent-hardware probes. - fmt.Fprintf(&cmdline, - "console=ttyS0 reboot=k loglevel=3 pci=off i8042.noaux 8250.nr_uarts=1 boot=cocoon-overlay cocoon.layers=%s cocoon.cow=%s clocksource=kvm-clock rw", - strings.Join(layerDevs, ","), cowDev, + return hypervisor.BuildBaseCmdline( + "console=ttyS0 reboot=k loglevel=3 pci=off i8042.noaux 8250.nr_uarts=1", + strings.Join(layerDevs, ","), + DevPath(len(layerDevs)), + networkConfigs, vmName, dnsServers, ) - - if len(networkConfigs) > 0 { - cmdline.WriteString(" net.ifnames=0") - cmdline.WriteString(hypervisor.BuildIPParams(networkConfigs, vmName, dnsServers)) - } - - return cmdline.String() } // DevPath maps idx to vda..vdz, vdaa..vdaz, vdba..vdbz, ... diff --git a/hypervisor/firecracker/direct.go b/hypervisor/firecracker/direct.go index c5e47ac5..439afdf8 100644 --- a/hypervisor/firecracker/direct.go +++ b/hypervisor/firecracker/direct.go @@ -8,8 +8,7 @@ import ( "github.com/cocoonstack/cocoon/types" ) -// DirectClone clones from a local snapshot dir. Per-type: hardlink mem, -// reflink/copy COW, plain copy metadata. +// DirectClone clones from a local snapshot dir. Per-type: hardlink mem, reflink/copy COW, plain copy metadata. func (fc *Firecracker) DirectClone(ctx context.Context, vmID string, vmCfg *types.VMConfig, net types.NetSetup, snapshotConfig *types.SnapshotConfig, srcDir string) (*types.VM, error) { return fc.DirectCloneBase(ctx, vmID, vmCfg, net, snapshotConfig, srcDir, cloneSnapshotFiles, fc.cloneAfterExtract) } diff --git a/hypervisor/firecracker/relay.go b/hypervisor/firecracker/relay.go index efeebdab..3774164e 100644 --- a/hypervisor/firecracker/relay.go +++ b/hypervisor/firecracker/relay.go @@ -122,8 +122,7 @@ func RunRelay(ctx context.Context) { } } -// relaySession handles one console connection: subscribes to the PTY broadcast, -// copies conn→master for input, and unsubscribes on disconnect. +// relaySession handles one console connection: subscribes to the PTY broadcast, copies conn→master for input, and unsubscribes on disconnect. func relaySession(ctx context.Context, master io.Writer, conn net.Conn, bc *broadcaster) { defer conn.Close() //nolint:errcheck diff --git a/hypervisor/firecracker/snapshot.go b/hypervisor/firecracker/snapshot.go index 32dcd39f..e49a1e5a 100644 --- a/hypervisor/firecracker/snapshot.go +++ b/hypervisor/firecracker/snapshot.go @@ -42,8 +42,7 @@ func (fc *Firecracker) Snapshot(ctx context.Context, ref string) (*types.Snapsho }) } -// buildSnapshotMeta rewrites kernel path to vmlinuz so clones get the portable -// artifact instead of the FC-specific vmlinux cache. +// buildSnapshotMeta rewrites kernel path to vmlinuz so clones get the portable artifact instead of the FC-specific vmlinux cache. func buildSnapshotMeta(rec *hypervisor.VMRecord, _ string) (*hypervisor.SnapshotMeta, error) { meta := &hypervisor.SnapshotMeta{ CPU: rec.Config.CPU, diff --git a/hypervisor/firecracker/stop.go b/hypervisor/firecracker/stop.go index 3f7d75ba..6ebae140 100644 --- a/hypervisor/firecracker/stop.go +++ b/hypervisor/firecracker/stop.go @@ -35,8 +35,7 @@ func (fc *Firecracker) stopOne(ctx context.Context, id string) error { return fc.HandleStopResult(ctx, id, rec.RunDir, runtimeFiles, shutdownErr) } -// gracefulStop sends SendCtrlAltDel with poll-and-escalate handled -// by the shared GracefulStop helper. +// gracefulStop sends SendCtrlAltDel with poll-and-escalate handled by the shared GracefulStop helper. func (fc *Firecracker) gracefulStop(ctx context.Context, hc *http.Client, vmID, sockPath string, pid int, timeout time.Duration) error { return fc.GracefulStop(ctx, vmID, pid, timeout, func() error { return sendCtrlAltDel(ctx, hc) }, diff --git a/hypervisor/hypervisor.go b/hypervisor/hypervisor.go index 839032d0..96bd149e 100644 --- a/hypervisor/hypervisor.go +++ b/hypervisor/hypervisor.go @@ -34,14 +34,12 @@ type Hypervisor interface { RegisterGC(*gc.Orchestrator) } -// Watchable is optionally implemented by hypervisors that support -// file-based state watching. +// Watchable is optionally implemented by hypervisors that support file-based state watching. type Watchable interface { WatchPath() string } -// Direct is an optional interface for hypervisors that support -// clone/restore from a local snapshot directory. +// Direct is an optional interface for hypervisors that support clone/restore from a local snapshot directory. type Direct interface { DirectClone(ctx context.Context, vmID string, vmCfg *types.VMConfig, net types.NetSetup, snapshotConfig *types.SnapshotConfig, srcDir string) (*types.VM, error) DirectRestore(ctx context.Context, vmRef string, vmCfg *types.VMConfig, srcDir string) (*types.VM, error) diff --git a/hypervisor/restore.go b/hypervisor/restore.go index f5d15025..a2269c1e 100644 --- a/hypervisor/restore.go +++ b/hypervisor/restore.go @@ -12,8 +12,7 @@ import ( "github.com/cocoonstack/cocoon/utils" ) -// KillForRestore stops the running VM via the backend-specific terminate hook -// and clears runtime files. +// KillForRestore stops the running VM via the backend-specific terminate hook and clears runtime files. func (b *Backend) KillForRestore(ctx context.Context, vmID string, rec *VMRecord, terminate func(pid int) error, runtimeFiles []string) error { killErr := b.WithRunningVM(ctx, rec, terminate) if killErr != nil && !errors.Is(killErr, ErrNotRunning) { @@ -67,8 +66,7 @@ func (b *Backend) FinalizeRestore(ctx context.Context, vmID string, vmCfg *types return &info, nil } -// RestoreSequence is the shared restore skeleton. Staging happens before -// the kill so a preflight failure leaves the original VM running. +// RestoreSequence is the shared restore skeleton. Staging happens before the kill so a preflight failure leaves the original VM running. func (b *Backend) RestoreSequence(ctx context.Context, vmRef string, spec RestoreSpec) (*types.VM, error) { if err := ValidateHostCPU(spec.VMCfg.CPU); err != nil { return nil, err @@ -116,8 +114,7 @@ func (b *Backend) RestoreSequence(ctx context.Context, vmRef string, spec Restor return result, nil } -// DirectRestoreSequence restores from a local snapshot directory; Populate -// replaces the tar staging+merge step used by RestoreSequence. +// DirectRestoreSequence restores from a local snapshot directory; Populate replaces the tar staging+merge step used by RestoreSequence. func (b *Backend) DirectRestoreSequence(ctx context.Context, vmRef string, spec DirectRestoreSpec) (*types.VM, error) { if err := ValidateHostCPU(spec.VMCfg.CPU); err != nil { return nil, err diff --git a/hypervisor/snapshot.go b/hypervisor/snapshot.go index 794fc30b..ef529b1a 100644 --- a/hypervisor/snapshot.go +++ b/hypervisor/snapshot.go @@ -13,8 +13,7 @@ import ( "github.com/cocoonstack/cocoon/utils" ) -// SnapshotMetaFile is the cocoon-owned sidecar carrying fields the hypervisor's -// native config can't hold (Role/MountPoint/FSType/DirectIO; FC CPU/Memory). +// SnapshotMetaFile is the cocoon-owned sidecar carrying fields the hypervisor's native config can't hold (Role/MountPoint/FSType/DirectIO; FC CPU/Memory). const SnapshotMetaFile = "cocoon.json" type SnapshotMeta struct { @@ -80,8 +79,7 @@ func CloneStorageConfigs(storageConfigs []*types.StorageConfig) []*types.Storage return out } -// IsUnderDir reports whether path is strictly under dir. An empty dir returns -// false (disables the check) rather than matching every path. +// IsUnderDir reports whether path is strictly under dir. An empty dir returns false (disables the check) rather than matching every path. func IsUnderDir(path, dir string) bool { if dir == "" { return false @@ -91,8 +89,7 @@ func IsUnderDir(path, dir string) bool { return strings.HasPrefix(cleaned, root+string(filepath.Separator)) } -// ValidateMetaPaths rejects sidecar paths escaping cocoon-managed roots; an -// imported snapshot's cocoon.json is otherwise untrusted. +// ValidateMetaPaths rejects sidecar paths escaping cocoon-managed roots; an imported snapshot's cocoon.json is otherwise untrusted. func ValidateMetaPaths(meta *SnapshotMeta, rootDir, runDir string) error { for _, sc := range meta.StorageConfigs { if !IsUnderDir(sc.Path, rootDir) && !IsUnderDir(sc.Path, runDir) { @@ -110,8 +107,7 @@ func ValidateMetaPaths(meta *SnapshotMeta, rootDir, runDir string) error { return nil } -// ReverseLayers projects Role==Layer entries through fn in reverse order -// (topmost layer first, matching overlayfs lowerdir semantics). +// ReverseLayers projects Role==Layer entries through fn in reverse order (topmost layer first, matching overlayfs lowerdir semantics). func ReverseLayers[T any](storageConfigs []*types.StorageConfig, project func(idx int, sc *types.StorageConfig) T) []T { var layers []*types.StorageConfig for _, sc := range storageConfigs { diff --git a/hypervisor/start.go b/hypervisor/start.go index 7bf8c01b..51fd8f9d 100644 --- a/hypervisor/start.go +++ b/hypervisor/start.go @@ -11,8 +11,7 @@ import ( "github.com/cocoonstack/cocoon/utils" ) -// StartAll runs startOne for each ref and batch-flips the succeeded set -// to Running so a partial batch doesn't leave half-Running state. +// StartAll runs startOne for each ref and batch-flips the succeeded set to Running so a partial batch doesn't leave half-Running state. func (b *Backend) StartAll(ctx context.Context, refs []string, startOne func(context.Context, string) error) ([]string, error) { ids, err := b.ResolveRefs(ctx, refs) if err != nil { diff --git a/hypervisor/state.go b/hypervisor/state.go index 747bcff3..d3ef4de0 100644 --- a/hypervisor/state.go +++ b/hypervisor/state.go @@ -31,7 +31,7 @@ func (b *Backend) WithRunningVM(ctx context.Context, rec *VMRecord, fn func(pid // Covers pidfile/socket cleaned up before VMM exited. Fail-closed if scan errors so callers don't treat inconclusive state as ErrNotRunning. scanned, scanErr := utils.FindVMMByCmdline(b.Conf.BinaryName(), sockPath) if scanErr != nil { - return fmt.Errorf("VM %s: pidfile-based check failed and /proc scan errored: %w (resolve the host issue and retry)", rec.ID, scanErr) + return fmt.Errorf("vm %s: pidfile-based check failed and /proc scan errored: %w (resolve the host issue and retry)", rec.ID, scanErr) } if len(scanned) == 0 { return ErrNotRunning diff --git a/hypervisor/utils.go b/hypervisor/utils.go index 41fbe431..a716b54f 100644 --- a/hypervisor/utils.go +++ b/hypervisor/utils.go @@ -90,6 +90,17 @@ func PrefixToNetmask(prefix int) string { return net.IP(mask).String() } +// BuildBaseCmdline composes the cocoon-shared cmdline: prefix (backend-specific console+quirks) + boot/layers/cow + per-NIC ip= params. +func BuildBaseCmdline(prefix, layers, cow string, networkConfigs []*types.NetworkConfig, vmName string, dnsServers []string) string { + var b strings.Builder + fmt.Fprintf(&b, "%s boot=cocoon-overlay cocoon.layers=%s cocoon.cow=%s clocksource=kvm-clock rw", prefix, layers, cow) + if len(networkConfigs) > 0 { + b.WriteString(" net.ifnames=0") + b.WriteString(BuildIPParams(networkConfigs, vmName, dnsServers)) + } + return b.String() +} + func BuildIPParams(networkConfigs []*types.NetworkConfig, vmName string, dnsServers []string) string { var params strings.Builder fmt.Fprintf(¶ms, " cocoon.hostname=%s", vmName) @@ -297,8 +308,7 @@ func ValidateRoleSequence(sidecar, rec []*types.StorageConfig) error { return nil } -// ExpandRawImage truncates path up to targetSize. No-op if path is already -// at least targetSize. Used by both backends for raw COW expansion. +// ExpandRawImage truncates path up to targetSize; no-op if path already meets it. func ExpandRawImage(path string, targetSize int64) error { fi, err := os.Stat(path) if err != nil { diff --git a/hypervisor/utils_test.go b/hypervisor/utils_test.go index 2c1d7e32..e9141dae 100644 --- a/hypervisor/utils_test.go +++ b/hypervisor/utils_test.go @@ -179,3 +179,43 @@ func TestIsDataDiskFile(t *testing.T) { } } } + +func TestBuildBaseCmdline(t *testing.T) { + // Locks the cmdline format so a refactor of the shared builder can't silently + // shift kernel boot parameters for either backend. + const ( + chPrefix = "console=hvc0 loglevel=3" + fcPrefix = "console=ttyS0 reboot=k loglevel=3 pci=off i8042.noaux 8250.nr_uarts=1" + ) + nics := []*types.NetworkConfig{ + {Network: &types.Network{IP: "10.0.0.2", Gateway: "10.0.0.1", Prefix: 24}}, + } + + tests := []struct { + name, prefix, layers, cow string + nics []*types.NetworkConfig + dns []string + want string + }{ + { + name: "ch no network", prefix: chPrefix, layers: "L0,L1", cow: "cow", + want: "console=hvc0 loglevel=3 boot=cocoon-overlay cocoon.layers=L0,L1 cocoon.cow=cow clocksource=kvm-clock rw", + }, + { + name: "fc no network", prefix: fcPrefix, layers: "/dev/vda", cow: "/dev/vdb", + want: "console=ttyS0 reboot=k loglevel=3 pci=off i8042.noaux 8250.nr_uarts=1 boot=cocoon-overlay cocoon.layers=/dev/vda cocoon.cow=/dev/vdb clocksource=kvm-clock rw", + }, + { + name: "ch with nic + dns", prefix: chPrefix, layers: "L", cow: "C", nics: nics, dns: []string{"1.1.1.1"}, + want: "console=hvc0 loglevel=3 boot=cocoon-overlay cocoon.layers=L cocoon.cow=C clocksource=kvm-clock rw net.ifnames=0 cocoon.hostname=vm ip=10.0.0.2::10.0.0.1:255.255.255.0:vm:eth0:off:1.1.1.1", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := BuildBaseCmdline(tt.prefix, tt.layers, tt.cow, tt.nics, "vm", tt.dns) + if got != tt.want { + t.Errorf("BuildBaseCmdline mismatch:\n got: %q\nwant: %q", got, tt.want) + } + }) + } +} diff --git a/images/digest.go b/images/digest.go index f443fdb4..3c6b6346 100644 --- a/images/digest.go +++ b/images/digest.go @@ -2,8 +2,7 @@ package images import godigest "github.com/opencontainers/go-digest" -// Digest represents a content-addressable digest in "algorithm:hex" format -// (e.g., "sha256:abcdef..."). Backed by opencontainers/go-digest. +// Digest represents a content-addressable digest in "algorithm:hex" format (e.g., "sha256:abcdef..."). Backed by opencontainers/go-digest. type Digest string func NewDigest(hex string) Digest { diff --git a/images/gc.go b/images/gc.go index b2d50900..f8ce0947 100644 --- a/images/gc.go +++ b/images/gc.go @@ -36,7 +36,6 @@ type GCModuleConfig[I any] struct { } // BuildGCModule constructs a gc.Module from the config. -// This eliminates the near-identical GCModule() methods in oci/ and cloudimg/. func BuildGCModule[I any](cfg GCModuleConfig[I]) gc.Module[ImageGCSnapshot] { return gc.Module[ImageGCSnapshot]{ Name: cfg.Name, diff --git a/images/oci/oci.go b/images/oci/oci.go index a153c0c8..efcf96a2 100644 --- a/images/oci/oci.go +++ b/images/oci/oci.go @@ -25,8 +25,7 @@ const ( var _ images.Images = (*OCI)(nil) -// OCI implements the images.Images interface using OCI container images -// converted to EROFS filesystems for use with Cloud Hypervisor. +// OCI implements the images.Images interface using OCI container images converted to EROFS filesystems for use with Cloud Hypervisor. type OCI struct { conf *Config store storage.Store[imageIndex] @@ -66,8 +65,7 @@ func New(ctx context.Context, conf *config.Config) (*OCI, error) { // Type returns the image backend identifier. func (o *OCI) Type() string { return typ } -// Pull downloads an OCI image from a container registry, extracts boot files -// (kernel, initrd), and converts each layer to EROFS concurrently. +// Pull downloads an OCI image from a container registry, extracts boot files (kernel, initrd), and converts each layer to EROFS concurrently. func (o *OCI) Pull(ctx context.Context, image string, _ bool, tracker progress.Tracker) error { _, err, _ := o.pullGroup.Do(image, func() (any, error) { return nil, pull(ctx, o.conf, o.store, image, tracker) diff --git a/images/oci/pull.go b/images/oci/pull.go index d3cc847b..e6de73b7 100644 --- a/images/oci/pull.go +++ b/images/oci/pull.go @@ -24,8 +24,7 @@ type pullLayerResult struct { initrdPath string // non-empty if this layer contains an initrd } -// pull downloads an OCI image, extracts boot files, and converts each layer -// to EROFS concurrently. +// pull downloads an OCI image, extracts boot files, and converts each layer to EROFS concurrently. func pull(ctx context.Context, conf *Config, store storage.Store[imageIndex], imageRef string, tracker progress.Tracker) error { logger := log.WithFunc("oci.pull") diff --git a/metadata/fat12.go b/metadata/fat12.go index c67f570a..96231396 100644 --- a/metadata/fat12.go +++ b/metadata/fat12.go @@ -32,8 +32,7 @@ type dataEntry struct { numClusters int } -// fat12Builder constructs a FAT12 image in memory (FAT + root dir only) -// and streams the full image on writeTo. +// fat12Builder constructs a FAT12 image in memory (FAT + root dir only) and streams the full image on writeTo. type fat12Builder struct { label string fat []byte // single FAT copy (written twice) @@ -277,8 +276,7 @@ func generateShortName(name string, seq int) [11]byte { return result } -// makeLFNEntries creates VFAT long-filename directory entries in disk order -// (highest sequence number first, immediately before the SFN entry). +// makeLFNEntries creates VFAT long-filename directory entries in disk order (highest sequence number first, immediately before the SFN entry). func makeLFNEntries(name string, shortName [11]byte) [][]byte { runes := utf16.Encode([]rune(name)) chksum := lfnChecksum(shortName) diff --git a/network/cni/cni.go b/network/cni/cni.go index e7e5df9b..457aa251 100644 --- a/network/cni/cni.go +++ b/network/cni/cni.go @@ -38,8 +38,7 @@ type CNI struct { cniConf *libcni.CNIConfig } -// New creates a CNI provider; conflist loading is best-effort so Delete/Inspect/List -// still work when none are available — Add fails in that case. +// New creates a CNI provider; conflist loading is best-effort so Delete/Inspect/List still work when none are available — Add fails in that case. func New(conf *config.Config) (*CNI, error) { if conf == nil { return nil, fmt.Errorf("config is nil") diff --git a/network/utils.go b/network/utils.go index bcc36233..199a96f0 100644 --- a/network/utils.go +++ b/network/utils.go @@ -24,8 +24,7 @@ func ResolveQueueSize(qs int) int { return cmp.Or(qs, NetQueueSize) } -// VMIDPrefix returns the first 8 characters of a VM ID, matching the -// truncation used by both bridge and CNI TAP device naming. +// VMIDPrefix returns the first 8 characters of a VM ID, matching the truncation used by both bridge and CNI TAP device naming. func VMIDPrefix(vmID string) string { if len(vmID) > vmIDPrefixLen { return vmID[:vmIDPrefixLen] diff --git a/snapshot/envelope.go b/snapshot/envelope.go index 5af1ecaa..a0443ec4 100644 --- a/snapshot/envelope.go +++ b/snapshot/envelope.go @@ -16,8 +16,7 @@ const ( EnvelopeVersion = 1 ) -// ErrEnvelopeMissing wraps the not-found case so callers can render a -// dir-specific error instead of a raw open failure. +// ErrEnvelopeMissing wraps the not-found case so callers can render a dir-specific error instead of a raw open failure. var ErrEnvelopeMissing = errors.New("snapshot envelope missing") // ReadSnapshotEnvelope reads /snapshot.json into a SnapshotConfig. @@ -45,8 +44,7 @@ func MarshalEnvelope(cfg types.SnapshotConfig) ([]byte, error) { return append(data, '\n'), nil } -// WriteSnapshotEnvelope writes /snapshot.json atomically so a concurrent -// reader can't see a partial write. +// WriteSnapshotEnvelope writes /snapshot.json atomically so a concurrent reader can't see a partial write. func WriteSnapshotEnvelope(dir string, cfg types.SnapshotConfig) error { data, err := MarshalEnvelope(cfg) if err != nil { diff --git a/snapshot/localfile/import.go b/snapshot/localfile/import.go index 00b95a62..37f0f5dc 100644 --- a/snapshot/localfile/import.go +++ b/snapshot/localfile/import.go @@ -105,8 +105,7 @@ func unwrapGzip(r io.Reader) (io.Reader, io.Closer, error) { return full, nil, nil } -// readAndRemoveSnapshotJSON reads the envelope and deletes it; the registered -// snapshot dir keeps only runtime sidecars (cocoon.json), not import metadata. +// readAndRemoveSnapshotJSON reads the envelope and deletes it; the registered snapshot dir keeps only runtime sidecars (cocoon.json), not import metadata. func readAndRemoveSnapshotJSON(dataDir string) (types.SnapshotConfig, error) { cfg, err := snapshot.ReadSnapshotEnvelope(dataDir) if err != nil { diff --git a/snapshot/localfile/localfile.go b/snapshot/localfile/localfile.go index a11d4a61..4ba0bd2a 100644 --- a/snapshot/localfile/localfile.go +++ b/snapshot/localfile/localfile.go @@ -165,8 +165,7 @@ func (lf *LocalFile) Inspect(ctx context.Context, ref string) (*types.Snapshot, return &s, nil } -// Delete processes each id atomically (rm dir → DB update). A mid-loop -// failure leaves any rm-OK-then-DB-fail id as a stale DB record; GC reclaims it. +// Delete processes each id atomically (rm dir → DB update). A mid-loop failure leaves any rm-OK-then-DB-fail id as a stale DB record; GC reclaims it. func (lf *LocalFile) Delete(ctx context.Context, refs []string) ([]string, error) { var ids []string if err := lf.store.With(ctx, func(idx *snapshot.SnapshotIndex) error { @@ -249,8 +248,7 @@ func (lf *LocalFile) lookupRecord(ctx context.Context, ref string, touch bool) ( return rec, lf.store.With(ctx, apply) } -// snapshotRecordToConfig builds a detached SnapshotConfig from a record, -// deep-copying ImageBlobIDs so the caller can use it after the lock is released. +// snapshotRecordToConfig builds a detached SnapshotConfig from a record, deep-copying ImageBlobIDs so the caller can use it after the lock is released. func snapshotRecordToConfig(rec snapshot.SnapshotRecord) types.SnapshotConfig { cfg := rec.SnapshotConfig cfg.ImageBlobIDs = maps.Clone(rec.ImageBlobIDs) diff --git a/snapshot/snapshot.go b/snapshot/snapshot.go index ce7e81d7..afab0a9d 100644 --- a/snapshot/snapshot.go +++ b/snapshot/snapshot.go @@ -8,14 +8,12 @@ import ( "github.com/cocoonstack/cocoon/types" ) -// Direct is an optional interface for snapshot backends that expose -// the local data directory for per-file handling (hardlink, reflink, etc.). +// Direct is an optional interface for snapshot backends that expose the local data directory for per-file handling (hardlink, reflink, etc.). type Direct interface { DataDir(ctx context.Context, ref string) (string, types.SnapshotConfig, error) } -// CompressedExporter is an optional interface for backends that support -// exporting with compression (e.g. gzip). The default Export produces raw tar. +// CompressedExporter is an optional interface for backends that support exporting with compression (e.g. gzip). The default Export produces raw tar. type CompressedExporter interface { ExportCompressed(ctx context.Context, ref string) (io.ReadCloser, error) } diff --git a/storage/storage.go b/storage/storage.go index 91630814..30dd7c1f 100644 --- a/storage/storage.go +++ b/storage/storage.go @@ -4,8 +4,7 @@ import ( "context" ) -// Initer is optionally implemented by T to initialize zero-value fields -// (e.g., nil maps) after deserialization or when the backing store is empty. +// Initer is optionally implemented by T to initialize zero-value fields (e.g., nil maps) after deserialization or when the backing store is empty. type Initer interface { Init() } diff --git a/types/storage.go b/types/storage.go index 6bb43336..ae824341 100644 --- a/types/storage.go +++ b/types/storage.go @@ -18,8 +18,7 @@ const ( FSTypeNone = "none" ) -// dataDiskNameRe caps length at 20 to match Linux's -// /dev/disk/by-id/virtio- truncation. +// dataDiskNameRe caps length at 20 to match Linux's /dev/disk/by-id/virtio- truncation. var dataDiskNameRe = regexp.MustCompile(`^[a-z][a-z0-9_-]{0,19}$`) // StorageRole classifies a disk's purpose in the VM. Required on every @@ -37,8 +36,7 @@ type StorageConfig struct { DirectIO *bool `json:"direct_io,omitempty"` // Role==Data only; nil inherits VM-level NoDirectIO } -// DataDiskSpec is the user-facing description of an extra data disk parsed -// from --data-disk. Transient — never persisted. +// DataDiskSpec is the user-facing description of an extra data disk parsed from --data-disk. Transient — never persisted. type DataDiskSpec struct { Name string Size int64 diff --git a/utils/file.go b/utils/file.go index 41484b9d..ade7eb1b 100644 --- a/utils/file.go +++ b/utils/file.go @@ -27,8 +27,7 @@ func EnsureDirs(dirs ...string) error { return nil } -// FileHead returns up to n bytes from offset 0 without moving the read -// position. Truncated at EOF, no error if shorter than n. +// FileHead returns up to n bytes from offset 0 without moving the read position. Truncated at EOF, no error if shorter than n. func FileHead(f *os.File, n int) ([]byte, error) { buf := make([]byte, n) m, err := f.ReadAt(buf, 0) @@ -44,8 +43,7 @@ func ValidFile(path string) bool { return err == nil && info.Mode().IsRegular() && info.Size() > 0 } -// ScanFileStems returns the name-without-suffix of every file in dir whose -// name ends with suffix. Used by GC to enumerate on-disk blobs. +// ScanFileStems returns the name-without-suffix of every file in dir whose name ends with suffix. func ScanFileStems(dir, suffix string) ([]string, error) { return scanDir(dir, func(e os.DirEntry) (string, bool) { if !e.IsDir() && strings.HasSuffix(e.Name(), suffix) { @@ -56,7 +54,6 @@ func ScanFileStems(dir, suffix string) ([]string, error) { } // ScanSubdirs returns the names of all immediate subdirectories of dir. -// Used by GC to enumerate per-VM runtime and log directories. func ScanSubdirs(dir string) ([]string, error) { return scanDir(dir, func(e os.DirEntry) (string, bool) { if e.IsDir() { @@ -89,8 +86,7 @@ func DirSize(dir string) (int64, error) { return total, err } -// FilterUnreferenced returns the elements of candidates not present in refs -// or any of the optional exclude sets. Used by GC Resolve to compute deletions. +// FilterUnreferenced returns elements of candidates not present in refs or any of the optional exclude sets. func FilterUnreferenced(candidates []string, refs map[string]struct{}, exclude ...map[string]struct{}) []string { var out []string for _, s := range candidates { diff --git a/utils/hugepages_other.go b/utils/hugepages_other.go index f55ab2e2..587b6898 100644 --- a/utils/hugepages_other.go +++ b/utils/hugepages_other.go @@ -2,6 +2,5 @@ package utils -// DetectHugePages returns false on non-Linux platforms where -// /proc/sys/vm/nr_hugepages is not available. +// DetectHugePages returns false on non-Linux platforms where /proc/sys/vm/nr_hugepages is not available. func DetectHugePages() bool { return false } diff --git a/utils/map.go b/utils/map.go index b859303c..a78ff0a2 100644 --- a/utils/map.go +++ b/utils/map.go @@ -5,8 +5,7 @@ import ( "maps" ) -// LookupCopy returns a shallow copy at key. Pointer/slice/map fields inside T -// still alias the original — callers must not mutate them without deep-copying. +// LookupCopy returns a shallow copy at key. Pointer/slice/map fields inside T still alias the original — callers must not mutate them without deep-copying. func LookupCopy[T any](m map[string]*T, key string) (T, error) { v := m[key] if v == nil { diff --git a/utils/pidfd_linux.go b/utils/pidfd_linux.go index a16b3914..5cf7cd52 100644 --- a/utils/pidfd_linux.go +++ b/utils/pidfd_linux.go @@ -10,8 +10,7 @@ import ( "golang.org/x/sys/unix" ) -// terminateWithPidfd uses pidfd_open + pidfd_send_signal for TOCTOU-safe -// process termination. Returns false if pidfd is unavailable (kernel < 5.3). +// terminateWithPidfd uses pidfd_open + pidfd_send_signal for TOCTOU-safe process termination. Returns false if pidfd is unavailable (kernel < 5.3). func terminateWithPidfd(ctx context.Context, pid int, binaryName, expectArg string, gracePeriod time.Duration) (handled bool, err error) { if !VerifyProcessCmdline(pid, binaryName, expectArg) { return true, nil diff --git a/utils/poll.go b/utils/poll.go index 133728e9..ade36e0e 100644 --- a/utils/poll.go +++ b/utils/poll.go @@ -6,8 +6,7 @@ import ( "time" ) -// WaitFor polls check at the given interval until it returns (true, nil), -// returns a non-nil error, or the timeout/context expires. +// WaitFor polls check at the given interval until it returns (true, nil), returns a non-nil error, or the timeout/context expires. func WaitFor(ctx context.Context, timeout, interval time.Duration, check func() (done bool, err error)) error { ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() diff --git a/utils/process.go b/utils/process.go index 9fc43be8..adbbd32d 100644 --- a/utils/process.go +++ b/utils/process.go @@ -40,8 +40,7 @@ func IsProcessAlive(pid int) bool { return err == nil || errors.Is(err, syscall.EPERM) } -// VerifyProcessCmdline matches pid against binaryName + expectArg in -// /proc//cmdline; falls back to IsProcessAlive on non-Linux or read errors. +// VerifyProcessCmdline matches pid against binaryName + expectArg in /proc//cmdline; falls back to IsProcessAlive on non-Linux or read errors. func VerifyProcessCmdline(pid int, binaryName, expectArg string) bool { if pid <= 0 { return false diff --git a/utils/reflink_linux.go b/utils/reflink_linux.go index dc5570db..22ab5ac2 100644 --- a/utils/reflink_linux.go +++ b/utils/reflink_linux.go @@ -11,8 +11,7 @@ import ( // ficlone is the ioctl number for btrfs/xfs/bcachefs CoW file cloning. const ficlone = 0x40049409 -// ReflinkCopy copies a single file, preferring FICLONE (O(1) CoW on -// btrfs/xfs/bcachefs) and falling back to SparseCopy on any error. +// ReflinkCopy copies a single file, preferring FICLONE (O(1) CoW on btrfs/xfs/bcachefs) and falling back to SparseCopy on any error. func ReflinkCopy(dst, src string) error { if err := tryFiclone(dst, src); err == nil { return nil diff --git a/utils/stream.go b/utils/stream.go index aebed4ba..2542c7cd 100644 --- a/utils/stream.go +++ b/utils/stream.go @@ -15,8 +15,7 @@ type PipeStreamReader struct { close func() error } -// NewPipeStreamReader pairs pr with the producer's done channel so Close -// surfaces background errors and runs cleanup exactly once. +// NewPipeStreamReader pairs pr with the producer's done channel so Close surfaces background errors and runs cleanup exactly once. func NewPipeStreamReader(pr *io.PipeReader, done <-chan error, cleanup func()) *PipeStreamReader { return &PipeStreamReader{ PipeReader: pr, diff --git a/utils/uuid.go b/utils/uuid.go index 85f47584..62f8d64f 100644 --- a/utils/uuid.go +++ b/utils/uuid.go @@ -2,8 +2,7 @@ package utils import "github.com/google/uuid" -// UUIDv5 generates a deterministic UUID v5 from the given name using the URL -// namespace. Compatible with the uuid_v5() bash function in os-image/start.sh. +// UUIDv5 generates a deterministic UUID v5 from the given name using the URL namespace. Compatible with the uuid_v5() bash function in os-image/start.sh. func UUIDv5(name string) string { return uuid.NewSHA1(uuid.NameSpaceURL, []byte(name)).String() }