From da5dd220e1a91a8a3e9827d8d86a91ff396e32d2 Mon Sep 17 00:00:00 2001 From: J <95346075+GCHQDeveloper609@users.noreply.github.com> Date: Thu, 7 Nov 2024 21:42:07 +0000 Subject: [PATCH] feat: vmware_distributed_virtual_switch_pvlan_mapping resource (#2291) --- vsphere/distributed_virtual_switch_helper.go | 31 ++++ .../distributed_virtual_switch_structure.go | 2 +- vsphere/provider.go | 87 +++++------ ...urce_vsphere_distributed_virtual_switch.go | 5 + ...istributed_virtual_switch_pvlan_mapping.go | 142 ++++++++++++++++++ ...buted_virtual_switch_pvlan_mapping_test.go | 108 +++++++++++++ ...vsphere_distributed_virtual_switch_test.go | 6 +- ...source_vsphere_host_virtual_switch_test.go | 2 +- 8 files changed, 335 insertions(+), 48 deletions(-) create mode 100644 vsphere/resource_vsphere_distributed_virtual_switch_pvlan_mapping.go create mode 100644 vsphere/resource_vsphere_distributed_virtual_switch_pvlan_mapping_test.go diff --git a/vsphere/distributed_virtual_switch_helper.go b/vsphere/distributed_virtual_switch_helper.go index 3328272be..a2735260d 100644 --- a/vsphere/distributed_virtual_switch_helper.go +++ b/vsphere/distributed_virtual_switch_helper.go @@ -145,3 +145,34 @@ func enableDVSNetworkResourceManagement(client *govmomi.Client, dvs *object.Vmwa return nil } + +func updateDVSPvlanMappings(dvs *object.VmwareDistributedVirtualSwitch, pvlanConfig []types.VMwareDVSPvlanConfigSpec) error { + // Load current properties, required to get the 'config version' to provide back when updating + props, err := dvsProperties(dvs) + if err != nil { + return fmt.Errorf("cannot read properties of distributed_virtual_switch: %s", err) + } + dvsConfig := props.Config.(*types.VMwareDVSConfigInfo) + + updateSpec := types.VMwareDVSConfigSpec{ + DVSConfigSpec: types.DVSConfigSpec{ + ConfigVersion: dvsConfig.ConfigVersion, + }, + PvlanConfigSpec: pvlanConfig, + } + + // Start ReconfigureDvs_Task + ctx, cancel := context.WithTimeout(context.Background(), defaultAPITimeout) + defer cancel() + task, err := dvs.Reconfigure(ctx, &updateSpec) + if err != nil { + return fmt.Errorf("error reconfiguring DVS: %s", err) + } + + // Wait for ReconfigureDvs_Task to finish + tctx, tcancel := context.WithTimeout(context.Background(), defaultAPITimeout) + defer tcancel() + task.Wait(tctx) + + return nil +} diff --git a/vsphere/distributed_virtual_switch_structure.go b/vsphere/distributed_virtual_switch_structure.go index 27dafe931..3f848dac9 100644 --- a/vsphere/distributed_virtual_switch_structure.go +++ b/vsphere/distributed_virtual_switch_structure.go @@ -126,7 +126,7 @@ func schemaVMwareDVSConfigSpec() map[string]*schema.Schema { "ignore_other_pvlan_mappings": { Type: schema.TypeBool, Optional: true, - Description: "Whether to ignore existing PVLAN mappings not managed by this resource. Defaults to false.", + Description: "Whether to ignore existing PVLAN mappings not managed by this resource.", Default: false, }, diff --git a/vsphere/provider.go b/vsphere/provider.go index e0e6e0fc0..d2faede14 100644 --- a/vsphere/provider.go +++ b/vsphere/provider.go @@ -102,49 +102,50 @@ func Provider() *schema.Provider { }, ResourcesMap: map[string]*schema.Resource{ - "vsphere_compute_cluster": resourceVSphereComputeCluster(), - "vsphere_compute_cluster_host_group": resourceVSphereComputeClusterHostGroup(), - "vsphere_compute_cluster_vm_affinity_rule": resourceVSphereComputeClusterVMAffinityRule(), - "vsphere_compute_cluster_vm_anti_affinity_rule": resourceVSphereComputeClusterVMAntiAffinityRule(), - "vsphere_compute_cluster_vm_dependency_rule": resourceVSphereComputeClusterVMDependencyRule(), - "vsphere_compute_cluster_vm_group": resourceVSphereComputeClusterVMGroup(), - "vsphere_compute_cluster_vm_host_rule": resourceVSphereComputeClusterVMHostRule(), - "vsphere_content_library": resourceVSphereContentLibrary(), - "vsphere_content_library_item": resourceVSphereContentLibraryItem(), - "vsphere_custom_attribute": resourceVSphereCustomAttribute(), - "vsphere_datacenter": resourceVSphereDatacenter(), - "vsphere_datastore_cluster": resourceVSphereDatastoreCluster(), - "vsphere_datastore_cluster_vm_anti_affinity_rule": resourceVSphereDatastoreClusterVMAntiAffinityRule(), - "vsphere_distributed_port_group": resourceVSphereDistributedPortGroup(), - "vsphere_distributed_virtual_switch": resourceVSphereDistributedVirtualSwitch(), - "vsphere_drs_vm_override": resourceVSphereDRSVMOverride(), - "vsphere_dpm_host_override": resourceVSphereDPMHostOverride(), - "vsphere_file": resourceVSphereFile(), - "vsphere_folder": resourceVSphereFolder(), - "vsphere_ha_vm_override": resourceVSphereHAVMOverride(), - "vsphere_host_port_group": resourceVSphereHostPortGroup(), - "vsphere_host_virtual_switch": resourceVSphereHostVirtualSwitch(), - "vsphere_license": resourceVSphereLicense(), - "vsphere_offline_software_depot": resourceVsphereOfflineSoftwareDepot(), - "vsphere_resource_pool": resourceVSphereResourcePool(), - "vsphere_tag": resourceVSphereTag(), - "vsphere_tag_category": resourceVSphereTagCategory(), - "vsphere_virtual_disk": resourceVSphereVirtualDisk(), - "vsphere_virtual_machine": resourceVSphereVirtualMachine(), - "vsphere_virtual_machine_class": resourceVsphereVmClass(), - "vsphere_virtual_machine_snapshot": resourceVSphereVirtualMachineSnapshot(), - "vsphere_nas_datastore": resourceVSphereNasDatastore(), - "vsphere_storage_drs_vm_override": resourceVSphereStorageDrsVMOverride(), - "vsphere_supervisor": resourceVsphereSupervisor(), - "vsphere_vapp_container": resourceVSphereVAppContainer(), - "vsphere_vapp_entity": resourceVSphereVAppEntity(), - "vsphere_vmfs_datastore": resourceVSphereVmfsDatastore(), - "vsphere_host": resourceVsphereHost(), - "vsphere_vnic": resourceVsphereNic(), - "vsphere_vm_storage_policy": resourceVMStoragePolicy(), - "vsphere_role": resourceVsphereRole(), - "vsphere_entity_permissions": resourceVsphereEntityPermissions(), - "vsphere_guest_os_customization": resourceVSphereGuestOsCustomization(), + "vsphere_compute_cluster": resourceVSphereComputeCluster(), + "vsphere_compute_cluster_host_group": resourceVSphereComputeClusterHostGroup(), + "vsphere_compute_cluster_vm_affinity_rule": resourceVSphereComputeClusterVMAffinityRule(), + "vsphere_compute_cluster_vm_anti_affinity_rule": resourceVSphereComputeClusterVMAntiAffinityRule(), + "vsphere_compute_cluster_vm_dependency_rule": resourceVSphereComputeClusterVMDependencyRule(), + "vsphere_compute_cluster_vm_group": resourceVSphereComputeClusterVMGroup(), + "vsphere_compute_cluster_vm_host_rule": resourceVSphereComputeClusterVMHostRule(), + "vsphere_content_library": resourceVSphereContentLibrary(), + "vsphere_content_library_item": resourceVSphereContentLibraryItem(), + "vsphere_custom_attribute": resourceVSphereCustomAttribute(), + "vsphere_datacenter": resourceVSphereDatacenter(), + "vsphere_datastore_cluster": resourceVSphereDatastoreCluster(), + "vsphere_datastore_cluster_vm_anti_affinity_rule": resourceVSphereDatastoreClusterVMAntiAffinityRule(), + "vsphere_distributed_port_group": resourceVSphereDistributedPortGroup(), + "vsphere_distributed_virtual_switch": resourceVSphereDistributedVirtualSwitch(), + "vsphere_distributed_virtual_switch_pvlan_mapping": resourceVSphereDistributedVirtualSwitchPvlanMapping(), + "vsphere_drs_vm_override": resourceVSphereDRSVMOverride(), + "vsphere_dpm_host_override": resourceVSphereDPMHostOverride(), + "vsphere_file": resourceVSphereFile(), + "vsphere_folder": resourceVSphereFolder(), + "vsphere_ha_vm_override": resourceVSphereHAVMOverride(), + "vsphere_host_port_group": resourceVSphereHostPortGroup(), + "vsphere_host_virtual_switch": resourceVSphereHostVirtualSwitch(), + "vsphere_license": resourceVSphereLicense(), + "vsphere_offline_software_depot": resourceVsphereOfflineSoftwareDepot(), + "vsphere_resource_pool": resourceVSphereResourcePool(), + "vsphere_tag": resourceVSphereTag(), + "vsphere_tag_category": resourceVSphereTagCategory(), + "vsphere_virtual_disk": resourceVSphereVirtualDisk(), + "vsphere_virtual_machine": resourceVSphereVirtualMachine(), + "vsphere_virtual_machine_class": resourceVsphereVmClass(), + "vsphere_virtual_machine_snapshot": resourceVSphereVirtualMachineSnapshot(), + "vsphere_nas_datastore": resourceVSphereNasDatastore(), + "vsphere_storage_drs_vm_override": resourceVSphereStorageDrsVMOverride(), + "vsphere_supervisor": resourceVsphereSupervisor(), + "vsphere_vapp_container": resourceVSphereVAppContainer(), + "vsphere_vapp_entity": resourceVSphereVAppEntity(), + "vsphere_vmfs_datastore": resourceVSphereVmfsDatastore(), + "vsphere_host": resourceVsphereHost(), + "vsphere_vnic": resourceVsphereNic(), + "vsphere_vm_storage_policy": resourceVMStoragePolicy(), + "vsphere_role": resourceVsphereRole(), + "vsphere_entity_permissions": resourceVsphereEntityPermissions(), + "vsphere_guest_os_customization": resourceVSphereGuestOsCustomization(), }, DataSourcesMap: map[string]*schema.Resource{ diff --git a/vsphere/resource_vsphere_distributed_virtual_switch.go b/vsphere/resource_vsphere_distributed_virtual_switch.go index ed6e37e97..d24551155 100644 --- a/vsphere/resource_vsphere_distributed_virtual_switch.go +++ b/vsphere/resource_vsphere_distributed_virtual_switch.go @@ -132,6 +132,7 @@ func resourceVSphereDistributedVirtualSwitchRead(d *schema.ResourceData, meta in if err := viapi.ValidateVirtualCenter(client); err != nil { return err } + id := d.Id() dvs, err := dvsFromUUID(client, id) if err != nil { @@ -195,6 +196,8 @@ func resourceVSphereDistributedVirtualSwitchUpdate(d *schema.ResourceData, meta return err } + vsphereDistributedVirtualSwitchModificationMutex.Lock() + id := d.Id() dvs, err := dvsFromUUID(client, id) if err != nil { @@ -256,6 +259,8 @@ func resourceVSphereDistributedVirtualSwitchUpdate(d *schema.ResourceData, meta } } + vsphereDistributedVirtualSwitchModificationMutex.Unlock() + return resourceVSphereDistributedVirtualSwitchRead(d, meta) } diff --git a/vsphere/resource_vsphere_distributed_virtual_switch_pvlan_mapping.go b/vsphere/resource_vsphere_distributed_virtual_switch_pvlan_mapping.go new file mode 100644 index 000000000..3fb0b3ce5 --- /dev/null +++ b/vsphere/resource_vsphere_distributed_virtual_switch_pvlan_mapping.go @@ -0,0 +1,142 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package vsphere + +import ( + "fmt" + "sync" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/hashicorp/terraform-provider-vsphere/vsphere/internal/helper/viapi" + "github.com/vmware/govmomi/vim25/types" +) + +var vsphereDistributedVirtualSwitchModificationMutex sync.Mutex + +func resourceVSphereDistributedVirtualSwitchPvlanMapping() *schema.Resource { + s := map[string]*schema.Schema{ + "distributed_virtual_switch_id": { + Type: schema.TypeString, + Description: "The ID of the distributed virtual switch to attach this mapping to.", + Required: true, + ForceNew: true, + }, + "primary_vlan_id": { + Type: schema.TypeInt, + Required: true, + Description: "The primary VLAN ID. The VLAN IDs of 0 and 4095 are reserved and cannot be used in this property.", + ValidateFunc: validation.IntBetween(1, 4094), + ForceNew: true, + }, + "secondary_vlan_id": { + Type: schema.TypeInt, + Required: true, + Description: "The secondary VLAN ID. The VLAN IDs of 0 and 4095 are reserved and cannot be used in this property.", + ValidateFunc: validation.IntBetween(1, 4094), + ForceNew: true, + }, + "pvlan_type": { + Type: schema.TypeString, + Required: true, + Description: "The private VLAN type. Valid values are promiscuous, community and isolated.", + ValidateFunc: validation.StringInSlice(privateVLANTypeAllowedValues, false), + ForceNew: true, + }, + } + + return &schema.Resource{ + Create: resourceVSphereDistributedVirtualSwitchPvlanMappingCreate, + Read: resourceVSphereDistributedVirtualSwitchPvlanMappingRead, + Delete: resourceVSphereDistributedVirtualSwitchPvlanMappingDelete, + Schema: s, + } +} + +func resourceVSphereDistributedVirtualSwitchPvlanMappingOperation(operation types.ConfigSpecOperation, d *schema.ResourceData, meta interface{}) error { + client := meta.(*Client).vimClient + if err := viapi.ValidateVirtualCenter(client); err != nil { + return err + } + + // vSphere only allows one modification operation at a time, so lock locally + // to avoid errors if multiple pvlan mappings are created at once. + vsphereDistributedVirtualSwitchModificationMutex.Lock() + + // Fetch the target dvswitch + dvs, err := dvsFromUUID(client, d.Get("distributed_virtual_switch_id").(string)) + if err != nil { + return fmt.Errorf("cannot locate distributed_virtual_switch: %s", err) + } + + // Perform operation + entry := types.VMwareDVSPvlanMapEntry{ + PrimaryVlanId: int32(d.Get("primary_vlan_id").(int)), + SecondaryVlanId: int32(d.Get("secondary_vlan_id").(int)), + PvlanType: d.Get("pvlan_type").(string), + } + + pvlanConfig := []types.VMwareDVSPvlanConfigSpec{ + { + Operation: string(operation), + PvlanEntry: entry, + }, + } + + err = updateDVSPvlanMappings(dvs, pvlanConfig) + if err != nil { + return fmt.Errorf("cannot reconfigure distributed virtual switch: %s", err) + } + + vsphereDistributedVirtualSwitchModificationMutex.Unlock() + return nil +} + +func resourceVSphereDistributedVirtualSwitchPvlanMappingCreate(d *schema.ResourceData, meta interface{}) error { + err := resourceVSphereDistributedVirtualSwitchPvlanMappingOperation(types.ConfigSpecOperationAdd, d, meta) + if err != nil { + return err + } + + // Try to read the mapping back, which will also generate the ID. + return resourceVSphereDistributedVirtualSwitchPvlanMappingRead(d, meta) +} + +func resourceVSphereDistributedVirtualSwitchPvlanMappingDelete(d *schema.ResourceData, meta interface{}) error { + return resourceVSphereDistributedVirtualSwitchPvlanMappingOperation(types.ConfigSpecOperationRemove, d, meta) +} + +func resourceVSphereDistributedVirtualSwitchPvlanMappingRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*Client).vimClient + if err := viapi.ValidateVirtualCenter(client); err != nil { + return err + } + + // Ensure the DVSwitch still exists + dvs, err := dvsFromUUID(client, d.Get("distributed_virtual_switch_id").(string)) + if err != nil { + return fmt.Errorf("cannot locate distributed_virtual_switch: %s", err) + } + + // Get its properties + props, err := dvsProperties(dvs) + if err != nil { + return fmt.Errorf("cannot read properties of distributed_virtual_switch: %s", err) + } + d.Set("distributed_virtual_switch_id", props.Uuid) + + // Loop through the existing mappings on the switch to try and find one matching our spec + for _, mapping := range props.Config.(*types.VMwareDVSConfigInfo).PvlanConfig { + // Check if the existing mapping matches the one specified by the resource + if mapping.PrimaryVlanId == int32(d.Get("primary_vlan_id").(int)) && mapping.SecondaryVlanId == int32(d.Get("secondary_vlan_id").(int)) && mapping.PvlanType == d.Get("pvlan_type").(string) { + d.SetId(fmt.Sprintf("dvswitch-%s-mapping-%d-%d-%s", props.Config.(*types.VMwareDVSConfigInfo).Uuid, mapping.PrimaryVlanId, mapping.SecondaryVlanId, mapping.PvlanType)) + return nil + } + } + + // If we don't find a mapping on the switch matching the current spec, then tell Terraform + // that the resource no longer exists + d.SetId("") + return nil +} diff --git a/vsphere/resource_vsphere_distributed_virtual_switch_pvlan_mapping_test.go b/vsphere/resource_vsphere_distributed_virtual_switch_pvlan_mapping_test.go new file mode 100644 index 000000000..a4f4eb7ce --- /dev/null +++ b/vsphere/resource_vsphere_distributed_virtual_switch_pvlan_mapping_test.go @@ -0,0 +1,108 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package vsphere + +import ( + "fmt" + "strconv" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-provider-vsphere/vsphere/internal/helper/testhelper" + "github.com/hashicorp/terraform-provider-vsphere/vsphere/internal/helper/viapi" + "github.com/vmware/govmomi/vim25/types" +) + +func TestAccResourceVSphereDistributedVirtualSwitchPvlanMapping_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { + RunSweepers() + testAccPreCheck(t) + testAccResourceVSphereDistributedVirtualSwitchPreCheck(t) + }, + Providers: testAccProviders, + CheckDestroy: testAccResourceVSphereDistributedVirtualSwitchPvlanMappingExists(false), + Steps: []resource.TestStep{ + { + Config: testAccResourceVSphereDistributedVirtualSwitchPvlanMappingConfig(), + Check: resource.ComposeTestCheckFunc( + testAccResourceVSphereDistributedVirtualSwitchPvlanMappingExists(true), + ), + }, + }, + }) +} + +func testAccResourceVSphereDistributedVirtualSwitchPvlanMappingConfig() string { + return fmt.Sprintf(` +%s + +resource "vsphere_distributed_virtual_switch_pvlan_mapping" "mapping" { + distributed_virtual_switch_id = vsphere_distributed_virtual_switch.dvs.id + + primary_vlan_id = 1005 + secondary_vlan_id = 1005 + pvlan_type = "promiscuous" +} + +resource "vsphere_distributed_virtual_switch" "dvs" { + name = "testacc-dvs2" + datacenter_id = "${data.vsphere_datacenter.rootdc1.id}" + ignore_other_pvlan_mappings = true + + host { + host_system_id = data.vsphere_host.roothost2.id + devices = ["%s"] + } +} +`, + testhelper.CombineConfigs(testhelper.ConfigDataRootDC1(), testhelper.ConfigDataRootHost2()), + testhelper.HostNic0, + ) +} + +func testAccResourceVSphereDistributedVirtualSwitchPvlanMappingExists(expected bool) resource.TestCheckFunc { + return func(s *terraform.State) error { + props, err := testGetDVSProperties(s, "dvs") + if err != nil { + if viapi.IsAnyNotFoundError(err) && expected == false { + // Expected missing + return nil + } + + return err + } + + mappingToSearchFor, err := testClientVariablesForResource(s, "vsphere_distributed_virtual_switch_pvlan_mapping.mapping") + if err != nil { + return fmt.Errorf("could not find pvlan mapping resource: %s", err) + } + + primary_vlan_id, err := strconv.Atoi(mappingToSearchFor.resourceAttributes["primary_vlan_id"]) + if err != nil { + return err + } + secondary_vlan_id, err := strconv.Atoi(mappingToSearchFor.resourceAttributes["secondary_vlan_id"]) + if err != nil { + return err + } + pvlan_type := mappingToSearchFor.resourceAttributes["pvlan_type"] + + for _, mapping := range props.Config.(*types.VMwareDVSConfigInfo).PvlanConfig { + if mapping.PrimaryVlanId == int32(primary_vlan_id) && mapping.SecondaryVlanId == int32(secondary_vlan_id) && mapping.PvlanType == pvlan_type { + if !expected { + return fmt.Errorf("found PVLAN mapping when not expecting to") + } + return nil + } + } + + if expected { + return fmt.Errorf("could not find PVLAN mapping") + } + + return nil + } +} diff --git a/vsphere/resource_vsphere_distributed_virtual_switch_test.go b/vsphere/resource_vsphere_distributed_virtual_switch_test.go index 31554be92..c6ae4d289 100644 --- a/vsphere/resource_vsphere_distributed_virtual_switch_test.go +++ b/vsphere/resource_vsphere_distributed_virtual_switch_test.go @@ -21,7 +21,7 @@ import ( const ( testAccResourceVSphereDistributedVirtualSwitchUpperVersion = "7.0.0" - testAccResourceVSphereDistributedVirtualSwitchLowerVersion = "6.5.0" + testAccResourceVSphereDistributedVirtualSwitchLowerVersion = "6.6.0" ) func TestAccResourceVSphereDistributedVirtualSwitch_basic(t *testing.T) { @@ -44,7 +44,7 @@ func TestAccResourceVSphereDistributedVirtualSwitch_basic(t *testing.T) { ResourceName: "vsphere_distributed_virtual_switch.dvs", ImportState: true, ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"vlan_range"}, + ImportStateVerifyIgnore: []string{"vlan_range", "ignore_other_pvlan_mappings"}, ImportStateIdFunc: func(s *terraform.State) (string, error) { dvs, err := testGetDVS(s, "dvs") if err != nil { @@ -436,7 +436,7 @@ func TestAccResourceVSphereDistributedVirtualSwitch_multiCustomAttribute(t *test func testAccResourceVSphereDistributedVirtualSwitchPreCheck(t *testing.T) { if os.Getenv("TF_VAR_VSPHERE_NFS_DS_NAME") == "" { - t.Skip("set TF_VAR_VSPHERE_ESXI_HOST to run vsphere_host_virtual_switch acceptance tests") + t.Skip("set TF_VAR_VSPHERE_NFS_DS_NAME to run vsphere_host_virtual_switch acceptance tests") } } diff --git a/vsphere/resource_vsphere_host_virtual_switch_test.go b/vsphere/resource_vsphere_host_virtual_switch_test.go index 423d017b1..6f38e0fe4 100644 --- a/vsphere/resource_vsphere_host_virtual_switch_test.go +++ b/vsphere/resource_vsphere_host_virtual_switch_test.go @@ -166,7 +166,7 @@ func TestAccResourceVSphereHostVirtualSwitch_removeAllNICs(t *testing.T) { func testAccResourceVSphereHostVirtualSwitchPreCheck(t *testing.T) { if os.Getenv("TF_VAR_VSPHERE_NFS_DS_NAME") == "" { - t.Skip("set TF_VAR_VSPHERE_ESXI_HOST to run vsphere_host_virtual_switch acceptance tests") + t.Skip("set TF_VAR_VSPHERE_NFS_DS_NAME to run vsphere_host_virtual_switch acceptance tests") } }