Skip to content

Commit 3980067

Browse files
authored
vm inspect: expose the guest console path (#202)
* review: drop clone-side watchdog leftovers CloneVMConfigFromFlags passes the snapshot policy straight through, so the local only existed for the removed override branch. The clone-path validateBackendFlags call is unreachable by construction: every knob it gates is inherited from the snapshot, and create already rejects them on Firecracker. * vm: expose the guest console path in inspect (#201) Consumers driving cocoon over the CLI cannot query the CH API socket (0700, owner-only) to find where a VM's console lives. Inspect now reports console_path for running VMs: the console.sock UDS (UEFI serial, FC relay) resolved by a stat, or the CH-allocated PTY for direct boot, which each boot path (start, clone, restore) queries once via vm.info after the VMM is up and saves to console.pty in the run dir, pidfile-style. Inspect and list stay free of API calls; boot paths pay one vm.info GET plus a small buffered write, direct boot only, after resume. ToVM now also zeroes the runtime socket fields for non-running VMs: clone and restore persist boot-time paths into the record, which previously leaked as stale socket_path/vsock_socket on stopped clones, contradicting the documented State==running contract. * vm: drop runtime paths from stale-running inspect output ReconcileState flips a dead-PID VM to stopped (stale) for display, but ToVM had already populated socket_path/vsock_socket/console_path from the persisted Running state; a reused PTY number could point a supervisor at another process's terminal. Clear the paths at the flip. * vm: reconcile stale-running state in one-shot status JSON statusOnce serialized ToVM output directly, so vm status/list --format json skipped the dead-PID flip and its runtime-path clearing that inspect, table, and event modes already apply.
1 parent 0234381 commit 3980067

16 files changed

Lines changed: 214 additions & 12 deletions

File tree

cmd/core/utils.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,10 @@ func RouteRefs(ctx context.Context, hypers []hypervisor.Hypervisor, refs []strin
6262
return result, nil
6363
}
6464

65+
// ReconcileState returns the effective display state; a stale-running VM also loses its runtime paths — they died with the process, and a reused PTY number must not be advertised.
6566
func ReconcileState(vm *types.VM) string {
6667
if vm.State == types.VMStateRunning && !utils.IsProcessAlive(vm.PID) {
68+
vm.SocketPath, vm.VsockSocket, vm.ConsolePath = "", "", ""
6769
return "stopped (stale)"
6870
}
6971
return string(vm.State)

cmd/core/utils_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,31 @@ func TestPersistSnapshotDirCleansCaptureOnDirectError(t *testing.T) {
200200
}
201201
}
202202

203+
func TestReconcileStateClearsRuntimePathsOnStaleRunning(t *testing.T) {
204+
vm := &types.VM{
205+
State: types.VMStateRunning,
206+
PID: 0,
207+
SocketPath: "/run/api.sock",
208+
VsockSocket: "/run/vsock.uds",
209+
ConsolePath: "/dev/pts/3",
210+
}
211+
if got := ReconcileState(vm); got != "stopped (stale)" {
212+
t.Fatalf("ReconcileState = %q, want %q", got, "stopped (stale)")
213+
}
214+
if vm.SocketPath != "" || vm.VsockSocket != "" || vm.ConsolePath != "" {
215+
t.Errorf("stale-running VM keeps runtime paths: socket=%q vsock=%q console=%q",
216+
vm.SocketPath, vm.VsockSocket, vm.ConsolePath)
217+
}
218+
219+
alive := &types.VM{State: types.VMStateRunning, PID: os.Getpid(), ConsolePath: "/dev/pts/3"}
220+
if got := ReconcileState(alive); got != string(types.VMStateRunning) {
221+
t.Fatalf("ReconcileState = %q, want running", got)
222+
}
223+
if alive.ConsolePath == "" {
224+
t.Error("live VM lost its console path")
225+
}
226+
}
227+
203228
// directErrSnap is a DirectCreator whose CreateFromDir always fails; the embedded interface panics on any other call, so the test proves the failure path alone.
204229
type directErrSnap struct {
205230
snapshot.Snapshot

cmd/core/vmconfig.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,6 @@ func CloneVMConfigFromFlags(cmd *cobra.Command, snapCfg types.SnapshotConfig) (*
108108
if cmd.Flags().Changed("no-direct-io") {
109109
noDirectIO, _ = cmd.Flags().GetBool("no-direct-io")
110110
}
111-
noWatchdog := snapCfg.NoWatchdog
112111

113112
restoreMode, err := restoreModeFromFlags(cmd)
114113
if err != nil {
@@ -133,7 +132,7 @@ func CloneVMConfigFromFlags(cmd *cobra.Command, snapCfg types.SnapshotConfig) (*
133132
ImageType: snapCfg.ImageType,
134133
Network: network,
135134
NoDirectIO: noDirectIO,
136-
NoWatchdog: noWatchdog,
135+
NoWatchdog: snapCfg.NoWatchdog,
137136
Windows: snapCfg.Windows,
138137
SharedMemory: snapCfg.SharedMemory,
139138
HugePages: snapCfg.HugePages,

cmd/vm/run.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -328,9 +328,6 @@ func (h Handler) prepareClone(ctx context.Context, cmd *cobra.Command, conf *con
328328
if err = vmCfg.Validate(); err != nil {
329329
return cloneSetup{}, err
330330
}
331-
if err = validateBackendFlags(conf, vmCfg); err != nil {
332-
return cloneSetup{}, err
333-
}
334331
// Envelope pins share create's digest-lock window; a record-backed clone's source pin already protects these.
335332
releasePins, err := cmdcore.PinEnvelopeBlobs(ctx, conf, cfg.ImageBlobIDs)
336333
if err != nil {
@@ -479,7 +476,7 @@ func (h Handler) createVM(cmd *cobra.Command, image string) (context.Context, *t
479476
return ctx, info, hyper, nil
480477
}
481478

482-
// validateBackendFlags fast-fails flag combinations the selected backend can never launch; boot-mode-dependent checks live in validateBootCompat. Shared by create, clone, and debug so the capability gate list cannot drift.
479+
// validateBackendFlags fast-fails flag combinations the selected backend can never launch; boot-mode-dependent checks live in validateBootCompat. Shared by create and debug so the capability gate list cannot drift.
483480
func validateBackendFlags(conf *config.Config, vmCfg *types.VMConfig) error {
484481
if !conf.UseFirecracker {
485482
return nil

cmd/vm/status.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ func statusOnce(ctx context.Context, hypers []hypervisor.Hypervisor, filters []s
9797
}
9898
vms = applyFilters(vms, filters)
9999
sortVMs(vms)
100+
// JSON serializes vms as-is, so stale-running records must reconcile here, not per output row.
101+
for _, vm := range vms {
102+
vm.State = types.VMState(cmdcore.ReconcileState(vm))
103+
}
100104
return renderVMList(vms, format, scopeDir)
101105
}
102106

docs/cli.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,8 @@ Applies to `cocoon vm debug`:
282282
| ---------------- | -------- | ------------------------------------------------- |
283283
| `--escape-char` | `^]` | Escape character (single char or `^X` caret notation) |
284284

285+
For a running VM, `cocoon vm inspect` reports the console resolved at boot (start, clone, restore) as `console_path`: the `console.sock` UDS (UEFI serial, Firecracker relay) or the Cloud Hypervisor-allocated PTY (`/dev/pts/N`, direct-boot OCI). External supervisors can read the console from there without opening the owner-only API socket. A direct-boot VM booted by an older cocoon reports it from its next start.
286+
285287
### Exec Flags
286288

287289
`cocoon vm exec` runs a command inside a running VM via the cocoon-agent (vsock, no SSH). Stdin/stdout/stderr stream like `kubectl exec`; the host shell sees the guest command's exit code.

hypervisor/backend.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
const (
1717
APISocketName = "api.sock"
1818
ConsoleSockName = "console.sock"
19+
ConsolePTYName = "console.pty"
1920
VsockSockName = "vsock.uds"
2021

2122
// VsockGuestCID is constant — per-VM isolation comes from distinct UDS paths.

hypervisor/cloudhypervisor/clone.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ func (ch *CloudHypervisor) cloneAfterExtractParsed(ctx context.Context, vmID str
147147
}); err != nil {
148148
return nil, err
149149
}
150+
saveConsolePTY(ctx, vmID, runDir, sockPath, directBoot)
150151

151152
info := &types.VM{
152153
ID: vmID, Hypervisor: typ, State: types.VMStateRunning,

hypervisor/cloudhypervisor/restore.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ func (ch *CloudHypervisor) restoreAfterExtract(ctx context.Context, vmID string,
117117
if err = resumeVM(ctx, hc); err != nil {
118118
return nil, fmt.Errorf("vm.resume: %w", err)
119119
}
120+
saveConsolePTY(ctx, vmID, rec.RunDir, sockPath, directBoot)
120121

121122
logger.Infof(ctx, "VM %s restored from snapshot", vmID)
122123
return ch.FinalizeRestore(ctx, vmID, vmCfg, rec, pid)

hypervisor/cloudhypervisor/start.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ func (ch *CloudHypervisor) startOne(ctx context.Context, id string) error {
2424
ch.saveCmdline(ctx, rec, args)
2525
return ch.launchProcess(ctx, rec, args, rec.ResolvedNetnsPath(), false)
2626
},
27+
PostLaunch: func(ctx context.Context, rec *hypervisor.VMRecord, sockPath string, _ int) error {
28+
saveConsolePTY(ctx, rec.ID, rec.RunDir, sockPath, hypervisor.IsDirectBoot(rec.BootConfig))
29+
return nil
30+
},
2731
})
2832
}
2933

0 commit comments

Comments
 (0)