Skip to content
63 changes: 30 additions & 33 deletions cmd/core/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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`)
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
177 changes: 88 additions & 89 deletions cmd/vm/lifecycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion cmd/vm/netresize.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
6 changes: 2 additions & 4 deletions cmd/vm/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
3 changes: 1 addition & 2 deletions cmd/vm/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 1 addition & 2 deletions extend/fs/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
3 changes: 1 addition & 2 deletions extend/vfio/vfio.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 3 additions & 6 deletions hypervisor/clone.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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,
Expand Down
3 changes: 1 addition & 2 deletions hypervisor/cloudhypervisor/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
Loading
Loading