Skip to content

Add LCOW live migration support across the controller stacks#2790

Merged
rawahars merged 3 commits into
microsoft:mainfrom
rawahars:lm_save
Jul 9, 2026
Merged

Add LCOW live migration support across the controller stacks#2790
rawahars merged 3 commits into
microsoft:mainfrom
rawahars:lm_save

Conversation

@rawahars

Copy link
Copy Markdown
Contributor

Implement end-to-end live migration support for LCOW pods so a running guest and its workloads can be moved between hosts. Every controller in the stack — VM, pod, Linux container, process, network, and SCSI/Plan9/VPCI devices — gains a save/import lifecycle plus the destination-side patch and resume plumbing needed to rebind local resources and bring the workload back online.

Migration lifecycle:

  • Source: Save serializes a controller's state into a self-describing protobuf envelope and freezes the controller (StateSourceMigrating) so no mutating ops race the transfer; Resume rolls the freeze back, and a finalize Stop or VM teardown terminates it.
  • Destination: Import rehydrates a controller into a migrating state, Patch repoints saved resources (layer VHDs, process IO/bundle, network namespace) at the destination host, and Resume binds the live VM, guest, and devices and republishes events so containerd treats the task as locally running. AbortMigrated discards an imported-but-never-resumed controller and emits synthetic exits so Delete can proceed.

VM controller:

  • Add the source/destination migrating states and the full HCS migration lifecycle: InitializeLiveMigrationOnSource, StartLiveMigrationOnSource, StartLiveMigrationTransfer, FinalizeLiveMigration, plus StartWithMigrationOptions on the destination.
  • Exchange the opaque compatibility blob, retain the final HCS document so the destination can recreate an identical VM, and recover the GCS port/bridge-id allocator floors so reissued ids cannot collide.
  • Make SCSI initialization lazy (built on first use from the HCS document) and handle never-started/destination teardown paths, including the already-stopped HCS error.

Controller-specific changes:

  • SCSI controller switches to an RWMutex and rejects all ops while migrating; ReserveForRootfs now carries the full disk config.
  • Process, network, container, and VM state machines document and enforce the new migrating states and transitions.
  • Pod gains a migrating guard, AbortMigrated fan-out, and routes new containers through lazy SCSI init.

Includes accompanying unit tests for the new save/import/patch/resume paths across all controllers.

@rawahars rawahars requested a review from a team as a code owner June 24, 2026 12:02
@rawahars rawahars force-pushed the lm_save branch 5 times, most recently from 3ae59a0 to e4fc26e Compare June 24, 2026 18:32
@rawahars rawahars requested a review from jterry75 June 30, 2026 17:09
type Controller struct {
// mu serializes all public operations on the Controller.
mu sync.Mutex
mu sync.RWMutex

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You changed no usage of this why did it become rw?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Comment thread internal/controller/network/network.go Outdated

type Controller struct {
mu sync.Mutex
mu sync.RWMutex

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same why?


// nextGuestPort is the GCS IO port-allocator floor restored from a
// migration snapshot; consumed by [Controller.Resume].
nextGuestPort uint32

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought you didnt want to store a policy at this layer?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately for LM, we need to make this exception. These fields are part of the saved state payload and can only be decoded by VM controller. Now we already decode the saved state during Import but these need to be set on the new bridge which happens in Resume. Alternate would be for migration controller to store the opaque state and then send the state as Resume param where it gets decoded again.

That is just redundancy and hence I stored it in the controller layer during import.

// hcsDocument is the final HCS document used to create this VM,
// retained for lazy SCSI controller construction and for shipping to
// the destination during live migration.
hcsDocument *hcsschema.ComputeSystem

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You would have to maintain it over time right? How can it be lazy?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So storing the hcsDocument serves 2 purposes-

  1. Lazy initialization of SCSI controller which needs to reserve the rootfs slots
  2. It is shipped to the destination where it is used to recreate the VM

For the LM capable workloads, when we create the VM on the destination, we override the SCSI map with that from the imported controller and therefore it is consistent.

Apart from the SCSI controller, nothing else needs maintenance over time as those features are not supported. When they are, we would override them in a similar manner.

Implement end-to-end live migration support for LCOW pods so a running guest and
its workloads can be moved between hosts. Every controller in the stack —
VM, pod, Linux container, process, network, and SCSI/Plan9/VPCI devices —
gains a save/import lifecycle plus the destination-side patch and resume
plumbing needed to rebind local resources and bring the workload back
online.

Migration lifecycle:
- Source: Save serializes a controller's state into a self-describing
  protobuf envelope and freezes the controller (StateSourceMigrating) so
  no mutating ops race the transfer; Resume rolls the freeze back, and a
  finalize Stop or VM teardown terminates it.
- Destination: Import rehydrates a controller into a migrating state,
  Patch repoints saved resources (layer VHDs, process IO/bundle, network
  namespace) at the destination host, and Resume binds the live VM, guest,
  and devices and republishes events so containerd treats the task as
  locally running. AbortMigrated discards an imported-but-never-resumed
  controller and emits synthetic exits so Delete can proceed.

VM controller:
- Add the source/destination migrating states and the full HCS migration
  lifecycle: InitializeLiveMigrationOnSource, StartLiveMigrationOnSource,
  StartLiveMigrationTransfer, FinalizeLiveMigration, plus
  StartWithMigrationOptions on the destination.
- Exchange the opaque compatibility blob, retain the final HCS document so
  the destination can recreate an identical VM, and recover the GCS
  port/bridge-id allocator floors so reissued ids cannot collide.
- Make SCSI initialization lazy (built on first use from the HCS document)
  and handle never-started/destination teardown paths, including the
  already-stopped HCS error.

Controller-specific changes:
- SCSI controller switches to an RWMutex and rejects all ops while
  migrating; ReserveForRootfs now carries the full disk config.
- Process, network, container, and VM state machines document and enforce
  the new migrating states and transitions.
- Pod gains a migrating guard, AbortMigrated fan-out, and routes new
  containers through lazy SCSI init.

Includes accompanying unit tests for the new save/import/patch/resume
paths across all controllers.

Signed-off-by: Harsh Rawat <harshrawat@microsoft.com>

@marma-dev marma-dev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall: solid, cohesive design with strong tests. I'd want findings (nil-deref) and (mutex) resolved before merge, and (hcsDoc mutation) / (Resume Guards) / (Disks() vs ReserveForRootFs) clarified.

return fmt.Errorf("cannot create VM in state %s: imported HCS document has no VirtualMachine", c.vmState)
}
hcsDocument = c.hcsDocument
hcsDocument.VirtualMachine.MigrationOptions = opts.MigrationOptions

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can opts.MigrationOptions ever be nil? If so we might dereference a nil MigrationOptions two lines below. Maybe add a precondition check (return an error if opts.MigrationOptions == nil in StateMigratingImported)

// SCSI controller is the source of truth for the destination
// topology (rootfs + hot-added, path-patched); use it verbatim.
if c.scsiController != nil {
hcsDocument.VirtualMachine.Devices.Scsi = c.scsiController.HCSAttachments()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code block mutates c.hcsDocument in place (hcsDocument is the same pointer). Since this document is the "source of truth" retained for lazy SCSI init and re-shipped on save, mutating MigrationOptions/Devices.Scsi on the shared instance could bite on any retry/re-entry. Consider deep-copying before stamping migration-specific fields

Comment thread internal/controller/device/scsi/save.go Outdated
Comment on lines +144 to +152
func (c *Controller) Resume(ctx context.Context, vm VMSCSIOps, guest GuestSCSIOps) {
c.mu.Lock()
defer c.mu.Unlock()

c.vm = vm
c.guest = guest
c.isMigrating = false

log.G(ctx).Debug("resumed scsi controller")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resume unconditionally sets c.vm, c.guest, and clears isMigrating. Every other controller's Resume guards on the migrating state and errors otherwise. A stray Resume here would silently overwrite the live vm/guest. Worth a guard for consistency/safety (even if callers are currently correct).

defer c.mu.RUnlock()

// Collect the config of each indexed disk.
configs := make([]disk.Config, 0, len(c.disksByPath))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems ReserveForRootfs populates controllerSlots but not disksByPath, while Disks() iterates disksByPath.

I think on the destination, vm.Patch calls scsiController.Disks() to GrantVmAccess and since Import does populate disksByPath from the saved host paths, it likely works for the migration path — but the asymmetry seems a little fragile.

Let's confirm rootfs disks are always represented in disksByPath post-import (a test asserting Disks() includes the rootfs would lock this down).

type Controller struct {
// mu serializes all public operations on the Controller.
mu sync.Mutex
mu sync.RWMutex

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

rawahars added 2 commits July 9, 2026 01:31
Signed-off-by: Harsh Rawat <harshrawat@microsoft.com>
…ume and Patch methods

Signed-off-by: Harsh Rawat <harshrawat@microsoft.com>

// Save captures the migrating VM's state into a serialized snapshot that the
// destination host consumes to recreate an equivalent VM.
func (c *Controller) Save(ctx context.Context) (*anypb.Any, error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really nice that every controller here comes with save/import tests. One gap I noticed: the vm controller's migration surface here (Save/Import/Patch/Resume) and vm_migration.go don't seem to have a corresponding _test.go, while pod/network/process/linuxcontainer/scsi all do.
These are the destination side steps that run right before and after the CreateVM branch in vm.go (the one with the nil MigrationOptions case), so a vm-layer test would exercise that whole sequence. Would it be worth adding one? might have missed them if they're coming in the follow-up PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a very astute observation! Yes, we would have that as a follow-up PR.
We have an existing PR which adds base VM Controller tests- #2738

Once this one is merged, we will have the mocks we need to accomplish the migration tests. I created a Issue #2812 to track the same.

@marma-dev marma-dev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Issue #2812 tracks the VM Controller Tests.

@rawahars rawahars merged commit 3a9e957 into microsoft:main Jul 9, 2026
20 checks passed
@rawahars rawahars deleted the lm_save branch July 9, 2026 16:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants