Skip to content

Commit 30a9d2d

Browse files
committed
refactor: require usb_version with usb
BREAKING CHANGE: `usb_version` field is required when `usb` is enabled" - Adds `usb_version` string field with values "2.0" or "3.1" - Requires explicit `usb_version `when `usb = true` - Adds error handling for requiring both fields. - Adds error handling for desktop hypervisor on Apple Silicon. Signed-off-by: Ryan Johnson <rya@tenthirtyam.org>
1 parent 1982713 commit 30a9d2d

12 files changed

Lines changed: 471 additions & 45 deletions

File tree

.web-docs/components/builder/iso/README.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,11 +134,17 @@ JSON Example:
134134

135135
- `sound` (bool) - Enable virtual sound card device. Defaults to `false`.
136136

137-
- `usb` (bool) - Enable USB 2.0 controllers for the virtual machine.
137+
- `usb` (bool) - Enable USB controller for the virtual machine.
138138
Defaults to `false`.
139139

140-
~> **Note:** To enable USB 3.0 controllers, set a `usb_xhci.present`
141-
key to `true` in the `vmx_data` option.
140+
~> **Note:** The plugin automatically enables this on Apple Silicon-based
141+
systems to ensure plugin functionality.
142+
143+
- `usb_version` (string) - USB version to use when USB is enabled. Required when `usb` is enabled.
144+
Allowed values are "2.0" and "3.1".
145+
146+
~> **Note:** The plugin automatically enables version 3.1 on Apple
147+
Silicon-based systems to ensure plugin functionality.
142148

143149
- `serial` (string) - Add a serial port to the virtual machine. Use a format of
144150
`Type:option1,option2,...`. Allowed values for the field `Type` include:

builder/vmware/common/driver.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ const (
124124
cdromAdapterSata = "sata"
125125
cdromAdapterScsi = "scsi"
126126

127+
// USB version types.
128+
UsbVersion20 = "2.0"
129+
UsbVersion31 = "3.1"
130+
127131
// Shutdown operation timings.
128132
shutdownPollInterval = 150 * time.Millisecond
129133
shutdownLockTimeout = 120 * time.Second
@@ -202,6 +206,12 @@ var AllowedCdromAdapterTypes = []string{
202206
cdromAdapterScsi,
203207
}
204208

209+
// AllowedUsbVersions defines the allowed USB versions for a virtual machine.
210+
var AllowedUsbVersions = []string{
211+
UsbVersion20,
212+
UsbVersion31,
213+
}
214+
205215
// The allowed values for the `ToolsUploadFlavor`.
206216
var allowedToolsFlavorValues = []string{
207217
toolsFlavorMacOS,

builder/vmware/common/hw_config.go

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package common
77

88
import (
99
"fmt"
10+
"log"
1011
"path/filepath"
1112
"runtime"
1213
"slices"
@@ -45,12 +46,18 @@ type HWConfig struct {
4546
NetworkAdapterType string `mapstructure:"network_adapter_type" required:"false"`
4647
// Enable virtual sound card device. Defaults to `false`.
4748
Sound bool `mapstructure:"sound" required:"false"`
48-
// Enable USB 2.0 controllers for the virtual machine.
49+
// Enable USB controller for the virtual machine.
4950
// Defaults to `false`.
5051
//
51-
// ~> **Note:** To enable USB 3.0 controllers, set a `usb_xhci.present`
52-
// key to `true` in the `vmx_data` option.
52+
// ~> **Note:** The plugin automatically enables this on Apple Silicon-based
53+
// systems to ensure plugin functionality.
5354
USB bool `mapstructure:"usb" required:"false"`
55+
// USB version to use when USB is enabled. Required when `usb` is enabled.
56+
// Allowed values are "2.0" and "3.1".
57+
//
58+
// ~> **Note:** The plugin automatically enables version 3.1 on Apple
59+
// Silicon-based systems to ensure plugin functionality.
60+
USBVersion string `mapstructure:"usb_version" required:"false"`
5461
// Add a serial port to the virtual machine. Use a format of
5562
// `Type:option1,option2,...`. Allowed values for the field `Type` include:
5663
// `FILE`, `DEVICE`, `PIPE`, `AUTO`, or `NONE`.
@@ -138,13 +145,47 @@ func (c *HWConfig) Prepare(ctx *interpolate.Context) []error {
138145
errs = append(errs, fmt.Errorf("invalid 'network_adapter_type' type specified: %s; must be one of %s", c.NetworkAdapterType, strings.Join(allowedNetworkAdapterTypes, ", ")))
139146
}
140147

141-
// Peripherals
142148
if !c.Sound {
143149
c.Sound = false
144150
}
145151

146-
if !c.USB {
147-
c.USB = false
152+
// Handle USB configuration
153+
if c.USB {
154+
// Require USB version to be explicitly specified
155+
if c.USBVersion == "" {
156+
errs = append(errs, fmt.Errorf("usb_version is required when usb is enabled; must be one of %v", AllowedUsbVersions))
157+
} else {
158+
// Validate USB version against allowed versions
159+
validVersion := false
160+
for _, allowedVersion := range AllowedUsbVersions {
161+
if c.USBVersion == allowedVersion {
162+
validVersion = true
163+
break
164+
}
165+
}
166+
if !validVersion {
167+
errs = append(errs, fmt.Errorf("usb_version must be one of %v, got '%s'", AllowedUsbVersions, c.USBVersion))
168+
}
169+
170+
// VMware Fusion on Apple Silicon requires USB 3.1 to send keyboard
171+
// inputs to the guest operating system.
172+
if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
173+
if c.USBVersion == UsbVersion20 {
174+
errs = append(errs, fmt.Errorf("USB 3.1 is required on Apple Silicon-based systems for the plugin to work; use usb_version '%s'", UsbVersion31))
175+
}
176+
}
177+
}
178+
} else if c.USBVersion != "" {
179+
errs = append(errs, fmt.Errorf("usb_version can only be set when usb is enabled"))
180+
}
181+
182+
// VMware Fusion on Apple Silicon requires USB 3.1 to send keyboard
183+
// inputs to the guest operating system. Auto-enable if not explicitly
184+
// configured.
185+
if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" && !c.USB && c.USBVersion == "" {
186+
log.Printf("[INFO] Auto-enabling USB 3.1 for plugin functionality.")
187+
c.USB = true
188+
c.USBVersion = UsbVersion31
148189
}
149190

150191
if c.Parallel == "" {

builder/vmware/common/hw_config_test.go

Lines changed: 158 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package common
55

66
import (
7+
"runtime"
78
"strings"
89
"testing"
910

@@ -35,8 +36,20 @@ func TestHWConfigPrepare(t *testing.T) {
3536
t.Errorf("peripheral choice (sound) should be conservative: %t", c.Sound)
3637
}
3738

38-
if c.USB {
39-
t.Errorf("peripheral choice (usb) should be conservative: %t", c.USB)
39+
if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
40+
if !c.USB {
41+
t.Errorf("USB should be automatically enabled on Apple Silicon: %t", c.USB)
42+
}
43+
if c.USBVersion != "3.1" {
44+
t.Errorf("USB version should be automatically set to 3.1 on Apple Silicon: %s", c.USBVersion)
45+
}
46+
} else {
47+
if c.USB {
48+
t.Errorf("peripheral choice (usb) should be conservative: %t", c.USB)
49+
}
50+
if c.USBVersion != "" {
51+
t.Errorf("USB version should not be set when USB is disabled: %s", c.USBVersion)
52+
}
4053
}
4154

4255
if strings.ToUpper(c.Parallel) != "NONE" {
@@ -327,3 +340,146 @@ func TestHWConfigSerial_None(t *testing.T) {
327340
t.Errorf("serial port shouldn't exist")
328341
}
329342
}
343+
344+
func TestHWConfigUSBValidation_USB2Only(t *testing.T) {
345+
c := new(HWConfig)
346+
c.NetworkAdapterType = "vmxnet3"
347+
c.USB = true
348+
c.USBVersion = UsbVersion20
349+
350+
errs := c.Prepare(interpolate.NewContext())
351+
352+
if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
353+
if len(errs) == 0 {
354+
t.Fatal("expected error when USB 2.0 is enabled on Apple Silicon")
355+
}
356+
return
357+
}
358+
359+
if len(errs) > 0 {
360+
t.Fatalf("err: %#v", errs)
361+
}
362+
363+
if !c.USB {
364+
t.Errorf("USB should be enabled: %t", c.USB)
365+
}
366+
367+
if c.USBVersion != UsbVersion20 {
368+
t.Errorf("USB version should be 2.0: %s", c.USBVersion)
369+
}
370+
}
371+
372+
func TestHWConfigUSBValidation_USB3Only(t *testing.T) {
373+
c := new(HWConfig)
374+
c.NetworkAdapterType = "vmxnet3"
375+
c.USB = true
376+
c.USBVersion = UsbVersion31
377+
378+
if errs := c.Prepare(interpolate.NewContext()); len(errs) > 0 {
379+
t.Fatalf("err: %#v", errs)
380+
}
381+
382+
if !c.USB {
383+
t.Errorf("USB should be enabled: %t", c.USB)
384+
}
385+
386+
if c.USBVersion != UsbVersion31 {
387+
t.Errorf("USB version should be 3.1: %s", c.USBVersion)
388+
}
389+
}
390+
391+
func TestHWConfigUSBValidation_USBVersionRequired(t *testing.T) {
392+
c := new(HWConfig)
393+
c.NetworkAdapterType = "vmxnet3"
394+
c.USB = true
395+
396+
errs := c.Prepare(interpolate.NewContext())
397+
if len(errs) == 0 {
398+
t.Fatal("expected validation error when USB is enabled but usb_version is not specified")
399+
}
400+
401+
expectedError := "usb_version is required when usb is enabled; must be one of [2.0 3.1]"
402+
found := false
403+
for _, err := range errs {
404+
if err.Error() == expectedError {
405+
found = true
406+
break
407+
}
408+
}
409+
if !found {
410+
t.Errorf("expected error message not found. Got errors: %v", errs)
411+
}
412+
}
413+
414+
func TestHWConfigUSBValidation_USBDisabled(t *testing.T) {
415+
c := new(HWConfig)
416+
c.NetworkAdapterType = "vmxnet3"
417+
418+
if errs := c.Prepare(interpolate.NewContext()); len(errs) > 0 {
419+
t.Fatalf("err: %#v", errs)
420+
}
421+
422+
if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
423+
if !c.USB {
424+
t.Errorf("USB should be automatically enabled on Apple Silicon: %t", c.USB)
425+
}
426+
if c.USBVersion != UsbVersion31 {
427+
t.Errorf("USB version should be automatically set to 3.1 on Apple Silicon: %s", c.USBVersion)
428+
}
429+
} else {
430+
if c.USB {
431+
t.Errorf("USB should be disabled by default: %t", c.USB)
432+
}
433+
if c.USBVersion != "" {
434+
t.Errorf("USB version should not be set when USB is disabled: %s", c.USBVersion)
435+
}
436+
}
437+
}
438+
439+
func TestHWConfigUSBValidation_InvalidVersion(t *testing.T) {
440+
c := new(HWConfig)
441+
c.NetworkAdapterType = "vmxnet3"
442+
c.USB = true
443+
c.USBVersion = "1.1" // Invalid version.
444+
445+
errs := c.Prepare(interpolate.NewContext())
446+
if len(errs) == 0 {
447+
t.Fatal("expected validation error for invalid USB version")
448+
}
449+
450+
expectedError := "usb_version must be one of [2.0 3.1], got '1.1'"
451+
found := false
452+
for _, err := range errs {
453+
if err.Error() == expectedError {
454+
found = true
455+
break
456+
}
457+
}
458+
if !found {
459+
t.Errorf("expected error message not found. Got errors: %v", errs)
460+
}
461+
}
462+
463+
func TestHWConfigUSBValidation_VersionWithoutUSB(t *testing.T) {
464+
c := new(HWConfig)
465+
c.NetworkAdapterType = "vmxnet3"
466+
c.USB = false
467+
c.USBVersion = UsbVersion31 // Set the version, but disabled.
468+
469+
errs := c.Prepare(interpolate.NewContext())
470+
if len(errs) == 0 {
471+
t.Fatal("expected validation error when USB version is set but USB is disabled")
472+
}
473+
474+
expectedError := "usb_version can only be set when usb is enabled"
475+
found := false
476+
for _, err := range errs {
477+
if err.Error() == expectedError {
478+
found = true
479+
break
480+
}
481+
}
482+
if !found {
483+
t.Errorf("expected error message not found. Got errors: %v", errs)
484+
}
485+
}

builder/vmware/iso/config.hcl2spec.go

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

builder/vmware/iso/step_create_vmx.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ type vmxTemplateData struct {
4040

4141
SoundPresent string
4242
UsbPresent string
43+
UsbVersion string
4344

4445
SerialPresent string
4546
SerialType string
@@ -175,6 +176,7 @@ func (s *stepCreateVMX) Run(ctx context.Context, state multistep.StateBag) multi
175176

176177
SoundPresent: map[bool]string{true: "TRUE", false: "FALSE"}[config.Sound],
177178
UsbPresent: map[bool]string{true: "TRUE", false: "FALSE"}[config.USB],
179+
UsbVersion: config.USBVersion,
178180

179181
SerialPresent: "FALSE",
180182
ParallelPresent: "FALSE",
@@ -463,9 +465,6 @@ pciBridge7.pciSlotNumber = "24"
463465
pciBridge7.present = "TRUE"
464466
pciBridge7.virtualDev = "pcieRootPort"
465467
466-
ehci.present = "TRUE"
467-
ehci.pciSlotNumber = "34"
468-
469468
vmci0.present = "TRUE"
470469
vmci0.id = "1861462627"
471470
vmci0.pciSlotNumber = "35"
@@ -505,9 +504,10 @@ sound.present = "{{ .SoundPresent }}"
505504
sound.fileName = "-1"
506505
sound.autodetect = "TRUE"
507506
508-
// USB
509-
usb.pciSlotNumber = "32"
510-
usb.present = "{{ .UsbPresent }}"
507+
// USB Controllers
508+
{{ if .UsbPresent }}usb.present = "{{ .UsbPresent }}"{{ end }}
509+
{{ if .UsbPresent }}ehci.present = "{{ .UsbPresent }}"{{ end }}
510+
{{ if and .UsbPresent (eq .UsbVersion "3.1") }}usb_xhci.present = "{{ .UsbPresent }}"{{ end }}
511511
512512
// Serial
513513
serial0.present = "{{ .SerialPresent }}"

0 commit comments

Comments
 (0)