Skip to content
Closed
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
68 changes: 68 additions & 0 deletions internal/builder/container/lcow/devices.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//go:build windows && lcow

package lcow

import (
"context"
"fmt"

"github.com/Microsoft/hcsshim/internal/builder/container"
"github.com/Microsoft/hcsshim/internal/controller/device/vpci"
"github.com/Microsoft/hcsshim/internal/log"
"github.com/Microsoft/hcsshim/internal/logfields"

"github.com/Microsoft/go-winio/pkg/guid"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/sirupsen/logrus"
)

// reserveAndUpdateDevices reserves vPCI devices on the host and updates each
// device's ID in the spec to the resulting VMBus channel GUID.
//
// On partial failure the successfully reserved IDs are still returned so the
// caller's top-level cleanup can release them.
func reserveAndUpdateDevices(
ctx context.Context,
vpciReserver container.VPCIReserver,
specDevs []specs.WindowsDevice,
) ([]guid.GUID, error) {
log.G(ctx).WithField("devices", log.Format(ctx, specDevs)).Trace("reserving vPCI devices")

var reservations []guid.GUID

for deviceIdx := range specDevs {
device := &specDevs[deviceIdx]

// Validate that the device type is supported before attempting reservation.
if !vpci.IsValidDeviceType(device.IDType) {
return reservations, fmt.Errorf("reserve device %s: unsupported type %s", device.ID, device.IDType)
}

// Parse the device path into a PCI ID and optional virtual function index.
pciID, virtualFunctionIndex := vpci.GetDeviceInfoFromPath(device.ID)

// Reserve the device on the host and obtain the VMBus channel GUID.
vmBusGUID, err := vpciReserver.Reserve(ctx, vpci.Device{
DeviceInstanceID: pciID,
VirtualFunctionIndex: virtualFunctionIndex,
})
if err != nil {
return reservations, fmt.Errorf("reserve device %s: %w", device.ID, err)
}

log.G(ctx).WithFields(logrus.Fields{
logfields.DeviceID: pciID,
logfields.VFIndex: virtualFunctionIndex,
logfields.VMBusGUID: vmBusGUID.String(),
}).Trace("reserved vPCI device")

// Update the spec entry so GCS references the VMBus GUID
// instead of the original device path.
device.ID = vmBusGUID.String()
reservations = append(reservations, vmBusGUID)
}

log.G(ctx).Debug("all vPCI devices reserved successfully")

return reservations, nil
}
265 changes: 265 additions & 0 deletions internal/builder/container/lcow/devices_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
//go:build windows && lcow

package lcow

import (
"errors"
"testing"

"github.com/Microsoft/go-winio/pkg/guid"
"go.uber.org/mock/gomock"

"github.com/Microsoft/hcsshim/internal/builder/container/mocks"
"github.com/Microsoft/hcsshim/internal/controller/device/vpci"
"github.com/opencontainers/runtime-spec/specs-go"
)

// ─────────────────────────────────────────────────────────────────────────────
// Test helpers
// ─────────────────────────────────────────────────────────────────────────────

// newGUID generates a random GUID and fails the test on error.
func newGUID(t *testing.T) guid.GUID {
t.Helper()
id, err := guid.NewV4()
if err != nil {
t.Fatalf("failed to generate GUID: %v", err)
}
return id
}

// ─────────────────────────────────────────────────────────────────────────────
// reserveAndUpdateDevices — empty device list
// ─────────────────────────────────────────────────────────────────────────────

// TestReserveDevices_EmptyList verifies that an empty device slice produces
// no reservations and no error.
func TestReserveDevices_EmptyList(t *testing.T) {
t.Parallel()
vpciReserver := mocks.NewMockVPCIReserver(gomock.NewController(t))

reservations, err := reserveAndUpdateDevices(t.Context(), vpciReserver, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(reservations) != 0 {
t.Errorf("expected 0 reservations, got %d", len(reservations))
}
}

// ─────────────────────────────────────────────────────────────────────────────
// reserveAndUpdateDevices — single valid device
// ─────────────────────────────────────────────────────────────────────────────

// TestReserveDevices_SingleDevice verifies that a single valid device is
// reserved and its spec ID is rewritten to the VMBus GUID.
func TestReserveDevices_SingleDevice(t *testing.T) {
t.Parallel()
vpciReserver := mocks.NewMockVPCIReserver(gomock.NewController(t))

vmBusGUID := newGUID(t)
devicePath := `PCI\VEN_1234&DEV_5678`

vpciReserver.EXPECT().Reserve(gomock.Any(), vpci.Device{
DeviceInstanceID: devicePath,
VirtualFunctionIndex: 0,
}).Return(vmBusGUID, nil)

specDevs := []specs.WindowsDevice{
{ID: devicePath, IDType: vpci.DeviceIDType},
}

reservations, err := reserveAndUpdateDevices(t.Context(), vpciReserver, specDevs)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(reservations) != 1 {
t.Fatalf("expected 1 reservation, got %d", len(reservations))
}
if reservations[0] != vmBusGUID {
t.Errorf("expected reservation GUID %s, got %s", vmBusGUID, reservations[0])
}
if specDevs[0].ID != vmBusGUID.String() {
t.Errorf("expected spec device ID rewritten to %s, got %s", vmBusGUID, specDevs[0].ID)
}
}

// ─────────────────────────────────────────────────────────────────────────────
// reserveAndUpdateDevices — device with virtual function index
// ─────────────────────────────────────────────────────────────────────────────

// TestReserveDevices_WithVirtualFunctionIndex verifies that a device path
// containing a trailing VF index (e.g. "DEVICE_ID/2") is parsed into the
// correct DeviceInstanceID and VirtualFunctionIndex.
func TestReserveDevices_WithVirtualFunctionIndex(t *testing.T) {
t.Parallel()
vpciReserver := mocks.NewMockVPCIReserver(gomock.NewController(t))

vmBusGUID := newGUID(t)
devicePath := `PCI\VEN_1234&DEV_5678`

vpciReserver.EXPECT().Reserve(gomock.Any(), vpci.Device{
DeviceInstanceID: devicePath,
VirtualFunctionIndex: 3,
}).Return(vmBusGUID, nil)

specDevs := []specs.WindowsDevice{
{ID: devicePath + `/3`, IDType: vpci.DeviceIDType},
}

reservations, err := reserveAndUpdateDevices(t.Context(), vpciReserver, specDevs)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(reservations) != 1 {
t.Fatalf("expected 1 reservation, got %d", len(reservations))
}
if specDevs[0].ID != vmBusGUID.String() {
t.Errorf("expected spec device ID rewritten to %s, got %s", vmBusGUID, specDevs[0].ID)
}
}

// ─────────────────────────────────────────────────────────────────────────────
// reserveAndUpdateDevices — multiple devices in order
// ─────────────────────────────────────────────────────────────────────────────

// TestReserveDevices_MultipleDevices verifies that multiple devices are
// reserved in order and all spec IDs are rewritten.
func TestReserveDevices_MultipleDevices(t *testing.T) {
t.Parallel()
vpciReserver := mocks.NewMockVPCIReserver(gomock.NewController(t))

guid1, guid2 := newGUID(t), newGUID(t)
path1, path2 := `PCI\DEV_A`, `PCI\DEV_B`

gomock.InOrder(
vpciReserver.EXPECT().Reserve(gomock.Any(), vpci.Device{
DeviceInstanceID: path1,
}).Return(guid1, nil),
vpciReserver.EXPECT().Reserve(gomock.Any(), vpci.Device{
DeviceInstanceID: path2,
}).Return(guid2, nil),
)

specDevs := []specs.WindowsDevice{
{ID: path1, IDType: vpci.DeviceIDTypeLegacy},
{ID: path2, IDType: vpci.GpuDeviceIDType},
}

reservations, err := reserveAndUpdateDevices(t.Context(), vpciReserver, specDevs)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(reservations) != 2 {
t.Fatalf("expected 2 reservations, got %d", len(reservations))
}
if reservations[0] != guid1 || reservations[1] != guid2 {
t.Errorf("unexpected reservation GUIDs: %v", reservations)
}
if specDevs[0].ID != guid1.String() || specDevs[1].ID != guid2.String() {
t.Errorf("spec device IDs not rewritten: %q, %q", specDevs[0].ID, specDevs[1].ID)
}
}

// ─────────────────────────────────────────────────────────────────────────────
// reserveAndUpdateDevices — unsupported device type
// ─────────────────────────────────────────────────────────────────────────────

// TestReserveDevices_UnsupportedType verifies that an unsupported device type
// returns an error without calling Reserve.
func TestReserveDevices_UnsupportedType(t *testing.T) {
t.Parallel()
vpciReserver := mocks.NewMockVPCIReserver(gomock.NewController(t))

// No Reserve expectations — Reserve must not be called.
specDevs := []specs.WindowsDevice{
{ID: `PCI\DEV_X`, IDType: "unsupported-type"},
}

_, err := reserveAndUpdateDevices(t.Context(), vpciReserver, specDevs)
if err == nil {
t.Fatal("expected error for unsupported device type")
}
}

// ─────────────────────────────────────────────────────────────────────────────
// reserveAndUpdateDevices — reserve failure returns partial results
// ─────────────────────────────────────────────────────────────────────────────

// TestReserveDevices_ReserveFailure verifies that when Reserve fails on the
// second device, the first reservation is still returned for cleanup.
func TestReserveDevices_ReserveFailure(t *testing.T) {
t.Parallel()
vpciReserver := mocks.NewMockVPCIReserver(gomock.NewController(t))

guid1 := newGUID(t)
path1, path2 := `PCI\DEV_A`, `PCI\DEV_B`

gomock.InOrder(
vpciReserver.EXPECT().Reserve(gomock.Any(), vpci.Device{
DeviceInstanceID: path1,
}).Return(guid1, nil),
vpciReserver.EXPECT().Reserve(gomock.Any(), vpci.Device{
DeviceInstanceID: path2,
}).Return(guid.GUID{}, errors.New("reservation failed")),
)

specDevs := []specs.WindowsDevice{
{ID: path1, IDType: vpci.DeviceIDType},
{ID: path2, IDType: vpci.DeviceIDType},
}

reservations, err := reserveAndUpdateDevices(t.Context(), vpciReserver, specDevs)
if err == nil {
t.Fatal("expected error from Reserve failure")
}
// The first successful reservation must still be returned.
if len(reservations) != 1 {
t.Fatalf("expected 1 partial reservation, got %d", len(reservations))
}
if reservations[0] != guid1 {
t.Errorf("expected partial reservation GUID %s, got %s", guid1, reservations[0])
}
if specDevs[0].ID != guid1.String() {
t.Errorf("expected first device ID rewritten to %s, got %s", guid1, specDevs[0].ID)
}
if specDevs[1].ID != path2 {
t.Errorf("expected failing device ID to remain %s, got %s", path2, specDevs[1].ID)
}
}

// ─────────────────────────────────────────────────────────────────────────────
// reserveAndUpdateDevices — unsupported type after valid device
// ─────────────────────────────────────────────────────────────────────────────

// TestReserveDevices_UnsupportedTypeAfterValid verifies that an unsupported
// type on the second device returns the first successful reservation.
func TestReserveDevices_UnsupportedTypeAfterValid(t *testing.T) {
t.Parallel()
vpciReserver := mocks.NewMockVPCIReserver(gomock.NewController(t))

guid1 := newGUID(t)

vpciReserver.EXPECT().Reserve(gomock.Any(), vpci.Device{
DeviceInstanceID: `PCI\DEV_A`,
}).Return(guid1, nil)

specDevs := []specs.WindowsDevice{
{ID: `PCI\DEV_A`, IDType: vpci.DeviceIDType},
{ID: `PCI\DEV_B`, IDType: "bad-type"},
}

reservations, err := reserveAndUpdateDevices(t.Context(), vpciReserver, specDevs)
if err == nil {
t.Fatal("expected error for unsupported device type")
}
if len(reservations) != 1 {
t.Fatalf("expected 1 partial reservation, got %d", len(reservations))
}
if specDevs[0].ID != guid1.String() {
t.Errorf("expected first device ID rewritten to %s, got %s", guid1, specDevs[0].ID)
}
if specDevs[1].ID != `PCI\DEV_B` {
t.Errorf("expected unsupported device ID to remain %s, got %s", `PCI\DEV_B`, specDevs[1].ID)
}
}
39 changes: 39 additions & 0 deletions internal/builder/container/lcow/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//go:build windows && lcow

// Package lcow prepares everything needed to create a Linux container inside a
// utility VM. The container [controller] drives it in two phases:
//
// 1. Resource reservation — [ReserveAll] orchestrates [parseAndReserveLayers],
// [reserveAndUpdateMounts], and [reserveAndUpdateDevices] to claim host-side
// SCSI, Plan9, and vPCI resources. It rewrites the OCI spec in place so that
// mount sources and device IDs reference their guest-visible paths.
// Each sub-function returns partial results on error so that a single
// deferred [container.ResourcePlan.Release] in ReserveAll cleans up every
// reservation that was successfully made — no per-function rollback needed.
//
// 2. Spec generation — [GenerateSpecs] produces a sanitized copy of the OCI
// spec suitable for the Linux GCS, stripping unsupported fields and
// applying safe defaults.
//
// The resulting [container.ResourcePlan] and spec are handed back to the
// controller, which commits them to the VM and sends the final container
// document to GCS for container creation. Because reservations are tracked as
// individual IDs (not blanket closers), the controller can selectively release
// or transfer each resource during live migration save/restore.
//
// The controller's Create method drives the overall flow:
//
// // 1. Reserve resources (layers, mounts, devices) and rewrite the spec.
// reservations := lcow.ReserveAll(ctx, scsiReserver, plan9Reserver, vpciReserver, spec, cfg)
//
// // 2. Generate the sanitized OCI spec for the GCS.
// doc := generateContainerDocument(spec, reservations) // calls lcow.GenerateSpecs
//
// // 3. Allocate (attach/mount) the reserved resources into the VM.
// allocateContainerResources(reservations)
//
// // 4. Send the document to the GCS to create the container.
// guestMgr.CreateContainer(doc)
//
// [container.Controller]: github.com/Microsoft/hcsshim/internal/controller/container
package lcow
Loading
Loading