Skip to content

Commit f71836c

Browse files
author
Shreyansh Sancheti
committed
parity: add LCOW document permutation tests
Restructure parity test framework and add permutation-based tests for LCOW HCS document creation comparing legacy and v2 builder outputs. Signed-off-by: Shreyansh Sancheti <shreyanshjain7174@gmail.com> Signed-off-by: Shreyansh Sancheti <shsancheti@microsoft.com>
1 parent c70fd9f commit f71836c

File tree

7 files changed

+769
-7
lines changed

7 files changed

+769
-7
lines changed

internal/uvm/create.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ func verifyWCOWBootFiles(bootFiles *WCOWBootFiles) error {
160160
}
161161

162162
// Verifies that the final UVM options are correct and supported.
163-
func verifyOptions(_ context.Context, options interface{}) error {
163+
func VerifyOptions(_ context.Context, options interface{}) error {
164164
switch opts := options.(type) {
165165
case *OptionsLCOW:
166166
if opts.EnableDeferredCommit && !opts.AllowOvercommit {

internal/uvm/create_lcow.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,7 @@ func makeLCOWVMGSDoc(ctx context.Context, opts *OptionsLCOW, uvm *UtilityVM) (_
458458
// This is done prior to json seriaisation and sending to the HCS layer to actually do the work of creating the VM.
459459
// Many details are quite different (see the typical JSON examples), in particular it boots from a VMGS file
460460
// which contains both the kernel and initrd as well as kernel boot options.
461-
func makeLCOWSecurityDoc(ctx context.Context, opts *OptionsLCOW, uvm *UtilityVM) (_ *hcsschema.ComputeSystem, err error) {
461+
func MakeLCOWSecurityDoc(ctx context.Context, opts *OptionsLCOW, uvm *UtilityVM) (_ *hcsschema.ComputeSystem, err error) {
462462
doc, vmgsErr := makeLCOWVMGSDoc(ctx, opts, uvm)
463463
if vmgsErr != nil {
464464
return nil, vmgsErr
@@ -537,7 +537,7 @@ Example JSON document produced once the hcsschema.ComputeSytem returned by makeL
537537
*/
538538

539539
// Make the ComputeSystem document object that will be serialized to json to be presented to the HCS api.
540-
func makeLCOWDoc(ctx context.Context, opts *OptionsLCOW, uvm *UtilityVM) (_ *hcsschema.ComputeSystem, err error) {
540+
func MakeLCOWDoc(ctx context.Context, opts *OptionsLCOW, uvm *UtilityVM) (_ *hcsschema.ComputeSystem, err error) {
541541
if logrus.IsLevelEnabled(logrus.TraceLevel) {
542542
log.G(ctx).WithField("options", log.Format(ctx, opts)).Trace("makeLCOWDoc")
543543
}
@@ -931,22 +931,22 @@ func CreateLCOW(ctx context.Context, opts *OptionsLCOW) (_ *UtilityVM, err error
931931
uvm.scsiControllerCount = 4
932932
}
933933

934-
if err = verifyOptions(ctx, opts); err != nil {
934+
if err = VerifyOptions(ctx, opts); err != nil {
935935
return nil, errors.Wrap(err, errBadUVMOpts.Error())
936936
}
937937

938938
// HCS config for SNP isolated vm is quite different to the usual case
939939
var doc *hcsschema.ComputeSystem
940940
if opts.SecurityPolicyEnabled {
941-
doc, err = makeLCOWSecurityDoc(ctx, opts, uvm)
941+
doc, err = MakeLCOWSecurityDoc(ctx, opts, uvm)
942942
if logrus.IsLevelEnabled(logrus.TraceLevel) {
943943
log.G(ctx).WithFields(logrus.Fields{
944944
"doc": log.Format(ctx, doc),
945945
logrus.ErrorKey: err,
946946
}).Trace("create_lcow::CreateLCOW makeLCOWSecurityDoc result")
947947
}
948948
} else {
949-
doc, err = makeLCOWDoc(ctx, opts, uvm)
949+
doc, err = MakeLCOWDoc(ctx, opts, uvm)
950950
if logrus.IsLevelEnabled(logrus.TraceLevel) {
951951
log.G(ctx).WithFields(logrus.Fields{
952952
"doc": log.Format(ctx, doc),

internal/uvm/create_wcow.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -589,7 +589,7 @@ func CreateWCOW(ctx context.Context, opts *OptionsWCOW) (_ *UtilityVM, err error
589589
}
590590
}()
591591

592-
if err := verifyOptions(ctx, opts); err != nil {
592+
if err := VerifyOptions(ctx, opts); err != nil {
593593
return nil, errors.Wrap(err, errBadUVMOpts.Error())
594594
}
595595

internal/uvm/types.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,22 @@ func (uvm *UtilityVM) ScratchEncryptionEnabled() bool {
153153
return uvm.encryptScratch
154154
}
155155

156+
// NewUtilityVMForDoc creates a minimal UtilityVM with the fields needed by
157+
// MakeLCOWDoc and MakeLCOWSecurityDoc for HCS document generation. This is
158+
// not a runnable VM — it exists only for parity testing.
159+
func NewUtilityVMForDoc(id, owner string, scsiControllerCount, vpmemMaxCount uint32, vpmemMaxSizeBytes uint64, vpmemMultiMapping bool) *UtilityVM {
160+
return &UtilityVM{
161+
id: id,
162+
owner: owner,
163+
operatingSystem: "linux",
164+
scsiControllerCount: scsiControllerCount,
165+
vpmemMaxCount: vpmemMaxCount,
166+
vpmemMaxSizeBytes: vpmemMaxSizeBytes,
167+
vpciDevices: make(map[VPCIDeviceID]*VPCIDevice),
168+
vpmemMultiMapping: vpmemMultiMapping,
169+
}
170+
}
171+
156172
type WCOWBootFilesType uint8
157173

158174
const (

test/parity/vm/doc.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//go:build windows
2+
3+
// Package vm validates that the v2 VM document builders produce HCS
4+
// ComputeSystem documents equivalent to the legacy shim pipelines.
5+
//
6+
// Currently covers LCOW parity between:
7+
// - Legacy: OCI spec → oci.UpdateSpecFromOptions → oci.ProcessAnnotations →
8+
// oci.SpecToUVMCreateOpts → uvm.MakeLCOWDoc → *hcsschema.ComputeSystem
9+
// - V2: vm.Spec + runhcsopts.Options → lcow.BuildSandboxConfig →
10+
// *hcsschema.ComputeSystem + *SandboxOptions
11+
//
12+
// WCOW parity will be added in a future PR.
13+
package vm
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
//go:build windows
2+
3+
package vm
4+
5+
import (
6+
"context"
7+
"encoding/json"
8+
"fmt"
9+
"os"
10+
"path/filepath"
11+
"strings"
12+
"testing"
13+
14+
"github.com/opencontainers/runtime-spec/specs-go"
15+
16+
"github.com/google/go-cmp/cmp"
17+
18+
runhcsopts "github.com/Microsoft/hcsshim/cmd/containerd-shim-runhcs-v1/options"
19+
lcowbuilder "github.com/Microsoft/hcsshim/internal/builder/vm/lcow"
20+
hcsschema "github.com/Microsoft/hcsshim/internal/hcs/schema2"
21+
"github.com/Microsoft/hcsshim/internal/oci"
22+
"github.com/Microsoft/hcsshim/internal/uvm"
23+
"github.com/Microsoft/hcsshim/internal/vm/vmutils"
24+
"github.com/Microsoft/hcsshim/osversion"
25+
vm "github.com/Microsoft/hcsshim/sandbox-spec/vm/v2"
26+
)
27+
28+
// buildLegacyLCOWDocument creates the HCS document for an LCOW VM using the
29+
// legacy shim pipeline. It runs the same sequence as createInternal → createPod
30+
// → CreateLCOW: annotation processing, spec conversion, option verification,
31+
// and document generation.
32+
func buildLegacyLCOWDocument(
33+
ctx context.Context,
34+
spec specs.Spec,
35+
shimOpts *runhcsopts.Options,
36+
bundle string,
37+
) (*hcsschema.ComputeSystem, *uvm.OptionsLCOW, error) {
38+
// Step 1: Merge shim options into the OCI spec annotations.
39+
spec = oci.UpdateSpecFromOptions(spec, shimOpts)
40+
41+
// Step 2: Expand annotation groups (e.g., security toggles).
42+
if err := oci.ProcessAnnotations(ctx, spec.Annotations); err != nil {
43+
return nil, nil, fmt.Errorf("failed to expand OCI annotations: %w", err)
44+
}
45+
46+
// Step 3: Convert OCI spec + annotations into OptionsLCOW.
47+
rawOpts, err := oci.SpecToUVMCreateOpts(ctx, &spec, "test-parity@vm", "test-owner")
48+
if err != nil {
49+
return nil, nil, fmt.Errorf("failed to convert OCI spec to UVM create options: %w", err)
50+
}
51+
opts := rawOpts.(*uvm.OptionsLCOW)
52+
opts.BundleDirectory = bundle
53+
54+
// Step 4: Verify options constraints (same as CreateLCOW).
55+
if err := uvm.VerifyOptions(ctx, opts); err != nil {
56+
return nil, nil, fmt.Errorf("option verification failed: %w", err)
57+
}
58+
59+
// Step 5: Build the temporary UtilityVM with fields that MakeLCOWDoc reads.
60+
scsiCount := opts.SCSIControllerCount
61+
if osversion.Build() >= osversion.RS5 && opts.VPMemDeviceCount == 0 {
62+
scsiCount = 4
63+
}
64+
tempUVM := uvm.NewUtilityVMForDoc(
65+
opts.ID, opts.Owner,
66+
scsiCount, opts.VPMemDeviceCount, opts.VPMemSizeBytes,
67+
!opts.VPMemNoMultiMapping,
68+
)
69+
70+
// Step 6: Generate the HCS document.
71+
doc, err := uvm.MakeLCOWDoc(ctx, opts, tempUVM)
72+
if err != nil {
73+
return nil, nil, fmt.Errorf("failed to generate legacy LCOW HCS document: %w", err)
74+
}
75+
76+
return doc, opts, nil
77+
}
78+
79+
// buildV2LCOWDocument creates the HCS document and sandbox options from the
80+
// provided VM spec and runhcs options using the v2 modular builder.
81+
// The returned document can be used to create a VM directly via HCS.
82+
func buildV2LCOWDocument(
83+
ctx context.Context,
84+
shimOpts *runhcsopts.Options,
85+
spec *vm.Spec,
86+
bundle string,
87+
) (*hcsschema.ComputeSystem, *lcowbuilder.SandboxOptions, error) {
88+
return lcowbuilder.BuildSandboxConfig(ctx, "test-owner", bundle, shimOpts, spec)
89+
}
90+
91+
// setupBootFiles creates a temporary directory containing the kernel and rootfs
92+
// files that both document builders probe during boot configuration resolution.
93+
func setupBootFiles(t *testing.T) string {
94+
t.Helper()
95+
dir := t.TempDir()
96+
for _, name := range []string{
97+
vmutils.KernelFile,
98+
vmutils.UncompressedKernelFile,
99+
vmutils.InitrdFile,
100+
vmutils.VhdFile,
101+
} {
102+
if err := os.WriteFile(filepath.Join(dir, name), []byte("test"), 0644); err != nil {
103+
t.Fatalf("failed to create boot file %s: %v", name, err)
104+
}
105+
}
106+
return dir
107+
}
108+
109+
// jsonToString serializes v to indented JSON for test log output.
110+
func jsonToString(v interface{}) string {
111+
b, err := json.MarshalIndent(v, "", " ")
112+
if err != nil {
113+
panic(err)
114+
}
115+
return string(b)
116+
}
117+
118+
// normalizeKernelCmdLine trims leading/trailing whitespace from the kernel
119+
// command line in the document. The legacy builder has a minor quirk that
120+
// produces a leading space for initrd+KernelDirect boot. The v2 builder
121+
// does not. Since HCS trims whitespace from kernel args, this difference
122+
// is harmless and we normalize it away.
123+
func normalizeKernelCmdLine(doc *hcsschema.ComputeSystem) {
124+
if doc == nil || doc.VirtualMachine == nil || doc.VirtualMachine.Chipset == nil {
125+
return
126+
}
127+
if kd := doc.VirtualMachine.Chipset.LinuxKernelDirect; kd != nil {
128+
kd.KernelCmdLine = strings.TrimSpace(kd.KernelCmdLine)
129+
}
130+
if uefi := doc.VirtualMachine.Chipset.Uefi; uefi != nil && uefi.BootThis != nil {
131+
uefi.BootThis.OptionalData = strings.TrimSpace(uefi.BootThis.OptionalData)
132+
}
133+
}
134+
135+
// isOnlyKernelCmdLineWhitespaceDiff returns true if the only difference between
136+
// two documents is leading/trailing whitespace in the kernel command line.
137+
// This is a known legacy quirk where initrd+KernelDirect boot produces a
138+
// leading space that v2 correctly omits.
139+
func isOnlyKernelCmdLineWhitespaceDiff(legacy, v2 *hcsschema.ComputeSystem) bool {
140+
// Deep copy and normalize, then re-compare.
141+
legacyCopy := *legacy
142+
v2Copy := *v2
143+
// Shallow copy the VM and chipset to avoid mutating originals.
144+
if legacyCopy.VirtualMachine != nil {
145+
vmCopy := *legacyCopy.VirtualMachine
146+
legacyCopy.VirtualMachine = &vmCopy
147+
if vmCopy.Chipset != nil {
148+
chipCopy := *vmCopy.Chipset
149+
legacyCopy.VirtualMachine.Chipset = &chipCopy
150+
if chipCopy.LinuxKernelDirect != nil {
151+
lkdCopy := *chipCopy.LinuxKernelDirect
152+
legacyCopy.VirtualMachine.Chipset.LinuxKernelDirect = &lkdCopy
153+
}
154+
if chipCopy.Uefi != nil {
155+
uefiCopy := *chipCopy.Uefi
156+
legacyCopy.VirtualMachine.Chipset.Uefi = &uefiCopy
157+
if uefiCopy.BootThis != nil {
158+
btCopy := *uefiCopy.BootThis
159+
legacyCopy.VirtualMachine.Chipset.Uefi.BootThis = &btCopy
160+
}
161+
}
162+
}
163+
}
164+
if v2Copy.VirtualMachine != nil {
165+
vmCopy := *v2Copy.VirtualMachine
166+
v2Copy.VirtualMachine = &vmCopy
167+
if vmCopy.Chipset != nil {
168+
chipCopy := *vmCopy.Chipset
169+
v2Copy.VirtualMachine.Chipset = &chipCopy
170+
if chipCopy.LinuxKernelDirect != nil {
171+
lkdCopy := *chipCopy.LinuxKernelDirect
172+
v2Copy.VirtualMachine.Chipset.LinuxKernelDirect = &lkdCopy
173+
}
174+
if chipCopy.Uefi != nil {
175+
uefiCopy := *chipCopy.Uefi
176+
v2Copy.VirtualMachine.Chipset.Uefi = &uefiCopy
177+
if uefiCopy.BootThis != nil {
178+
btCopy := *uefiCopy.BootThis
179+
v2Copy.VirtualMachine.Chipset.Uefi.BootThis = &btCopy
180+
}
181+
}
182+
}
183+
}
184+
normalizeKernelCmdLine(&legacyCopy)
185+
normalizeKernelCmdLine(&v2Copy)
186+
return cmp.Diff(&legacyCopy, &v2Copy) == ""
187+
}

0 commit comments

Comments
 (0)