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
61 changes: 61 additions & 0 deletions internal/vm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# VM Package

This directory defines the utility VM (UVM) contracts and separates responsibilities into three layers. The goal is to keep
configuration, host-side management, and guest-side actions distinct so each layer can evolve independently.

1. **Builder**: constructs an HCS compute system configuration used to create a VM.
2. **VM Manager**: manages host-side VM configuration and lifecycle (NICs, SCSI, VPMem, etc.).
3. **Guest Manager**: intended for guest-side actions (for example, mounting a disk).

**Note that** this layer does not store UVM host or guest side state. That will be part of the orchestration layer above it.

## Packages and Responsibilities

- `internal/vm`
- Interface definitions and shared types.
- `UVM` interface exposes APIs for lifecycle, resources, and device management of running VM.
- `Builder` exposes APIs for shaping the configuration used to create a VM.
- `internal/vm/builder`
- Concrete implementation of `Builder` for building `hcsschema.ComputeSystem` documents.
- Provides a fluent API for configuring all aspects of the VM document.
- Presently, this package is tightly coupled with HCS backend.
- `internal/vm/vmmanager`
- Concrete implementation of `UVM` for running and managing a UVM instance.
- Presently, this package is tightly coupled with HCS backend and only runs HCS backed UVMs.
- Owns lifecycle calls (start/terminate/close) and host-side modifications (NICs, SCSI, VPMem, pipes, VSMB, Plan9).
- Allows creation of the UVM using `vmmanager.Create` which takes a `Builder` and produces a running UVM.
- `internal/vm/guestmanager`
- Reserved for guest-level actions such as mounting disks or performing in-guest configuration.
- Currently empty and intended to grow as guest actions are formalized.

## Typical Flow

1. Build the config using the builder interfaces.
2. Create the VM using the VM manager.
3. Use manager interfaces for lifecycle and host-side changes.
4. Use guest manager interfaces for in-guest actions (when available).

## Example (High Level)

```
builder, _ := builder.New("owner", vm.Linux)

// Configure the VM document.
builder.Memory().SetMemoryLimit(1024)
builder.Processor().SetProcessorConfig(&vm.ProcessorConfig{Count: 2})
// ... other builder configuration

// Create and start the VM.
uvm, _ := vmmanager.Create(ctx, "uvm-id", builder)
_ = uvm.LifetimeManager().Start(ctx)

// Apply host-side updates.
_ = uvm.NetworkManager().AddNIC(ctx, nicID, endpointID, macAddr)
```

## Layer Boundaries (Quick Reference)

- **Builder**: static, pre-create configuration only. No host mutations.
- **VM Manager**: host-side changes and lifecycle operations on an existing UVM.
- **Guest Manager**: guest-side actions, scoped to work that requires in-guest context.

102 changes: 58 additions & 44 deletions internal/vm/builder.go
Original file line number Diff line number Diff line change
@@ -1,74 +1,88 @@
package vm

import (
"context"
)
import hcsschema "github.com/Microsoft/hcsshim/internal/hcs/schema2"

type UVMBuilder interface {
// Create will create the Utility VM in a paused/powered off state with whatever is present in the implementation
// of the interfaces config at the time of the call.
Create(ctx context.Context) (UVM, error)
}

type MemoryBackingType uint8

const (
MemoryBackingTypeVirtual MemoryBackingType = iota
MemoryBackingTypePhysical
)

// MemoryConfig holds the memory options that should be configurable for a Utility VM.
type MemoryConfig struct {
BackingType MemoryBackingType
DeferredCommit bool
HotHint bool
ColdHint bool
ColdDiscardHint bool
// Builder is the base builder interface for constructing Utility VMs.
// It provides access to various manager interfaces for configuring VM resources such as
// memory, processors, serial consoles, boot options, and storage QoS settings.
type Builder interface {
GuestOS() GuestOS
// Get returns the current config document being built, which can be used
// for creating or modifying a Utility VM.
Get() any
// Memory returns the MemoryManager for configuring VM memory settings.
Memory() MemoryManager
// Processor returns the ProcessorManager for configuring VM processor settings.
Processor() ProcessorManager
// Numa returns the NumaManager for configuring VM NUMA settings.
Numa() NumaManager
// Device returns the DeviceManager for configuring VM devices such as PCI and serial consoles.
Device() DeviceManager
// Boot returns the BootManager for configuring VM boot options.
Boot() BootManager
// StorageQoS returns the StorageQoSManager for configuring VM storage quality of service settings.
StorageQoS() StorageQoSManager
}

// MemoryManager handles setting and managing memory configurations for the Utility VM.
type MemoryManager interface {
// SetMemoryLimit sets the amount of memory in megabytes that the Utility VM will be assigned.
SetMemoryLimit(memoryMB uint64) error
SetMemoryLimit(memoryMB uint64)
// SetMemoryConfig sets an array of different memory configuration options available. This includes things like the
// type of memory to back the VM (virtual/physical).
SetMemoryConfig(config *MemoryConfig) error
SetMemoryConfig(config *MemoryConfig)
// SetMMIOConfig sets memory mapped IO configurations for the Utility VM.
SetMMIOConfig(lowGapMB uint64, highBaseMB uint64, highGapMB uint64) error
SetMMIOConfig(lowGapMB uint64, highBaseMB uint64, highGapMB uint64)
// SetFirmwareFallbackMeasuredSlit sets the SLIT type as "FirmwareFallbackMeasured" for the Utility VM.
SetFirmwareFallbackMeasuredSlit()
}

// ProcessorManager handles setting and managing processor configurations for the Utility VM.
type ProcessorManager interface {
// SetProcessorCount sets the number of virtual processors that will be assigned to the Utility VM.
SetProcessorCount(count uint32) error
// SetProcessorConfig sets an array of different processor configuration options available.
SetProcessorConfig(config *ProcessorConfig)
// SetCPUGroup sets the CPU group that the Utility VM will belong to on a Windows host.
SetCPUGroup(id string)
}

// SerialManager manages setting up serial consoles for the Utility VM.
type SerialManager interface {
type NumaManager interface {
// SetNUMAProcessorsSettings sets the NUMA processor settings for the Utility VM.
SetNUMAProcessorsSettings(numaProcessors *hcsschema.NumaProcessors)
// SetNUMASettings sets the NUMA settings for the Utility VM.
SetNUMASettings(numa *hcsschema.Numa)
}

type DeviceManager interface {
// AddVPCIDevice adds a PCI device to the Utility VM.
// If the device is already added, we return an error.
AddVPCIDevice(device VPCIDeviceID, numaAffinity bool) error
// AddSCSIController adds a SCSI controller to the Utility VM with the specified ID.
AddSCSIController(id string)
// AddSCSIDisk adds a SCSI disk to the Utility VM under the specified controller and LUN.
AddSCSIDisk(controller string, lun string, path string, typ SCSIDiskType, readOnly bool) error
// AddVPMemController adds a VPMem controller to the Utility VM with the specified maximum devices and size.
AddVPMemController(maximumDevices uint32, maximumSizeBytes uint64)
// AddVPMemDevice adds a VPMem device to the Utility VM under the VPMem controller.
AddVPMemDevice(id string, path string, readOnly bool, imageFormat string) error
// AddVSMB adds a VSMB share to the Utility VM with the specified path, name, allowed file access, and options.
AddVSMB(path string, name string, allowed []string, options *hcsschema.VirtualSmbShareOptions) error
// SetSerialConsole sets up a serial console for `port`. Output will be relayed to the listener specified
// by `listenerPath`. For HCS `listenerPath` this is expected to be a path to a named pipe.
SetSerialConsole(port uint32, listenerPath string) error
// EnableGraphicsConsole enables a graphics console for the Utility VM.
EnableGraphicsConsole()
}

// BootManager manages boot configurations for the Utility VM.
type BootManager interface {
// SetUEFIBoot sets UEFI configurations for booting a Utility VM.
SetUEFIBoot(dir string, path string, args string) error
SetUEFIBoot(dir string, path string, args string)
// SetLinuxKernelDirectBoot sets Linux direct boot configurations for booting a Utility VM.
SetLinuxKernelDirectBoot(kernel string, initRD string, cmd string) error
}

// StorageQosManager manages setting storage limits on the Utility VM.
type StorageQosManager interface {
// SetStorageQos sets storage related options for the Utility VM
SetStorageQos(iopsMaximum int64, bandwidthMaximum int64) error
// StorageQoSManager manages setting storage limits on the Utility VM.
type StorageQoSManager interface {
// SetStorageQoS sets storage related options for the Utility VM
SetStorageQoS(iopsMaximum int64, bandwidthMaximum int64)
}

// WindowsConfigManager manages options specific to a Windows host (cpugroups etc.)
type WindowsConfigManager interface {
// SetCPUGroup sets the CPU group that the Utility VM will belong to on a Windows host.
SetCPUGroup(ctx context.Context, id string) error
}

// LinuxConfigManager manages options specific to a Linux host.
type LinuxConfigManager interface{}
10 changes: 7 additions & 3 deletions internal/vm/hcs/boot.go → internal/vm/builder/boot.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
//go:build windows

package hcs
package builder

import (
hcsschema "github.com/Microsoft/hcsshim/internal/hcs/schema2"
"github.com/Microsoft/hcsshim/internal/vm"
"github.com/Microsoft/hcsshim/osversion"
"github.com/pkg/errors"
)

func (uvmb *utilityVMBuilder) SetUEFIBoot(dir string, path string, args string) error {
func (uvmb *utilityVMBuilder) Boot() vm.BootManager {
return uvmb
}

func (uvmb *utilityVMBuilder) SetUEFIBoot(dir string, path string, args string) {
uvmb.doc.VirtualMachine.Chipset.Uefi = &hcsschema.Uefi{
BootThis: &hcsschema.UefiBootEntry{
DevicePath: path,
Expand All @@ -17,7 +22,6 @@ func (uvmb *utilityVMBuilder) SetUEFIBoot(dir string, path string, args string)
OptionalData: args,
},
}
return nil
}

func (uvmb *utilityVMBuilder) SetLinuxKernelDirectBoot(kernel string, initRD string, cmd string) error {
Expand Down
50 changes: 50 additions & 0 deletions internal/vm/builder/boot_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//go:build windows

package builder

import (
"testing"

"github.com/Microsoft/hcsshim/internal/vm"
)

func TestBootConfig(t *testing.T) {
b, cs := newBuilder(t, vm.Linux)
b.Boot().SetUEFIBoot("root", "path", "args")

if cs.VirtualMachine.Chipset.Uefi == nil || cs.VirtualMachine.Chipset.Uefi.BootThis == nil {
t.Fatal("UEFI boot not applied")
}

boot := cs.VirtualMachine.Chipset.Uefi.BootThis
if boot.DevicePath != "path" {
t.Fatalf("UEFI DevicePath = %q, want %q", boot.DevicePath, "path")
}
if boot.DeviceType != "VmbFs" {
t.Fatalf("UEFI DeviceType = %q, want %q", boot.DeviceType, "VmbFs")
}
if boot.VmbFsRootPath != "root" {
t.Fatalf("UEFI VmbFsRootPath = %q, want %q", boot.VmbFsRootPath, "root")
}
if boot.OptionalData != "args" {
t.Fatalf("UEFI OptionalData = %q, want %q", boot.OptionalData, "args")
}

err := b.Boot().SetLinuxKernelDirectBoot("kernel", "initrd", "cmd")
if err != nil {
t.Fatalf("SetLinuxKernelDirectBoot error = %v", err)
}
if cs.VirtualMachine.Chipset.LinuxKernelDirect == nil {
t.Fatal("LinuxKernelDirect not applied")
}
lkd := cs.VirtualMachine.Chipset.LinuxKernelDirect
if lkd.KernelFilePath != "kernel" {
t.Fatalf("LinuxKernelDirect KernelFilePath = %q, want %q", lkd.KernelFilePath, "kernel")
}
if lkd.InitRdPath != "initrd" {
t.Fatalf("LinuxKernelDirect InitRdPath = %q, want %q", lkd.InitRdPath, "initrd")
}
if lkd.KernelCmdLine != "cmd" {
t.Fatalf("LinuxKernelDirect KernelCmdLine = %q, want %q", lkd.KernelCmdLine, "cmd")
}
}
68 changes: 68 additions & 0 deletions internal/vm/builder/builder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//go:build windows

package builder

import (
hcsschema "github.com/Microsoft/hcsshim/internal/hcs/schema2"
"github.com/Microsoft/hcsshim/internal/schemaversion"
"github.com/Microsoft/hcsshim/internal/vm"
)

var _ vm.Builder = &utilityVMBuilder{}

type utilityVMBuilder struct {
guestOS vm.GuestOS
doc *hcsschema.ComputeSystem
assignedDevices map[vm.VPCIDeviceID]*vPCIDevice
}

func New(owner string, guestOS vm.GuestOS) (vm.Builder, error) {
doc := &hcsschema.ComputeSystem{
Owner: owner,
SchemaVersion: schemaversion.SchemaV21(),
// Terminate the UVM when the last handle is closed.
// When we need to support impactless updates this will need to be configurable.
ShouldTerminateOnLastHandleClosed: true,
VirtualMachine: &hcsschema.VirtualMachine{
StopOnReset: true,
Chipset: &hcsschema.Chipset{},
ComputeTopology: &hcsschema.Topology{
Memory: &hcsschema.VirtualMachineMemory{},
Processor: &hcsschema.VirtualMachineProcessor{},
},
Devices: &hcsschema.Devices{
HvSocket: &hcsschema.HvSocket2{
HvSocketConfig: &hcsschema.HvSocketSystemConfig{
// Allow administrators and SYSTEM to bind to vsock sockets
// so that we can create a GCS log socket.
DefaultBindSecurityDescriptor: "D:P(A;;FA;;;SY)(A;;FA;;;BA)",
ServiceTable: make(map[string]hcsschema.HvSocketServiceConfig),
},
},
},
},
}

switch guestOS {
case vm.Windows:
doc.VirtualMachine.Devices.VirtualSmb = &hcsschema.VirtualSmb{}
case vm.Linux:
doc.VirtualMachine.Devices.Plan9 = &hcsschema.Plan9{}
default:
return nil, vm.ErrUnknownGuestOS
}

return &utilityVMBuilder{
guestOS: guestOS,
doc: doc,
assignedDevices: make(map[vm.VPCIDeviceID]*vPCIDevice),
}, nil
}

func (uvmb *utilityVMBuilder) GuestOS() vm.GuestOS {
return uvmb.guestOS
}

func (uvmb *utilityVMBuilder) Get() any {
return uvmb.doc
}
48 changes: 48 additions & 0 deletions internal/vm/builder/builder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//go:build windows

package builder

import (
"testing"

hcsschema "github.com/Microsoft/hcsshim/internal/hcs/schema2"
"github.com/Microsoft/hcsshim/internal/vm"
"github.com/pkg/errors"
)

func newBuilder(t *testing.T, guestOS vm.GuestOS) (vm.Builder, *hcsschema.ComputeSystem) {
t.Helper()
b, err := New("owner", guestOS)
if err != nil {
t.Fatalf("New() error = %v", err)
}
cs, ok := b.Get().(*hcsschema.ComputeSystem)
if !ok {
t.Fatalf("builder.Get() type = %T, want *hcsschema.ComputeSystem", b.Get())
}
return b, cs
}

func TestNewBuilder_InvalidGuestOS(t *testing.T) {
if _, err := New("owner", vm.GuestOS("unknown")); !errors.Is(err, vm.ErrUnknownGuestOS) {
t.Fatalf("New() error = %v, want %v", err, vm.ErrUnknownGuestOS)
}
}

func TestNewBuilder_DefaultDevices(t *testing.T) {
_, cs := newBuilder(t, vm.Windows)
if cs.VirtualMachine.Devices.VirtualSmb == nil {
t.Fatal("VirtualSmb should be initialized for Windows")
}
if cs.VirtualMachine.Devices.Plan9 != nil {
t.Fatal("Plan9 should be nil for Windows")
}

_, cs = newBuilder(t, vm.Linux)
if cs.VirtualMachine.Devices.Plan9 == nil {
t.Fatal("Plan9 should be initialized for Linux")
}
if cs.VirtualMachine.Devices.VirtualSmb != nil {
t.Fatal("VirtualSmb should be nil for Linux")
}
}
Loading