Skip to content

Commit

Permalink
Add more VM class methods, rename VDC to Region Quota, add more Regio…
Browse files Browse the repository at this point in the history
…nQuotaStoragePolicy methods, fix NSX (#748)

Signed-off-by: abarreiro <[email protected]>
  • Loading branch information
adambarreiro authored Feb 12, 2025
1 parent 1db423b commit 333338d
Show file tree
Hide file tree
Showing 21 changed files with 703 additions and 471 deletions.
9 changes: 5 additions & 4 deletions .changes/v3.0.0/720-features.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
* Added types `TmVdc` and `types.TmVdc` for managing Tenant Manager Org VDCs with methods
`VCDClient.CreateTmVdc`, `VCDClient.GetAllTmVdcs`, `VCDClient.GetTmVdcByName`,
`VCDClient.GetTmVdcById`, `VCDClient.GetTmVdcByNameAndOrgId`, `TmVdc.Update`, `TmVdc.Delete`
[GH-720, GH-738]
* Added types `RegionQuota` and `types.TmVdc` for managing Tenant Manager Org VDCs with methods
`VCDClient.CreateRegionQuota`, `VCDClient.GetAllRegionQuotas`, `VCDClient.GetRegionQuotaByName`,
`VCDClient.GetRegionQuotaByNameAndOrgId`, `VCDClient.GetRegionQuotaById`, `RegionQuota.Update`, `RegionQuota.Delete`,
`VCDClient.AssignVmClassesToRegionQuota`, `RegionQuota.AssignVmClasses`,
`VCDClient.GetVmClassesFromRegionQuota` [GH-720, GH-738, GH-748]
* Added types `Zone` and `types.Zone` for reading Region Zones with methods `VCDClient.GetAllZones`,
`VCDClient.GetZoneByName`, `VCDClient.GetZoneById`, `Region.GetAllZones`, `Region.GetZoneByName`
[GH-720]
10 changes: 6 additions & 4 deletions .changes/v3.0.0/734-features.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
* Added types `TmVdcStoragePolicy`, `types.VirtualDatacenterStoragePolicies` and `types.VirtualDatacenterStoragePolicy`
to manage VDC Storage Policies, with methods `TmVdc.CreateStoragePolicies`, `VCDClient.GetAllTmVdcStoragePolicies`,
`TmVdc.GetAllStoragePolicies`, `VCDClient.GetTmVdcStoragePolicyById`,
`TmVdc.GetStoragePolicyById`, `TmVdc.Update`, `TmVdc.Delete` [GH-734]
* Added types `RegionQuotaStoragePolicy`, `types.VirtualDatacenterStoragePolicies` and `types.VirtualDatacenterStoragePolicy`
to manage VDC Storage Policies, with methods `VCDClient.CreateRegionQuotaStoragePolicies`, `RegionQuota.CreateStoragePolicies`,
`VCDClient.GetAllRegionQuotaStoragePolicies`, `RegionQuota.GetAllStoragePolicies`,
`RegionQuota.GetStoragePolicyByName`, `VCDClient.GetRegionQuotaStoragePolicyById`,
`RegionQuota.GetStoragePolicyById`, `VCDClient.UpdateRegionQuotaStoragePolicy`, `RegionQuotaStoragePolicy.Update`,
`VCDClient.DeleteRegionQuotaStoragePolicy`, `RegionQuotaStoragePolicy.Delete` [GH-734, GH-748]
3 changes: 3 additions & 0 deletions .changes/v3.0.0/748-features.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
* Added types `RegionVirtualMachineClass` to read Region VM Classes with methods
`VCDClient.GetAllRegionVirtualMachineClasses`, `VCDClient.GetRegionVirtualMachineClassByNameAndRegionId`,
`VCDClient.GetRegionVirtualMachineClassById` [GH-748]
29 changes: 17 additions & 12 deletions govcd/api_vcd.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ func init() {
requestCounter = &counter
}

var minVcdApiVersion = "37.0" // supported by 10.4+
var minApiVersion = "37.0" // supported by 10.4+
const minVcfaApiVersion = "40.0" // Minimum version for VCFA

// VCDClientOption defines signature for customizing VCDClient using
// functional options pattern.
Expand Down Expand Up @@ -140,22 +141,13 @@ func (vcdClient *VCDClient) vcdCloudApiAuthorize(user, pass, org string) (*http.
// NewVCDClient initializes VMware VMware Cloud Director client with reasonable defaults.
// It accepts functions of type VCDClientOption for adjusting defaults.
func NewVCDClient(vcdEndpoint url.URL, insecure bool, options ...VCDClientOption) *VCDClient {
userDefinedApiVersion := os.Getenv("GOVCD_API_VERSION")
if userDefinedApiVersion != "" {
_, err := semver.NewVersion(userDefinedApiVersion)
if err != nil {
// We do not have error in return of this function signature.
// To avoid breaking API the only thing we can do is panic.
panic(fmt.Sprintf("unable to initialize VCD client from environment variable GOVCD_API_VERSION. Version '%s' is not valid: %s", userDefinedApiVersion, err))
}
minVcdApiVersion = userDefinedApiVersion
}
overrideApiVersion()

// Setting defaults
// #nosec G402 -- InsecureSkipVerify: insecure - This allows connecting to VCDs with self-signed certificates
vcdClient := &VCDClient{
Client: Client{
APIVersion: minVcdApiVersion,
APIVersion: minApiVersion,
// UserAgent cannot embed exact version by default because this is source code and is supposed to be used by programs,
// but any client can customize or disable it at all using WithHttpUserAgent() configuration options function.
UserAgent: "go-vcloud-director",
Expand Down Expand Up @@ -192,6 +184,19 @@ func NewVCDClient(vcdEndpoint url.URL, insecure bool, options ...VCDClientOption
return vcdClient
}

func overrideApiVersion() {
userDefinedApiVersion := os.Getenv("GOVCD_API_VERSION")
if userDefinedApiVersion != "" {
_, err := semver.NewVersion(userDefinedApiVersion)
if err != nil {
// We do not have error in return of this function signature.
// To avoid breaking API the only thing we can do is panic.
panic(fmt.Sprintf("unable to initialize VCD client from environment variable GOVCD_API_VERSION. Version '%s' is not valid: %s", userDefinedApiVersion, err))
}
minApiVersion = userDefinedApiVersion
}
}

// Authenticate is a helper function that performs a login in VMware Cloud Director.
func (vcdClient *VCDClient) Authenticate(username, password, org string) error {
_, err := vcdClient.GetAuthResponse(username, password, org)
Expand Down
12 changes: 12 additions & 0 deletions govcd/api_vcd_versions.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ func (client *Client) vcdCheckSupportedVersion(version string) error {
// Checks if there is at least one specified version matching the list returned by vCD.
// Constraint format can be in format ">= 27.0, < 32",">= 30" ,"= 27.0".
func (client *Client) checkSupportedVersionConstraint(versionConstraint string) error {
isVcfa := false
for _, versionInfo := range client.supportedVersions.VersionInfos {
versionMatch, err := client.apiVersionMatchesConstraint(versionInfo.Version, versionConstraint)
if err != nil {
Expand All @@ -191,6 +192,17 @@ func (client *Client) checkSupportedVersionConstraint(versionConstraint string)
if versionMatch {
return nil
}

// TODO: TM: Improve this as feels odd and out of place
isVcfa, err = client.apiVersionMatchesConstraint(versionInfo.Version, fmt.Sprintf(">= %s", minVcfaApiVersion))
if err != nil {
return fmt.Errorf("cannot match VCFA version: %s", err)
}
if isVcfa {
client.APIVersion = minVcfaApiVersion
overrideApiVersion()
return nil
}
}
return fmt.Errorf("version %s is not supported", versionConstraint)
}
Expand Down
10 changes: 5 additions & 5 deletions govcd/openapi_endpoints_unit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ import (
// * Automatically generated tests where available VCD version does not satisfy it
// * Automatically generated tests to check if each elevated API version is returned for endpoints that have it defined
func TestClient_getOpenApiHighestElevatedVersion(t *testing.T) {
semverMinVcdApiVersion, err := semver.NewSemver(minVcdApiVersion)
semverMinVcdApiVersion, err := semver.NewSemver(minApiVersion)
if err != nil {
t.Fatalf("error parsing 'minVcdApiVersion': %s", err)
t.Fatalf("error parsing 'minApiVersion': %s", err)
}

type testCase struct {
Expand All @@ -30,7 +30,7 @@ func TestClient_getOpenApiHighestElevatedVersion(t *testing.T) {
wantVersion string
wantErr bool
// overrideClientMinApiVersion is an option to override default expected version that is
// defined in global variable`minVcdApiVersion`
// defined in global variable`minApiVersion`
overrideClientMinApiVersion string
}

Expand Down Expand Up @@ -64,7 +64,7 @@ func TestClient_getOpenApiHighestElevatedVersion(t *testing.T) {
name: "VCD_version_higher_than_elevated_version_entries",
supportedVersions: renderSupportedVersions([]string{"37.0", "37.1", "37.2", "37.3", "38.0", "38.1", "39.0"}),
endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointFirewallGroups,
wantVersion: minVcdApiVersion,
wantVersion: minApiVersion,
wantErr: false,
},
}
Expand Down Expand Up @@ -165,7 +165,7 @@ func TestClient_getOpenApiHighestElevatedVersion(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
client := &Client{
supportedVersions: tt.supportedVersions,
APIVersion: minVcdApiVersion,
APIVersion: minApiVersion,
}

if tt.overrideClientMinApiVersion != "" {
Expand Down
12 changes: 12 additions & 0 deletions govcd/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,18 @@ func (task *Task) Refresh() error {
return fmt.Errorf("cannot refresh, Object is empty")
}

// TODO: TM: There's a bug where VCFA returns local K8s service hostname instead of public VCFA url,
// like https://xxxxxxx.svc.cluster.local/api/task/1f344a7a-cdb0-435c-82f6-c47fb4847798
if strings.Contains(task.Task.HREF, "svc.cluster.local") {
util.Logger.Printf("[WARN] Caught task with malformed HREF: %s", task.Task.HREF)
fixedTaskUrl, err := url.ParseRequestURI(task.Task.HREF)
if err != nil {
return fmt.Errorf("error parsing task href: %s", err)
}
fixedTaskUrl.Host = task.client.VCDHREF.Host
task.Task.HREF = fixedTaskUrl.String()
}

refreshUrl := urlParseRequestURI(task.Task.HREF)

req := task.client.NewRequest(map[string]string{}, http.MethodGet, *refreshUrl, nil)
Expand Down
20 changes: 10 additions & 10 deletions govcd/tm_common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,9 +264,9 @@ func createOrg(vcd *TestVCD, check *C, canManageOrgs bool) (*TmOrg, func()) {
}
}

// Creates a VDC (Region Quota) for testing in Tenant Manager and configures it with
// Creates a Region Quota for testing in Tenant Manager and configures it with
// the first found VM class and the configured Storage Class.
func createVdc(vcd *TestVCD, org *TmOrg, region *Region, check *C) (*TmVdc, func()) {
func createRegionQuota(vcd *TestVCD, org *TmOrg, region *Region, check *C) (*RegionQuota, func()) {
if vcd.config.Tm.StorageClass == "" {
check.Fatal("testing configuration property 'tm.storageClass' is required")
}
Expand Down Expand Up @@ -309,33 +309,33 @@ func createVdc(vcd *TestVCD, org *TmOrg, region *Region, check *C) (*TmVdc, func
},
}},
}
vdc, err := vcd.client.CreateTmVdc(cfg)
rq, err := vcd.client.CreateRegionQuota(cfg)
check.Assert(err, IsNil)
check.Assert(vdc, NotNil)
check.Assert(rq, NotNil)

PrependToCleanupListOpenApi(vdc.TmVdc.ID, cfg.Name, types.OpenApiPathVcf+types.OpenApiEndpointTmVdcs+vdc.TmVdc.ID)
PrependToCleanupListOpenApi(rq.TmVdc.ID, cfg.Name, types.OpenApiPathVcf+types.OpenApiEndpointTmVdcs+rq.TmVdc.ID)

err = vdc.AssignVmClasses(&types.RegionVirtualMachineClasses{
err = rq.AssignVmClasses(&types.RegionVirtualMachineClasses{
Values: types.OpenApiReferences{{Name: vmClasses[0].Name, ID: vmClasses[0].ID}},
})
check.Assert(err, IsNil)
_, err = vdc.CreateStoragePolicies(&types.VirtualDatacenterStoragePolicies{
_, err = rq.CreateStoragePolicies(&types.VirtualDatacenterStoragePolicies{
Values: []types.VirtualDatacenterStoragePolicy{
{
RegionStoragePolicy: types.OpenApiReference{
ID: sp.RegionStoragePolicy.ID,
},
StorageLimitMiB: 100,
VirtualDatacenter: types.OpenApiReference{
ID: vdc.TmVdc.ID,
ID: rq.TmVdc.ID,
},
},
},
})
check.Assert(err, IsNil)

return vdc, func() {
err = vdc.Delete()
return rq, func() {
err = rq.Delete()
check.Assert(err, IsNil)
}
}
Expand Down
2 changes: 1 addition & 1 deletion govcd/tm_content_library_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ func (vcd *TestVCD) Test_ContentLibraryTenant(check *C) {
defer orgCleanup()

// A Region Quota is needed to have Storage classes available in the Organization
_, regionQuotaCleanup := createVdc(vcd, org, region, check)
_, regionQuotaCleanup := createRegionQuota(vcd, org, region, check)
defer regionQuotaCleanup()

cls, err := org.GetAllContentLibraries(nil)
Expand Down
168 changes: 168 additions & 0 deletions govcd/tm_region_quota.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package govcd

/*
* Copyright 2024 VMware, Inc. All rights reserved. Licensed under the Apache v2 License.
*/

import (
"fmt"
"net/url"

"github.com/vmware/go-vcloud-director/v3/types/v56"
)

const labelRegionQuota = "Org Region Quota"

// RegionQuota defines Region Quota structure in Tenant Manager
type RegionQuota struct {
TmVdc *types.TmVdc
vcdClient *VCDClient
}

// wrap is a hidden helper that facilitates the usage of a generic CRUD function
//
//lint:ignore U1000 this method is used in generic functions, but annoys staticcheck
func (g RegionQuota) wrap(inner *types.TmVdc) *RegionQuota {
g.TmVdc = inner
return &g
}

// CreateRegionQuota sets up a new Region Quota
func (vcdClient *VCDClient) CreateRegionQuota(config *types.TmVdc) (*RegionQuota, error) {
c := crudConfig{
entityLabel: labelRegionQuota,
endpoint: types.OpenApiPathVcf + types.OpenApiEndpointTmVdcs,
requiresTm: true,
}
outerType := RegionQuota{vcdClient: vcdClient}
return createOuterEntity(&vcdClient.Client, outerType, c, config)
}

// GetAllRegionQuotas retrieves all Region Quotas
func (vcdClient *VCDClient) GetAllRegionQuotas(queryParameters url.Values) ([]*RegionQuota, error) {
c := crudConfig{
entityLabel: labelRegionQuota,
endpoint: types.OpenApiPathVcf + types.OpenApiEndpointTmVdcs,
queryParameters: queryParameters,
requiresTm: true,
}

outerType := RegionQuota{vcdClient: vcdClient}
return getAllOuterEntities(&vcdClient.Client, outerType, c)
}

// GetRegionQuotaByName retrieves a Region Quota by a given name
func (vcdClient *VCDClient) GetRegionQuotaByName(name string) (*RegionQuota, error) {
if name == "" {
return nil, fmt.Errorf("%s lookup requires name", labelRegionQuota)
}

queryParams := url.Values{}
queryParams.Add("filter", fmt.Sprintf("name==%s", name))
filteredEntities, err := vcdClient.GetAllRegionQuotas(queryParams)
if err != nil {
return nil, err
}

singleResult, err := oneOrError("name", name, filteredEntities)
if err != nil {
return nil, err
}

return singleResult, nil
}

// GetRegionQuotaByNameAndOrgId retrieves a Region Quota by Name and Org ID
func (vcdClient *VCDClient) GetRegionQuotaByNameAndOrgId(name, orgId string) (*RegionQuota, error) {
if name == "" {
return nil, fmt.Errorf("%s lookup requires name and Org ID to be present", labelRegionQuota)
}

queryParams := url.Values{}
queryParams.Add("filter", fmt.Sprintf("org.id==%s;name==%s", orgId, name))

filteredEntities, err := vcdClient.GetAllRegionQuotas(queryParams)
if err != nil {
return nil, err
}

singleResult, err := oneOrError("name", name, filteredEntities)
if err != nil {
return nil, err
}

return singleResult, nil
}

// GetRegionQuotaById retrieves a Region Quota by a given ID
func (vcdClient *VCDClient) GetRegionQuotaById(id string) (*RegionQuota, error) {
c := crudConfig{
entityLabel: labelRegionQuota,
endpoint: types.OpenApiPathVcf + types.OpenApiEndpointTmVdcs,
endpointParams: []string{id},
requiresTm: true,
}

outerType := RegionQuota{vcdClient: vcdClient}
return getOuterEntity(&vcdClient.Client, outerType, c)
}

// Update updates the receiver Region Quota
func (o *RegionQuota) Update(tmVdcConfig *types.TmVdc) (*RegionQuota, error) {
c := crudConfig{
entityLabel: labelRegionQuota,
endpoint: types.OpenApiPathVcf + types.OpenApiEndpointTmVdcs,
endpointParams: []string{o.TmVdc.ID},
requiresTm: true,
}
outerType := RegionQuota{vcdClient: o.vcdClient}
return updateOuterEntity(&o.vcdClient.Client, outerType, c, tmVdcConfig)
}

// Delete deletes the receiver Region Quota
func (o *RegionQuota) Delete() error {
c := crudConfig{
entityLabel: labelRegionQuota,
endpoint: types.OpenApiPathVcf + types.OpenApiEndpointTmVdcs,
endpointParams: []string{o.TmVdc.ID},
requiresTm: true,
}
return deleteEntityById(&o.vcdClient.Client, c)
}

// AssignVmClassesToRegionQuota assigns VM Classes to the receiver Region Quota
func (o *VCDClient) AssignVmClassesToRegionQuota(regionQuotaId string, vmClasses *types.RegionVirtualMachineClasses) error {
c := crudConfig{
entityLabel: labelRegionQuota,
endpoint: types.OpenApiPathVcf + types.OpenApiEndpointTmVdcsVmClasses,
endpointParams: []string{regionQuotaId},
requiresTm: true,
}
// It's a PUT call with OpenAPI references, so we reuse generic functions for simplicity
_, err := updateInnerEntity[types.RegionVirtualMachineClasses](&o.Client, c, vmClasses)
if err != nil {
return err
}
return nil
}

// AssignVmClasses assigns VM Classes to the receiver Region Quota
func (o *RegionQuota) AssignVmClasses(vmClasses *types.RegionVirtualMachineClasses) error {
return o.vcdClient.AssignVmClassesToRegionQuota(o.TmVdc.ID, vmClasses)
}

// GetVmClassesFromRegionQuota returns all VM Classes of the given Region Quota
func (o *VCDClient) GetVmClassesFromRegionQuota(regionQuotaId string) (*types.RegionVirtualMachineClasses, error) {
c := crudConfig{
entityLabel: labelRegionQuota,
endpoint: types.OpenApiPathVcf + types.OpenApiEndpointTmVdcsVmClasses,
endpointParams: []string{regionQuotaId},
requiresTm: true,
}
// It's a GET call with OpenAPI references, so we reuse generic functions for simplicity
result, err := getInnerEntity[types.RegionVirtualMachineClasses](&o.Client, c)
if err != nil {
return nil, err
}
return result, nil
}
Loading

0 comments on commit 333338d

Please sign in to comment.