Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for DVPG backed imported NSX-T Org VDC networks #564

Merged
merged 10 commits into from
Apr 13, 2023
Merged
4 changes: 4 additions & 0 deletions .changes/v2.20.0/564-features.md
Original file line number Diff line number Diff line change
@@ -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]
1 change: 1 addition & 0 deletions govcd/api_vcd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
38 changes: 38 additions & 0 deletions govcd/generic_functions.go
Original file line number Diff line number Diff line change
@@ -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
}
102 changes: 102 additions & 0 deletions govcd/generic_functions_unit_test.go
Original file line number Diff line number Diff line change
@@ -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
}
122 changes: 122 additions & 0 deletions govcd/importable_dvpg.go
Original file line number Diff line number Diff line change
@@ -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
}
50 changes: 50 additions & 0 deletions govcd/importable_dvpg_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
1 change: 1 addition & 0 deletions govcd/openapi_endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading