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
31 changes: 19 additions & 12 deletions internal/builder/vm/lcow/kernel_args.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ func buildKernelArgs(
kernelDirect bool,
hasConsole bool,
rootFsFile string,
LiveMigrationSupportEnabled bool,
) (string, error) {

log.G(ctx).WithField("rootFsFile", rootFsFile).Debug("buildKernelArgs: starting kernel arguments construction")
Expand Down Expand Up @@ -81,7 +82,7 @@ func buildKernelArgs(
args = append(args, "brd.rd_nr=0", "pmtmr=0")

// 8. Init arguments (passed after "--" separator)
initArgs := buildInitArgs(ctx, opts, writableOverlayDirs, disableTimeSyncService, processDumpLocation, rootFsFile, hasConsole)
initArgs := buildInitArgs(ctx, opts, writableOverlayDirs, disableTimeSyncService, processDumpLocation, rootFsFile, hasConsole, LiveMigrationSupportEnabled)
args = append(args, "--", initArgs)

result := strings.Join(args, " ")
Expand Down Expand Up @@ -150,6 +151,7 @@ func buildInitArgs(
processDumpLocation string,
rootFsFile string,
hasConsole bool,
LiveMigrationSupportEnabled bool,
) string {
log.G(ctx).WithFields(logrus.Fields{
"rootFsFile": rootFsFile,
Expand All @@ -159,7 +161,7 @@ func buildInitArgs(
entropyArgs := fmt.Sprintf("-e %d", vmutils.LinuxEntropyVsockPort)

// Build GCS execution command
gcsCmd := buildGCSCommand(opts, disableTimeSyncService, processDumpLocation)
gcsCmd := buildGCSCommand(opts, disableTimeSyncService, processDumpLocation, LiveMigrationSupportEnabled)

// Construct init arguments
var initArgsList []string
Expand Down Expand Up @@ -193,14 +195,8 @@ func buildGCSCommand(
opts *runhcsoptions.Options,
disableTimeSyncService bool,
processDumpLocation string,
LiveMigrationSupportEnabled bool,
) string {
// Start with vsockexec wrapper
var cmdParts []string
cmdParts = append(cmdParts, "/bin/vsockexec")

// Add logging vsock port
cmdParts = append(cmdParts, fmt.Sprintf("-e %d", vmutils.LinuxLogVsockPort))

// Determine log level
logLevel := "info"
if opts != nil && opts.LogLevel != "" {
Expand Down Expand Up @@ -229,8 +225,19 @@ func buildGCSCommand(
gcsParts = append(gcsParts, "-core-dump-location", processDumpLocation)
}

// Combine vsockexec and GCS command
cmdParts = append(cmdParts, strings.Join(gcsParts, " "))
gcsCmd := strings.Join(gcsParts, " ")

// Live-migratable pods skip the /bin/vsockexec wrapper. The wrapper exists
// solely to forward GCS stderr to the host-side log listener, but that listener
// is host-local state that live migration does not transfer, so the host
// does not run it for these pods.
// Without a listener, vsockexec's outbound connect would block and stall guest init,
// so we emit /bin/gcs directly instead.
if LiveMigrationSupportEnabled {
return gcsCmd
}

return strings.Join(cmdParts, " ")
// vsockexec `-e <port>` wires gcs's stderr to LinuxLogVsockPort, which
// the host listener reads and republishes.
return fmt.Sprintf("/bin/vsockexec -e %d %s", vmutils.LinuxLogVsockPort, gcsCmd)
}
4 changes: 4 additions & 0 deletions internal/builder/vm/lcow/sandbox_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ type SandboxOptions struct {
// ConfidentialConfig carries confidential computing fields that are not
// part of the HCS document but are needed for confidential VM setup.
ConfidentialConfig *ConfidentialConfig

// LiveMigrationSupportEnabled indicates that the live migration feature set is
// enabled for the sandbox, constraining it to migration-compatible features.
LiveMigrationSupportEnabled bool
}

// ConfidentialConfig carries confidential computing configuration that is not
Expand Down
10 changes: 6 additions & 4 deletions internal/builder/vm/lcow/specs.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ func BuildSandboxConfig(
bootOptions.LinuxKernelDirect != nil, // isKernelDirectBoot
comPorts != nil, // hasConsole
filepath.Base(rootFsFullPath),
sandboxOptions.LiveMigrationSupportEnabled,
)
if err != nil {
return nil, nil, fmt.Errorf("failed to build kernel args: %w", err)
Expand Down Expand Up @@ -330,10 +331,11 @@ func parseSandboxOptions(ctx context.Context, platform string, annotations map[s
log.G(ctx).WithField("platform", platform).Debug("parseSandboxOptions: starting sandbox options parsing")
sandboxOptions := &SandboxOptions{
// Extract architecture from platform string (e.g., "linux/amd64" -> "amd64")
Architecture: platform[strings.IndexByte(platform, '/')+1:],
FullyPhysicallyBacked: oci.ParseAnnotationsBool(ctx, annotations, shimannotations.FullyPhysicallyBacked, false),
PolicyBasedRouting: oci.ParseAnnotationsBool(ctx, annotations, iannotations.NetworkingPolicyBasedRouting, false),
NoWritableFileShares: oci.ParseAnnotationsBool(ctx, annotations, shimannotations.DisableWritableFileShares, false),
Architecture: platform[strings.IndexByte(platform, '/')+1:],
FullyPhysicallyBacked: oci.ParseAnnotationsBool(ctx, annotations, shimannotations.FullyPhysicallyBacked, false),
PolicyBasedRouting: oci.ParseAnnotationsBool(ctx, annotations, iannotations.NetworkingPolicyBasedRouting, false),
NoWritableFileShares: oci.ParseAnnotationsBool(ctx, annotations, shimannotations.DisableWritableFileShares, false),
LiveMigrationSupportEnabled: oci.ParseAnnotationsBool(ctx, annotations, shimannotations.LiveMigrationSupportEnabled, false),
}

// Determine if this is a confidential VM early, as it affects boot options parsing
Expand Down
155 changes: 155 additions & 0 deletions internal/builder/vm/lcow/specs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2127,3 +2127,158 @@ func TestBuildSandboxConfig_CPUClamping(t *testing.T) {
t.Errorf("expected processor count to be clamped to host count %d, got %d", hostCount, actualCount)
}
}

// TestBuildSandboxConfig_LiveMigration validates the wiring for the
// io.microsoft.migration.support.enabled sandbox annotation. The annotation is parsed
// into SandboxOptions.LiveMigrationSupportEnabled and threaded down into the kernel
// command line: live-migratable sandboxes must skip the /bin/vsockexec wrapper
// (which would otherwise stall init waiting for a host log listener that the
// LM-enabled host does not run), while non-LM sandboxes must continue to use
// vsockexec so that GCS stderr is forwarded over LinuxLogVsockPort.
func TestBuildSandboxConfig_LiveMigration(t *testing.T) {
ctx := context.Background()

validBootFilesPath := newBootFilesPath(t)
defaultOpts := defaultSandboxOpts(validBootFilesPath)

// Pre-format the vsockexec prefix once so the assertions are obviously
// driven by the same constant the production code uses.
vsockexecPrefix := fmt.Sprintf("/bin/vsockexec -e %d", vmutils.LinuxLogVsockPort)

tests := []specTestCase{
{
name: "live migration disabled by default",
validate: func(t *testing.T, doc *hcsschema.ComputeSystem, sandboxOpts *SandboxOptions) {
t.Helper()
if sandboxOpts.LiveMigrationSupportEnabled {
t.Errorf("expected LiveMigrationSupportEnabled=false by default, got true")
}
kernelArgs := getKernelArgs(doc)
if !strings.Contains(kernelArgs, vsockexecPrefix) {
t.Errorf("expected vsockexec wrapper %q in kernel args (LM disabled), got %q", vsockexecPrefix, kernelArgs)
}
if !strings.Contains(kernelArgs, "/bin/gcs") {
t.Errorf("expected /bin/gcs in kernel args, got %q", kernelArgs)
}
},
},
{
name: "live migration explicitly disabled",
spec: &vm.Spec{
Annotations: map[string]string{
shimannotations.LiveMigrationSupportEnabled: "false",
},
},
validate: func(t *testing.T, doc *hcsschema.ComputeSystem, sandboxOpts *SandboxOptions) {
t.Helper()
if sandboxOpts.LiveMigrationSupportEnabled {
t.Errorf("expected LiveMigrationSupportEnabled=false when annotation=\"false\", got true")
}
kernelArgs := getKernelArgs(doc)
if !strings.Contains(kernelArgs, vsockexecPrefix) {
t.Errorf("expected vsockexec wrapper %q in kernel args, got %q", vsockexecPrefix, kernelArgs)
}
},
},
{
name: "live migration enabled drops vsockexec wrapper",
spec: &vm.Spec{
Annotations: map[string]string{
shimannotations.LiveMigrationSupportEnabled: "true",
},
},
validate: func(t *testing.T, doc *hcsschema.ComputeSystem, sandboxOpts *SandboxOptions) {
t.Helper()
if !sandboxOpts.LiveMigrationSupportEnabled {
t.Errorf("expected LiveMigrationSupportEnabled=true when annotation=\"true\", got false")
}
kernelArgs := getKernelArgs(doc)
// The vsockexec wrapper must not appear at all when LM is on:
// neither the prefix nor the binary path on its own.
if strings.Contains(kernelArgs, "vsockexec") {
t.Errorf("expected no vsockexec in kernel args when LM enabled, got %q", kernelArgs)
}
if strings.Contains(kernelArgs, fmt.Sprintf("-e %d", vmutils.LinuxLogVsockPort)) {
t.Errorf("expected no log vsock port (%d) wiring when LM enabled, got %q", vmutils.LinuxLogVsockPort, kernelArgs)
}
// /bin/gcs must still be invoked - just without the wrapper.
if !strings.Contains(kernelArgs, "/bin/gcs") {
t.Errorf("expected /bin/gcs in kernel args even when LM enabled, got %q", kernelArgs)
}
},
},
{
name: "live migration combined with debug log level",
opts: &runhcsoptions.Options{
SandboxPlatform: "linux/amd64",
BootFilesRootPath: validBootFilesPath,
LogLevel: "debug",
},
spec: &vm.Spec{
Annotations: map[string]string{
shimannotations.LiveMigrationSupportEnabled: "true",
},
},
validate: func(t *testing.T, doc *hcsschema.ComputeSystem, sandboxOpts *SandboxOptions) {
t.Helper()
if !sandboxOpts.LiveMigrationSupportEnabled {
t.Errorf("expected LiveMigrationSupportEnabled=true, got false")
}
kernelArgs := getKernelArgs(doc)
// Other GCS flags must still be threaded through the command
// even when the vsockexec wrapper is removed.
if !strings.Contains(kernelArgs, "-loglevel debug") {
t.Errorf("expected -loglevel debug in kernel args when LM enabled, got %q", kernelArgs)
}
if strings.Contains(kernelArgs, "vsockexec") {
t.Errorf("expected no vsockexec when LM enabled, got %q", kernelArgs)
}
},
},
{
name: "live migration with disable time sync still drops vsockexec",
spec: &vm.Spec{
Annotations: map[string]string{
shimannotations.LiveMigrationSupportEnabled: "true",
shimannotations.DisableLCOWTimeSyncService: "true",
},
},
validate: func(t *testing.T, doc *hcsschema.ComputeSystem, sandboxOpts *SandboxOptions) {
t.Helper()
if !sandboxOpts.LiveMigrationSupportEnabled {
t.Errorf("expected LiveMigrationSupportEnabled=true, got false")
}
kernelArgs := getKernelArgs(doc)
if !strings.Contains(kernelArgs, "-disable-time-sync") {
t.Errorf("expected -disable-time-sync flag in kernel args, got %q", kernelArgs)
}
if strings.Contains(kernelArgs, "vsockexec") {
t.Errorf("expected no vsockexec when LM enabled, got %q", kernelArgs)
}
},
},
{
name: "live migration invalid annotation value falls back to default (false)",
spec: &vm.Spec{
Annotations: map[string]string{
// ParseAnnotationsBool returns the default value (false) on
// unparseable input, so the sandbox should behave like the
// default-disabled case rather than failing the build.
shimannotations.LiveMigrationSupportEnabled: "not-a-bool",
},
},
validate: func(t *testing.T, doc *hcsschema.ComputeSystem, sandboxOpts *SandboxOptions) {
t.Helper()
if sandboxOpts.LiveMigrationSupportEnabled {
t.Errorf("expected LiveMigrationSupportEnabled=false on invalid annotation value, got true")
}
kernelArgs := getKernelArgs(doc)
if !strings.Contains(kernelArgs, vsockexecPrefix) {
t.Errorf("expected vsockexec wrapper %q in kernel args, got %q", vsockexecPrefix, kernelArgs)
}
},
},
}

runTestCases(t, ctx, defaultOpts, tests)
}
26 changes: 26 additions & 0 deletions internal/controller/vm/vm_lcow.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/Microsoft/hcsshim/internal/controller/device/plan9"
"github.com/Microsoft/hcsshim/internal/controller/network"
hcsschema "github.com/Microsoft/hcsshim/internal/hcs/schema2"
"github.com/Microsoft/hcsshim/internal/log"
"github.com/Microsoft/hcsshim/internal/protocol/guestresource"
"github.com/Microsoft/hcsshim/internal/vm/vmmanager"
"github.com/Microsoft/hcsshim/internal/vm/vmutils"
Expand Down Expand Up @@ -165,6 +166,31 @@ func (c *Controller) setupEntropyListener(ctx context.Context, group *errgroup.G
// running inside the Linux VM. The logs are parsed and
// forwarded to the host's logging system for monitoring and debugging.
func (c *Controller) setupLoggingListener(ctx context.Context, group *errgroup.Group) error {
// Live-migratable sandboxes intentionally run without a host-side GCS log
// listener.
//
// The log listener is host-local state: GCS inside the guest connects out to
// a host-side hvsocket on LinuxLogVsockPort and streams its stderr to it. That
// connection, and the goroutine reading from it, are bound to the *source*
// host and are not part of the guest state that live migration transfers.
// After the VM is migrated to a destination host there is no equivalent
// listener to reconnect to, so a guest that depended on the log socket would
// block on its outbound connect and stall the boot path. To keep the guest
// migratable we skip the listener here and drop the matching /bin/vsockexec
// wrapper from the kernel command line, so GCS never attempts the connection.
//
// Re-enabling host-side log collection for live-migratable pods requires a
// migration-aware log transport: GCS must tolerate the listener going away
// and reconnect to a freshly established listener on the destination host once
// migration completes, and the host must (re)create the listener and re-attach
// the log-parsing goroutine on the destination. Until that work lands we forgo
// host-side GCS logs for these pods.
if c.sandboxOptions != nil && c.sandboxOptions.LiveMigrationSupportEnabled {
log.G(ctx).Info("skipping GCS log listener: pod is live-migratable")
close(c.logOutputDone)
return nil
}

// The GCS will connect to this port to stream log output.
logConn, err := winio.ListenHvsock(&winio.HvsockAddr{
VMID: c.uvm.RuntimeID(),
Expand Down
15 changes: 7 additions & 8 deletions pkg/annotations/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -539,14 +539,13 @@ const (

// Live Migration annotations.
const (
// LiveMigrationAllowed is a gatekeeping annotation scoped to a pod/sandbox that indicates
// the pod is intended to be live-migratable. When set on a pod, any container within that
// pod which requests a feature incompatible with live migration will fail to be created.
//
// For example, if a pod is started with this annotation and a container within it
// subsequently requests a plan9 share (which is not compatible with live migration),
// the container creation will be failed.
LiveMigrationAllowed = "io.microsoft.migration.allowed"
// LiveMigrationSupportEnabled is a sandbox-scoped annotation that enables the live
// migration feature set for a pod. When enabled, the pod is constrained to the subset
// of features that are compatible with live migration.
//
// For example, the sandbox runs without the host-side GCS log listener,
// since that listener is host-local and cannot survive migration.
LiveMigrationSupportEnabled = "io.microsoft.migration.support-enabled"

// LiveMigrationSourceContainerID is used only on the destination node during a live
// migration. It is set on the NewTask request to identify the corresponding container
Expand Down
Loading