Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions cmd/containerd-shim-lcow-v2/service/mocks/mock_service.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 20 additions & 3 deletions cmd/containerd-shim-lcow-v2/service/service_sandbox_internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ const (
SandboxStateReady = "SANDBOX_READY"
// SandboxStateNotReady indicates the sandbox is not ready.
SandboxStateNotReady = "SANDBOX_NOTREADY"

// vmProcessorRequirementsInfoKey is the verbose SandboxStatus info key under
// which the UVM's processor feature/compatibility set (raw HCS JSON) is reported.
vmProcessorRequirementsInfoKey = "vmProcessorRequirements"
)

// createSandboxInternal is the implementation for CreateSandbox.
Expand Down Expand Up @@ -194,7 +198,7 @@ func (s *Service) waitSandboxInternal(ctx context.Context, request *sandbox.Wait
// It synthesizes a status response from the current vmController state.
// When verbose is true, the response may be extended with additional
// diagnostic information.
func (s *Service) sandboxStatusInternal(_ context.Context, request *sandbox.SandboxStatusRequest) (*sandbox.SandboxStatusResponse, error) {
func (s *Service) sandboxStatusInternal(ctx context.Context, request *sandbox.SandboxStatusRequest) (*sandbox.SandboxStatusResponse, error) {
if s.sandboxID != request.SandboxID {
return nil, fmt.Errorf("sandbox ID mismatch, expected %s, got %s", s.sandboxID, request.SandboxID)
}
Expand Down Expand Up @@ -223,8 +227,21 @@ func (s *Service) sandboxStatusInternal(_ context.Context, request *sandbox.Sand
resp.ExitedAt = timestamppb.New(stoppedStatus.StoppedTime)
}

if request.Verbose { //nolint:staticcheck
// TODO: Add compat info and any other details.
if request.Verbose {
// Surface the UVM's processor feature/compatibility set for diagnostics.
// Best-effort: a status query must not fail because this optional property
// could not be read.
procReqs, err := s.vmController.ProcessorRequirements(ctx)
if err != nil {
log.G(ctx).WithError(err).Debug("failed to query VM processor requirements for verbose sandbox status")
}

if len(procReqs) > 0 {
if resp.Info == nil {
resp.Info = make(map[string]string)
}
resp.Info[vmProcessorRequirementsInfoKey] = string(procReqs)
}
}

return resp, nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package service

import (
"context"
"encoding/json"
"errors"
"os"
"path/filepath"
Expand Down Expand Up @@ -35,6 +36,7 @@ var (
errVMWait = errors.New("vm wait failed")
errVMExitStat = errors.New("vm exit status unavailable")
errVMStats = errors.New("vm stats unavailable")
errVMProcReqs = errors.New("vm processor requirements unavailable")
)

// newTestService builds a [Service] wired to a mock vm controller.
Expand Down Expand Up @@ -599,6 +601,59 @@ func TestSandboxStatus_TerminatedExitStatusFailure(t *testing.T) {
}
}

// TestSandboxStatus_VerboseIncludesProcessorRequirements verifies that a verbose
// status request on a running VM surfaces the processor requirements JSON under
// the vmProcessorRequirementsInfoKey info key.
func TestSandboxStatus_VerboseIncludesProcessorRequirements(t *testing.T) {
t.Parallel()
startedAt := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
procReqs := json.RawMessage(`{"ProcessorFeatures":["Sse3"],"CacheLineFlushSize":8}`)

svc, mockCtrl := newTestService(t)
svc.sandboxID = "test-sandbox"

mockCtrl.EXPECT().State().Return(vm.StateRunning)
mockCtrl.EXPECT().StartTime().Return(startedAt)
mockCtrl.EXPECT().ProcessorRequirements(gomock.Any()).Return(procReqs, nil)

resp, err := svc.sandboxStatusInternal(context.Background(), &sandboxsvc.SandboxStatusRequest{
SandboxID: "test-sandbox",
Verbose: true,
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got := resp.Info[vmProcessorRequirementsInfoKey]; got != string(procReqs) {
t.Errorf("Info[%q] = %q, want %q", vmProcessorRequirementsInfoKey, got, string(procReqs))
}
}

// TestSandboxStatus_VerboseProcessorRequirementsBestEffort verifies that a
// failure to query processor requirements does not fail the status call; the
// info key is simply omitted.
func TestSandboxStatus_VerboseProcessorRequirementsBestEffort(t *testing.T) {
t.Parallel()
startedAt := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)

svc, mockCtrl := newTestService(t)
svc.sandboxID = "test-sandbox"

mockCtrl.EXPECT().State().Return(vm.StateRunning)
mockCtrl.EXPECT().StartTime().Return(startedAt)
mockCtrl.EXPECT().ProcessorRequirements(gomock.Any()).Return(nil, errVMProcReqs)

resp, err := svc.sandboxStatusInternal(context.Background(), &sandboxsvc.SandboxStatusRequest{
SandboxID: "test-sandbox",
Verbose: true,
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if _, ok := resp.Info[vmProcessorRequirementsInfoKey]; ok {
t.Errorf("expected %q to be absent from Info on query failure", vmProcessorRequirementsInfoKey)
}
}

// ─── pingSandboxInternal tests ────────────────────────────────────────────

// TestPingSandbox_NotImplemented verifies that pingSandboxInternal returns
Expand Down
5 changes: 5 additions & 0 deletions cmd/containerd-shim-lcow-v2/service/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package service

import (
"context"
"encoding/json"
"time"

"github.com/Microsoft/hcsshim/cmd/containerd-shim-runhcs-v1/stats"
Expand Down Expand Up @@ -58,6 +59,10 @@ type vmController interface {
// Stats returns runtime statistics for the VM.
Stats(ctx context.Context) (*stats.VirtualMachineStatistics, error)

// ProcessorRequirements returns the UVM's processor feature/compatibility
// set as raw JSON.
ProcessorRequirements(ctx context.Context) (json.RawMessage, error)

// UpdatePolicyFragment injects a security policy fragment into the running guest.
UpdatePolicyFragment(ctx context.Context, fragment guestresource.SecurityPolicyFragment) error

Expand Down
26 changes: 26 additions & 0 deletions internal/controller/vm/vm.go
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,32 @@ func (c *Controller) Stats(ctx context.Context) (*stats.VirtualMachineStatistics
return s, nil
}

// ProcessorRequirements returns the UVM's processor feature and compatibility
// set (processor features, xsave features, synthetic features, cache line flush
// size, etc.) as the raw JSON emitted by HCS. The VM must be in [StateRunning].
func (c *Controller) ProcessorRequirements(ctx context.Context) (json.RawMessage, error) {
c.mu.RLock()
defer c.mu.RUnlock()

if c.vmState != StateRunning {
return nil, fmt.Errorf("cannot query processor requirements: VM is in state %s", c.vmState)
}

props, err := c.uvm.PropertiesV3(ctx, &hcsschema.PropertyQuery{
Queries: map[string]interface{}{hcsschema.VMProcessorRequirementsProperty: nil},
})
if err != nil {
return nil, fmt.Errorf("query processor requirements: %w", err)
}

resp, ok := props.PropertyResponses[hcsschema.VMProcessorRequirementsProperty]
if !ok || len(resp.Response) == 0 {
return nil, fmt.Errorf("processor requirements not present in property response")
}

return resp.Response, nil
}

// TerminateVM forcefully terminates a running VM, closes the guest connection,
// and releases HCS resources.
//
Expand Down
8 changes: 2 additions & 6 deletions internal/controller/vm/vm_migration.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@ import (
"github.com/Microsoft/hcsshim/internal/vm/vmutils"
)

// compatibilityInfoProperty is the HCS property name used to retrieve the
// VM's opaque migration-compatibility blob via PropertiesV3.
const compatibilityInfoProperty = "CompatibilityInfo"

// InitializeLiveMigrationOnSource prepares the running source VM for an
// outgoing live migration. Once it succeeds the VM accepts only live-migration
// calls until the migration completes or is rolled back.
Expand Down Expand Up @@ -76,14 +72,14 @@ func (c *Controller) CompatibilityInfo(ctx context.Context) ([]byte, error) {

// Ask the HCS for the compatibility property.
props, err := c.uvm.PropertiesV3(ctx, &hcsschema.PropertyQuery{
Queries: map[string]interface{}{compatibilityInfoProperty: nil},
Queries: map[string]interface{}{hcsschema.CompatibilityInfoProperty: nil},
})
if err != nil {
return nil, fmt.Errorf("query compatibility info: %w", err)
}

// Pull the raw blob out of the property response.
resp, ok := props.PropertyResponses[compatibilityInfoProperty]
resp, ok := props.PropertyResponses[hcsschema.CompatibilityInfoProperty]
if !ok || len(resp.Response) == 0 {
return nil, fmt.Errorf("compatibility info not present in property response")
}
Expand Down
10 changes: 10 additions & 0 deletions internal/hcs/schema2/property_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,13 @@ const (
PTCPUGroup PropertyType = "CpuGroup"
PTSystemGUID PropertyType = "SystemGUID"
)

const (
// CompatibilityInfoProperty is the HCS property name used to retrieve the
// VM's opaque migration-compatibility blob via PropertiesV3.
CompatibilityInfoProperty = "CompatibilityInfo"

// VMProcessorRequirementsProperty is the HCS property name used to retrieve the
// VM's processor feature/compatibility.
VMProcessorRequirementsProperty = "VmProcessorRequirements"
)
Loading