diff --git a/.changes/v2.20.0/564-features.md b/.changes/v2.20.0/564-features.md new file mode 100644 index 000000000..90ba6cf04 --- /dev/null +++ b/.changes/v2.20.0/564-features.md @@ -0,0 +1,4 @@ +* Add support for importable Distributed Virtual Port Group (DVPG) read via types + `VcenterImportableDvpg` and `types.VcenterImportableDvpg` and methods + `VCDClient.GetVcenterImportableDvpgByName`, `VCDClient.GetAllVcenterImportableDvpgs`, + `Vdc.GetVcenterImportableDvpgByName`, `Vdc.GetAllVcenterImportableDvpgs` [GH-564] diff --git a/govcd/api_vcd_test.go b/govcd/api_vcd_test.go index df323ea61..3f105d119 100644 --- a/govcd/api_vcd_test.go +++ b/govcd/api_vcd_test.go @@ -181,6 +181,7 @@ type TestConfig struct { Manager string `yaml:"manager"` Tier0router string `yaml:"tier0router"` Tier0routerVrf string `yaml:"tier0routerVrf"` + NsxtDvpg string `yaml:"nsxtDvpg"` GatewayQosProfile string `yaml:"gatewayQosProfile"` Vdc string `yaml:"vdc"` ExternalNetwork string `yaml:"externalNetwork"` diff --git a/govcd/generic_functions.go b/govcd/generic_functions.go new file mode 100644 index 000000000..16f2b8c79 --- /dev/null +++ b/govcd/generic_functions.go @@ -0,0 +1,38 @@ +package govcd + +import "fmt" + +// oneOrError is used to cover up a common pattern in this codebase which is usually used in +// GetXByName functions. +// API endpoint returns N elements for an object we are looking (most commonly because API does not +// support filtering) and final filtering by Name must be done in code. +// After filtering returned entities one must be sure that exactly one was found and handle 3 cases: +// * If 0 entities are found - an error containing ErrorEntityNotFound must be returned +// * If >1 entities are found - an error containing the number of entities must be returned +// * If 1 entity was found - return it +// +// An example of code that was previously handled in non generic way - we had a lot of these +// occurrences throughout the code: +// +// if len(nsxtEdgeClusters) == 0 { +// // ErrorEntityNotFound is injected here for the ability to validate problem using ContainsNotFound() +// return nil, fmt.Errorf("%s: no NSX-T Tier-0 Edge Cluster with name '%s' for Org VDC with id '%s' found", +// ErrorEntityNotFound, name, vdc.Vdc.ID) +// } + +// if len(nsxtEdgeClusters) > 1 { +// return nil, fmt.Errorf("more than one (%d) NSX-T Edge Cluster with name '%s' for Org VDC with id '%s' found", +// len(nsxtEdgeClusters), name, vdc.Vdc.ID) +// } +func oneOrError[T any](key, name string, entitySlice []*T) (*T, error) { + if len(entitySlice) > 1 { + return nil, fmt.Errorf("got more than one entity by %s '%s' %d", key, name, len(entitySlice)) + } + + if len(entitySlice) == 0 { + // No entity found - returning ErrorEntityNotFound as it must be wrapped in the returned error + return nil, fmt.Errorf("%s: got zero entities by %s '%s'", ErrorEntityNotFound, key, name) + } + + return entitySlice[0], nil +} diff --git a/govcd/generic_functions_unit_test.go b/govcd/generic_functions_unit_test.go new file mode 100644 index 000000000..fc27dc7fb --- /dev/null +++ b/govcd/generic_functions_unit_test.go @@ -0,0 +1,102 @@ +//go:build unit || ALL + +/* +* Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "reflect" + "testing" +) + +func Test_oneOrError(t *testing.T) { + type args struct { + key string + name string + entitySlice []*testEntity + } + tests := []struct { + name string + args args + want *testEntity + wantErr bool + wantErrEntityNotFound bool + }{ + { + name: "SingleEntity", + args: args{ + key: "name", + name: "test", + entitySlice: []*testEntity{{name: "test"}}, + }, + want: &testEntity{name: "test"}, + wantErr: false, + }, + { + name: "NoEntities", + args: args{ + key: "name", + name: "test", + entitySlice: []*testEntity{}, + }, + want: nil, + wantErr: true, + wantErrEntityNotFound: true, + }, + { + name: "TwoEntities", + args: args{ + key: "name", + name: "test", + entitySlice: []*testEntity{{name: "test"}, {name: "best"}}, + }, + want: nil, + wantErr: true, + }, + { + name: "ThreeEntities", + args: args{ + key: "name", + name: "test", + entitySlice: []*testEntity{{name: "test"}, {name: "best"}, {name: "rest"}}, + }, + want: nil, + wantErr: true, + }, + { + name: "NilEntities", + args: args{ + key: "name", + name: "test", + entitySlice: nil, + }, + want: nil, + wantErr: true, + wantErrEntityNotFound: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := oneOrError(tt.args.key, tt.args.name, tt.args.entitySlice) + if (err != nil) != tt.wantErr { + t.Errorf("oneOrError() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if tt.wantErr && tt.wantErrEntityNotFound && !ContainsNotFound(err) { + t.Errorf("oneOrError() error = %v, wantErrEntityNotFound %v", err, tt.wantErrEntityNotFound) + return + } + + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("oneOrError() = %v, want %v", got, tt.want) + } + }) + } +} + +type testEntity struct { + name string +} diff --git a/govcd/importable_dvpg.go b/govcd/importable_dvpg.go new file mode 100644 index 000000000..9dd04cc18 --- /dev/null +++ b/govcd/importable_dvpg.go @@ -0,0 +1,122 @@ +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "net/url" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +// VcenterImportableDvpg is a read only structure that allows to get information about a Distributed +// Virtual Port Group (DVPG) network backing that is available for import. +// +// Note. API returns only unused DVPGs. If the DVPG is already consumed - it will not be returned. +type VcenterImportableDvpg struct { + VcenterImportableDvpg *types.VcenterImportableDvpg + client *Client +} + +// GetVcenterImportableDvpgByName retrieves a DVPG by name +// +// Note. API returns only unused DVPGs. If the DVPG is already consumed - it will not be returned. +func (vcdClient *VCDClient) GetVcenterImportableDvpgByName(name string) (*VcenterImportableDvpg, error) { + if name == "" { + return nil, fmt.Errorf("empty importable Distributed Virtual Port Group Name specified") + } + + vcImportableDvpgs, err := vcdClient.GetAllVcenterImportableDvpgs(nil) + if err != nil { + return nil, fmt.Errorf("could not find Distributed Virtual Port Group with Name '%s' for vCenter with ID '%s': %s", + name, "", err) + } + + filteredVcImportableDvpgs := filterVcImportableDvpgsByName(name, vcImportableDvpgs) + + return oneOrError("name", name, filteredVcImportableDvpgs) +} + +// GetAllVcenterImportableDvpgs retrieves all DVPGs that are available for import. +// +// Note. API returns only unused DVPGs. If the DVPG is already consumed - it will not be returned. +func (vcdClient *VCDClient) GetAllVcenterImportableDvpgs(queryParameters url.Values) ([]*VcenterImportableDvpg, error) { + return getAllVcenterImportableDvpgs(&vcdClient.Client, queryParameters) +} + +// GetVcenterImportableDvpgByName retrieves a DVPG that is available for import within the Org VDC. +func (vdc *Vdc) GetVcenterImportableDvpgByName(name string) (*VcenterImportableDvpg, error) { + if name == "" { + return nil, fmt.Errorf("empty importable Distributed Virtual Port Group Name specified") + } + + vcImportableDvpgs, err := vdc.GetAllVcenterImportableDvpgs(nil) + if err != nil { + return nil, fmt.Errorf("could not find Distributed Virtual Port Group with name '%s': %s", name, err) + } + + filteredVcImportableDvpgs := filterVcImportableDvpgsByName(name, vcImportableDvpgs) + + return oneOrError("name", name, filteredVcImportableDvpgs) +} + +// GetAllVcenterImportableDvpgs retrieves all DVPGs that are available for import within the Org VDC. +// +// Note. API returns only unused DVPGs. If the DVPG is already consumed - it will not be returned. +func (vdc *Vdc) GetAllVcenterImportableDvpgs(queryParameters url.Values) ([]*VcenterImportableDvpg, error) { + if vdc == nil || vdc.Vdc == nil || vdc.Vdc.ID == "" { + return nil, fmt.Errorf("cannot get Importable DVPGs without VDC ID") + } + + queryParams := copyOrNewUrlValues(queryParameters) + queryParams = queryParameterFilterAnd("orgVdcId=="+vdc.Vdc.ID, queryParams) + + return getAllVcenterImportableDvpgs(vdc.client, queryParams) + +} + +func getAllVcenterImportableDvpgs(client *Client, queryParameters url.Values) ([]*VcenterImportableDvpg, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointImportableDvpgs + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + queryParams := copyOrNewUrlValues(queryParameters) + + typeResponses := []*types.VcenterImportableDvpg{{}} + err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParams, &typeResponses, nil) + if err != nil { + return nil, err + } + + returnObjects := make([]*VcenterImportableDvpg, len(typeResponses)) + for sliceIndex := range typeResponses { + returnObjects[sliceIndex] = &VcenterImportableDvpg{ + VcenterImportableDvpg: typeResponses[sliceIndex], + client: client, + } + } + + return returnObjects, nil +} + +// filterVcImportableDvpgsByName is created as a fix for local filtering instead of using +// FIQL filter (because it does not support it). +func filterVcImportableDvpgsByName(name string, allNVcImportableDvpgs []*VcenterImportableDvpg) []*VcenterImportableDvpg { + filteredVcImportableDvpgs := make([]*VcenterImportableDvpg, 0) + for _, VcImportableDvpg := range allNVcImportableDvpgs { + if VcImportableDvpg.VcenterImportableDvpg.BackingRef.Name == name { + filteredVcImportableDvpgs = append(filteredVcImportableDvpgs, VcImportableDvpg) + } + } + + return filteredVcImportableDvpgs +} diff --git a/govcd/importable_dvpg_test.go b/govcd/importable_dvpg_test.go new file mode 100644 index 000000000..0adb0bdef --- /dev/null +++ b/govcd/importable_dvpg_test.go @@ -0,0 +1,50 @@ +//go:build network || nsxt || functional || openapi || ALL + +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_VcenterImportableDvpg(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + skipNoNsxtConfiguration(vcd, check) + + if vcd.config.VCD.Nsxt.NsxtDvpg == "" { + check.Skip("No NSX-T Dvpg provided") + } + + // Get all DVPGs + dvpgs, err := vcd.client.GetAllVcenterImportableDvpgs(nil) + check.Assert(err, IsNil) + check.Assert(len(dvpgs) > 0, Equals, true) + + // Get DVPG by name + dvpgByName, err := vcd.client.GetVcenterImportableDvpgByName(vcd.config.VCD.Nsxt.NsxtDvpg) + check.Assert(err, IsNil) + check.Assert(dvpgByName, NotNil) + check.Assert(dvpgByName.VcenterImportableDvpg.BackingRef.Name, Equals, vcd.config.VCD.Nsxt.NsxtDvpg) + + // Get all DVPGs withing NSX-T VDC + nsxtVdc, err := vcd.org.GetVDCByName(vcd.config.VCD.Nsxt.Vdc, false) + check.Assert(err, IsNil) + check.Assert(nsxtVdc, NotNil) + + allDvpgsWithingVdc, err := nsxtVdc.GetAllVcenterImportableDvpgs(nil) + check.Assert(err, IsNil) + check.Assert(len(allDvpgsWithingVdc) > 0, Equals, true) + + // Get DVPG by name within NSX-T VDC + dvpgByNameWithinVdc, err := nsxtVdc.GetVcenterImportableDvpgByName(vcd.config.VCD.Nsxt.NsxtDvpg) + check.Assert(err, IsNil) + check.Assert(dvpgByNameWithinVdc, NotNil) + check.Assert(dvpgByNameWithinVdc.VcenterImportableDvpg.BackingRef.Name, Equals, vcd.config.VCD.Nsxt.NsxtDvpg) +} diff --git a/govcd/openapi_endpoints.go b/govcd/openapi_endpoints.go index d6612d1cc..b8a6b0b12 100644 --- a/govcd/openapi_endpoints.go +++ b/govcd/openapi_endpoints.go @@ -26,6 +26,7 @@ var endpointMinApiVersions = map[string]string{ types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRoles + types.OpenApiEndpointRights: "31.0", types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAuditTrail: "33.0", types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointImportableTier0Routers: "32.0", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointImportableDvpgs: "36.0", types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointTestConnection: "34.0", // OpenApiEndpointExternalNetworks endpoint support was introduced with version 32.0 however it was still not stable // enough to be used. (i.e. it did not support update "PUT") diff --git a/govcd/openapi_org_network_test.go b/govcd/openapi_org_network_test.go index ccfba37e5..43b597494 100644 --- a/govcd/openapi_org_network_test.go +++ b/govcd/openapi_org_network_test.go @@ -112,7 +112,7 @@ func (vcd *TestVCD) Test_NsxtOrgVdcNetworkRouted(check *C) { runOpenApiOrgVdcNetworkWithVdcGroupTest(check, vcd, orgVdcNetworkConfig, types.OrgVdcNetworkTypeRouted, []dhcpConfigFunc{nsxtRoutedDhcpConfigEdgeMode, nsxtDhcpConfigNetworkMode}) } -func (vcd *TestVCD) Test_NsxtOrgVdcNetworkImported(check *C) { +func (vcd *TestVCD) Test_NsxtOrgVdcNetworkImportedNsxtLogicalSwitch(check *C) { if vcd.skipAdminTests { check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) } @@ -165,6 +165,59 @@ func (vcd *TestVCD) Test_NsxtOrgVdcNetworkImported(check *C) { runOpenApiOrgVdcNetworkWithVdcGroupTest(check, vcd, orgVdcNetworkConfig, types.OrgVdcNetworkTypeOpaque, nil) } +func (vcd *TestVCD) Test_NsxtOrgVdcNetworkImportedDistributedVirtualPortGroup(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointOrgVdcNetworks) + skipNoNsxtConfiguration(vcd, check) + + if vcd.config.VCD.Nsxt.NsxtDvpg == "" { + check.Skip("Distributed Virtual Port Group was not provided") + } + + dvpg, err := vcd.nsxtVdc.GetVcenterImportableDvpgByName(vcd.config.VCD.Nsxt.NsxtDvpg) + check.Assert(err, IsNil) + + orgVdcNetworkConfig := &types.OpenApiOrgVdcNetwork{ + Name: check.TestName(), + Description: check.TestName() + "-description", + + OwnerRef: &types.OpenApiReference{ID: vcd.nsxtVdc.Vdc.ID}, + + NetworkType: types.OrgVdcNetworkTypeOpaque, + // BackingNetworkId contains Distributed Virtual Port Group ID for Imported networks + BackingNetworkId: dvpg.VcenterImportableDvpg.BackingRef.ID, + BackingNetworkType: types.OrgVdcNetworkBackingTypeDvPortgroup, + + Subnets: types.OrgVdcNetworkSubnets{ + Values: []types.OrgVdcNetworkSubnetValues{ + { + Gateway: "2.1.1.1", + PrefixLength: 24, + DNSServer1: "8.8.8.8", + DNSServer2: "8.8.4.4", + DNSSuffix: "foo.bar", + IPRanges: types.OrgVdcNetworkSubnetIPRanges{ + Values: []types.OrgVdcNetworkSubnetIPRangeValues{ + { + StartAddress: "2.1.1.20", + EndAddress: "2.1.1.30", + }, + { + StartAddress: "2.1.1.40", + EndAddress: "2.1.1.50", + }, + }}, + }, + }, + }, + } + + // Org VDC network backed by Distributed Virtual Port Group can only be created in VDC (not VDC Group) + runOpenApiOrgVdcNetworkTest(check, vcd, vcd.nsxtVdc, orgVdcNetworkConfig, types.OrgVdcNetworkTypeOpaque, nil) +} + func (vcd *TestVCD) Test_NsxvOrgVdcNetworkIsolated(check *C) { skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointOrgVdcNetworks) diff --git a/govcd/sample_govcd_test_config.yaml b/govcd/sample_govcd_test_config.yaml index 7d3d426f7..7aa332f08 100644 --- a/govcd/sample_govcd_test_config.yaml +++ b/govcd/sample_govcd_test_config.yaml @@ -90,6 +90,8 @@ vcd: externalNetwork: tier0-backed-external-network # Existing NSX-T based VDC vdc: nsxt-vdc-name + # Distributed Virtual Port Group in vSphere that is available for NSX-T cluster + nsxtDvpg: test-nsxt-dvpg-no-uplink # Existing NSX-T edge gateway edgeGateway: nsxt-gw-name # Existing NSX-T segment to test NSX-T Imported Org Vdc network diff --git a/types/v56/constants.go b/types/v56/constants.go index b08e5911f..b0dec9413 100644 --- a/types/v56/constants.go +++ b/types/v56/constants.go @@ -357,6 +357,7 @@ const ( OpenApiEndpointAuditTrail = "auditTrail/" OpenApiEndpointImportableTier0Routers = "nsxTResources/importableTier0Routers" OpenApiEndpointImportableSwitches = "/network/orgvdcnetworks/importableswitches" + OpenApiEndpointImportableDvpgs = "virtualCenters/resources/importableDvpgs" OpenApiEndpointEdgeClusters = "nsxTResources/edgeClusters" OpenApiEndpointQosProfiles = "nsxTResources/gatewayQoSProfiles" OpenApiEndpointExternalNetworks = "externalNetworks/" @@ -444,10 +445,19 @@ const ( OrgVdcNetworkTypeRouted = "NAT_ROUTED" // OrgVdcNetworkTypeIsolated can be used to create NSX-T or NSX-V isolated Org Vdc network OrgVdcNetworkTypeIsolated = "ISOLATED" - // OrgVdcNetworkTypeOpaque type is used to create NSX-T imported Org Vdc network - OrgVdcNetworkTypeOpaque = "OPAQUE" // OrgVdcNetworkTypeDirect can be used to create NSX-V direct Org Vdc network OrgVdcNetworkTypeDirect = "DIRECT" + // OrgVdcNetworkTypeOpaque type is used to create NSX-T imported Org Vdc network + OrgVdcNetworkTypeOpaque = "OPAQUE" +) + +const ( + // OrgVdcNetworkBackingTypeVirtualWire matches Org VDC network backing type for NSX-V + OrgVdcNetworkBackingTypeVirtualWire = "VIRTUAL_WIRE" + // OrgVdcNetworkBackingTypeNsxtFlexibleSegment matches Org VDC network backing type for NSX-T networks + OrgVdcNetworkBackingTypeNsxtFlexibleSegment = "NSXT_FLEXIBLE_SEGMENT" + // OrgVdcNetworkBackingTypeDvPortgroup matches Org VDC network backing type for NSX-T Imported network backed by DV Portgroup + OrgVdcNetworkBackingTypeDvPortgroup = "DV_PORTGROUP" ) const ( diff --git a/types/v56/nsxt_types.go b/types/v56/nsxt_types.go index 61fad9c42..719270f4a 100644 --- a/types/v56/nsxt_types.go +++ b/types/v56/nsxt_types.go @@ -169,8 +169,10 @@ type OpenApiOrgVdcNetwork struct { // backingNetworkId contains the NSX ID of the backing network. BackingNetworkId string `json:"backingNetworkId,omitempty"` - // backingNetworkType contains object type of the backing network. ('VIRTUAL_WIRE' for NSX-V, 'NSXT_FLEXIBLE_SEGMENT' - // for NSX-T) + // backingNetworkType contains object type of the backing network. + // * 'VIRTUAL_WIRE' for NSX-V' + // * 'NSXT_FLEXIBLE_SEGMENT' for NSX-T networks + // * 'DV_PORTGROUP' for NSX-T Imported network backed by DV Portgroup BackingNetworkType string `json:"backingNetworkType,omitempty"` // ParentNetwork should have external network ID specified when creating NSX-V direct network @@ -1681,3 +1683,17 @@ type NsxtEdgeGatewayQos struct { EgressProfile *OpenApiReference `json:"egressProfile"` IngressProfile *OpenApiReference `json:"ingressProfile"` } + +// VcenterImportableDvpg defines a Distributed Port Group that can be imported into VCD +// from a vCenter Server. +// +// Note. This is a read-only structure. +type VcenterImportableDvpg struct { + BackingRef *OpenApiReference `json:"backingRef"` + DvSwitch struct { + BackingRef *OpenApiReference `json:"backingRef"` + VirtualCenter *OpenApiReference `json:"virtualCenter"` + } `json:"dvSwitch"` + VirtualCenter *OpenApiReference `json:"virtualCenter"` + Vlan string `json:"vlan"` +}