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..58882eee2 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,157 @@ func resourceVSphereVirtualMachineUpdate(d *schema.ResourceData, meta interface{ if spec.DeviceChange, err = applyVirtualDevices(d, client, devices); err != nil { return err } + + // Check for changes and add/remove USB controllers if necessary + if d.HasChange("usb_controller") { + old, new := d.GetChange("usb_controller") + oldUSB := old.([]interface{}) + newUSB := new.([]interface{}) + + // Initialize a key counter + keyCounter := -100 + + // Initialize controller presence flags + usb2ControllerPresent := false + usb3ControllerPresent := false + + // Map to store existing USB controllers and their keys + existingUSBControllers := make(map[string]int32) + + // Check for existing USB controllers and store their keys + for _, dev := range vprops.Config.Hardware.Device { + switch controller := dev.(type) { + case *types.VirtualUSBController: + usb2ControllerPresent = true + existingUSBControllers["2.0"] = controller.Key + case *types.VirtualUSBXHCIController: + usb3ControllerPresent = true + existingUSBControllers["3.x"] = controller.Key + } + } + + // Remove USB controllers that are no longer in the configuration + for _, oldUSBControllerInterface := range oldUSB { + oldUSBController := oldUSBControllerInterface.(map[string]interface{}) + oldUSBVersion := oldUSBController["usb_version"].(string) + + found := false + for _, newUSBControllerInterface := range newUSB { + newUSBController := newUSBControllerInterface.(map[string]interface{}) + newUSBVersion := newUSBController["usb_version"].(string) + if oldUSBVersion == newUSBVersion { + found = true + break + } + } + + if !found { + var device types.BaseVirtualDevice + switch oldUSBVersion { + case "2.0": + if key, ok := existingUSBControllers["2.0"]; ok { + device = &types.VirtualUSBController{ + VirtualController: types.VirtualController{ + VirtualDevice: types.VirtualDevice{ + Key: key, + }, + }, + } + } + case "3.1", "3.2": + if key, ok := existingUSBControllers["3.x"]; ok { + device = &types.VirtualUSBXHCIController{ + VirtualController: types.VirtualController{ + VirtualDevice: types.VirtualDevice{ + Key: key, + }, + }, + } + } + default: + return fmt.Errorf("unsupported USB version: %s", oldUSBVersion) + } + + if device != nil { + // Power off the VM before removing the USB controller + if vprops.Runtime.PowerState != types.VirtualMachinePowerStatePoweredOff { + log.Printf("[DEBUG] Powering off VM to remove USB controller: %s", oldUSBVersion) + timeout := d.Get("shutdown_wait_timeout").(int) + force := d.Get("force_power_off").(bool) + if err := virtualmachine.GracefulPowerOff(client, vm, timeout, force); err != nil { + return fmt.Errorf("error powering off virtual machine: %s", err) + } + } + + spec.DeviceChange = append(spec.DeviceChange, &types.VirtualDeviceConfigSpec{ + Operation: types.VirtualDeviceConfigSpecOperationRemove, + Device: device, + }) + log.Printf("[DEBUG] Removed USB controller: %s", oldUSBVersion) + } + } + } + + // Add new USB controllers + for _, usbControllerInterface := range newUSB { + usbController := usbControllerInterface.(map[string]interface{}) + usbVersion := usbController["usb_version"].(string) + + var ehciEnabled *bool + var device types.BaseVirtualDevice + + switch usbVersion { + case "2.0": + if usb2ControllerPresent { + log.Printf("[DEBUG] USB 2.0 controller already exists, skipping addition") + continue + } + 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 { + log.Printf("[DEBUG] USB 3.x controller already exists, skipping addition") + continue + } + 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 controller: %s", usbVersion) + keyCounter-- + } + + // Power on the VM after making changes + if vprops.Runtime.PowerState != types.VirtualMachinePowerStatePoweredOn { + log.Printf("[DEBUG] Powering on VM after USB controller changes") + if err := virtualmachine.PowerOn(vm, timeout); err != nil { + return fmt.Errorf("error powering on virtual machine: %s", err) + } + } + } + // 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) @@ -1042,6 +1230,8 @@ func resourceVSphereVirtualMachineCustomizeDiff(_ context.Context, d *schema.Res _ = d.ForceNew(k) } } + + return nil } // Validate hardware version changes. @@ -1058,6 +1248,11 @@ func resourceVSphereVirtualMachineCustomizeDiff(_ context.Context, d *schema.Res return err } + // Call the USB controller diff customization function + if err := resourceVSphereVirtualMachineCustomizeDiffUSBController(d, meta); err != nil { + return err + } + log.Printf("[DEBUG] %s: Diff customization and validation complete", resourceVSphereVirtualMachineIDString(d)) return nil } @@ -1366,6 +1561,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 +2269,126 @@ 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 +} + +func resourceVSphereVirtualMachineCustomizeDiffUSBController(d *schema.ResourceDiff, meta interface{}) error { + client := meta.(*Client).vimClient + + // Check if the VM exists + vm, err := virtualmachine.FromUUID(client, d.Id()) + if err != nil { + if strings.Contains(err.Error(), "not found") { + log.Printf("[DEBUG] VM with UUID %q does not exist, skipping existing USB controller check", d.Id()) + vm = nil + } else { + return fmt.Errorf("error locating virtual machine with UUID %q: %s", d.Id(), err) + } + } + + var existingUSB2Controller, existingUSB3Controller bool + + if vm != nil { + vprops, err := virtualmachine.Properties(vm) + if err != nil { + return fmt.Errorf("error fetching VM properties: %s", err) + } + + // Check for existing USB controllers + for _, dev := range vprops.Config.Hardware.Device { + switch dev.(type) { + case *types.VirtualUSBController: + existingUSB2Controller = true + case *types.VirtualUSBXHCIController: + existingUSB3Controller = true + } + } + } + + if d.HasChange("usb_controller") { + usb := d.Get("usb_controller").([]interface{}) + if len(usb) == 0 { + return fmt.Errorf("usb_controller is empty") + } + + // Initialize controller presence flags + usb2ControllerPresent := existingUSB2Controller + usb3ControllerPresent := existingUSB3Controller + + for _, usbControllerInterface := range usb { + usbController := usbControllerInterface.(map[string]interface{}) + usbVersion := usbController["usb_version"].(string) + + switch usbVersion { + case "2.0": + if usb2ControllerPresent { + log.Printf("[DEBUG] USB 2.0 controller already exists, skipping addition") + continue + } + usb2ControllerPresent = true + case "3.1", "3.2": + if usb3ControllerPresent { + log.Printf("[DEBUG] USB 3.x controller already exists, skipping addition") + continue + } + usb3ControllerPresent = true + default: + return fmt.Errorf("unsupported USB version: %s", usbVersion) + } + } + } + return 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..882256988 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 for vsphere 5.x to 7.x. Supported versions include 2.0 or 3.2 for vsphere 8.x. This setting is only available on new builds, reconfiguration to add a USB controller and removal of usb controller. For any reconfiguration option, the machine will power off and back on; + ## 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.