diff --git a/CHANGELOG.md b/CHANGELOG.md index 600308d50..826ae31b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # +## 2.10.0 (Not Released) + +FEATURES: + +- `resource/vsphere_virtual_machine`: Adds ability to add `usb_controller` to virtual machine on creation or clone. + [#2280](https://github.com/hashicorp/terraform-provider-vsphere/pull/2280) +- `data/vsphere_virtual_machine`: Adds ability read `usb_controller` on virtual machine; will return `true` or `false` based on the configuration. + [#2280](https://github.com/hashicorp/terraform-provider-vsphere/pull/2280) + ## 2.9.3 (October 8, 2024) BUG FIX: diff --git a/vsphere/data_source_vsphere_virtual_machine.go b/vsphere/data_source_vsphere_virtual_machine.go index 26db5b9b8..b4aedb983 100644 --- a/vsphere/data_source_vsphere_virtual_machine.go +++ b/vsphere/data_source_vsphere_virtual_machine.go @@ -165,6 +165,20 @@ func dataSourceVSphereVirtualMachine() *schema.Resource { Computed: true, Description: "Instance UUID of this virtual machine.", }, + "usb_controller": { + Type: schema.TypeList, + Computed: true, + Description: "List of virtual USB controllers present on the virtual machine, including their versions.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "version": { + Type: schema.TypeString, + Computed: true, + Description: "The version of the USB controller.", + }, + }, + }, + }, } // Merge the VirtualMachineConfig structure so that we can include the number of @@ -283,6 +297,26 @@ func dataSourceVSphereVirtualMachineRead(d *schema.ResourceData, meta interface{ return fmt.Errorf("error setting guest IP addresses: %s", err) } } + + var usbControllers []map[string]interface{} + + for _, dev := range props.Config.Hardware.Device { + switch dev.(type) { + case *types.VirtualUSBController: + usbControllers = append(usbControllers, map[string]interface{}{ + "version": "2.x", + }) + case *types.VirtualUSBXHCIController: + usbControllers = append(usbControllers, map[string]interface{}{ + "version": "3.x", + }) + } + } + + if err := d.Set("usb_controller", usbControllers); err != nil { + return fmt.Errorf("error setting usb_controller: %s", err) + } + log.Printf("[DEBUG] VM search for %q completed successfully (UUID %q)", name, props.Config.Uuid) return nil } diff --git a/vsphere/resource_vsphere_virtual_machine.go b/vsphere/resource_vsphere_virtual_machine.go index 2e59c5af7..20217cca7 100644 --- a/vsphere/resource_vsphere_virtual_machine.go +++ b/vsphere/resource_vsphere_virtual_machine.go @@ -74,6 +74,8 @@ https://www.terraform.io/docs/commands/taint.html const questionCheckIntervalSecs = 5 +var usbControllerVersions = []string{"2.0", "3.1", "3.2"} + func resourceVSphereVirtualMachine() *schema.Resource { s := map[string]*schema.Schema{ "resource_pool_id": { @@ -283,6 +285,23 @@ func resourceVSphereVirtualMachine() *schema.Resource { Computed: true, Description: "The power state of the virtual machine.", }, + "usb_controller": { + Type: schema.TypeList, + Optional: true, + Description: "A specification for a USB controller on the virtual machine.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "usb_version": { + Type: schema.TypeString, + Optional: true, + Default: "2.0", + Description: "The version of the USB controller.", + ValidateFunc: validation.StringInSlice(usbControllerVersions, false), + }, + }, + }, + }, + vSphereTagAttributeKey: tagsSchema(), customattribute.ConfigKey: customattribute.ConfigSchema(), } @@ -594,6 +613,24 @@ func resourceVSphereVirtualMachineRead(d *schema.ResourceData, meta interface{}) d.Set("power_state", "suspended") } + usbControllers := d.Get("usb_controller").([]interface{}) + var desiredUSBVersions []string + for _, usbController := range usbControllers { + controller := usbController.(map[string]interface{}) + desiredUSBVersions = append(desiredUSBVersions, controller["usb_version"].(string)) + } + + // Check for existing USB controllers + usbControllersState, err := readUSBControllers(vprops.Config, desiredUSBVersions) + if err != nil { + return fmt.Errorf("error reading USB controllers: %s", err) + } + + // Set the presence of USB controllers in the resource data + if err := d.Set("usb_controller", usbControllersState); err != nil { + return fmt.Errorf("error setting usb_controller: %s", err) + } + log.Printf("[DEBUG] %s: Read complete", resourceVSphereVirtualMachineIDString(d)) return nil } @@ -708,6 +745,69 @@ func resourceVSphereVirtualMachineUpdate(d *schema.ResourceData, meta interface{ if spec.DeviceChange, err = applyVirtualDevices(d, client, devices); err != nil { return err } + + // Check for changes and add new USB controllers if allowed + if d.HasChange("usb_controller") { + usb := d.Get("usb_controller").([]interface{}) + if len(usb) == 0 { + return fmt.Errorf("usb_controller is empty") + } + + // Initialize a key counter + keyCounter := -100 + + // Initialize controller presence flags + usb2ControllerPresent := false + usb3ControllerPresent := false + + for _, usbControllerInterface := range usb { + usbController := usbControllerInterface.(map[string]interface{}) + usbVersion := usbController["usb_version"].(string) + + var ehciEnabled *bool + var device types.BaseVirtualDevice + + switch usbVersion { + case "2.0": + if usb2ControllerPresent { + return fmt.Errorf("only one USB 2.0 controller can be specified") + } + usb2ControllerPresent = true + enabled := true + ehciEnabled = &enabled + device = &types.VirtualUSBController{ + VirtualController: types.VirtualController{ + VirtualDevice: types.VirtualDevice{ + Key: int32(keyCounter), + }, + }, + EhciEnabled: ehciEnabled, + } + case "3.1", "3.2": + if usb3ControllerPresent { + return fmt.Errorf("only one USB 3.x controller (3.1 or 3.2) can be specified") + } + usb3ControllerPresent = true + device = &types.VirtualUSBXHCIController{ + VirtualController: types.VirtualController{ + VirtualDevice: types.VirtualDevice{ + Key: int32(keyCounter), + }, + }, + } + + default: + return fmt.Errorf("unsupported USB version: %s", usbVersion) + } + + spec.DeviceChange = append(spec.DeviceChange, &types.VirtualDeviceConfigSpec{ + Operation: types.VirtualDeviceConfigSpecOperationAdd, + Device: device, + }) + log.Printf("[DEBUG] Added USB 3.x controller") + keyCounter-- + } + } // Only carry out the reconfigure if we actually have a change to process. cv := virtualmachine.GetHardwareVersionNumber(vprops.Config.Version) tv := d.Get("hardware_version").(int) @@ -1366,6 +1466,56 @@ func resourceVSphereVirtualMachineCreateBareStandard( VmPathName: fmt.Sprintf("[%s]", ds.Name()), } + // Add USB controller + if usb, ok := d.GetOk("usb_controller"); ok && len(usb.([]interface{})) > 0 { + var usb2ControllerSpecified bool + var usb3xControllerSpecified bool + for _, usbControllerInterface := range usb.([]interface{}) { + usbController := usbControllerInterface.(map[string]interface{}) + usbVersion := usbController["usb_version"].(string) + + var ehciEnabled *bool + var device types.BaseVirtualDevice + + switch usbVersion { + case "2.0": + if usb2ControllerSpecified { + return nil, fmt.Errorf("only one USB 2.0 controller can be specified") + } + usb2ControllerSpecified = true + enabled := true + ehciEnabled = &enabled + device = &types.VirtualUSBController{ + VirtualController: types.VirtualController{ + VirtualDevice: types.VirtualDevice{ + Key: -1, + }, + }, + EhciEnabled: ehciEnabled, + } + case "3.1", "3.2": + if usb3xControllerSpecified { + return nil, fmt.Errorf("only one USB 3.x controller (3.1 or 3.2) can be specified") + } + usb3xControllerSpecified = true + device = &types.VirtualUSBXHCIController{ + VirtualController: types.VirtualController{ + VirtualDevice: types.VirtualDevice{ + Key: -1, + }, + }, + } + default: + return nil, fmt.Errorf("unsupported USB version: %s", usbVersion) + } + + spec.DeviceChange = append(spec.DeviceChange, &types.VirtualDeviceConfigSpec{ + Operation: types.VirtualDeviceConfigSpecOperationAdd, + Device: device, + }) + } + } + timeout := meta.(*Client).timeout vm, err := virtualmachine.Create(client, fo, spec, pool, hs, timeout) if err != nil { @@ -2024,3 +2174,58 @@ func NewOvfHelperParamsFromVMResource(d *schema.ResourceData) *ovfdeploy.OvfHelp } return ovfParams } + +func readUSBControllers(vprops *types.VirtualMachineConfigInfo, desiredUSBVersions []string) ([]map[string]interface{}, error) { + var usbControllers []map[string]interface{} + var usb2ControllerPresent bool + var usb3ControllerPresent bool + + for _, dev := range vprops.Hardware.Device { + switch dev.(type) { + case *types.VirtualUSBController: + if usb2ControllerPresent { + return nil, fmt.Errorf("more than one USB 2.0 controller found") + } + usb2ControllerPresent = true + for _, version := range desiredUSBVersions { + if version == "2.0" { + usbControllers = append(usbControllers, map[string]interface{}{ + "usb_version": version, + }) + break + } + } + case *types.VirtualUSBXHCIController: + if usb3ControllerPresent { + return nil, fmt.Errorf("more than one USB 3.x controller found") + } + usb3ControllerPresent = true + // Use the specific version from desiredUSBVersions + for _, version := range desiredUSBVersions { + if version == "3.1" || version == "3.2" { + usbControllers = append(usbControllers, map[string]interface{}{ + "usb_version": version, + }) + break + } + } + default: + log.Printf("[DEBUG] Found other device type: %T", dev) + } + } + + // Use the desired USB versions specified in the main.tf + for _, version := range desiredUSBVersions { + if version == "2.0" && !usb2ControllerPresent { + usbControllers = append(usbControllers, map[string]interface{}{ + "usb_version": version, + }) + } else if (version == "3.1" || version == "3.2") && !usb3ControllerPresent { + usbControllers = append(usbControllers, map[string]interface{}{ + "usb_version": version, + }) + } + } + + return usbControllers, nil +} diff --git a/website/docs/d/virtual_machine.html.markdown b/website/docs/d/virtual_machine.html.markdown index a7faf9fa1..438962b3b 100644 --- a/website/docs/d/virtual_machine.html.markdown +++ b/website/docs/d/virtual_machine.html.markdown @@ -159,6 +159,7 @@ The following attributes are exported: the VM is powered off, this value will be blank. * `guest_ip_addresses` - A list of IP addresses as reported by VMware Tools. * `instance_uuid` - The instance UUID of the virtual machine or template. +* `usb_controller` - Indicates whether a virtual USB controller device is present on the virtual machine. ~> **NOTE:** Keep in mind when using the results of `scsi_type` and `network_interface_types`, that the `vsphere_virtual_machine` resource only diff --git a/website/docs/r/virtual_machine.html.markdown b/website/docs/r/virtual_machine.html.markdown index 7aaaee32f..7ff9bd6da 100644 --- a/website/docs/r/virtual_machine.html.markdown +++ b/website/docs/r/virtual_machine.html.markdown @@ -1518,6 +1518,25 @@ When cloning from a template, there are additional requirements in both the reso You can use the [`vsphere_virtual_machine`][tf-vsphere-virtual-machine-ds] data source, which provides disk attributes, network interface types, SCSI bus types, and the guest ID of the source template, to return this information. See the section on [cloning and customization](#cloning-and-customization) for more information. + +## USB Controller + +When creating a virtual machine or cloning one from a template, you have the option to add a virtual USB controller device. + +**Example**: + +```hcl +resource "vsphere_virtual_machine" "vm" { + # ... other configuration ... + usb_controller { + usb_version = "3.1" + } + # ... other configuration ... +} +``` + +~> **NOTE:** Supported versions include 2.0 or 3.1. This setting is only available on new builds and reconfiguration to add a USB controller; removal is not supported in the provider. + ## Virtual Machine Migration The `vsphere_virtual_machine` resource supports live migration both on the host and storage level. You can migrate the virtual machine to another host, cluster, resource pool, or datastore. You can also migrate or pin a virtual disk to a specific datastore.