diff --git a/.changes/README.md b/.changes/README.md new file mode 100644 index 000000000..1e8061a0d --- /dev/null +++ b/.changes/README.md @@ -0,0 +1,30 @@ +# Handling of CHANGELOG entries + +## PR operations + +* All PRs, instead of updating CHANGELOG.md directly, will create individual files in a directory .changes/$version +* The files will be named `xxx-section_name.md`, where xxx is the PR number, and `section_name` is one of + * features + * improvements + * bug-fixes + * deprecations + * notes + * removals + +* The changes files must NOT contain the header + +* You can update the file `.changes/sections` to add more headers +* To see the full change set for the current version (or an old one), use `./scripts/make-changelog.sh [version]` + + +## Post release initialization + +After a release, the changelog will be initialized with the following template: + +``` + ## $VERSION (Unreleased) + + Changes in progress for v$VERSION are available at [.changes/v$VERSION](https://github.com/vmware/go-vcloud-director/tree/main/.changes/v$VERSION) until the release. +``` + +Run `.changes/init.sh version` to get the needed text diff --git a/.changes/init.sh b/.changes/init.sh new file mode 100755 index 000000000..515650fd9 --- /dev/null +++ b/.changes/init.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +# This script is used at the start of a new release cycle, to +# initialize the CHANGELOG +# Run at the top of the repository, as +# $ .changes/init.sh VERSION +# (without the initial 'v') + +VERSION=$1 + +if [ -z "$VERSION" ] +then + echo "Syntax: $0 VERSION (without initial 'v')" + + exit 1 +fi + +starts_with_v=$(echo $VERSION | grep '^v') +if [ -n "$starts_with_v" ] +then + echo "The version should be without the initial 'v'" + exit 1 +fi + +echo "Copy the following lines at the top of CHANGELOG.md" +echo "" +echo "" +echo "## $VERSION (Unreleased)" +echo "" +echo "Changes in progress for v$VERSION are available at [.changes/v$VERSION](https://github.com/vmware/go-vcloud-director/tree/main/.changes/v$VERSION) until the release." +echo "" + diff --git a/.changes/sections b/.changes/sections new file mode 100644 index 000000000..d78ccbbd4 --- /dev/null +++ b/.changes/sections @@ -0,0 +1,7 @@ +features +improvements +bug-fixes +deprecations +notes +removals +breaking-changes diff --git a/.changes/v2.12.0/364-deprecations.md b/.changes/v2.12.0/364-deprecations.md new file mode 100644 index 000000000..8cb9eb1fe --- /dev/null +++ b/.changes/v2.12.0/364-deprecations.md @@ -0,0 +1,2 @@ +* Deprecated `vdc.GetEdgeGatewayRecordsType` [GH-364] + diff --git a/.changes/v2.12.0/364-features.md b/.changes/v2.12.0/364-features.md new file mode 100644 index 000000000..53902b11c --- /dev/null +++ b/.changes/v2.12.0/364-features.md @@ -0,0 +1,2 @@ +* Added method `vdc.QueryEdgeGateway` [GH-364] + diff --git a/.changes/v2.12.0/367-improvements.md b/.changes/v2.12.0/367-improvements.md new file mode 100644 index 000000000..c4f1f9bff --- /dev/null +++ b/.changes/v2.12.0/367-improvements.md @@ -0,0 +1,3 @@ +* Only send xml.Header when payload is not empty (some WAFs block empty requests with XML header) + [GH-367] + diff --git a/.changes/v2.12.0/368-features.md b/.changes/v2.12.0/368-features.md new file mode 100644 index 000000000..1dab8d30b --- /dev/null +++ b/.changes/v2.12.0/368-features.md @@ -0,0 +1,14 @@ +* Added NSX-T Firewall Group type (which represents a Security Group or an IP Set) support by using + structures `NsxtFirewallGroup` and `NsxtFirewallGroupMemberVms`. The following methods are + introduced for managing Security Groups and Ip Sets: `Vdc.CreateNsxtFirewallGroup`, + `NsxtEdgeGateway.CreateNsxtFirewallGroup`, `Org.GetAllNsxtFirewallGroups`, + `Vdc.GetAllNsxtFirewallGroups`, `Org.GetNsxtFirewallGroupByName`, + `Vdc.GetNsxtFirewallGroupByName`, `NsxtEdgeGateway.GetNsxtFirewallGroupByName`, + `Org.GetNsxtFirewallGroupById`, `Vdc.GetNsxtFirewallGroupById`, + `NsxtEdgeGateway.GetNsxtFirewallGroupById`, `NsxtFirewallGroup.Update`, + `NsxtFirewallGroup.Delete`, `NsxtFirewallGroup.GetAssociatedVms`, + `NsxtFirewallGroup.IsSecurityGroup`, `NsxtFirewallGroup.IsIpSet` + [GH-368] +* Added methods Org.QueryVmList and Org.QueryVmById to find VM by ID in an Org + [GH-368] + diff --git a/.changes/v2.12.0/368-improvements.md b/.changes/v2.12.0/368-improvements.md new file mode 100644 index 000000000..34a78e2fc --- /dev/null +++ b/.changes/v2.12.0/368-improvements.md @@ -0,0 +1,5 @@ +* Improved test entity cleanup to find standalone VMs in any VDC (not only default NSX-V one) + [GH-368] +* Improved test entity cleanup to allow specifying parent VDC for vApp removals + [GH-368] + diff --git a/.changes/v2.12.0/371-breaking-changes.md b/.changes/v2.12.0/371-breaking-changes.md new file mode 100644 index 000000000..62c95ee2a --- /dev/null +++ b/.changes/v2.12.0/371-breaking-changes.md @@ -0,0 +1,5 @@ +* Field `types.Disk.Size` is replaced with `types.Disk.SizeMb` as size in Kilobytes is not supported in V33.0 + [GH-371] +* Field `types.DiskRecordType.SizeB` is replaced with `types.DiskRecordType.SizeMb` as size in Kilobytes is not + supported in V33.0 [GH-371] + diff --git a/.changes/v2.12.0/371-improvements.md b/.changes/v2.12.0/371-improvements.md new file mode 100644 index 000000000..5652ee1eb --- /dev/null +++ b/.changes/v2.12.0/371-improvements.md @@ -0,0 +1,5 @@ +* Methods `GetVDCById` and `GetVDCByName` for `Org` now use queries behind the scenes because Org + structure does not list child VDCs anymore [GH-371], [GH-376], [GH-382] +* Methods `GetCatalogById` and `GetCatalogByName` for `Org` now use queries behind the scenes because Org + structure does not list child Catalogs anymore [GH-371] [GH-376] + diff --git a/.changes/v2.12.0/371-notes.md b/.changes/v2.12.0/371-notes.md new file mode 100644 index 000000000..cf564cc7e --- /dev/null +++ b/.changes/v2.12.0/371-notes.md @@ -0,0 +1,5 @@ +* Dropped support for VCD 9.7 which is EOL now [GH-371] +* Bumped Default API Version to V33.0 [GH-371] +* Drop legacy authentication mechanism (vcdAuthorize) and use only new Cloud API provided (vcdCloudApiAuthorize) as + API V33.0 is sufficient for it [GH-371] + diff --git a/.changes/v2.12.0/372-breaking-changes.md b/.changes/v2.12.0/372-breaking-changes.md new file mode 100644 index 000000000..172f8c897 --- /dev/null +++ b/.changes/v2.12.0/372-breaking-changes.md @@ -0,0 +1,3 @@ +* Added parameter `description` to method `vdc.ComposeRawVapp` [GH-372] +* Added methods `vapp.Rename`, `vapp.UpdateDescription`, `vapp.UpdateNameDescription` [GH-372] + diff --git a/.changes/v2.12.0/378-features.md b/.changes/v2.12.0/378-features.md new file mode 100644 index 000000000..fca323337 --- /dev/null +++ b/.changes/v2.12.0/378-features.md @@ -0,0 +1,3 @@ +* Added `NsxtAppPortProfile` and `types.NsxtAppPortProfile` for NSX-T Application Port Profile management + [GH-378] + diff --git a/.changes/v2.12.0/378-improvements.md b/.changes/v2.12.0/378-improvements.md new file mode 100644 index 000000000..d56333f2e --- /dev/null +++ b/.changes/v2.12.0/378-improvements.md @@ -0,0 +1,5 @@ +* Improved `OpenApiGetAllItems` to still follow pages in VCD endpoints with BUG which don't return 'nextPage' link for + pagination [GH-378] +* Improved LDAP container related tests to use correct port mapping for latest LDAP container version + [GH-378] + diff --git a/.changes/v2.12.0/380-breaking-changes.md b/.changes/v2.12.0/380-breaking-changes.md new file mode 100644 index 000000000..1b1886ed6 --- /dev/null +++ b/.changes/v2.12.0/380-breaking-changes.md @@ -0,0 +1,6 @@ +* Added parameter `additionalHeader map[string]string` to functions `Client.OpenApiDeleteItem`, `Client.OpenApiGetAllItems`, + `Client.OpenApiGetItem`, `Client.OpenApiPostItem`, `Client.OpenApiPutItem`, `Client.OpenApiPutItemAsync`, + `Client.OpenApiPutItemSync` [GH-380] +* Renamed functions `GetOpenApiRoleById` -> `GetRoleById`, `GetOpenApiRoleByName` -> `GetRoleByName`, + `GetAllOpenApiRoles` -> `GetAllRoles` [GH-380] + diff --git a/.changes/v2.12.0/380-deprecations.md b/.changes/v2.12.0/380-deprecations.md new file mode 100644 index 000000000..a9aab731a --- /dev/null +++ b/.changes/v2.12.0/380-deprecations.md @@ -0,0 +1,2 @@ +* Removed deprecated method `adminOrg.GetRole` [GH-380] + diff --git a/.changes/v2.12.0/380-features.md b/.changes/v2.12.0/380-features.md new file mode 100644 index 000000000..189b6a479 --- /dev/null +++ b/.changes/v2.12.0/380-features.md @@ -0,0 +1,22 @@ +* Added Tenant Context management functions `Client.RemoveCustomHeader`, `Client.SetCustomHeader`, `WithHttpHeader`, + and many private methods to retrieve tenant context down the hierarchy. More details in `CODING_GUIDELINES.md` + [GH-380] +* Added Rights management methods `AdminOrg.GetAllRights`, `AdminOrg.GetAllRightsCategories`, `AdminOrg.GetRightById`, + `AdminOrg.GetRightByName`, `Client.GetAllRights`, `Client.GetAllRightsCategories`, `Client.GetRightById`, + `Client.GetRightByName`, `client.GetRightsCategoryById`, `AdminOrg.GetRightsCategoryById` [GH-380] +* Added Global Role management methods `Client.GetAllGlobalRoles`, `Client.CreateGlobalRole`, `Client.GetGlobalRoleById`, + `Client.GetGlobalRoleByName`, `GlobalRole.AddRights`, `GlobalRole.Delete`, `GlobalRole.GetRights`, + `GlobalRole.GetTenants`, `GlobalRole.PublishAllTenants`, `GlobalRole.PublishTenants`, `GlobalRole.RemoveAllRights`, + `GlobalRole.RemoveRights`, `GlobalRole.ReplacePublishedTenants`, `GlobalRole.UnpublishAllTenants`, + `GlobalRole.UnpublishTenants`, `GlobalRole.Update`, `GlobalRole.UpdateRights` [GH-380] +* Added Rights Bundle management methods `Client.CreateRightsBundle`, `Client.GetAllRightsBundles`, + `Client.GetRightsBundleById`, `Client.GetRightsBundleByName`, `RightsBundle.AddRights`, `RightsBundle.Delete`, + `RightsBundle.GetRights`, `RightsBundle.GetTenants`, `RightsBundle.PublishAllTenants`, `RightsBundle.PublishTenants`, + `RightsBundle.RemoveAllRights`, `RightsBundle.RemoveRights`, `RightsBundle.ReplacePublishedTenants`, + `RightsBundle.UnpublishAllTenants`, `RightsBundle.UnpublishTenants`, `RightsBundle.Update`, `RightsBundle.UpdateRights` + [GH-380] +* Added Role managemnt methods `AdminOrg.GetAllRoles`, `AdminOrg.GetRoleById`, `AdminOrg.GetRoleByName`, + `Client.GetAllRoles`, `Role.AddRights`, `Role.GetRights`, `Role.RemoveAllRights`, `Role.RemoveRights`, `Role.UpdateRights` + [GH-380] +* Added convenience function `FindMissingImpliedRights` [GH-380] + diff --git a/.changes/v2.12.0/381-features.md b/.changes/v2.12.0/381-features.md new file mode 100644 index 000000000..b6aec5bc4 --- /dev/null +++ b/.changes/v2.12.0/381-features.md @@ -0,0 +1,3 @@ +* Added methods `NsxtEdgeGateway.UpdateNsxtFirewall()`, `NsxtEdgeGateway.GetNsxtFirewall()`, `nsxtFirewall.DeleteAllRules()`, + `nsxtFirewall.DeleteRuleById` [GH-381] + diff --git a/.changes/v2.12.0/381-improvements.md b/.changes/v2.12.0/381-improvements.md new file mode 100644 index 000000000..3608f0ef0 --- /dev/null +++ b/.changes/v2.12.0/381-improvements.md @@ -0,0 +1,3 @@ +* Cleanup a few unnecessary type conversions detected by new staticcheck version + [GH-381] + diff --git a/.changes/v2.12.0/382-features.md b/.changes/v2.12.0/382-features.md new file mode 100644 index 000000000..835a839de --- /dev/null +++ b/.changes/v2.12.0/382-features.md @@ -0,0 +1,4 @@ +* Added NSX-T NAT support with types `NsxtNatRule` and `types.NsxtNatRule` as well as methods `edge.GetAllNsxtNatRules`, + `edge.GetNsxtNatRuleByName`, `edge.GetNsxtNatRuleById`, `edge.CreateNatRule`, `nsxtNatRule.Update`, `nsxtNatRule.Delete`, + `nsxtNatRule.IsEqualTo` [GH-382] + diff --git a/.changes/v2.12.0/385-features.md b/.changes/v2.12.0/385-features.md new file mode 100644 index 000000000..a45fb3f1b --- /dev/null +++ b/.changes/v2.12.0/385-features.md @@ -0,0 +1,3 @@ +* Added `NsxtIpSecVpnTunnel` and `types.NsxtIpSecVpnTunnel` for NSX-T IPsec VPN Tunnel configuration + [GH-385] + diff --git a/.changes/v2.12.0/387-bug-fixes.md b/.changes/v2.12.0/387-bug-fixes.md new file mode 100644 index 000000000..74db11212 --- /dev/null +++ b/.changes/v2.12.0/387-bug-fixes.md @@ -0,0 +1,3 @@ +* Deprecated methods `vdc.ComposeRawVApp` and `vdc.ComposeVApp` [#387](https://github.com/vmware/go-vcloud-director/pull/387) +* Added method `vdc.CreateRawVApp` [#387](https://github.com/vmware/go-vcloud-director/pull/387) + diff --git a/.changes/v2.12.1/389-bug-fixes.md b/.changes/v2.12.1/389-bug-fixes.md new file mode 100644 index 000000000..885c7e925 --- /dev/null +++ b/.changes/v2.12.1/389-bug-fixes.md @@ -0,0 +1,3 @@ +* org.GetCatalogByName and org.GetCatalogById could not retrieve shared catalogs from different Orgs + [GH-389] + diff --git a/.changes/v2.13.0/391-bug-fixes.md b/.changes/v2.13.0/391-bug-fixes.md new file mode 100644 index 000000000..b104f8913 --- /dev/null +++ b/.changes/v2.13.0/391-bug-fixes.md @@ -0,0 +1,2 @@ +* Fix handling of `staticcheck` in GitGub Actions [GH-391] + diff --git a/.changes/v2.13.0/391-improvements.md b/.changes/v2.13.0/391-improvements.md new file mode 100644 index 000000000..904cf478e --- /dev/null +++ b/.changes/v2.13.0/391-improvements.md @@ -0,0 +1,2 @@ +* Add `.changes` directory for changelog items [GH-391] + diff --git a/.changes/v2.13.0/392-bug-fixes.md b/.changes/v2.13.0/392-bug-fixes.md new file mode 100644 index 000000000..73828ae91 --- /dev/null +++ b/.changes/v2.13.0/392-bug-fixes.md @@ -0,0 +1,2 @@ +* Fix Issue #390: `catalog.Delete()` ignores returned task and responds immediately which could have caused failures [GH-392] + diff --git a/.changes/v2.13.0/393-deprecations.md b/.changes/v2.13.0/393-deprecations.md new file mode 100644 index 000000000..9e3325129 --- /dev/null +++ b/.changes/v2.13.0/393-deprecations.md @@ -0,0 +1,4 @@ +* Deprecated `GetStorageProfileByHref` in favor of either `client.GetStorageProfileByHref` or `vcdClient.GetStorageProfileByHref` [GH-393] +* Deprecated `QueryProviderVdcStorageProfileByName` in favor of `VCDClient.QueryProviderVdcStorageProfileByName` [GH-393] +* Deprecated `VCDClient.QueryProviderVdcStorageProfiles` in favor of either `client.QueryProviderVdcStorageProfiles` or `client.QueryAllProviderVdcStorageProfiles` [GH-393] +* Deprecated `Vdc.GetDefaultStorageProfileReference` in favor of `adminVdc.GetDefaultStorageProfileReference` [GH-393] diff --git a/.changes/v2.13.0/393-features.md b/.changes/v2.13.0/393-features.md new file mode 100644 index 000000000..2ef8cd77c --- /dev/null +++ b/.changes/v2.13.0/393-features.md @@ -0,0 +1,11 @@ +* Added method `AdminVdc.AddStorageProfile` [GH-393] +* Added method `AdminVdc.AddStorageProfileWait` [GH-393] +* Added method `AdminVdc.RemoveStorageProfile` [GH-393] +* Added method `AdminVdc.RemoveStorageProfileWait` [GH-393] +* Added method `AdminVdc.SetDefaultStorageProfile` [GH-393] +* Added method `AdminVdc.GetDefaultStorageProfileReference` [GH-393] +* Added method `VCDClient.GetStorageProfileByHref` [GH-393] +* Added method `Client.GetStorageProfileByHref` [GH-393] +* Added method `VCDClient.QueryProviderVdcStorageProfileByName` [GH-393] +* Added method `Client.QueryAllProviderVdcStorageProfiles` [GH-393] +* Added method `Client.QueryProviderVdcStorageProfiles` [GH-393] diff --git a/.changes/v2.13.0/396-improvements.md b/.changes/v2.13.0/396-improvements.md new file mode 100644 index 000000000..0864ea6ee --- /dev/null +++ b/.changes/v2.13.0/396-improvements.md @@ -0,0 +1,2 @@ +* Align build tags to match go fmt with Go 1.17 [GH-396] +* Improve `test-tags.sh` script to handle new build tag format [GH-396] diff --git a/.changes/v2.13.0/397-bug-fixes.md b/.changes/v2.13.0/397-bug-fixes.md new file mode 100644 index 000000000..de55bf8c2 --- /dev/null +++ b/.changes/v2.13.0/397-bug-fixes.md @@ -0,0 +1 @@ +* Fixes Issue #395 "BUG: can't update EGW - there is no ownerRef field" [GH-397] diff --git a/.changes/v2.13.0/398-features.md b/.changes/v2.13.0/398-features.md new file mode 100644 index 000000000..a64271d79 --- /dev/null +++ b/.changes/v2.13.0/398-features.md @@ -0,0 +1,14 @@ +* Added types `NsxtAlbController` and `types.NsxtAlbController` for handling NSX-T ALB Controllers with corresponding + functions `GetAllAlbControllers`, `GetAlbControllerByName`, `GetAlbControllerById`, `GetAlbControllerByUrl`, + `CreateNsxtAlbController`, `Update`, `Delete` [GH-398] +* Added types `NsxtAlbCloud` and `types.NsxtAlbCloud` for handling NSX-T ALB Clouds with corresponding functions + `GetAllAlbClouds`, `GetAlbCloudByName`, `GetAlbCloudById`, `CreateAlbCloud`, `Delete` [GH-398] +* Added type `NsxtAlbImportableCloud` and `types.NsxtAlbImportableCloud` for listing NSX-T ALB Importable Clouds with + corresponding functions `GetAllAlbImportableClouds`, `GetAlbImportableCloudByName`, `GetAlbImportableCloudById` + [GH-398] +* Added types `NsxtAlbServiceEngineGroup` and `types.NsxtAlbServiceEngineGroup` for handling NSX-T ALB Service Engine + Groups with corresponding functions `GetAllNsxtAlbServiceEngineGroups`, `GetAlbServiceEngineGroupByName`, + `GetAlbServiceEngineGroupById`, `CreateNsxtAlbServiceEngineGroup`, `Update`, `Delete`, `Sync` [GH-398] +* Added types `NsxtAlbImportableServiceEngineGroups` and `types.NsxtAlbImportableServiceEngineGroups` for listing NSX-T + ALB Importable Service Engine Groups with corresponding functions `GetAllAlbImportableServiceEngineGroups`, + `GetAlbImportableServiceEngineGroupByName`, `GetAlbImportableServiceEngineGroupById` [GH-398] diff --git a/.changes/v2.13.0/399-improvements.md b/.changes/v2.13.0/399-improvements.md new file mode 100644 index 000000000..3010d8ce0 --- /dev/null +++ b/.changes/v2.13.0/399-improvements.md @@ -0,0 +1,6 @@ +* External network type ExternalNetworkV2 automatically elevates API version to maximum available out of 33.0, 35.0 and + 36.0, so that new functionality can be consumed. It uses a controlled version elevation mechanism to consume the newer + features, but at the same time remain tested by not choosing the latest untested version blindly (more information in + openapi_endpoints.go) [GH-399] +* Added new field BackingTypeValue in favor of deprecated BackingType to types.ExternalNetworkV2Backing [GH-399] +* Add new function `GetFilteredNsxtImportableSwitches` to query NSX-T Importable Switches (Segments) [GH-399] diff --git a/.changes/v2.14.0/403-features.md b/.changes/v2.14.0/403-features.md new file mode 100644 index 000000000..d6acd3f48 --- /dev/null +++ b/.changes/v2.14.0/403-features.md @@ -0,0 +1,2 @@ +* Add type `NsxtAlbConfig` and functions `NsxtEdgeGateway.UpdateAlbSettings`, `NsxtEdgeGateway.GetAlbSettings`, + `NsxtEdgeGateway.DisableAlb` [GH-403] diff --git a/.changes/v2.14.0/404-features.md b/.changes/v2.14.0/404-features.md new file mode 100644 index 000000000..0de477517 --- /dev/null +++ b/.changes/v2.14.0/404-features.md @@ -0,0 +1,3 @@ +* Add types `Certificate` and `types.CertificateLibraryItem` for handling Certificates in Certificate Library with corresponding + methods `client.GetCertificateFromLibraryById`, `client.AddCertificateToLibrary`, `client.GetAllCertificatesFromLibrary`, `client.GetCertificateFromLibraryByName`, `adminOrg.GetCertificateFromLibraryById`, `adminOrg.AddCertificateToLibrary`, `adminOrg.GetAllCertificatesFromLibrary`, `adminOrg.GetCertificateFromLibraryByName`, + `certificate.Update`, `certificate.Delete` [GH-404] diff --git a/.changes/v2.14.0/405-features.md b/.changes/v2.14.0/405-features.md new file mode 100644 index 000000000..0f0e17ce1 --- /dev/null +++ b/.changes/v2.14.0/405-features.md @@ -0,0 +1,4 @@ +* Add support for ALB Service Engine Group Assignment to NSX-T Edge Gateway via type + `NsxtAlbServiceEngineGroupAssignment` and functions `GetAllAlbServiceEngineGroupAssignments`, + `GetAlbServiceEngineGroupAssignmentById`, `GetAlbServiceEngineGroupAssignmentByName`, + `CreateAlbServiceEngineGroupAssignment`, `Update`, `Delete` [GH-405] diff --git a/.changes/v2.14.0/406-features.md b/.changes/v2.14.0/406-features.md new file mode 100644 index 000000000..93864c9e2 --- /dev/null +++ b/.changes/v2.14.0/406-features.md @@ -0,0 +1,3 @@ +* Add type `types.ApiTokenRefresh` to contain data from API token refresh [GH-406] +* Add method `VCDClient.GetBearerTokenFromApiToken` to get a bearer token from an API token [GH-406] +* Add method `VCDClient.SetApiToken` to set a token and get a bearer token using and API token and get token details in return [GH-406] diff --git a/.changes/v2.14.0/406-improvements.md b/.changes/v2.14.0/406-improvements.md new file mode 100644 index 000000000..df018148f --- /dev/null +++ b/.changes/v2.14.0/406-improvements.md @@ -0,0 +1 @@ +* `VCDClient.SetToken` has now the ability of transparently setting a bearer token when receiving an API token [GH-406] diff --git a/.changes/v2.14.0/408-improvements.md b/.changes/v2.14.0/408-improvements.md new file mode 100644 index 000000000..ebedba992 --- /dev/null +++ b/.changes/v2.14.0/408-improvements.md @@ -0,0 +1 @@ +* Remove Coverity warnings from code [GH-408, GH-412] diff --git a/.changes/v2.14.0/409-improvements.md b/.changes/v2.14.0/409-improvements.md new file mode 100644 index 000000000..e1265a74a --- /dev/null +++ b/.changes/v2.14.0/409-improvements.md @@ -0,0 +1 @@ +* Add session info to go-vcloud-director logs [GH-409] diff --git a/.changes/v2.14.0/410-features.md b/.changes/v2.14.0/410-features.md new file mode 100644 index 000000000..92dbac953 --- /dev/null +++ b/.changes/v2.14.0/410-features.md @@ -0,0 +1,2 @@ +* Add types `VdcGroup`, `types.VdcGroup`, `types.ParticipatingOrgVdcs`, `types.CandidateVdc`, `types.DfwPolicies` and `types.DefaultPolicy` for handling VDC groups with corresponding + methods `adminOrg.CreateNsxtVdcGroup`, `adminOrg.CreateVdcGroup`, `adminOrg.GetAllNsxtVdcGroupCandidates`, `adminOrg.GetAllVdcGroupCandidates`, `adminOrg.GetAllVdcGroups`, `adminOrg.GetVdcGroupByName`, `adminOrg.GetVdcGroupById`, `vdcGroup.Update`, `vdcGroup.GenericUpdate`, `vdcGroup.Delete`, `vdcGroup.DisableDefaultPolicy`, `vdcGroup.EnableDefaultPolicy`, `vdcGroup.GetDfwPolicies`, `vdcGroup.DeActivateDfw`, `vdcGroup.ActivateDfw`, `vdcGroup.UpdateDefaultDfwPolicies`, `vdcGroup.UpdateDfwPolicies` [GH-410] diff --git a/.changes/v2.14.0/414-features.md b/.changes/v2.14.0/414-features.md new file mode 100644 index 000000000..f0de0c1c0 --- /dev/null +++ b/.changes/v2.14.0/414-features.md @@ -0,0 +1,3 @@ +* Add support for ALB Pool to NSX-T Edge Gateway via type `NsxtAlbPool` and functions `GetAllAlbPools`, + `GetAllAlbPoolSummaries`, `GetAlbPoolByName`, `GetAlbPoolById`, `CreateNsxtAlbPool`, `nsxtAlbPool.Update`, + `nsxtAlbPool.Delete` [GH-414] diff --git a/.changes/v2.14.0/417-features.md b/.changes/v2.14.0/417-features.md new file mode 100644 index 000000000..c04fd9086 --- /dev/null +++ b/.changes/v2.14.0/417-features.md @@ -0,0 +1,3 @@ +* Add support for ALB Virtual Services to NSX-T Edge Gateway via type `NsxtAlbVirtualService` and functions `GetAllAlbVirtualServices`, + `GetAllAlbGetAllAlbVirtualServiceSummaries`, `GetAlbVirtualServiceByName`, `GetAlbVirtualServiceById`, + `CreateNsxtAlbVirtualService`, `NsxtAlbVirtualService.Update`, `NsxtAlbVirtualService.Delete` [GH-417] diff --git a/.changes/v2.14.0/418-bug-fixes.md b/.changes/v2.14.0/418-bug-fixes.md new file mode 100644 index 000000000..a21406fd8 --- /dev/null +++ b/.changes/v2.14.0/418-bug-fixes.md @@ -0,0 +1 @@ +* Fix Issue #728: `vm.UpdateInternalDisksAsync()` didn't send VM description and as a result would delete VM description [GH-418] diff --git a/.changes/v2.14.0/419-bug-fixes.md b/.changes/v2.14.0/419-bug-fixes.md new file mode 100644 index 000000000..853d1d4b1 --- /dev/null +++ b/.changes/v2.14.0/419-bug-fixes.md @@ -0,0 +1 @@ +* Remove hardcoded 0 value for Weight field in `ChangeCPUCountWithCore` function to avoid overriding shares [GH-419] diff --git a/.changes/v2.14.0/420-bug-fixes.md b/.changes/v2.14.0/420-bug-fixes.md new file mode 100644 index 000000000..6988c8af5 --- /dev/null +++ b/.changes/v2.14.0/420-bug-fixes.md @@ -0,0 +1,2 @@ +* Fix issue #421 "Wrong xml name in SourcedVmTemplateParams" [GH-420] + diff --git a/.changes/v2.14.0/420-improvements.md b/.changes/v2.14.0/420-improvements.md new file mode 100644 index 000000000..aa65f09cb --- /dev/null +++ b/.changes/v2.14.0/420-improvements.md @@ -0,0 +1,3 @@ +* Add type `types.UpdateLeaseSettingsSection` to handle vApp lease settings. [GH-420] +* Add methods `vApp.GetLease` and `vApp.RenewLease`, to query the state of the vApp lease and eventually modify it. [GH-420] +* Add `LeaseSettingsSection` to `types.VApp` structure. [GH-420] diff --git a/.changes/v2.15.0/383-features.md b/.changes/v2.15.0/383-features.md new file mode 100644 index 000000000..5ccb529ca --- /dev/null +++ b/.changes/v2.15.0/383-features.md @@ -0,0 +1,4 @@ +* Added support for Shareable disks, i.e., independent disks that can be attached to multiple VMs which is available from + API v35.0 onwards. Also added uuid to the Disk structure which is a new member that is returned from v36.0 onwards. This + member holds a UUID that can be used to correlate the disk that is attached to a particular VM from the VCD side and the + VM host side. [GH-383] diff --git a/.changes/v2.15.0/422-features.md b/.changes/v2.15.0/422-features.md new file mode 100644 index 000000000..414fa7d0b --- /dev/null +++ b/.changes/v2.15.0/422-features.md @@ -0,0 +1,2 @@ +* Added support for uploading OVF using URL `catalog.UploadOvfByLink` [GH-422], [GH-426] +* Added support for updating vApp template `vAppTemplate.UpdateAsync` and `vAppTemplate.Update` [GH-422] diff --git a/.changes/v2.15.0/424-features.md b/.changes/v2.15.0/424-features.md new file mode 100644 index 000000000..4838315b7 --- /dev/null +++ b/.changes/v2.15.0/424-features.md @@ -0,0 +1 @@ +* Add methods `catalog.PublishToExternalOrganizations` and `adminCatalog.PublishToExternalOrganizations` [GH-424] diff --git a/.changes/v2.15.0/430-deprecations.md b/.changes/v2.15.0/430-deprecations.md new file mode 100644 index 000000000..d3bf47b13 --- /dev/null +++ b/.changes/v2.15.0/430-deprecations.md @@ -0,0 +1,20 @@ +* Deprecated `vm.DeleteMetadata` in favor of `vm.DeleteMetadataEntry` [GH-430] +* Deprecated `vm.AddMetadata` in favor of `vm.AddMetadataEntry` [GH-430] +* Deprecated `vdc.DeleteMetadata` in favor of `vdc.DeleteMetadataEntry` [GH-430] +* Deprecated `vdc.AddMetadata` in favor of `vdc.AddMetadataEntry` [GH-430] +* Deprecated `vdc.AddMetadataAsync` in favor of `vdc.AddMetadataEntryAsync` [GH-430] +* Deprecated `vdc.DeleteMetadataAsync` in favor of `vdc.DeleteMetadataEntryAsync` [GH-430] +* Deprecated `vApp.DeleteMetadata` in favor of `vApp.DeleteMetadataEntry` [GH-430] +* Deprecated `vApp.AddMetadata` in favor of `vApp.AddMetadataEntry` [GH-430] +* Deprecated `vAppTemplate.AddMetadata` in favor of `vAppTemplate.AddMetadataEntry` [GH-430] +* Deprecated `vAppTemplate.AddMetadataAsync` in favor of `vAppTemplate.AddMetadataEntryAsync` [GH-430] +* Deprecated `vAppTemplate.DeleteMetadata` in favor of `vAppTemplate.DeleteMetadataEntry` [GH-430] +* Deprecated `vAppTemplate.DeleteMetadataAsync` in favor of `vAppTemplate.DeleteMetadataEntryAsync` [GH-430] +* Deprecated `mediaRecord.AddMetadata` in favor of `mediaRecord.AddMetadataEntry` [GH-430] +* Deprecated `mediaRecord.AddMetadataAsync` in favor of `mediaRecord.AddMetadataEntryAsync` [GH-430] +* Deprecated `mediaRecord.DeleteMetadata` in favor of `mediaRecord.DeleteMetadataEntry` [GH-430] +* Deprecated `mediaRecord.DeleteMetadataAsync` in favor of `mediaRecord.DeleteMetadataEntryAsync` [GH-430] +* Deprecated `media.AddMetadata` in favor of `media.AddMetadataEntry` [GH-430] +* Deprecated `media.AddMetadataAsync` in favor of `media.AddMetadataEntryAsync` [GH-430] +* Deprecated `media.DeleteMetadata` in favor of `media.DeleteMetadataEntry` [GH-430] +* Deprecated `media.DeleteMetadataAsync` in favor of `media.DeleteMetadataEntryAsync` [GH-430] diff --git a/.changes/v2.15.0/430-features.md b/.changes/v2.15.0/430-features.md new file mode 100644 index 000000000..8cace7caa --- /dev/null +++ b/.changes/v2.15.0/430-features.md @@ -0,0 +1,8 @@ +* Added types `types.MetadataStringValue`, `types.MetadataNumberValue`, `types.MetadataDateTimeValue` and `types.MetadataBooleanValue` + for adding different kind of metadata to entities [GH-430] +* Added support to set, get and delete metadata to AdminCatalog with the methods + `AdminCatalog.AddMetadataEntry`, `AdminCatalog.AddMetadataEntryAsync`, `AdminCatalog.GetMetadata`, + `AdminCatalog.DeleteMetadataEntry` and `AdminCatalog.DeleteMetadataEntryAsync`. [GH-430] +* Added support to get metadata from Catalog with method `Catalog.GetMetadata` [GH-430] +* Added to *VM* and *VApp* the methods `DeleteMetadataEntry`, `DeleteMetadataEntryAsync`, `AddMetadataEntry` and `AddMetadataEntryAsync` + so it follows the same convention as the rest of entities that uses metadata. [GH-430] diff --git a/.changes/v2.15.0/432-deprecations.md b/.changes/v2.15.0/432-deprecations.md new file mode 100644 index 000000000..2d65915c7 --- /dev/null +++ b/.changes/v2.15.0/432-deprecations.md @@ -0,0 +1,2 @@ +* Deprecated `vm.ChangeMemorySize` in favor of `vm.ChangeMemory` [GH-432] +* Deprecated `vm.ChangeCPUCount` and `vm.ChangeCPUCountWithCore` in favor of `vm.ChangeCPU` [GH-432] diff --git a/.changes/v2.15.0/432-features.md b/.changes/v2.15.0/432-features.md new file mode 100644 index 000000000..384765888 --- /dev/null +++ b/.changes/v2.15.0/432-features.md @@ -0,0 +1 @@ +* Add methods `vm.ChangeCPU` and `vm.ChangeMemory` which uses the latest API structure instead deprecated ones [GH-432] diff --git a/.changes/v2.15.0/433-bug-fixes.md b/.changes/v2.15.0/433-bug-fixes.md new file mode 100644 index 000000000..ac30ce087 --- /dev/null +++ b/.changes/v2.15.0/433-bug-fixes.md @@ -0,0 +1,2 @@ +* Fixes Issue #431 "Wrong order in Task structure" [GH-433] +* Fixes Issue where VDC creation with storage profile `enabled=false` wasn't working. `VdcStorageProfile.enabled` and `VdcStorageProfileConfiguration.enabled` changed to pointers [GH-433] diff --git a/.changes/v2.15.0/434-features.md b/.changes/v2.15.0/434-features.md new file mode 100644 index 000000000..df64d4374 --- /dev/null +++ b/.changes/v2.15.0/434-features.md @@ -0,0 +1 @@ +* Add environment variable `GOVCD_API_VERSION` so API version can be set manually [GH-434] diff --git a/.changes/v2.15.0/434-improvements.md b/.changes/v2.15.0/434-improvements.md new file mode 100644 index 000000000..edf9a5cef --- /dev/null +++ b/.changes/v2.15.0/434-improvements.md @@ -0,0 +1 @@ +* Bump Default API Version to V35.0 [GH-434] diff --git a/.changes/v2.15.0/435-bug-fixes.md b/.changes/v2.15.0/435-bug-fixes.md new file mode 100644 index 000000000..08be0a6f5 --- /dev/null +++ b/.changes/v2.15.0/435-bug-fixes.md @@ -0,0 +1 @@ +* Fix method `client.GetStorageProfileByHref` to return IOPS `IopsSettings` [GH-435] diff --git a/.changes/v2.15.0/436-bug-fixes.md b/.changes/v2.15.0/436-bug-fixes.md new file mode 100644 index 000000000..c1271546f --- /dev/null +++ b/.changes/v2.15.0/436-bug-fixes.md @@ -0,0 +1 @@ +* `Vms.VmReference` changed to array to fix incorrect deserialization [GH-436] diff --git a/.changes/v2.15.0/436-features.md b/.changes/v2.15.0/436-features.md new file mode 100644 index 000000000..fa39ea7cf --- /dev/null +++ b/.changes/v2.15.0/436-features.md @@ -0,0 +1,2 @@ +* Disk methods have now the ability to access new properties from API version 36.0. They are: `DiskRecordType.SharingType`, `DiskRecordType.UUID`, `DiskRecordType.Encrypted`, `Disk.SharingType`, `Disk.UUID` and `Disk.Encrypted` [GH-436] +* New method added `Disk.GetAttachedVmsHrefs` [GH-436] diff --git a/.changes/v2.15.0/438-features.md b/.changes/v2.15.0/438-features.md new file mode 100644 index 000000000..051392335 --- /dev/null +++ b/.changes/v2.15.0/438-features.md @@ -0,0 +1,8 @@ +* Added support to set, get and delete metadata to AdminOrg with the methods + `AdminOrg.AddMetadataEntry`, `AdminOrg.AddMetadataEntryAsync`, `AdminOrg.GetMetadata`, + `AdminOrg.DeleteMetadataEntry` and `AdminOrg.DeleteMetadataEntryAsync`. [GH-438] +* Added support to get metadata to Org with the method + `Org.GetMetadata`. [GH-438] +* Added support to set, get and delete metadata to Disk with the methods + `Disk.AddMetadataEntry`, `Disk.AddMetadataEntryAsync`, `Disk.GetMetadata`, + `Disk.DeleteMetadataEntry` and `Disk.DeleteMetadataEntryAsync`. [GH-438] diff --git a/.changes/v2.15.0/439-improvements.md b/.changes/v2.15.0/439-improvements.md new file mode 100644 index 000000000..6cd81592a --- /dev/null +++ b/.changes/v2.15.0/439-improvements.md @@ -0,0 +1,3 @@ +* Add support for `User` entities imported from LDAP, with `IsExternal` property [GH-439] +* Add support for users list attribute for `Group` [GH-439] +* Improve `group.Update()` to avoid sending the users list to VCD to avoid unwanted errors [GH-449] diff --git a/.changes/v2.15.0/440-improvements.md b/.changes/v2.15.0/440-improvements.md new file mode 100644 index 000000000..7f53e7b74 --- /dev/null +++ b/.changes/v2.15.0/440-improvements.md @@ -0,0 +1,6 @@ +* NSX-T Edge Gateway now supports VDC Groups by switching from `OrgVdc` to `OwnerRef` field. + Additional methods `NsxtEdgeGateway.MoveToVdcOrVdcGroup()`, + `Org.GetNsxtEdgeGatewayByNameAndOwnerId()`, `VdcGroup.GetNsxtEdgeGatewayByName()`, + `VdcGroup.GetAllNsxtEdgeGateways()`, `org.GetVdcGroupById` [GH-440] +* Additional helper functions `OwnerIsVdcGroup()`, `OwnerIsVdc()`, `VdcGroup.GetCapabilities()`, + `VdcGroup.IsNsxt()` [GH-440] \ No newline at end of file diff --git a/.changes/v2.15.0/441-bug-fixes.md b/.changes/v2.15.0/441-bug-fixes.md new file mode 100644 index 000000000..3711730a8 --- /dev/null +++ b/.changes/v2.15.0/441-bug-fixes.md @@ -0,0 +1 @@ +* `Catalog.QueryMediaList` method was not working because fmt.Sprintf was being misused [GH-441] diff --git a/.changes/v2.15.0/442-improvements.md b/.changes/v2.15.0/442-improvements.md new file mode 100644 index 000000000..2b400ee1a --- /dev/null +++ b/.changes/v2.15.0/442-improvements.md @@ -0,0 +1,3 @@ +* Added support to set, get and delete metadata to VDC Networks with the methods + `OrgVDCNetwork.AddMetadataEntry`, `OrgVDCNetwork.AddMetadataEntryAsync`, `OrgVDCNetwork.GetMetadata`, + `OrgVDCNetwork.DeleteMetadataEntry` and `OrgVDCNetwork.DeleteMetadataEntryAsync` [GH-442] diff --git a/.changes/v2.15.0/443-features.md b/.changes/v2.15.0/443-features.md new file mode 100644 index 000000000..ef725886f --- /dev/null +++ b/.changes/v2.15.0/443-features.md @@ -0,0 +1,8 @@ +* Added new structure `AnyTypeEdgeGateway` which supports retreving both types of Edge Gateways + (NSX-V and NSX-T) with methods `AdminOrg.GetAnyTypeEdgeGatewayById`, + `Org.GetAnyTypeEdgeGatewayById`, `AnyTypeEdgeGateway.IsNsxt`, `AnyTypeEdgeGateway.IsNsxv`, + `AnyTypeEdgeGateway.GetNsxtEdgeGateway` [GH-443] +* Added functions `VdcGroup.GetCapabilities`, `VdcGroup.IsNsxt`, + `VdcGroup.GetOpenApiOrgVdcNetworkByName`, `VdcGroup.GetAllOpenApiOrgVdcNetworks`, + `Org.GetOpenApiOrgVdcNetworkByNameAndOwnerId` [GH-443] + diff --git a/.changes/v2.15.0/444-improvements.md b/.changes/v2.15.0/444-improvements.md new file mode 100644 index 000000000..8b77c3177 --- /dev/null +++ b/.changes/v2.15.0/444-improvements.md @@ -0,0 +1 @@ +* Add `CanPublishExternally` and `CanSubscribe` attributes to `OrgGeneralSettings` struct. [GH-444] diff --git a/.changes/v2.15.0/450-features.md b/.changes/v2.15.0/450-features.md new file mode 100644 index 000000000..fb8dd0fbe --- /dev/null +++ b/.changes/v2.15.0/450-features.md @@ -0,0 +1,2 @@ +* Add method `AdminOrg.FindCatalogRecords` that allows to query `types.CatalogRecord` by their catalog name. [GH-450] +* Add methods `Client.QueryWithNotEncodedParamsWithHeaders` and `Client.QueryWithNotEncodedParamsWithApiVersionWithHeaders` so HTTP headers can be added now when doing API queries. [GH-450] diff --git a/.changes/v2.15.0/451-features.md b/.changes/v2.15.0/451-features.md new file mode 100644 index 000000000..7e7ac2324 --- /dev/null +++ b/.changes/v2.15.0/451-features.md @@ -0,0 +1 @@ +* Added functions `VdcGroup.GetNsxtFirewallGroupByName` and `VdcGroup.GetNsxtFirewallGroupById` [GH-451] diff --git a/.changes/v2.15.0/452-features.md b/.changes/v2.15.0/452-features.md new file mode 100644 index 000000000..0e86d9749 --- /dev/null +++ b/.changes/v2.15.0/452-features.md @@ -0,0 +1,5 @@ +* Add support for for Network Context Profile lookup using `GetAllNetworkContextProfiles` and + `GetNetworkContextProfilesByNameScopeAndContext` functions [GH-452] +* Add support for NSX-T Distributed Firewall rule management using type `DistributedFirewall` and +`VdcGroup.GetDistributedFirewall`, `VdcGroup.UpdateDistributedFirewall`, +`VdcGroup.DeleteAllDistributedFirewallRules`, `DistributedFirewall.DeleteAllRules` [GH-452] diff --git a/.changes/v2.15.0/454-features.md b/.changes/v2.15.0/454-features.md new file mode 100644 index 000000000..aca216320 --- /dev/null +++ b/.changes/v2.15.0/454-features.md @@ -0,0 +1,5 @@ +* Added support to set, get and delete metadata to the following resources via its HREF: + `catalog`, `catalog item`, `edge gateway`, `independent disk`, `media`, `network`, `org`, `PVDC`, `PVDC storage profile`, `vApp`, `vApp template`,`VDC` and `VDC storage profile`; + with the methods + `VCDClient.GetMetadataByHref`, `VCDClient.AddMetadataEntryByHref`, `VCDClient.AddMetadataEntryByHrefAsync`, + `VCDClient.DeleteMetadataEntryByHref` and `VCDClient.DeleteMetadataEntryByHrefAsync` [GH-454] diff --git a/.changes/v2.15.0/456-features.md b/.changes/v2.15.0/456-features.md new file mode 100644 index 000000000..27ef6a71b --- /dev/null +++ b/.changes/v2.15.0/456-features.md @@ -0,0 +1 @@ +* Added functions `VdcGroup.GetOpenApiOrgVdcNetworkById` and `VdcGroup.CreateOpenApiOrgVdcNetwork` [GH-456] diff --git a/.changes/v2.15.0/457-notes.md b/.changes/v2.15.0/457-notes.md new file mode 100644 index 000000000..ab250c697 --- /dev/null +++ b/.changes/v2.15.0/457-notes.md @@ -0,0 +1 @@ +* Bump `staticcheck` version to 2022.1 with Go 1.18 support [GH-457] \ No newline at end of file diff --git a/.changes/v2.15.0/458-improvements.md b/.changes/v2.15.0/458-improvements.md new file mode 100644 index 000000000..ecfd7041a --- /dev/null +++ b/.changes/v2.15.0/458-improvements.md @@ -0,0 +1,2 @@ +* Add workaround to tests for Org Catalog publishing bug when dealing with LDAP [GH-458] +* Add clean-up actions to some tests that were uploading vAppTemplates/medias to catalogs [GH-458] diff --git a/.changes/v2.15.0/459-improvements.md b/.changes/v2.15.0/459-improvements.md new file mode 100644 index 000000000..4b0d58810 --- /dev/null +++ b/.changes/v2.15.0/459-improvements.md @@ -0,0 +1,3 @@ +* Added support to set, get and delete metadata to OpenAPI VDC Networks through XML with the methods + `OpenApiOrgVdcNetwork.AddMetadataEntry`, `OpenApiOrgVdcNetwork.GetMetadata`, + `OpenApiOrgVdcNetwork.DeleteMetadataEntry` [GH-459] diff --git a/.changes/v2.15.0/460-improvements.md b/.changes/v2.15.0/460-improvements.md new file mode 100644 index 000000000..09e4b2d9f --- /dev/null +++ b/.changes/v2.15.0/460-improvements.md @@ -0,0 +1,2 @@ +* Added `Vdc.GetNsxtAppPortProfileByName` and `VdcGroup.GetNsxtAppPortProfileByName` for NSX-T + Application Port Profile lookup [GH-460] \ No newline at end of file diff --git a/.changes/v2.16.0/465-features.md b/.changes/v2.16.0/465-features.md new file mode 100644 index 000000000..cf15776e2 --- /dev/null +++ b/.changes/v2.16.0/465-features.md @@ -0,0 +1 @@ +* Added support for `DnsServers` on `OpenApiOrgVdcNetworkDhcp` struct [GH-465] diff --git a/.changes/v2.16.0/466-bug-fixes.md b/.changes/v2.16.0/466-bug-fixes.md new file mode 100644 index 000000000..fce73a99d --- /dev/null +++ b/.changes/v2.16.0/466-bug-fixes.md @@ -0,0 +1 @@ +* Fixed method `adminOrg.FindCatalogRecords` to escape name in query URL [GH-466] diff --git a/.changes/v2.16.0/467-features.md b/.changes/v2.16.0/467-features.md new file mode 100644 index 000000000..b9d13e165 --- /dev/null +++ b/.changes/v2.16.0/467-features.md @@ -0,0 +1,2 @@ +* Added new methods `Org.GetAllSecurityTaggedEntities`, `Org.GetAllSecurityTaggedEntitiesByName`, `Org.GetAllSecurityTagValues`, `VM.GetVMSecurityTags`, `Org.UpdateSecurityTag` and `VM.UpdateVMSecurityTags` to deal with security tags [GH-467] +* Added new structs `types.SecurityTag`, `types.SecurityTaggedEntity`, `types.SecurityTagValue` and `types.EntitySecurityTags` [GH-467] diff --git a/.changes/v2.16.0/468-deprecations.md b/.changes/v2.16.0/468-deprecations.md new file mode 100644 index 000000000..58be4d2ff --- /dev/null +++ b/.changes/v2.16.0/468-deprecations.md @@ -0,0 +1,2 @@ +* Deprecated `org.GetVdcComputePolicyById`, `adminOrg.GetVdcComputePolicyById` [GH-468] +* Deprecated `org.GetAllVdcComputePolicies`, `adminOrg.GetAllVdcComputePolicies`, `org.CreateVdcComputePolicy` [GH-468] diff --git a/.changes/v2.16.0/468-improvements.md b/.changes/v2.16.0/468-improvements.md new file mode 100644 index 000000000..ea78d23bd --- /dev/null +++ b/.changes/v2.16.0/468-improvements.md @@ -0,0 +1 @@ +* Added methods `client.CreateVdcComputePolicy`, `client.GetVdcComputePolicyById`, `client.GetAllVdcComputePolicies` [GH-468] diff --git a/.changes/v2.16.0/469-improvements.md b/.changes/v2.16.0/469-improvements.md new file mode 100644 index 000000000..b26f2a777 --- /dev/null +++ b/.changes/v2.16.0/469-improvements.md @@ -0,0 +1,3 @@ +* Added additional methods for convenience of NSX-T Org Network DHCP handling + `OpenApiOrgVdcNetwork.GetOpenApiOrgVdcNetworkDhcp`, `OpenApiOrgVdcNetwork.DeletNetworkDhcp` + `OpenApiOrgVdcNetwork.UpdateDhcp` [GH-469] diff --git a/.changes/v2.16.0/470-features.md b/.changes/v2.16.0/470-features.md new file mode 100644 index 000000000..ffa814433 --- /dev/null +++ b/.changes/v2.16.0/470-features.md @@ -0,0 +1 @@ +* Added `Vdc.GetControlAccess`, `Vdc.SetControlAccess` and `Vdc.DeleteControlAccess` to get, set and delete control access capabilities to VDCs [GH-470] diff --git a/.changes/v2.16.0/471-features.md b/.changes/v2.16.0/471-features.md new file mode 100644 index 000000000..8e2cd9a0c --- /dev/null +++ b/.changes/v2.16.0/471-features.md @@ -0,0 +1,3 @@ +* Added support to set, get and delete metadata to CatalogItem with the methods + `CatalogItem.AddMetadataEntry`, `CatalogItem.AddMetadataEntryAsync`, `CatalogItem.GetMetadata`, + `CatalogItem.DeleteMetadataEntry` and `CatalogItem.DeleteMetadataEntryAsync`. [GH-471] diff --git a/.changes/v2.16.0/473-features.md b/.changes/v2.16.0/473-features.md new file mode 100644 index 000000000..346753509 --- /dev/null +++ b/.changes/v2.16.0/473-features.md @@ -0,0 +1,7 @@ +* Added `AdminCatalog.MergeMetadata`,`AdminCatalog.MergeMetadataAsync`, `AdminOrg.MergeMetadata`, `AdminOrg.MergeMetadataAsync`, +`CatalogItem.MergeMetadata`, `CatalogItem.MergeMetadataAsync`, `Disk.MergeMetadata`, `Disk.MergeMetadataAsync`, `Media.MergeMetadata`, +`Media.MergeMetadataAsync`, `MediaRecord.MergeMetadata`, `MediaRecord.MergeMetadataAsync`, `OpenAPIOrgVdcNetwork.MergeMetadata`, +`OpenAPIOrgVdcNetwork.MergeMetadataAsync`, `OrgVDCNetwork.MergeMetadata`, `OrgVDCNetwork.MergeMetadataAsync`, +`VApp.MergeMetadata`, `VApp.MergeMetadataAsync`, `VAppTemplate.MergeMetadata`, `VAppTemplate.MergeMetadataAsync`, +`VM.MergeMetadata`, `VM.MergeMetadataAsync`, `Vdc.MergeMetadata`, `Vdc.MergeMetadataAsync` to merge metadata, +which both updates existing metadata with same key and adds new entries for the non-existent ones [GH-473] diff --git a/.changes/v2.16.0/478-features.md b/.changes/v2.16.0/478-features.md new file mode 100644 index 000000000..768729581 --- /dev/null +++ b/.changes/v2.16.0/478-features.md @@ -0,0 +1,7 @@ +* Added NSX-T Edge Gateway methods `NsxtEdgeGateway.GetNsxtRouteAdvertisement`, + `NsxtEdgeGateway.GetNsxtRouteAdvertisementWithContext`, + `NsxtEdgeGateway.UpdateNsxtRouteAdvertisement`, + `NsxtEdgeGateway.UpdateNsxtRouteAdvertisementWithContext`, + `NsxtEdgeGateway.DeleteNsxtRouteAdvertisement` and + `NsxtEdgeGateway.DeleteNsxtRouteAdvertisementWithContext` that allow to manage NSX-T Route + Advertisement [GH-478, GH-480] diff --git a/.changes/v2.16.0/479-improvements.md b/.changes/v2.16.0/479-improvements.md new file mode 100644 index 000000000..d6f2e5960 --- /dev/null +++ b/.changes/v2.16.0/479-improvements.md @@ -0,0 +1 @@ +* Added additional support for UDF type ISO files in `catalog.UploadMediaImage` [GH-479] diff --git a/.changes/v2.16.0/480-features.md b/.changes/v2.16.0/480-features.md new file mode 100644 index 000000000..c2123ffc9 --- /dev/null +++ b/.changes/v2.16.0/480-features.md @@ -0,0 +1,5 @@ +* Added new methods `NsxtEdgeGateway.GetBgpConfiguration`, `NsxtEdgeGateway.UpdateBgpConfiguration`, + `NsxtEdgeGateway.DisableBgpConfiguration` for BGP Configuration management on NSX-T Edge Gateway + [GH-480] +* Added new structs `types.EdgeBgpConfig`, `types.EdgeBgpGracefulRestartConfig`, + `types.EdgeBgpConfigVersion` for BGP Configuration management on NSX-T Edge Gateway [GH-480] diff --git a/.changes/v2.16.0/481-bug-fixes.md b/.changes/v2.16.0/481-bug-fixes.md new file mode 100644 index 000000000..e62b8367f --- /dev/null +++ b/.changes/v2.16.0/481-bug-fixes.md @@ -0,0 +1 @@ +* Fixed method `vm.WaitForDhcpIpByNicIndexes` to ignore not found Edge Gateway [GH-481] diff --git a/.changes/v2.16.0/485-improvements.md b/.changes/v2.16.0/485-improvements.md new file mode 100644 index 000000000..b078a39b3 --- /dev/null +++ b/.changes/v2.16.0/485-improvements.md @@ -0,0 +1,3 @@ +* Added `SupportedFeatureSet` attribute to `NsxtAlbServiceEngineGroup` and `NsxtAlbConfig` to +support v37.0 license management for AVI Load Balancer and replace `LicenseType` from +`NsxtAlbController` [GH-485] diff --git a/.changes/v2.16.0/487-features.md b/.changes/v2.16.0/487-features.md new file mode 100644 index 000000000..6389b22ed --- /dev/null +++ b/.changes/v2.16.0/487-features.md @@ -0,0 +1,4 @@ +* Added support for Dynamic Security Groups in VCD 10.3 by expanding `types.NsxtFirewallGroup` to + accommodate fields required for Dynamic Security Groups, implemented automatic API elevation to + v36.0. Added New functions `VdcGroup.CreateNsxtFirewallGroup`, + `NsxtFirewallGroup.IsDynamicSecurityGroup` [GH-487] diff --git a/.changes/v2.16.0/488-features.md b/.changes/v2.16.0/488-features.md new file mode 100644 index 000000000..d72012da5 --- /dev/null +++ b/.changes/v2.16.0/488-features.md @@ -0,0 +1,3 @@ +* Added support for managing NSX-T Edge Gateway BGP IP Prefix Lists. It is done by adding types `EdgeBgpIpPrefixList` and +`types.EdgeBgpIpPrefixList` with functions `CreateBgpIpPrefixList`, `GetAllBgpIpPrefixLists`, +`GetBgpIpPrefixListByName`, `GetBgpIpPrefixListById`, `Update` and `Delete` [GH-488] diff --git a/.changes/v2.16.0/489-features.md b/.changes/v2.16.0/489-features.md new file mode 100644 index 000000000..c8000da22 --- /dev/null +++ b/.changes/v2.16.0/489-features.md @@ -0,0 +1,3 @@ +* Added support for managing NSX-T Edge Gateway BGP Neighbor. It is done by adding types `EdgeBgpNeighbor` and + `types.EdgeBgpNeighbor` with functions `CreateBgpNeighbor`, `GetAllBgpNeighbors`, + `GetBgpNeighborByIp`, `GetBgpNeighborById`, `Update` and `Delete` [GH-489] diff --git a/.changes/v2.17.0/413-improvements.md b/.changes/v2.17.0/413-improvements.md new file mode 100644 index 000000000..d8977837f --- /dev/null +++ b/.changes/v2.17.0/413-improvements.md @@ -0,0 +1 @@ +* Added method `VM.Shutdown` to shut down guest OS [GH-413], [GH-496] diff --git a/.changes/v2.17.0/491-improvements.md b/.changes/v2.17.0/491-improvements.md new file mode 100644 index 000000000..1ab32842d --- /dev/null +++ b/.changes/v2.17.0/491-improvements.md @@ -0,0 +1 @@ +* Add support for MoRef ID on VM record type. Using the MoRef ID, we can then correlate that back to vCenter Server and find the VM with matching MoRef ID [GH-491] diff --git a/.changes/v2.17.0/494-bug-fixes.md b/.changes/v2.17.0/494-bug-fixes.md new file mode 100644 index 000000000..899fca4d4 --- /dev/null +++ b/.changes/v2.17.0/494-bug-fixes.md @@ -0,0 +1 @@ +* Fixed type `types.AdminVdc.ResourcePoolRefs` to make unmarshaling work (read-only) [GH-494] diff --git a/.changes/v2.17.0/495-features.md b/.changes/v2.17.0/495-features.md new file mode 100644 index 000000000..8134915eb --- /dev/null +++ b/.changes/v2.17.0/495-features.md @@ -0,0 +1,5 @@ +* Added new functions to get vApp Templates `Catalog.GetVAppTemplateByName`, `Catalog.GetVAppTemplateById`, `Catalog.GetVAppTemplateByNameOrId`, + `Vdc.GetVAppTemplateByName`, `VCDClient.GetVAppTemplateByHref` and `VCDClient.GetVAppTemplateById` [GH-495], [GH-520] +* Added new functions to query vApp Templates by name `Catalog.QueryVappTemplateWithName`, `Vdc.QueryVappTemplateWithName`, `AdminVdc.QueryVappTemplateWithName` [GH-495] +* Added new functions to delete vApp Templates `VAppTemplate.DeleteAsync` and `VAppTemplate.Delete` [GH-495] +* Added new functions to extract information from vApp Templates `VAppTemplate.GetCatalogName` and `VAppTemplate.GetVdcName` [GH-495] diff --git a/.changes/v2.17.0/497-notes.md b/.changes/v2.17.0/497-notes.md new file mode 100644 index 000000000..4f7e0e991 --- /dev/null +++ b/.changes/v2.17.0/497-notes.md @@ -0,0 +1,10 @@ +* Ran `make fmt` using Go 1.19 release (`fmt` automatically changes doc comment structure). This + will prevent `make static` errors when running tests in pipeline using Go 1.19 [GH-497] +* Updated branding `vCloud Director` -> `VMware Cloud Director` [GH-497] +* Go officially supports 2 last releases. With Go 1.19 being released it means that Go 1.18 is the + minimum officially supported Go version and this set our hands free to use generics in this SDK + (if there is a need for it). `go.mod` is updated to reflect Go minimum version 1.18 [GH-497] +* package `io/ioutil` is deprecated as of Go 1.16. `staticcheck` started complaining about usage of + deprecated packages. As a result this PR switches packages to either `io` or `os` (still the same + functions are used) [GH-497] +* Adjusted `staticcheck` version naming to new format (from `2021.1.2` to `v0.3.3`) [GH-497] diff --git a/.changes/v2.17.0/498-bug-fixes.md b/.changes/v2.17.0/498-bug-fixes.md new file mode 100644 index 000000000..511baa464 --- /dev/null +++ b/.changes/v2.17.0/498-bug-fixes.md @@ -0,0 +1 @@ +* Fixed Test_NsxtSecurityGroupGetAssociatedVms which had name clash [GH-498] diff --git a/.changes/v2.17.0/499-improvements.md b/.changes/v2.17.0/499-improvements.md new file mode 100644 index 000000000..9311128cc --- /dev/null +++ b/.changes/v2.17.0/499-improvements.md @@ -0,0 +1,5 @@ +* Added support for querying VdcStorageProfile: + - functions `QueryAdminOrgVdcStorageProfileByID` and `QueryOrgVdcStorageProfileByID` + - query types `QtOrgVdcStorageProfile` and `QtAdminOrgVdcStorageProfile` + - data struct `QueryResultAdminOrgVdcStorageProfileRecordType` (non admin struct already was here) + [GH-499] diff --git a/.changes/v2.17.0/500-improvements.md b/.changes/v2.17.0/500-improvements.md new file mode 100644 index 000000000..83cbffab8 --- /dev/null +++ b/.changes/v2.17.0/500-improvements.md @@ -0,0 +1 @@ +* Bumped Default API Version to V36.0 (VCD 10.3+) [GH-500] diff --git a/.changes/v2.17.0/501-features.md b/.changes/v2.17.0/501-features.md new file mode 100644 index 000000000..e072730c8 --- /dev/null +++ b/.changes/v2.17.0/501-features.md @@ -0,0 +1,6 @@ +* Added `Client.TestConnection` method to check remote VCD endpoints [GH-447], [GH-501] +* Added `Client.TestConnectionWithDefaults` method that uses `Client.TestConnection` with some + default values [GH-447], [GH-501] +* Changed behavior of `Client.OpenApiPostItem` and `Client.OpenApiPostItemSync` so they accept + response code 200 OK as valid. The reason is `TestConnection` endpoint requires a POST request and + returns a 200OK when successful [GH-447], [GH-501] diff --git a/.changes/v2.17.0/502-features.md b/.changes/v2.17.0/502-features.md new file mode 100644 index 000000000..40e0c9bde --- /dev/null +++ b/.changes/v2.17.0/502-features.md @@ -0,0 +1,4 @@ +* Added new methods `VCDClient.GetProviderVdcByHref`, `VCDClient.GetProviderVdcById`, `VCDClient.GetProviderVdcByName` and `ProviderVdc.Refresh` to retrieve Provider VDCs [GH-502] +* Added new methods `VCDClient.GetProviderVdcExtendedByHref`, `VCDClient.GetProviderVdcExtendedById`, `VCDClient.GetProviderVdcExtendedByName` and `ProviderVdcExtended.Refresh` to retrieve the extended flavor of Provider VDCs [GH-502] +* Added new methods `ProviderVdcExtended.ToProviderVdc`, to convert from an extended Provider VDC to a regular one [GH-502] +* Added new methods `ProviderVdc.GetMetadata`, `ProviderVdc.AddMetadataEntry`, `ProviderVdc.AddMetadataEntryAsync`, `ProviderVdc.MergeMetadataAsync`, `ProviderVdc.MergeMetadata`, `ProviderVdc.DeleteMetadataEntry` and `ProviderVdc.DeleteMetadataEntryAsync` to manage Provider VDCs metadata [GH-502] diff --git a/.changes/v2.17.0/502-improvements.md b/.changes/v2.17.0/502-improvements.md new file mode 100644 index 000000000..fb1a92b5c --- /dev/null +++ b/.changes/v2.17.0/502-improvements.md @@ -0,0 +1,4 @@ +* Created new VDC Compute Policies CRUD methods using OpenAPI v2.0.0: + `VCDClient.GetVdcComputePolicyV2ById`, `VCDClient.GetAllVdcComputePoliciesV2`, `VCDClient.CreateVdcComputePolicyV2`, + `VdcComputePolicyV2.Update`, `VdcComputePolicyV2.Delete` and `AdminVdc.GetAllAssignedVdcComputePoliciesV2`. + This version supports more filtering options like `isVgpuPolicy` [GH-502], [GH-504], [GH-507] diff --git a/.changes/v2.17.0/503-enhancement.md b/.changes/v2.17.0/503-enhancement.md new file mode 100644 index 000000000..184f609dd --- /dev/null +++ b/.changes/v2.17.0/503-enhancement.md @@ -0,0 +1,3 @@ +* Added new field `HostName` in `QueryResultVMRecordType` struct: + * String field containing the hostName value from the XML VMAdminRecord body retrieved + * This info can help to link VM with a cluster since hypervisor is known [GH-503] \ No newline at end of file diff --git a/.changes/v2.17.0/504-bug-fixes.md b/.changes/v2.17.0/504-bug-fixes.md new file mode 100644 index 000000000..777821e58 --- /dev/null +++ b/.changes/v2.17.0/504-bug-fixes.md @@ -0,0 +1 @@ +* Changed `VdcComputePolicy.Description` to a non-omitempty pointer, to be able to send null values to VCD to set empty descriptions. [GH-504] diff --git a/.changes/v2.17.0/504-deprecations.md b/.changes/v2.17.0/504-deprecations.md new file mode 100644 index 000000000..0e16d3034 --- /dev/null +++ b/.changes/v2.17.0/504-deprecations.md @@ -0,0 +1,3 @@ +* Deprecated OpenAPI v1.0.0 VDC Compute Policies CRUD methods in favor of v2.0.0 ones: + `Client.GetVdcComputePolicyById`, `Client.GetAllVdcComputePolicies`, `Client.CreateVdcComputePolicy` + `VdcComputePolicy.Update`, `VdcComputePolicy.Delete` and `AdminVdc.GetAllAssignedVdcComputePolicies` [GH-504] diff --git a/.changes/v2.17.0/504-features.md b/.changes/v2.17.0/504-features.md new file mode 100644 index 000000000..8dcfa53ba --- /dev/null +++ b/.changes/v2.17.0/504-features.md @@ -0,0 +1,2 @@ +* Added new methods `VCDClient.GetVmGroupById`, `VCDClient.GetVmGroupByNamedVmGroupIdAndProviderVdcUrn` and `VCDClient.GetVmGroupByNameAndProviderVdcUrn` to retrieve VM Groups. These are useful to create VM Placement Policies [GH-504] +* Added new methods `VCDClient.GetLogicalVmGroupById`, `VCDClient.CreateLogicalVmGroup` and `LogicalVmGroup.Delete` to manage Logical VM Groups. These are useful to create VM Placement Policies [GH-504] diff --git a/.changes/v2.17.0/505-improvements.md b/.changes/v2.17.0/505-improvements.md new file mode 100644 index 000000000..371c33a38 --- /dev/null +++ b/.changes/v2.17.0/505-improvements.md @@ -0,0 +1 @@ +* Simplified `Test_LDAP` by using a pre-configured LDAP server [GH-505] diff --git a/.changes/v2.17.0/510-deprecations.md b/.changes/v2.17.0/510-deprecations.md new file mode 100644 index 000000000..db43c25f0 --- /dev/null +++ b/.changes/v2.17.0/510-deprecations.md @@ -0,0 +1,18 @@ +* Deprecated the functions `VCDClient.AddMetadataEntryByHrefAsync` + and `VCDClient.AddMetadataEntryByHref` in favor of `VCDClient.AddMetadataEntryWithVisibilityByHrefAsync` + and `VCDClient.AddMetadataEntryWithVisibilityByHref` [GH-510] +* Deprecated the functions `VCDClient.MergeMetadataByHrefAsync` + and `VCDClient.MergeMetadataByHref` in favor of `VCDClient.MergeMetadataWithVisibilityByHrefAsync` + and `VCDClient.MergeMetadataWithVisibilityByHref` [GH-510] +* Deprecated the functions `AddMetadataEntryAsync` and `AddMetadataEntry` from the following entities: + `VM`, `Vdc`, `AdminVdc`, `ProviderVdc`, `VApp`, `VAppTemplate`, `MediaRecord`, `Media`, `AdminCatalog`, `AdminOrg`, `Disk`, + `OrgVDCNetwork`, `CatalogItem` in favor of their `AddMetadataEntryWithVisibilityAsync` and `AddMetadataEntryWithVisibility` + counterparts [GH-510] +* Deprecated the functions `MergeMetadataAsync` and `MergeMetadataAsync` from the following entities: + `VM`, `Vdc`, `AdminVdc`, `ProviderVdc`, `VApp`, `VAppTemplate`, `MediaRecord`, `Media`, `AdminCatalog`, `AdminOrg`, `Disk`, + `OrgVDCNetwork`, `CatalogItem` in favor of their `MergeMetadataWithMetadataValuesAsync` and `MergeMetadataWithMetadataValues` + counterparts [GH-510] +* Deprecated the functions `DeleteMetadata` and `DeleteMetadataAsync` from the following entities: + `VM`, `Vdc`, `AdminVdc`, `ProviderVdc`, `VApp`, `VAppTemplate`, `MediaRecord`, `Media`, `AdminCatalog`, `AdminOrg`, `Disk`, + `OrgVDCNetwork`, `CatalogItem` in favor of their `DeleteMetadataWithDomainAsync` and `DeleteMetadataWithDomain` + counterparts [GH-510] diff --git a/.changes/v2.17.0/510-features.md b/.changes/v2.17.0/510-features.md new file mode 100644 index 000000000..e721518db --- /dev/null +++ b/.changes/v2.17.0/510-features.md @@ -0,0 +1,20 @@ +* Added the function `VCDClient.GetMetadataByKeyAndHref` to get a specific metadata value using an entity reference [GH-510] +* Added the functions `VCDClient.AddMetadataEntryWithVisibilityByHrefAsync` + and `VCDClient.AddMetadataEntryWithVisibilityByHref` to add metadata with both visibility and domain + to any entity by using its reference [GH-510] +* Added the functions `VCDClient.MergeMetadataWithVisibilityByHrefAsync` + and `VCDClient.MergeMetadataWithVisibilityByHref` to merge metadata data supporting also visibility and domain [GH-510] +* Added the functions `VCDClient.DeleteMetadataEntryWithDomainByHrefAsync` + and `VCDClient.DeleteMetadataEntryWithDomainByHref` to delete metadata from an entity using its reference [GH-510] +* Added the function `GetMetadataByKey` to the following entities: + `VM`, `Vdc`, `AdminVdc`, `ProviderVdc`, `VApp`, `VAppTemplate`, `MediaRecord`, `Media`, `Catalog`, `AdminCatalog`, + `Org`, `AdminOrg`, `Disk`, `OrgVDCNetwork`, `CatalogItem`, `OpenApiOrgVdcNetwork` to get a specific metadata value [GH-510] +* Added the functions `AddMetadataEntryWithVisibilityAsync` and `AddMetadataEntryWithVisibility` to the following entities: + `VM`, `AdminVdc`, `ProviderVdc`, `VApp`, `VAppTemplate`, `MediaRecord`, `Media`, `AdminCatalog`, `AdminOrg`, `Disk`, + `OrgVDCNetwork`, `CatalogItem`, `OpenApiOrgVdcNetwork` to add metadata with both visibility and domain to them [GH-510] +* Added the functions `MergeMetadataWithMetadataValuesAsync` and `MergeMetadataWithMetadataValues` to the following entities: + `VM`, `AdminVdc`, `ProviderVdc`, `VApp`, `VAppTemplate`, `MediaRecord`, `Media`, `AdminCatalog`, `AdminOrg`, `Disk`, + `OrgVDCNetwork`, `CatalogItem`, `OpenApiOrgVdcNetwork` to merge metadata data supporting also visibility and domain [GH-510] +* Added the functions `DeleteMetadataEntryWithDomainAsync` and `DeleteMetadataEntryWithDomain` to the following entities: + `VM`, `AdminVdc`, `ProviderVdc`, `VApp`, `VAppTemplate`, `MediaRecord`, `Media`, `AdminCatalog`, `AdminOrg`, `Disk`, + `OrgVDCNetwork`, `CatalogItem`, `OpenApiOrgVdcNetwork` to delete metadata with the domain, that allows deleting metadata present in SYSTEM [GH-510] diff --git a/.changes/v2.17.0/511-features.md b/.changes/v2.17.0/511-features.md new file mode 100644 index 000000000..cc589480c --- /dev/null +++ b/.changes/v2.17.0/511-features.md @@ -0,0 +1,25 @@ +* Switched `go.mod` to use Go 1.19 ([#511](https://github.com/vmware/go-vcloud-director/pull/511)) +* Added `AdminOrg` methods `CreateCatalogFromSubscriptionAsync` and `CreateCatalogFromSubscription` to create a + subscribed catalog [GH-511] +* Added method `AdminCatalog.FullSubscriptionUrl` to return the subscription URL of a published catalog [GH-511] +* Added method `AdminCatalog.WaitForTasks` to wait for catalog tasks to complete [GH-511] +* Added method `AdminCatalog.UpdateSubscriptionParams` to modify the terms of an existing subscription [GH-511] +* Added methods `Catalog.QueryTaskList` and `AdminCatalog.QueryTaskList` to retrieve the tasks associated with a catalog [GH-511] +* Added function `IsValidUrl` to determine if a URL is valid [GH-511] +* Added `AdminCatalog` methods `Sync` and `LaunchSync` to synchronise a subscribed catalog [GH-511] +* Added method `AdminCatalog.GetCatalogHref` to retrieve the HREF of a regular catalog [GH-511] +* Added `AdminCatalog` methods `QueryCatalogItemList`, `QueryVappTemplateList`, and `QueryMediaList` to retrieve lists of + dependent items [GH-511] +* Added `AdminCatalog` methods `LaunchSynchronisationVappTemplates`, `LaunchSynchronisationAllVappTemplates`, + `LaunchSynchronisationMediaItems`, and `LaunchSynchronisationAllMediaItems` to start synchronisation of dependent + items [GH-511] +* Added `AdminCatalog` methods `GetCatalogItemByHref` and `QueryCatalogItem` to retrieve a single Catalog Item [GH-511] +* Added method `CatalogItem.LaunchSync` to start synchronisation of a catalog item [GH-511] +* Added method `CatalogItem.Refresh` to get fresh contents for a catalog item [GH-511] +* Added function `WaitResource` to wait for tasks associated to a gioven resource [GH-511] +* Added function `MinimalShowTask` to display task progress with minimal info [GH-511] +* Added functions `ResourceInProgress` and `ResourceComplete` to check on task activity for a given entity [GH-511] +* Added functions `SkimTasksList`, `SkimTasksListMonitor`, `WaitTaskListCompletion`, `WaitTaskListCompletionMonitor` to + process lists of tasks and lists of task IDs [GH-511] +* Added `Client` methods `GetTaskByHREF` and `GetTaskById` to retrieve individual tasks [GH-511] +* Implemented `QueryItem` for `Task` and `AdminTask` (`GetHref`, `GetName`, `GetType`, `GetParentId`, `GetParentName`, `GetMetadataValue`, `GetDate`) [GH-511] diff --git a/.changes/v2.17.0/512-improvements.md b/.changes/v2.17.0/512-improvements.md new file mode 100644 index 000000000..48585c119 --- /dev/null +++ b/.changes/v2.17.0/512-improvements.md @@ -0,0 +1,4 @@ +* Added VCDClient.GetAllNsxtEdgeClusters for lookup of NSX-T Edge Clusters in wider scopes - + Provider VDC, VDC Group or VDC [GH-512] +* Switched VDC.GetAllNsxtEdgeClusters to use 'orgVdcId' filter instead of '_context' (now deprecated) + [GH-512] diff --git a/.changes/v2.17.0/513-improvements.md b/.changes/v2.17.0/513-improvements.md new file mode 100644 index 000000000..0882863ca --- /dev/null +++ b/.changes/v2.17.0/513-improvements.md @@ -0,0 +1,2 @@ +* Created `VM.UpdateComputePolicyV2` and `VM.UpdateComputePolicyV2Async` that uses v2.0.0 of VDC Compute Policy endpoint + of OpenAPI and allows updating VM Sizing Policies and also VM Placement Policies for a given VM [GH-513] diff --git a/.changes/v2.17.0/515-improvements.md b/.changes/v2.17.0/515-improvements.md new file mode 100644 index 000000000..285d1381a --- /dev/null +++ b/.changes/v2.17.0/515-improvements.md @@ -0,0 +1 @@ +* Added `[]tenant` structure to simplify org user testing [GH-515] diff --git a/.changes/v2.17.0/516-notes.md b/.changes/v2.17.0/516-notes.md new file mode 100644 index 000000000..9b4aa7d28 --- /dev/null +++ b/.changes/v2.17.0/516-notes.md @@ -0,0 +1 @@ +* Added a new GitHub Action to run `gosec` on every push and pull request [GH-516] diff --git a/.changes/v2.17.0/517-notes.md b/.changes/v2.17.0/517-notes.md new file mode 100644 index 000000000..afe0276dd --- /dev/null +++ b/.changes/v2.17.0/517-notes.md @@ -0,0 +1 @@ +* Improved documentation for `types.OpenApiOrgVdcNetworkDhcp` [GH-517] diff --git a/.changes/v2.17.0/518-bug-fixes.md b/.changes/v2.17.0/518-bug-fixes.md new file mode 100644 index 000000000..bb3fff609 --- /dev/null +++ b/.changes/v2.17.0/518-bug-fixes.md @@ -0,0 +1,2 @@ +* Fixed issue [#514](https://github.com/vmware/go-vcloud-director/issues/514) "ignoring pagination + in network queries" [GH-518] diff --git a/.changes/v2.17.0/520-features.md b/.changes/v2.17.0/520-features.md new file mode 100644 index 000000000..b88b11ff8 --- /dev/null +++ b/.changes/v2.17.0/520-features.md @@ -0,0 +1,5 @@ +* Added `VCDClient.QueryMediaById` to query a media record using a media ID [GH-520] +* Added `Vdc.QueryVappSynchronizedVmTemplate` to query a VM inside a vApp Template that must be synchronized in the catalog [GH-520] +* Added `VCDClient.QueryVmInVAppTemplateByHref` and `VCDClient.QuerySynchronizedVmInVAppTemplateByHref` to query a VM + inside a vApp Template by using the latter's hyper-reference [GH-520] +* Added `VCDClient.QuerySynchronizedVAppTemplateById` to get a synchronized vApp Template query record from a vApp Template ID [GH-520] diff --git a/.changes/v2.17.0/520-improvements.md b/.changes/v2.17.0/520-improvements.md new file mode 100644 index 000000000..4862e6ba1 --- /dev/null +++ b/.changes/v2.17.0/520-improvements.md @@ -0,0 +1 @@ +* Improved `Vdc.QueryVappVmTemplate` to avoid querying VMs in vApp Templates that are not synchronized in the catalog [GH-520] diff --git a/.changes/v2.18.0/530-bug-fixes.md b/.changes/v2.18.0/530-bug-fixes.md new file mode 100644 index 000000000..a665bc6c7 --- /dev/null +++ b/.changes/v2.18.0/530-bug-fixes.md @@ -0,0 +1 @@ +* Fixed issue that caused VM Group retrieval to fail if the Provider VDC had more than one Resource Pool [GH-530] diff --git a/.changes/v2.18.0/530-features.md b/.changes/v2.18.0/530-features.md new file mode 100644 index 000000000..f2a45d9c1 --- /dev/null +++ b/.changes/v2.18.0/530-features.md @@ -0,0 +1 @@ +* Added `VCDClient.GetAllAssignedVdcComputePoliciesV2` to retrieve Compute Policies without the need of an `AdminVdc` receiver [GH-530] diff --git a/.changes/v2.18.0/531-features.md b/.changes/v2.18.0/531-features.md new file mode 100644 index 000000000..d77f99488 --- /dev/null +++ b/.changes/v2.18.0/531-features.md @@ -0,0 +1 @@ +* Added `client` methods `QueryCatalogRecords` and `GetCatalogByHref` [GH-531] diff --git a/.changes/v2.18.0/533-bug-fixes.md b/.changes/v2.18.0/533-bug-fixes.md new file mode 100644 index 000000000..8e23aa8c1 --- /dev/null +++ b/.changes/v2.18.0/533-bug-fixes.md @@ -0,0 +1 @@ +* Fixed issue that prevented Org update because of wrong field position in LDAP settings [GH-533] diff --git a/.changes/v2.19.0/537-bug-fixes.md b/.changes/v2.19.0/537-bug-fixes.md new file mode 100644 index 000000000..384b312d2 --- /dev/null +++ b/.changes/v2.19.0/537-bug-fixes.md @@ -0,0 +1 @@ +* Removed URL checks from `CreateCatalogFromSubscriptionAsync` to allow catalog creation from non-VCD entities, such as vSphere shared library [GH-537] diff --git a/.changes/v2.19.0/537-features.md b/.changes/v2.19.0/537-features.md new file mode 100644 index 000000000..5cfa3d969 --- /dev/null +++ b/.changes/v2.19.0/537-features.md @@ -0,0 +1,3 @@ +* Added client methods `GetCatalogByHref`, `GetCatalogById`, `GetCatalogByName` to retrieve Catalogs without an Org object [GH-537] +* Added client methods `GetAdminCatalogByHref`, `GetAdminCatalogById`, `GetAdminCatalogByName` to retrieve AdminCatalogs without an AdminOrg object [GH-537] +* Added method `VAppTemplate.GetVappTemplateRecord` to retrieve a VAppTemplate query record [GH-537] diff --git a/.changes/v2.19.0/538-notes.md b/.changes/v2.19.0/538-notes.md new file mode 100644 index 000000000..8eccdbdc9 --- /dev/null +++ b/.changes/v2.19.0/538-notes.md @@ -0,0 +1,2 @@ +* Amended a quirky test `Test_CreateOrgVdcWithFlex` that failed randomly due to recovered VDC Storage Profiles being unordered [GH-538] +* Amended a quirky test `Test_VMPowerOnPowerOff` that failed due to the testing VM not being powered off fast enough [GH-538] diff --git a/.changes/v2.19.0/540-bug-fixes.md b/.changes/v2.19.0/540-bug-fixes.md new file mode 100644 index 000000000..441e8ac7d --- /dev/null +++ b/.changes/v2.19.0/540-bug-fixes.md @@ -0,0 +1 @@ +* Fixed flaky test `Test_CatalogAccessAsOrgUsers` that failed randomly for timing issues [GH-540] diff --git a/.changes/v2.20.0/521-features.md b/.changes/v2.20.0/521-features.md new file mode 100644 index 000000000..605e408ab --- /dev/null +++ b/.changes/v2.20.0/521-features.md @@ -0,0 +1,5 @@ +* Added method `AdminVdc.IsNsxv` to detect whether an Admin VDC is NSX-V [GH-521] +* Added function `NewNsxvDistributedFirewall` to create a new NSX-V distributed firewall [GH-521] +* Added `NsxvDistributedFirewall` methods `GetConfiguration`, `IsEnabled`, `Enable`, `Disable`, `UpdateConfiguration`, `Refresh` to handle CRUD operations with NSX-V distributed firewalls [GH-521] +* Added `NsxvDistributedFirewall` methods `GetServices`, `GetServiceGroups`, `GetServiceById`, `GetServiceByName`, `GetServiceGroupById`, `GetServiceGroupByName` to retrieve specific services or service groups [GH-521] +* Added `NsxvDistributedFirewall` methods `GetServicesByRegex` and `GetServiceGroupsByRegex` to search services or service groups by regular expression [GH-521] diff --git a/.changes/v2.20.0/527-features.md b/.changes/v2.20.0/527-features.md new file mode 100644 index 000000000..ea4c6f3b8 --- /dev/null +++ b/.changes/v2.20.0/527-features.md @@ -0,0 +1,3 @@ +* Added support for Runtime Defined Entity Interfaces with client methods `VCDClient.CreateDefinedInterface`, `VCDClient.GetAllDefinedInterfaces`, + `VCDClient.GetDefinedInterface`, `VCDClient.GetDefinedInterfaceById` and methods to manipulate them `DefinedInterface.Update`, + `DefinedInterface.Delete` [GH-527, GH-566] diff --git a/.changes/v2.20.0/528-features.md b/.changes/v2.20.0/528-features.md new file mode 100644 index 000000000..588909410 --- /dev/null +++ b/.changes/v2.20.0/528-features.md @@ -0,0 +1 @@ +* Added method `VM.GetEnvironment` to retrieve OVF Environment [GH-528] \ No newline at end of file diff --git a/.changes/v2.20.0/532-features.md b/.changes/v2.20.0/532-features.md new file mode 100644 index 000000000..26e5d0222 --- /dev/null +++ b/.changes/v2.20.0/532-features.md @@ -0,0 +1,13 @@ +* Added `NsxtEdgeGateway.Refresh` method to reload NSX-T Edge Gateway structure [GH-532] +* Added `NsxtEdgeGateway.GetUsedIpAddresses` method to fetch used IP addresses in NSX-T Edge + Gateway [GH-532] +* Added `NsxtEdgeGateway.GetUsedIpAddressSlice` method to fetch used IP addresses in a slice + [GH-532] +* Added `NsxtEdgeGateway.GetUnusedExternalIPAddresses` method that can help to find an unused + IP address in an Edge Gateway by given constraints [GH-532,GH-567] +* Added `NsxtEdgeGateway.GetAllUnusedExternalIPAddresses` method that can return all unused IP + addresses in an Edge Gateway [GH-532,GH-567] +* Added `NsxtEdgeGateway.GetAllocatedIpCount` method that sums up `TotalIPCount` fields in all + subnets [GH-532] +* Added `NsxtEdgeGateway.QuickDeallocateIpCount` and `NsxtEdgeGateway.DeallocateIpCount` + methods to manually alter Edge Gateway body for IP deallocation [GH-532] diff --git a/.changes/v2.20.0/536-bug-fixes.md b/.changes/v2.20.0/536-bug-fixes.md new file mode 100644 index 000000000..e8558e523 --- /dev/null +++ b/.changes/v2.20.0/536-bug-fixes.md @@ -0,0 +1,2 @@ +* Fix a bug that prevented returning a specific error while authenticating client with invalid + password [GH-536] diff --git a/.changes/v2.20.0/544-features.md b/.changes/v2.20.0/544-features.md new file mode 100644 index 000000000..43dc90fe1 --- /dev/null +++ b/.changes/v2.20.0/544-features.md @@ -0,0 +1,5 @@ +* Added support for Runtime Defined Entity instances with methods `DefinedEntityType.GetAllRdes`, `DefinedEntityType.GetRdeByName`, + `DefinedEntityType.GetRdeById`, `DefinedEntityType.CreateRde` and methods to manipulate them `DefinedEntity.Resolve`, + `DefinedEntity.Update`, `DefinedEntity.Delete` [GH-544] +* Add generic `Client` methods `OpenApiPostItemAndGetHeaders` and `OpenApiGetItemAndHeaders` to be able to retrieve the + response headers when performing a POST or GET operation to an OpenAPI endpoint [GH-544] diff --git a/.changes/v2.20.0/545-features.md b/.changes/v2.20.0/545-features.md new file mode 100644 index 000000000..eecf06613 --- /dev/null +++ b/.changes/v2.20.0/545-features.md @@ -0,0 +1,3 @@ +* Added support for Runtime Defined Entity Types with client methods `VCDClient.CreateRdeType`, `VCDClient.GetAllRdeTypes`, + `VCDClient.GetRdeType`, `VCDClient.GetRdeTypeById` and methods to manipulate them `DefinedEntityType.Update`, + `DefinedEntityType.Delete` [GH-545, GH-566] diff --git a/.changes/v2.20.0/546-notes.md b/.changes/v2.20.0/546-notes.md new file mode 100644 index 000000000..428b04689 --- /dev/null +++ b/.changes/v2.20.0/546-notes.md @@ -0,0 +1 @@ +* Created `Test_RenameCatalog` for making sure the contents of the Catalog don't change after rename [GH-546] diff --git a/.changes/v2.20.0/549-improvements.md b/.changes/v2.20.0/549-improvements.md new file mode 100644 index 000000000..9b7422833 --- /dev/null +++ b/.changes/v2.20.0/549-improvements.md @@ -0,0 +1,3 @@ +* NSX-T ALB settings for Edge Gateway gain support for IPv6 service network definition (VCD 10.4.0+) + and Transparent mode (VCD 10.4.1+) by adding new fields to `types.NsxtAlbConfig` and automatically + elevating API up to 37.1 [GH-549] diff --git a/.changes/v2.20.0/550-bug-fixes.md b/.changes/v2.20.0/550-bug-fixes.md new file mode 100644 index 000000000..d2e855581 --- /dev/null +++ b/.changes/v2.20.0/550-bug-fixes.md @@ -0,0 +1 @@ +* Fixed accessing uninitialized `Features` field while updating a vApp network [GH-550] \ No newline at end of file diff --git a/.changes/v2.20.0/550-improvements.md b/.changes/v2.20.0/550-improvements.md new file mode 100644 index 000000000..0d171eb2b --- /dev/null +++ b/.changes/v2.20.0/550-improvements.md @@ -0,0 +1 @@ +* Added support for using subnet prefix length while creating vApp networks [GH-550] \ No newline at end of file diff --git a/.changes/v2.20.0/553-improvements.md b/.changes/v2.20.0/553-improvements.md new file mode 100644 index 000000000..e2d76fbe0 --- /dev/null +++ b/.changes/v2.20.0/553-improvements.md @@ -0,0 +1,2 @@ +* Improve NSX-T IPSec VPN type `types.NsxtIpSecVpnTunnel` to support 'Certificate' Authentication + mode [GH-553] diff --git a/.changes/v2.20.0/560-improvements.md b/.changes/v2.20.0/560-improvements.md new file mode 100644 index 000000000..1a41e00c3 --- /dev/null +++ b/.changes/v2.20.0/560-improvements.md @@ -0,0 +1,5 @@ +* Add new field `TransparentModeEnabled` to `types.NsxtAlbVirtualService` which allows to preserve + client IP for NSX-T ALB Virtual Service (VCD 10.4.1+) [GH-560] +* Add new field `MemberGroupRef` to `types.NsxtAlbPool` which allows to define NSX-T ALB Pool + membership by using Edge Firewall Group (`NsxtFirewallGroup`) instead of plain IPs (VCD 10.4.1+) + [GH-560] diff --git a/.changes/v2.20.0/561-features.md b/.changes/v2.20.0/561-features.md new file mode 100644 index 000000000..ea6b08d8b --- /dev/null +++ b/.changes/v2.20.0/561-features.md @@ -0,0 +1,8 @@ +* Add support for NSX-T DHCP Bindings via `OpenApiOrgVdcNetworkDhcpBinding`, + `types.OpenApiOrgVdcNetworkDhcpBinding` and functions + `OpenApiOrgVdcNetwork.CreateOpenApiOrgVdcNetworkDhcpBinding`, + `OpenApiOrgVdcNetwork.GetAllOpenApiOrgVdcNetworkDhcpBindings`, + `OpenApiOrgVdcNetwork.GetOpenApiOrgVdcNetworkDhcpBindingById`, + `OpenApiOrgVdcNetwork.GetOpenApiOrgVdcNetworkDhcpBindingByName`, + `OpenApiOrgVdcNetworkDhcpBinding.Update`, `OpenApiOrgVdcNetworkDhcpBinding.Refresh`, + `OpenApiOrgVdcNetworkDhcpBinding.Delete` [GH-561] diff --git a/.changes/v2.20.0/561-improvements.md b/.changes/v2.20.0/561-improvements.md new file mode 100644 index 000000000..1ec0c39f6 --- /dev/null +++ b/.changes/v2.20.0/561-improvements.md @@ -0,0 +1,3 @@ +* `types.OpenApiOrgVdcNetwork` gets a new read only field `OrgVdcIsNsxTBacked` (available since API + 36.0) which indicates if an Org Network is backed by NSX-T and a function + `OpenApiOrgVdcNetwork.IsNsxt()` [GH-561] diff --git a/.changes/v2.20.0/562-improvements.md b/.changes/v2.20.0/562-improvements.md new file mode 100644 index 000000000..25d068960 --- /dev/null +++ b/.changes/v2.20.0/562-improvements.md @@ -0,0 +1,2 @@ +* Add `SetServiceAccountApiToken` method of `*VCDClient` that allows + authenticating using a service account token file and handles the refresh token rotation [GH-562] diff --git a/.changes/v2.20.0/563-features.md b/.changes/v2.20.0/563-features.md new file mode 100644 index 000000000..476f055f7 --- /dev/null +++ b/.changes/v2.20.0/563-features.md @@ -0,0 +1,4 @@ +* Added QoS Profile lookup functions `GetAllNsxtEdgeGatewayQosProfiles` and + `GetNsxtEdgeGatewayQosProfileByDisplayName` [GH-563] +* Added NSX-T Edge Gateway QoS (Rate Limiting) configuration support `NsxtEdgeGateway.GetQoS` and + `NsxtEdgeGateway.UpdateQoS` [GH-563] 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/.changes/v2.21.0/571-notes.md b/.changes/v2.21.0/571-notes.md new file mode 100644 index 000000000..d106f2493 --- /dev/null +++ b/.changes/v2.21.0/571-notes.md @@ -0,0 +1,2 @@ +* Internal - replaced 'takeStringPointer', 'takeIntAddress', 'takeBoolPointer' with generic 'addrOf' + [GH-571] \ No newline at end of file diff --git a/.changes/v2.21.0/572-notes.md b/.changes/v2.21.0/572-notes.md new file mode 100644 index 000000000..cf7ad67f3 --- /dev/null +++ b/.changes/v2.21.0/572-notes.md @@ -0,0 +1 @@ +* Changed Org enablement status during tests for VCD 10.4.2, to circumvent a VCD bug that prevents creation of disabled Orgs [GH-572] diff --git a/.changes/v2.21.0/573-features.md b/.changes/v2.21.0/573-features.md new file mode 100644 index 000000000..2cae80b9a --- /dev/null +++ b/.changes/v2.21.0/573-features.md @@ -0,0 +1,2 @@ +* Added NSX-T Edge Gateway DHCP forwarding configuration support `NsxtEdgeGateway.GetDhcpForwarder` and + `NsxtEdgeGateway.UpdateDhcpForwarder` [GH-573] diff --git a/.changes/v2.21.0/574-notes.md b/.changes/v2.21.0/574-notes.md new file mode 100644 index 000000000..871f992bf --- /dev/null +++ b/.changes/v2.21.0/574-notes.md @@ -0,0 +1 @@ +* Skipped test `Test_VdcDuplicatedVmPlacementPolicyGetsACleanError` in 10.4.2 as the relevant bug we check for is fixed in that version [GH-574] diff --git a/.changes/v2.21.0/575-features.md b/.changes/v2.21.0/575-features.md new file mode 100644 index 000000000..519d68cce --- /dev/null +++ b/.changes/v2.21.0/575-features.md @@ -0,0 +1,3 @@ +* Added methods to create, get, publish and delete UI Plugins `VCDClient.AddUIPlugin`, `VCDClient.GetAllUIPlugins`, + `VCDClient.GetUIPluginById`, `VCDClient.GetUIPlugin`, `UIPlugin.Update`, `UIPlugin.GetPublishedTenants`, + `UIPlugin.PublishAll`, `UIPlugin.UnpublishAll`, `UIPlugin.Publish`, `UIPlugin.Unpublish`, `UIPlugin.IsTheSameAs` and `UIPlugin.Delete` [GH-575] diff --git a/.changes/v2.21.0/576-features.md b/.changes/v2.21.0/576-features.md new file mode 100644 index 000000000..603341037 --- /dev/null +++ b/.changes/v2.21.0/576-features.md @@ -0,0 +1,5 @@ +* Added AdminOrg methods `GetFederationSettings`, `SetFederationSettings`, `UnsetFederationSettings` to handle organization SAML settings [GH-576] +* Added AdminOrg methods `GetServiceProviderSamlMetadata` and `RetrieveServiceProviderSamlMetadata` to retrieve service provider metadata for current organization [GH-576] +* Added method `Client.RetrieveRemoteDocument` to download a document from a URL [GH-576] +* Added function `ValidateSamlServiceProviderMetadata` to validate service oprovider metadata [GH-576] +* Added function `GetErrorMessageFromErrorSlice` to return a single string from a slice of errors [GH-576] diff --git a/.changes/v2.21.0/577-features.md b/.changes/v2.21.0/577-features.md new file mode 100644 index 000000000..1d4dcad97 --- /dev/null +++ b/.changes/v2.21.0/577-features.md @@ -0,0 +1,7 @@ +* Added Service Account CRUD support via `ServiceAccount` and `types.ServiceAccount`: `VCDClient.CreateServiceAccount`, + `Org.GetServiceAccountById`, `Org.GetAllServiceAccounts`, `Org.GetServiceAccountByName`, + `ServiceAccount.Update`, `ServiceAccount.Authorize`, `ServiceAccount.Grant`, `ServiceAccount.Refresh`, + `ServiceAccount.Revoke`, `*ServiceAccount.Delete`, `*ServiceAccount.GetInitialApiToken` [GH-577] +* Added API Token CRUD support via `Token` and `types.Token`: `VCDClient.CreateToken`,`VCDClient.GetTokenById`, +`VCDClient.GetAllTokens`,`VCDClient.GetTokenByNameAndUsername`, `VCDClient.RegisterToken` , `Token.GetInitialApiToken`, `Token.Delete`, `Client.GetApiToken` [GH-577] + diff --git a/.changes/v2.21.0/578-features.md b/.changes/v2.21.0/578-features.md new file mode 100644 index 000000000..945464900 --- /dev/null +++ b/.changes/v2.21.0/578-features.md @@ -0,0 +1,3 @@ +* Added IP Space CRUD support via `IpSpace` and `types.IpSpace` and `VCDClient.CreateIpSpace`, + `VCDClient.GetAllIpSpaceSummaries`, `VCDClient.GetIpSpaceById`, `VCDClient.GetIpSpaceByName`, + `VCDClient.GetIpSpaceByNameAndOrgId`, `IpSpace.Update`, `IpSpace.Delete` [GH-578] diff --git a/.changes/v2.21.0/579-features.md b/.changes/v2.21.0/579-features.md new file mode 100644 index 000000000..09265d830 --- /dev/null +++ b/.changes/v2.21.0/579-features.md @@ -0,0 +1,13 @@ +* Added IP Space Uplink CRUD support via `IpSpaceUplink` and `types.IpSpaceUplink` and + `VCDClient.CreateIpSpaceUplink`, `VCDClient.GetAllIpSpaceUplinks`, + `VCDClient.GetIpSpaceUplinkById`, `VCDClient.GetIpSpaceUplinkByName`, `IpSpaceUplink.Update`, + `IpSpaceUplink.Delete` [GH-579] +* Added IP Space Allocation CRUD support via `IpSpaceIpAllocation`, `types.IpSpaceIpAllocation`, + `types.IpSpaceIpAllocationRequest`, `types.IpSpaceIpAllocationRequestResult`. Methods + `IpSpace.AllocateIp`, `Org.IpSpaceAllocateIp`, `Org.GetIpSpaceAllocationByTypeAndValue`, + `IpSpace.GetAllIpSpaceAllocations`, `Org.GetIpSpaceAllocationById`, `IpSpaceIpAllocation.Update`, + `IpSpaceIpAllocation.Delete` [GH-579] +* Added IP Space Org assignment to support Custom Quotas via `IpSpaceOrgAssignment`, + `types.IpSpaceOrgAssignment`, `IpSpace.GetAllOrgAssignments`, `IpSpace.GetOrgAssignmentById`, + `IpSpace.GetOrgAssignmentByOrgName`, `IpSpace.GetOrgAssignmentByOrgId`, + `IpSpaceOrgAssignment.Update` [GH-579] \ No newline at end of file diff --git a/.changes/v2.21.0/579-improvements.md b/.changes/v2.21.0/579-improvements.md new file mode 100644 index 000000000..de2520b4a --- /dev/null +++ b/.changes/v2.21.0/579-improvements.md @@ -0,0 +1,2 @@ +* `ExternalNetworkV2` now supports IP Spaces on VCD 10.4.1+ with new fields `UsingIpSpace` and + `DedicatedOrg` [GH-579] \ No newline at end of file diff --git a/.changes/v2.21.0/580-bug-fixes.md b/.changes/v2.21.0/580-bug-fixes.md new file mode 100644 index 000000000..2f8827629 --- /dev/null +++ b/.changes/v2.21.0/580-bug-fixes.md @@ -0,0 +1 @@ +* Fixed [Issue #1066](https://github.com/vmware/terraform-provider-vcd/issues/1066) - Not possible to handle more than 128 storage profiles [GH-580] diff --git a/.changes/v2.21.0/580-features.md b/.changes/v2.21.0/580-features.md new file mode 100644 index 000000000..eaccd06c7 --- /dev/null +++ b/.changes/v2.21.0/580-features.md @@ -0,0 +1,13 @@ +* Added method `VCDClient.QueryNsxtManagerByHref` to retrieve a NSX-T manager by its ID/HREF [GH-580] +* Added method `VCDClient.CreateProviderVdc` to create a Provider VDC [GH-580] +* Added method `VCDClient.ResourcePoolsFromIds` to convert list of IDs to resource pools [GH-580] +* Added `ProviderVdcExtended` methods `AddResourcePools`, `AddStorageProfiles`, `Delete`, `DeleteResourcePools`,`DeleteStorageProfiles`,`Disable`,`Enable`,`GetResourcePools`,`IsEnabled`,`Rename`,`Update` to fully manage a provider VDC [GH-580] +* Added method `NetworkPool.GetOpenApiUrl` to generate the full URL of a network pool [GH-580] +* Added `ResourcePool` methods `GetAvailableHardwareVersions` and `GetDefaultHardwareVersion` to get hardware versions [GH-580] +* Added `VCDClient` method `GetAllResourcePools` to retrieve all resource pools regardless of vCenter affiliation [GH-580] +* Added `VCDClient` method `GetAllVcenters` to retrieve all vCenters [GH-580] +* Added `VCDClient` methods `GetNetworkPoolById`,`GetNetworkPoolByName`,`GetNetworkPoolSummaries` to retrieve network pools [GH-580] +* Added `VCDClient` methods `GetVcenterById`,`GetVcenterByName` to retrieve vCenters [GH-580] +* Added `VCenter` methods `GetAllResourcePools`,`VCenter.GetResourcePoolById`,`VCenter.GetResourcePoolByName` to retrieve resource pools [GH-580] +* Added `VCenter` methods `GetAllStorageProfiles`,`GetStorageProfileById`,`GetStorageProfileByName` to retrieve storage profiles [GH-580] +* Added method `VCenter.GetVimServerUrl` to retrieve the full URL of a vCenter within a VCD [GH-580] diff --git a/.changes/v2.21.0/581-improvements.md b/.changes/v2.21.0/581-improvements.md new file mode 100644 index 000000000..82e350e9a --- /dev/null +++ b/.changes/v2.21.0/581-improvements.md @@ -0,0 +1,2 @@ +* Add a new function `WithIgnoredMetadata` to configure the `Client` to ignore specific metadata entries + in all non-deprecated metadata CRUD methods [GH-581] diff --git a/.changes/v2.21.0/582-features.md b/.changes/v2.21.0/582-features.md new file mode 100644 index 000000000..779d792ca --- /dev/null +++ b/.changes/v2.21.0/582-features.md @@ -0,0 +1,2 @@ +* Added NSX-T Edge Gateway SLAAC Profile (DHCPv6) configuration support + `NsxtEdgeGateway.GetSlaacProfile` and `NsxtEdgeGateway.UpdateSlaacProfile` [GH-582] diff --git a/.changes/v2.21.0/582-improvements.md b/.changes/v2.21.0/582-improvements.md new file mode 100644 index 000000000..153b4ddd3 --- /dev/null +++ b/.changes/v2.21.0/582-improvements.md @@ -0,0 +1,4 @@ +* NSX-T ALB Virtual Service supports IPv6 Virtual Service using field`IPv6VirtualIpAddress` in + `types.NsxtAlbVirtualService` for VCD 10.4.0+ [GH-582] +* Add field `EnableDualSubnetNetwork` to enable Dual-Stack mode for Org VDC networks in + `types.OpenApiOrgVdcNetwork` [GH-582] diff --git a/.changes/v2.21.0/583-notes.md b/.changes/v2.21.0/583-notes.md new file mode 100644 index 000000000..62bb69c5c --- /dev/null +++ b/.changes/v2.21.0/583-notes.md @@ -0,0 +1 @@ +* Added `unit` step to GitHub Actions [GH-583] diff --git a/.changes/v2.21.0/584-features.md b/.changes/v2.21.0/584-features.md new file mode 100644 index 000000000..45f035ff7 --- /dev/null +++ b/.changes/v2.21.0/584-features.md @@ -0,0 +1,9 @@ +* Added RDE Defined Interface Behaviors support with methods `DefinedInterface.AddBehavior`, `DefinedInterface.GetAllBehaviors`, + `DefinedInterface.GetBehaviorById` `DefinedInterface.GetBehaviorByName`, `DefinedInterface.UpdateBehavior` and + `DefinedInterface.DeleteBehavior` [GH-584] +* Added RDE Defined Entity Type Behaviors support with methods `DefinedEntityType.GetAllBehaviors`, + `DefinedEntityType.GetBehaviorById` `DefinedEntityType.GetBehaviorByName`, `DefinedEntityType.UpdateBehaviorOverride` and + `DefinedEntityType.DeleteBehaviorOverride` [GH-584] +* Added RDE Defined Entity Type Behavior Access Controls support with methods `DefinedEntityType.GetAllBehaviorsAccessControls` and + `DefinedEntityType.SetBehaviorAccessControls` [GH-584] +* Added method to invoke Behaviors on Defined Entities `DefinedEntity.InvokeBehavior` and `DefinedEntity.InvokeBehaviorAndMarshal` [GH-584] diff --git a/.changes/v2.21.0/586-features.md b/.changes/v2.21.0/586-features.md new file mode 100644 index 000000000..bf4d65766 --- /dev/null +++ b/.changes/v2.21.0/586-features.md @@ -0,0 +1,6 @@ +* Added support for NSX-T Edge Gateway Static Route configuration via types + `NsxtEdgeGatewayStaticRoute`, `types.NsxtEdgeGatewayStaticRoute` and methods + `NsxtEdgeGateway.CreateStaticRoute`, `NsxtEdgeGateway.GetAllStaticRoutes`, + `NsxtEdgeGateway.GetStaticRouteByNetworkCidr`, `NsxtEdgeGateway.GetStaticRouteByName`, + `NsxtEdgeGateway.GetStaticRouteById`, `NsxtEdgeGatewayStaticRoute.Update`, + `NsxtEdgeGatewayStaticRoute.Delete` [GH-586] diff --git a/.changes/v2.21.0/587-features.md b/.changes/v2.21.0/587-features.md new file mode 100644 index 000000000..c8ea7650f --- /dev/null +++ b/.changes/v2.21.0/587-features.md @@ -0,0 +1,3 @@ +* Added types and methods `DistributedFirewallRule`, `VdcGroup.CreateDistributedFirewallRule`, + `DistributedFirewallRule.Update`, `.DistributedFirewallRuleDelete` to manage NSX-T Distributed + Firewall Rules one by one (opposed to managing all at once using `DistributedFirewall`) [GH-587] diff --git a/.changes/v2.21.0/588-deprecations.md b/.changes/v2.21.0/588-deprecations.md new file mode 100644 index 000000000..50c2c0c3c --- /dev/null +++ b/.changes/v2.21.0/588-deprecations.md @@ -0,0 +1 @@ +* Deprecated method `Vdc.InstantiateVAppTemplate` (wrong implementation and result) in favor of `Vdc.CreateVappFromTemplate` [GH-588] diff --git a/.changes/v2.21.0/588-features.md b/.changes/v2.21.0/588-features.md new file mode 100644 index 000000000..96edb44bb --- /dev/null +++ b/.changes/v2.21.0/588-features.md @@ -0,0 +1,3 @@ +* Added method `Vdc.CreateVappFromTemplate` to create a vApp from a vApp template containing one or more VMs [GH-588] +* Added method `Vdc.CloneVapp` to create a vApp from another vApp [GH-588] +* Added method `VApp.DiscardSuspendedState` to take a vApp out of suspended state [GH-588] diff --git a/.changes/v2.21.0/589-bug-fixes.md b/.changes/v2.21.0/589-bug-fixes.md new file mode 100644 index 000000000..fc66163ed --- /dev/null +++ b/.changes/v2.21.0/589-bug-fixes.md @@ -0,0 +1,2 @@ +* Fixed a bug that caused `Client.GetCertificateFromLibraryByName` and `AdminOrg.GetCertificateFromLibraryByName` to fail + retrieving certificates with `:` character in the name [GH-589] diff --git a/.changes/v2.22.0/557-features.md b/.changes/v2.22.0/557-features.md new file mode 100644 index 000000000..fa31857f6 --- /dev/null +++ b/.changes/v2.22.0/557-features.md @@ -0,0 +1,2 @@ +* Added metadata support to Runtime Defined Entities with methods `rde.GetMetadataByKey`, `rde.GetMetadataById` `rde.GetMetadata`, + `rde.AddMetadata` and generic metadata methods `openApiMetadataEntry.Update` and `openApiMetadataEntry.Delete` [GH-557, GH-632] diff --git a/.changes/v2.22.0/559-features.md b/.changes/v2.22.0/559-features.md new file mode 100644 index 000000000..842594751 --- /dev/null +++ b/.changes/v2.22.0/559-features.md @@ -0,0 +1 @@ +* Added methods `SetReadOnlyAccessControl` and `IsSharedReadOnly` for `Catalog` and `AdminCatalog`, to handle read-only catalog sharing [GH-559] diff --git a/.changes/v2.22.0/559-improvements.md b/.changes/v2.22.0/559-improvements.md new file mode 100644 index 000000000..64a388d23 --- /dev/null +++ b/.changes/v2.22.0/559-improvements.md @@ -0,0 +1 @@ +* Added catalog parent retrieval to `client.GetCatalogByHref` and `client.GetAdminCatalogByHref` to facilitate tenant context handling [GH-559] diff --git a/.changes/v2.22.0/590-bug-fixes.md b/.changes/v2.22.0/590-bug-fixes.md new file mode 100644 index 000000000..a65e5a7a7 --- /dev/null +++ b/.changes/v2.22.0/590-bug-fixes.md @@ -0,0 +1 @@ +* Added handling of catalog creation task, which was leaving the catalog not ready for action in some cases [GH-590, GH-602] diff --git a/.changes/v2.22.0/594-bug-fixes.md b/.changes/v2.22.0/594-bug-fixes.md new file mode 100644 index 000000000..0b7df733d --- /dev/null +++ b/.changes/v2.22.0/594-bug-fixes.md @@ -0,0 +1 @@ +* Fix nil pointer dereference bug while fetching a NSX-V Backed Edge Gateway [GH-594] diff --git a/.changes/v2.22.0/595-bug-fixes.md b/.changes/v2.22.0/595-bug-fixes.md new file mode 100644 index 000000000..c38b07644 --- /dev/null +++ b/.changes/v2.22.0/595-bug-fixes.md @@ -0,0 +1 @@ +* Fix vApp network related tests [GH-595] diff --git a/.changes/v2.22.0/597-improvements.md b/.changes/v2.22.0/597-improvements.md new file mode 100644 index 000000000..2ed786626 --- /dev/null +++ b/.changes/v2.22.0/597-improvements.md @@ -0,0 +1,2 @@ +* Add `VdcGroup.ForceDelete` function to optionally force VDC Group removal, which can be used for + removing VDC Group with child elements [GH-597] diff --git a/.changes/v2.22.0/598-bug-fixes.md b/.changes/v2.22.0/598-bug-fixes.md new file mode 100644 index 000000000..d5655092b --- /dev/null +++ b/.changes/v2.22.0/598-bug-fixes.md @@ -0,0 +1 @@ +* Fixed [Issue #1098](https://github.com/vmware/terraform-provider-vcd/issues/1098) crash in VDC creation [GH-598] diff --git a/.changes/v2.22.0/599-notes.md b/.changes/v2.22.0/599-notes.md new file mode 100644 index 000000000..e163f4e54 --- /dev/null +++ b/.changes/v2.22.0/599-notes.md @@ -0,0 +1 @@ +* Improved the testing configuration to allow customizing the UI Plugin path [GH-599] diff --git a/.changes/v2.22.0/600-notes.md b/.changes/v2.22.0/600-notes.md new file mode 100644 index 000000000..d7b598ed2 --- /dev/null +++ b/.changes/v2.22.0/600-notes.md @@ -0,0 +1 @@ +* Added a configurable timeout to the testing options available in the Makefile [GH-600] diff --git a/.changes/v2.22.0/601-notes.md b/.changes/v2.22.0/601-notes.md new file mode 100644 index 000000000..5d4afc22b --- /dev/null +++ b/.changes/v2.22.0/601-notes.md @@ -0,0 +1 @@ +* Improved test `Test_NsxtApplicationPortProfileTenant` [GH-601] diff --git a/.changes/v2.22.0/605-notes.md b/.changes/v2.22.0/605-notes.md new file mode 100644 index 000000000..cc01d760c --- /dev/null +++ b/.changes/v2.22.0/605-notes.md @@ -0,0 +1 @@ +* Added explicit removal for many resources in tests [GH-605] diff --git a/.changes/v2.22.0/607-deprecations.md b/.changes/v2.22.0/607-deprecations.md new file mode 100644 index 000000000..9b3b994f2 --- /dev/null +++ b/.changes/v2.22.0/607-deprecations.md @@ -0,0 +1 @@ +* Deprecated `UpdateInternalDisksAsync` in favor of `UpdateVmSpecSectionAsync` [GH-607] diff --git a/.changes/v2.22.0/607-features.md b/.changes/v2.22.0/607-features.md new file mode 100644 index 000000000..ca5999f38 --- /dev/null +++ b/.changes/v2.22.0/607-features.md @@ -0,0 +1,7 @@ +* Added `Firmware` field to `VmSpecSection` type and `BootOptions` to `Vm` type [GH-607] +* Added `Vdc` methods `GetHardwareVersion`, `GetHighestHardwareVersion`, +`FindOsFromId` [GH-607] +* Added `VM` methods `UpdateBootOptions`, `UpdateBootOptionsAsync` [GH-607] +* API calls for `AddRawVM`, `CreateStandaloneVmAsync`, `VM.Refresh`, +`VM.UpdateVmSpecSectionAsync`, `addEmptyVmAsyncV10`, `getVMByHrefV10` +and `UpdateBootOptionsAsync` get elevated to API version `37.1` if available, for `VmSpecSection.Firmware` and `BootOptions` support [GH-607] diff --git a/.changes/v2.22.0/608-notes.md b/.changes/v2.22.0/608-notes.md new file mode 100644 index 000000000..f9e2f6b39 --- /dev/null +++ b/.changes/v2.22.0/608-notes.md @@ -0,0 +1,3 @@ +* Improved test `Test_InsertOrEjectMedia` [GH-608] +* Addressed `gosec` 2.17.0 errors [GH-608] + \ No newline at end of file diff --git a/.changes/v2.22.0/609-improvements.md b/.changes/v2.22.0/609-improvements.md new file mode 100644 index 000000000..abd7e4f63 --- /dev/null +++ b/.changes/v2.22.0/609-improvements.md @@ -0,0 +1,3 @@ +* Bumped up minimal API version to 37.0 (drops support for VCD 10.3.x) [GH-609] +* Add struct `IopsResource` to `types.DiskSettings` (in replacement of dropped field `iops`) initially supported in API 37.0 [GH-609] +* Add field `SslEnabled` to struct `types.NsxtAlbPool` initially supported in API 37.0 [GH-609] diff --git a/.changes/v2.22.0/609-notes.md b/.changes/v2.22.0/609-notes.md new file mode 100644 index 000000000..47c2770b2 --- /dev/null +++ b/.changes/v2.22.0/609-notes.md @@ -0,0 +1 @@ +* Changed `Test_AddNewVMFromMultiVmTemplate` to use preloaded vApp template [GH-609] diff --git a/.changes/v2.22.0/609-removals.md b/.changes/v2.22.0/609-removals.md new file mode 100644 index 000000000..7b39133f2 --- /dev/null +++ b/.changes/v2.22.0/609-removals.md @@ -0,0 +1 @@ +* Removed field `iops` from `types.DiskSettings` (dropped in API version 37.0) [GH-609] diff --git a/.changes/v2.22.0/610-improvements.md b/.changes/v2.22.0/610-improvements.md new file mode 100644 index 000000000..00b799b1b --- /dev/null +++ b/.changes/v2.22.0/610-improvements.md @@ -0,0 +1,11 @@ +* New method `NsxtEdgeGateway.GetAllocatedIpCountByUplinkType` complementing existing + `NsxtEdgeGateway.GetAllocatedIpCount`. It will return allocated IP counts by uplink types (works + with VCD 10.4.1+) [GH-610] +* New method `NsxtEdgeGateway.GetPrimaryNetworkAllocatedIpCount` that will return total allocated IP + count for primary uplink (T0 or T0 VRF) [GH-610] +* New field `types.EdgeGatewayUplinks.BackingType` that defines backing type of NSX-T Edge Gateway + Uplink [GH-610] +* NSX-T Edge Gateway functions `GetNsxtEdgeGatewayById`, `GetNsxtEdgeGatewayByName`, + `GetNsxtEdgeGatewayByNameAndOwnerId`, `GetAllNsxtEdgeGateways`, `CreateNsxtEdgeGateway`, + `Refresh`, `Update` will additionally sort uplinks to ensure that element 0 contains primary + network (T0 or T0 VRF) [GH-610] diff --git a/.changes/v2.22.0/613-features.md b/.changes/v2.22.0/613-features.md new file mode 100644 index 000000000..8f8dfb9ba --- /dev/null +++ b/.changes/v2.22.0/613-features.md @@ -0,0 +1,5 @@ +* Added `VCDClient` methods `CreateNetworkPool`, `CreateStandaloneVmAsync`, `CreateNetworkPoolGeneve`, `CreateNetworkPoolVlan`, `CreateNetworkPoolPortGroup` to create a network pool [GH-613] +* Added method `VCDClient.GetAllNsxtTransportZones` to retrieve all NSX-T transport zones [GH-613] +* Added method `VCDClient.GetAllVcenterDistributedSwitches` to retrieve all distributed switches [GH-613] +* Added method `VCDClient.QueryNsxtManagers` to retrieve all NSX-T managers [GH-613] +* Added `NetworkPool` methods `Update`, `Delete`, `GetOpenApiUrl` to manage a network pool [GH-613] diff --git a/.changes/v2.22.0/615-improvements.md b/.changes/v2.22.0/615-improvements.md new file mode 100644 index 000000000..07e1e6f7b --- /dev/null +++ b/.changes/v2.22.0/615-improvements.md @@ -0,0 +1,2 @@ +* Makes `DefinedEntityType` method `SetBehaviorAccessControls` more robust to avoid NullPointerException errors in VCD +when the input is a nil slice [GH-615] diff --git a/.changes/v2.22.0/618-features.md b/.changes/v2.22.0/618-features.md new file mode 100644 index 000000000..272cc1ff2 --- /dev/null +++ b/.changes/v2.22.0/618-features.md @@ -0,0 +1,12 @@ +* Added `NsxtManager` type and function `VCDClient.GetNsxtManagerByName` [GH-618] +* Added support for Segment Profile Template management using new types `NsxtSegmentProfileTemplate` and `types.NsxtSegmentProfileTemplate` [GH-618] +* Added support for reading Segment Profiles provided by NSX-T via functions + `GetAllIpDiscoveryProfiles`, `GetIpDiscoveryProfileByName`, `GetAllMacDiscoveryProfiles`, + `GetMacDiscoveryProfileByName`, `GetAllSpoofGuardProfiles`, `GetSpoofGuardProfileByName`, + `GetAllQoSProfiles`, `GetQoSProfileByName`, `GetAllSegmentSecurityProfiles`, + `GetSegmentSecurityProfileByName` [GH-618] +* Added support for setting default Segment Profiles for NSX-T Org VDC Networks + `OpenApiOrgVdcNetwork.GetSegmentProfile()`, `OpenApiOrgVdcNetwork.UpdateSegmentProfile()` [GH-618] +* Added support for setting global default Segment Profiles + `VCDClient.GetGlobalDefaultSegmentProfileTemplates()`, + `VCDClient.UpdateGlobalDefaultSegmentProfileTemplates()` [GH-618] diff --git a/.changes/v2.22.0/619-features.md b/.changes/v2.22.0/619-features.md new file mode 100644 index 000000000..e51e378f4 --- /dev/null +++ b/.changes/v2.22.0/619-features.md @@ -0,0 +1,3 @@ +* Added new `types` for NSX-T L2 VPN Tunnel session management `NsxtL2VpnTunnel`, `EdgeL2VpnStretchedNetwork`, `types.EdgeL2VpnTunnelStatistics`, `types.EdgeL2VpnTunnelStatus` [GH-619] +* Added new `NsxtEdgeGateway` methods `CreateL2VpnTunnel`, `GetAllL2VpnTunnels`, `GetL2VpnTunnelByName`, `GetL2VpnTunnelById` for creation and retrieval of NSX-T L2 VPN Tunnel sessions [GH-619] +* Added `NsxtL2VpnTunnel` methods `Refresh`, `Update`, `Statistics`, `Status`, `Delete` [GH-619] diff --git a/.changes/v2.22.0/621-features.md b/.changes/v2.22.0/621-features.md new file mode 100644 index 000000000..f8c26e9be --- /dev/null +++ b/.changes/v2.22.0/621-features.md @@ -0,0 +1 @@ +* Added method `Catalog.UploadMediaFile` to upload any file as catalog Media [GH-621,GH-622] diff --git a/.changes/v2.22.0/622-features.md b/.changes/v2.22.0/622-features.md new file mode 100644 index 000000000..7c6a05ada --- /dev/null +++ b/.changes/v2.22.0/622-features.md @@ -0,0 +1 @@ +* Added method `Media.Download` to download a Media item as a byte stream [GH-622] diff --git a/.changes/v2.22.0/623-features.md b/.changes/v2.22.0/623-features.md new file mode 100644 index 000000000..ca99a6006 --- /dev/null +++ b/.changes/v2.22.0/623-features.md @@ -0,0 +1 @@ +* Added `VAppTemplate` methods `GetLease` and `RenewLease` to retrieve and change storage lease [GH-623] diff --git a/.changes/v2.22.0/625-bug-fixes.md b/.changes/v2.22.0/625-bug-fixes.md new file mode 100644 index 000000000..cbceb2e59 --- /dev/null +++ b/.changes/v2.22.0/625-bug-fixes.md @@ -0,0 +1 @@ +* Addressed Issue [1134](https://github.com/vmware/terraform-provider-vcd/issues/1134): Can't use SYSTEM `ldap_mode` [GH-625] diff --git a/.changes/v2.22.0/627-features.md b/.changes/v2.22.0/627-features.md new file mode 100644 index 000000000..60cb9f421 --- /dev/null +++ b/.changes/v2.22.0/627-features.md @@ -0,0 +1,4 @@ +* Added `NsxtEdgeGateway` methods `GetDnsConfig` and `UpdateDnsConfig` [GH-627] +* Added types `types.NsxtEdgeGatewayDns`, `types.NsxtDnsForwarderZoneConfig` + for creation and management of DNS forwarder configuration [GH-627] +* Added `NsxtEdgeGatewayDns` methods `Refresh`, `Update` and `Delete` [GH-627] diff --git a/.changes/v2.22.0/628-improvements.md b/.changes/v2.22.0/628-improvements.md new file mode 100644 index 000000000..d1158b7a7 --- /dev/null +++ b/.changes/v2.22.0/628-improvements.md @@ -0,0 +1,2 @@ +* `types.IpSpace` support Firewall and NAT rule autocreation configuration using + `types.DefaultGatewayServiceConfig` on VCD 10.5.0+ [GH-628] diff --git a/.changes/v2.22.0/629-notes.md b/.changes/v2.22.0/629-notes.md new file mode 100644 index 000000000..a5d19d4c9 --- /dev/null +++ b/.changes/v2.22.0/629-notes.md @@ -0,0 +1 @@ +* Amended `testMetadataIgnore`, used by all metadata tests, to be compatible with VCD 10.5.1 [GH-629] diff --git a/.changes/v2.22.0/632-improvements.md b/.changes/v2.22.0/632-improvements.md new file mode 100644 index 000000000..c60be993b --- /dev/null +++ b/.changes/v2.22.0/632-improvements.md @@ -0,0 +1,2 @@ +* Added metadata ignore support for Runtime Defined Entity metadata methods `rde.GetMetadataByKey`, `rde.GetMetadata`, `rde.AddMetadata`, + `rde.UpdateMetadata` and `rde.DeleteMetadata` [GH-632] diff --git a/.changes/v2.22.0/633-features.md b/.changes/v2.22.0/633-features.md new file mode 100644 index 000000000..4fcd66f1b --- /dev/null +++ b/.changes/v2.22.0/633-features.md @@ -0,0 +1,2 @@ +* Add type `VgpuProfile` and its methods `GetAllVgpuProfiles`, `GetVgpuProfilesByProviderVdc`, `GetVgpuProfileById`, `GetVgpuProfileByName`, `GetVgpuProfileByTenantFacingName`, `Update` and `Refresh` for managing vGPU profiles [GH-633] +* Update `ComputePolicyV2` type with new fields for managing vGPU policies [GH-633] diff --git a/.changes/v2.23.0/639-notes.md b/.changes/v2.23.0/639-notes.md new file mode 100644 index 000000000..598a4d731 --- /dev/null +++ b/.changes/v2.23.0/639-notes.md @@ -0,0 +1,2 @@ +* Removed the conditional API call with outdated API version from `Client.GetStorageProfileByHref` so it works + with the newest VCD versions [GH-639] diff --git a/.changes/v2.23.0/643-notes.md b/.changes/v2.23.0/643-notes.md new file mode 100644 index 000000000..fbf161589 --- /dev/null +++ b/.changes/v2.23.0/643-notes.md @@ -0,0 +1,3 @@ +* Added a delay for all LDAP tests `Test_LDAP` after LDAP configuration, but before using them + [GH-643] + diff --git a/.changes/v2.23.0/644-notes.md b/.changes/v2.23.0/644-notes.md new file mode 100644 index 000000000..3f7e4a4cd --- /dev/null +++ b/.changes/v2.23.0/644-notes.md @@ -0,0 +1,7 @@ +* Added internal generic functions to handle CRUD operations for inner and outer entities [GH-644] +* Added section about OpenAPI CRUD functions to `CODING_GUIDELINES.md` [GH-644] +* Converted `DefinedEntityType`, `DefinedEntity`, `DefinedInterface`, `IpSpace`, `IpSpaceUplink`, + `DistributedFirewall`, `DistributedFirewallRule`, `NsxtSegmentProfileTemplate`, + `GetAllIpDiscoveryProfiles`, `GetAllMacDiscoveryProfiles`, `GetAllSpoofGuardProfiles`, + `GetAllQoSProfiles`, `GetAllSegmentSecurityProfiles` to use newly introduced generic CRUD + functions [GH-644] diff --git a/.changes/v2.23.0/645-features.md b/.changes/v2.23.0/645-features.md new file mode 100644 index 000000000..54579bc8d --- /dev/null +++ b/.changes/v2.23.0/645-features.md @@ -0,0 +1,20 @@ +* Added the type `CseKubernetesCluster` to manage Container Service Extension Kubernetes clusters for versions 4.1.0, 4.1.1, + 4.2.0 and 4.2.1 [GH-645, GH-653, GH-655] +* Added methods `Org.CseCreateKubernetesCluster` and `Org.CseCreateKubernetesClusterAsync` to create Kubernetes clusters + in a VCD appliance with Container Service Extension installed [GH-645, GH-653, GH-655] +* Added methods `VCDClient.CseGetKubernetesClusterById` and `Org.CseGetKubernetesClustersByName` to retrieve a + Container Service Extension Kubernetes cluster [GH-645, GH-653, GH-655] +* Added the method `CseKubernetesCluster.GetKubeconfig` to retrieve the *kubeconfig* of a provisioned Container Service + Extension Kubernetes cluster [GH-645, GH-653, GH-655] +* Added the method `CseKubernetesCluster.Refresh` to refresh the information and properties of an existing Container + Service Extension Kubernetes cluster [GH-645, GH-653, GH-655] +* Added methods to update a Container Service Extension Kubernetes cluster: `CseKubernetesCluster.UpdateWorkerPools`, + `CseKubernetesCluster.AddWorkerPools`, `CseKubernetesCluster.UpdateControlPlane`, `CseKubernetesCluster.UpgradeCluster`, + `CseKubernetesCluster.SetNodeHealthCheck` and `CseKubernetesCluster.SetAutoRepairOnErrors` [GH-645, GH-653, GH-655] +* Added the method `CseKubernetesCluster.GetSupportedUpgrades` to retrieve all the valid TKGm OVAs that a given Container + Service Extension Kubernetes cluster can use to be upgraded [GH-645, GH-653, GH-655] +* Added the method `CseKubernetesCluster.Delete` to delete a cluster [GH-645, GH-653, GH-655] +* Added types `CseClusterSettings`, `CseControlPlaneSettings`, `CseWorkerPoolSettings` and `CseDefaultStorageClassSettings` + to configure the Container Service Extension Kubernetes clusters creation process [GH-645, GH-653, GH-655] +* Added types `CseClusterUpdateInput`, `CseControlPlaneUpdateInput` and `CseWorkerPoolUpdateInput` to configure the + Container Service Extension Kubernetes clusters update process [GH-645, GH-653, GH-655] diff --git a/.changes/v2.23.0/646-features.md b/.changes/v2.23.0/646-features.md new file mode 100644 index 000000000..733acedf7 --- /dev/null +++ b/.changes/v2.23.0/646-features.md @@ -0,0 +1 @@ +* Added method `client.QueryVmList` to search VMs across VDCs [GH-646] diff --git a/.changes/v2.23.0/646-improvements.md b/.changes/v2.23.0/646-improvements.md new file mode 100644 index 000000000..cd47aaf7e --- /dev/null +++ b/.changes/v2.23.0/646-improvements.md @@ -0,0 +1 @@ +* Added missing field `vdcName` to `types.QueryResultVMRecordType` [GH-646] diff --git a/.changes/v2.23.0/648-improvements.md b/.changes/v2.23.0/648-improvements.md new file mode 100644 index 000000000..466500fd9 --- /dev/null +++ b/.changes/v2.23.0/648-improvements.md @@ -0,0 +1,2 @@ +* Added `VCDClient.GetAllIpSpaceFloatingIpSuggestions` and `types.IpSpaceFloatingIpSuggestion` to + retrieve IP Space IP suggestions [GH-648] diff --git a/.changes/v2.23.0/650-improvements.md b/.changes/v2.23.0/650-improvements.md new file mode 100644 index 000000000..d29cd4baa --- /dev/null +++ b/.changes/v2.23.0/650-improvements.md @@ -0,0 +1,2 @@ +* Added support for VM disk consolidation using `vm.ConsolidateDisksAsync` and `vm.ConsolidateDisks` + [GH-650] diff --git a/.changes/v2.23.0/652-improvements.md b/.changes/v2.23.0/652-improvements.md new file mode 100644 index 000000000..2886e0abc --- /dev/null +++ b/.changes/v2.23.0/652-improvements.md @@ -0,0 +1,8 @@ +* Added public method `VApp.GetParentVDC` to retrieve parent VDC of vApp (previously it was private) + [GH-652] +* Added methods `Catalog.CaptureVappTemplate`, `Catalog.CaptureVappTemplateAsync` and type + `types.CaptureVAppParams` that add support for creating catalog template from existing vApp + [GH-652] +* Added method `Org.GetVAppByHref` to retrieve a vApp by given HREF [GH-652] +* Added methods `VAppTemplate.GetCatalogItemHref` and `VAppTemplate.GetCatalogItemId` that can return + related catalog item ID and HREF [GH-652] diff --git a/.changes/v2.24.0/657-features.md b/.changes/v2.24.0/657-features.md new file mode 100644 index 000000000..bd539af4e --- /dev/null +++ b/.changes/v2.24.0/657-features.md @@ -0,0 +1,2 @@ +* Added method `Client.QueryVappNetworks` to retrieve all vApp networks [GH-657] +* Added `VApp` methods `QueryAllVappNetworks`, `QueryVappNetworks`, `QueryVappOrgNetworks` to retrieve various types of vApp networks [GH-657] diff --git a/.changes/v2.24.0/663-bug-fixes.md b/.changes/v2.24.0/663-bug-fixes.md new file mode 100644 index 000000000..9cccb0fed --- /dev/null +++ b/.changes/v2.24.0/663-bug-fixes.md @@ -0,0 +1,5 @@ +* Fixed an issue that prevented CSE Kubernetes clusters from being upgraded to an OVA with higher Kubernetes version but same TKG version, + and to an OVA with a higher patch version of Kubernetes [GH-663] +* Fixed an issue that prevented CSE Kubernetes clusters from being upgraded to TKG v2.5.0 with Kubernetes v1.26.11 as it + performed an invalid upgrade of CoreDNS [GH-663] +* Fixed an issue that prevented reading the SSH Public Key from provisioned CSE Kubernetes clusters [GH-663] diff --git a/.changes/v2.25.0/656-improvements.md b/.changes/v2.25.0/656-improvements.md new file mode 100644 index 000000000..fdf5e6690 --- /dev/null +++ b/.changes/v2.25.0/656-improvements.md @@ -0,0 +1,7 @@ +* Improved log traceability by sending `X-VMWARE-VCLOUD-CLIENT-REQUEST-ID` header in requests. The + header will be formatted in such format `162-2024-04-11-08-41-34-171-` where the first number + (`162`) is the API call sequence number in the life of that particular process followed by a + hyphen separated date time with millisecond precision (`2024-04-11-08-41-34-171` for April 11th of + year 2024 at time 08:41:34.171). The trailing hyphen `-` is here to separate response header + `X-Vmware-Vcloud-Request-Id` suffix with double hyphen + `162-2024-04-11-08-41-34-171--40d78874-27a3-4cad-bd43-2764f557226b` [GH-656] \ No newline at end of file diff --git a/.changes/v2.25.0/658-improvements.md b/.changes/v2.25.0/658-improvements.md new file mode 100644 index 000000000..2374cea1f --- /dev/null +++ b/.changes/v2.25.0/658-improvements.md @@ -0,0 +1,2 @@ +* Fix bug in `Client.GetSpecificApiVersionOnCondition` that could result in using unsupported API + version [GH-658] diff --git a/.changes/v2.25.0/659-notes.md b/.changes/v2.25.0/659-notes.md new file mode 100644 index 000000000..b8077b9a2 --- /dev/null +++ b/.changes/v2.25.0/659-notes.md @@ -0,0 +1,2 @@ +* Patched `Test_NsxtL2VpnTunnel` to match PresharedKey of VCD 10.5.1.1+ as it started returning + `******` instead of PSK itself when performing GET [GH-659] diff --git a/.changes/v2.25.0/660-improvements.md b/.changes/v2.25.0/660-improvements.md new file mode 100644 index 000000000..9b9c391a1 --- /dev/null +++ b/.changes/v2.25.0/660-improvements.md @@ -0,0 +1,3 @@ +* Added fields `NatAndFirewallServiceIntention` and `NetworkRouteAdvertisementIntention` to + `types.ExternalNetworkV2`, which allow users to configure NAT, Firewall and Route Advertisement + intentions for provider gateways in VCD 10.5.1+ [GH-660] diff --git a/.changes/v2.25.0/661-improvements.md b/.changes/v2.25.0/661-improvements.md new file mode 100644 index 000000000..d4f70b7a1 --- /dev/null +++ b/.changes/v2.25.0/661-improvements.md @@ -0,0 +1,2 @@ +* Added field `ActionValue` to `types.NsxtFirewallRule` instead of `Action` that is deprecated in + VCD API. It allows users to use `REJECT` option [GH-661] diff --git a/.changes/v2.25.0/665-features.md b/.changes/v2.25.0/665-features.md new file mode 100644 index 000000000..6897021c6 --- /dev/null +++ b/.changes/v2.25.0/665-features.md @@ -0,0 +1,6 @@ +* Added types `SolutionLandingZone` and `types.SolutionLandingZone` for Solution Add-on Landing Zone configuration [GH-665] +* Added method `DefinedEntity.Refresh` to reload RDE state [GH-665] +* Added `VCDClient` methods `CreateSolutionLandingZone`, `GetAllSolutionLandingZones`, + `GetExactlyOneSolutionLandingZone`, `GetSolutionLandingZoneById` for handling Solution Landing Zones [GH-665] +* Added `SolutionLandingZone` methods `Refresh`, `RdeId`, `Update`, + `Delete` to help handling of Solution Landing Zones [GH-665] diff --git a/.changes/v2.25.0/666-features.md b/.changes/v2.25.0/666-features.md new file mode 100644 index 000000000..ed31e0bf7 --- /dev/null +++ b/.changes/v2.25.0/666-features.md @@ -0,0 +1 @@ +* Added `VM` methods `GetExtraConfig`, `UpdateExtraConfig`, `DeleteExtraConfig` to manage VM extra-configuration [GH-666, GH-691] diff --git a/.changes/v2.25.0/669-features.md b/.changes/v2.25.0/669-features.md new file mode 100644 index 000000000..07d88b9cb --- /dev/null +++ b/.changes/v2.25.0/669-features.md @@ -0,0 +1,16 @@ +* Added `Client` method `GetSite` to retrieve generic data about the current site [GH-669] +* Added `Client` methods `GetSiteAssociationData` and `GetSiteRawAssociationData` to retrieve association about from current site [GH-669] +* Added `Client` methods `GetSiteAssociations` and `QueryAllSiteAssociations` to retrieve all site associations from current site [GH-669] +* Added `Client` method `GetSiteAssociationBySiteId` to retrieve a specific site association from current site [GH-669] +* Added `Client` method `CheckSiteAssociation` to check the status of a site association [GH-669] +* Added `Client` methods `SetSiteAssociationAsync` and `SetSiteAssociation` to set a site association with current site [GH-669] +* Added `Client` methods `RemoveSiteAssociationAsync` and `RemoveSiteAssociation` to delete a site association from current site [GH-669] +* Added `Client` method `QueryAllOrgAssociations` and `GetOrgAssociations` to retrieve all org associations visible to current user [GH-669] +* Added `AdminOrg` method `GetOrgAssociationByOrgId` to retrieve a specific organization association from current org [GH-669] +* Added `AdminOrg` methods `GetOrgAssociationData` and `GetOrgRawAssociationData` to retrieve association about from current org [GH-669] +* Added `AdminOrg` method `CheckOrgAssociation` to check the status of an org association [GH-669] +* Added `AdminOrg` methods `SetOrgAssociationAsync` and `SetOrgAssociation` to set an organization association with current org [GH-669] +* Added `AdminOrg` methods `RemoveOrgAssociationAsync` and `RemoveOrgAssociation` to delete an organization association from current org [GH-669] +* Added function `RawDataToStructuredXml` and `ReadXmlDataFromFile` to extract specific data from string or file [GH-669] +* Added `AdminOrg` methods `QueryAllOrgs`, `QueryOrgByName`, `QueryOrgByID` to query organizations [GH-612,GH-669] +* Added `AdminOrg` methods `GetAllOrgs` and `GetAllOpenApiOrgVdcNetworks` to retrieve organizations and networks available to current user [GH-669] diff --git a/.changes/v2.25.0/670-features.md b/.changes/v2.25.0/670-features.md new file mode 100644 index 000000000..6dc044d0a --- /dev/null +++ b/.changes/v2.25.0/670-features.md @@ -0,0 +1,8 @@ +* Added types `SolutionAddOn`, `SolutionAddOnConfig` and `types.SolutionAddOn` for Solution Add-on + Landing configuration [GH-670] +* Added `VCDClient` methods `CreateSolutionAddOn`, `GetAllSolutionAddons`, `GetSolutionAddonById`, + `GetSolutionAddonByName` for handling Solution Add-Ons [GH-670] +* Added `SolutionAddOn` methods `Update`, `RdeId`, `Delete` to help handling of Solution Landing + Zones [GH-670] +* Added `VCDClient` method `TrustAddOnImageCertificate` to trust certificate if it is not yet + trusted [GH-670] \ No newline at end of file diff --git a/.changes/v2.25.0/671-features.md b/.changes/v2.25.0/671-features.md new file mode 100644 index 000000000..adea88743 --- /dev/null +++ b/.changes/v2.25.0/671-features.md @@ -0,0 +1,2 @@ +* Added `AdminOrg` methods `GetOpenIdConnectSettings`, `SetOpenIdConnectSettings` and `DeleteOpenIdConnectSettings` + to manage OpenID Connect settings [GH-671] diff --git a/.changes/v2.25.0/673-improvements.md b/.changes/v2.25.0/673-improvements.md new file mode 100644 index 000000000..a020fcd5e --- /dev/null +++ b/.changes/v2.25.0/673-improvements.md @@ -0,0 +1 @@ +* Added `DetectedGuestOS` to `QueryResultVMRecordType` [GH-673] diff --git a/.changes/v2.25.0/674-bug-fixes.md b/.changes/v2.25.0/674-bug-fixes.md new file mode 100644 index 000000000..f4bfa58d6 --- /dev/null +++ b/.changes/v2.25.0/674-bug-fixes.md @@ -0,0 +1,2 @@ +* Fixed a bug that caused CSE Kubernetes cluster creation to fail when the configured Organization VDC Network belongs to + a VDC Group [GH-674] diff --git a/.changes/v2.25.0/677-bug-fixes.md b/.changes/v2.25.0/677-bug-fixes.md new file mode 100644 index 000000000..9be4f6ad7 --- /dev/null +++ b/.changes/v2.25.0/677-bug-fixes.md @@ -0,0 +1,2 @@ +* Patched `vm.UpdateNetworkConnectionSection` method that could sometimes fail due to Go's XML + library mishandling XML namespaces when VCD returns irregular payload [GH-677] diff --git a/.changes/v2.25.0/678-features.md b/.changes/v2.25.0/678-features.md new file mode 100644 index 000000000..a3ad5525e --- /dev/null +++ b/.changes/v2.25.0/678-features.md @@ -0,0 +1,2 @@ +* Added autoscaling capabilities when creating or updating CSE Kubernetes clusters, with `CseWorkerPoolSettings.Autoscaler` + and `CseWorkerPoolUpdateInput.Autoscaler`, that allows to configure this mechanism on specific worker pools [GH-678] diff --git a/.changes/v2.25.0/679-features.md b/.changes/v2.25.0/679-features.md new file mode 100644 index 000000000..cc254ea29 --- /dev/null +++ b/.changes/v2.25.0/679-features.md @@ -0,0 +1,8 @@ +* Added types `SolutionAddOnInstance` and `types.SolutionAddOnInstance` for Solution Add-on Instance + management [GH-679] +* Added `VCDClient` methods `GetAllSolutionAddonInstanceByName`, `GetAllSolutionAddonInstances`, + `GetSolutionAddOnInstanceById` [GH-679] +* Added `SolutionAddOn` methods `CreateSolutionAddOnInstance`, `GetAllInstances`, + `GetInstanceByName`, `ValidateInputs`, `ConvertInputTypes` [GH-679] +* Added `SolutionAddOnInstance` methods `GetParentSolutionAddOn`, `ReadCreationInputValues` + `Delete`, `RdeId`, `Publishing` [GH-679] diff --git a/.changes/v2.25.0/679-improvements.md b/.changes/v2.25.0/679-improvements.md new file mode 100644 index 000000000..468ba87d6 --- /dev/null +++ b/.changes/v2.25.0/679-improvements.md @@ -0,0 +1,2 @@ +* Added convenience method `DefinedEntity.State()` that will automatically check if path to *State + has no nil pointers [GH-679] diff --git a/.changes/v2.25.0/680-bug-fixes.md b/.changes/v2.25.0/680-bug-fixes.md new file mode 100644 index 000000000..addc99284 --- /dev/null +++ b/.changes/v2.25.0/680-bug-fixes.md @@ -0,0 +1,2 @@ +* Patched `VdcGroup.CreateDistributedFirewallRule` method that returned incorrect single rule when + `optionalAboveRuleId` is specified [GH-680] diff --git a/.changes/v2.25.0/681-notes.md b/.changes/v2.25.0/681-notes.md new file mode 100644 index 000000000..fdebf4b70 --- /dev/null +++ b/.changes/v2.25.0/681-notes.md @@ -0,0 +1 @@ +* Amended the test `Test_RdeAndRdeType` to be compatible with VCD 10.6+ [GH-681] diff --git a/.changes/v2.25.0/682-improvements.md b/.changes/v2.25.0/682-improvements.md new file mode 100644 index 000000000..b407bf95f --- /dev/null +++ b/.changes/v2.25.0/682-improvements.md @@ -0,0 +1,3 @@ +* Added method `NsxtEdgeGateway.GetUsedAndUnusedExternalIPAddressCountWithLimit` to count used + and unused IPs assigned to Edge Gateway. It supports a `limitTo` argument that can prevent + exhausting system resources when counting IPs in assigned subnets [GH-682] diff --git a/.changes/v2.25.0/683-bug-fixes.md b/.changes/v2.25.0/683-bug-fixes.md new file mode 100644 index 000000000..1a136989e --- /dev/null +++ b/.changes/v2.25.0/683-bug-fixes.md @@ -0,0 +1,2 @@ +* Patched bug in core OpenAPI handling function `getOpenApiHighestElevatedVersion` that could + sometimes choose unsupported API versions in future VCD versions [GH-683] diff --git a/.changes/v2.25.0/684-notes.md b/.changes/v2.25.0/684-notes.md new file mode 100644 index 000000000..5de2e49c7 --- /dev/null +++ b/.changes/v2.25.0/684-notes.md @@ -0,0 +1 @@ +* Amended many tests to set `ResourceGuaranteedMemory` when spawning a `Flex` VDC [GH-684, GH-685] diff --git a/.changes/v2.25.0/686-features.md b/.changes/v2.25.0/686-features.md new file mode 100644 index 000000000..6fc7ea12b --- /dev/null +++ b/.changes/v2.25.0/686-features.md @@ -0,0 +1,5 @@ +* Added methods to create, read, update and delete VDC Templates: `VCDClient.CreateVdcTemplate`, `VCDClient.GetVdcTemplateById`, +`VCDClient.GetVdcTemplateByName`, `VdcTemplate.Update` and `VdcTemplate.Delete` [GH-686] +* Added methods to manage the access settings of VDC Templates: `VdcTemplate.SetAccessControl` and `VdcTemplate.GetAccessControl` [GH-686] +* Added the `VdcTemplate.InstantiateVdcAsync` and `VdcTemplate.InstantiateVdc` methods to instantiate VDC Templates [GH-686] +* Added the `VCDClient.QueryAdminVdcTemplates` and `Org.QueryVdcTemplates` methods to get all VDC Template records [GH-686] diff --git a/.changes/v2.25.0/688-bug-fixes.md b/.changes/v2.25.0/688-bug-fixes.md new file mode 100644 index 000000000..e4b26737d --- /dev/null +++ b/.changes/v2.25.0/688-bug-fixes.md @@ -0,0 +1,2 @@ +* Fixed an error that occurred when updating an Edge Gateway configuration, with an Edge cluster configuration section + (`OpenAPIEdgeGatewayEdgeClusterConfig`). If this section was added, the update operation failed in VCD 10.6+ [GH-688] diff --git a/.changes/v2.25.0/689-features.md b/.changes/v2.25.0/689-features.md new file mode 100644 index 000000000..52ecaec49 --- /dev/null +++ b/.changes/v2.25.0/689-features.md @@ -0,0 +1,17 @@ +* Added types `DataSolution` and `types.DataSolution` for Data Storage Extension (DSE) management + [GH-689] +* Added `DataSolution` methods `RdeId`, `Name`, `Update`, `Publish`, `Unpublish`, + `PublishRightsBundle`, `UnpublishRightsBundle`, `PublishAccessControls`, + `UnpublishAccessControls`, `GetAllAccessControls`, `GetAllAccessControlsForTenant`, + `GetAllInstanceTemplates`, `PublishAllInstanceTemplates`, `UnPublishAllInstanceTemplates`, + `GetAllDataSolutionOrgConfigs`, `GetDataSolutionOrgConfigForTenant` [GH-689] +* Added `VCDClient` methods `GetAllDataSolutions`, `GetDataSolutionById`, `GetDataSolutionByName`, + `GetAllInstanceTemplates` [GH-689] +* Added types `DataSolutionInstanceTemplate` and `types.DataSolutionInstanceTemplate` for Data + Storage Extension (DSE) Solution Instance Template management [GH-689] +* Added `DataSolutionInstanceTemplate` methods `Name`, `GetAllAccessControls`, + `GetAllAccessControlsForTenant`, `Publish`, `Unpublish`, `RdeId` [GH-689] +* Added types `DataSolutionOrgConfig` and `types.DataSolutionOrgConfig` for Data Storage Extension + (DSE) Solution Instance Org Configuration management [GH-689] +* Added `DataSolutionOrgConfig` methods `CreateDataSolutionOrgConfig`, + `GetAllDataSolutionOrgConfigs`, `Delete`, `RdeId` [GH-689] diff --git a/.changes/v2.25.0/689-improvements.md b/.changes/v2.25.0/689-improvements.md new file mode 100644 index 000000000..5a4f0b570 --- /dev/null +++ b/.changes/v2.25.0/689-improvements.md @@ -0,0 +1,4 @@ +* Added `Message` field to `types.DefinedEntity` that can return a message for an RDE [GH-689] +* Added convenience method `DefinedEntity.State()` that can return string value of state [GH-689] +* Added `DefinedEntity` methods `SetAccessControl`, `GetAllAccessControls`, `GetAccessControlById`, + `DeleteAccessControl` for managing RDE ACLs [GH-689] diff --git a/.changes/v2.25.0/690-bug-fixes.md b/.changes/v2.25.0/690-bug-fixes.md new file mode 100644 index 000000000..15decc4f3 --- /dev/null +++ b/.changes/v2.25.0/690-bug-fixes.md @@ -0,0 +1 @@ +* Patched `vm.updateExtraConfig` method that could sometimes fail due to random mishandling of XML namespaces in upstream libraries [GH-690] diff --git a/.drone.yml b/.drone.yml index 78fe91a5f..71ec28272 100644 --- a/.drone.yml +++ b/.drone.yml @@ -19,4 +19,4 @@ publish: server: https://coverage.vmware.run token: $$COVERAGE_TOKEN when: - branch: master + branch: main diff --git a/.github/workflows/check-code.yml b/.github/workflows/check-code.yml index 89b23402b..f0a8b7158 100644 --- a/.github/workflows/check-code.yml +++ b/.github/workflows/check-code.yml @@ -11,12 +11,12 @@ jobs: steps: - name: Set up Go 1.x - uses: actions/setup-go@v2 + uses: actions/setup-go@v3 with: - go-version: ^1.15 + go-version: '1.22' - name: Check out code into the Go module directory - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: vet run: make vet @@ -26,3 +26,6 @@ jobs: - name: verify run: make tagverify + + - name: unit + run: make testunit diff --git a/.github/workflows/check-security.yml b/.github/workflows/check-security.yml new file mode 100644 index 000000000..265e2f7af --- /dev/null +++ b/.github/workflows/check-security.yml @@ -0,0 +1,16 @@ +name: Run Gosec +on: [ push, pull_request ] +jobs: + gosec: + runs-on: ubuntu-latest + env: + GO111MODULE: on + steps: + - name: Set up Go 1.x + uses: actions/setup-go@v3 + with: + go-version: '1.22' + - name: Checkout Source + uses: actions/checkout@v2 + - name: gosec + run: make security diff --git a/.gitignore b/.gitignore index 677e192c5..e65a8f7b1 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,9 @@ secrets.yml # VScode IDE .vscode +# IntelliJ IDE +*.iml + # Test artifacts govcd/govcd_test_config*.yaml govcd/govcd_test_config*.json @@ -44,3 +47,5 @@ govcd/test_cleanup_list*.json *.bak *.swp +# File cache directory +govcd/test-resources/cache/* diff --git a/CHANGELOG.md b/CHANGELOG.md index 68f428fa1..aa43d46e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,969 @@ -## 2.12.0 (unreleased) +## 2.25.0 (July 2, 2024) +### FEATURES +* Added types `SolutionLandingZone` and `types.SolutionLandingZone` for Solution Add-on Landing Zone configuration ([#665](https://github.com/vmware/go-vcloud-director/pull/665)) +* Added method `DefinedEntity.Refresh` to reload RDE state ([#665](https://github.com/vmware/go-vcloud-director/pull/665)) +* Added `VCDClient` methods `CreateSolutionLandingZone`, `GetAllSolutionLandingZones`, + `GetExactlyOneSolutionLandingZone`, `GetSolutionLandingZoneById` for handling Solution Landing Zones ([#665](https://github.com/vmware/go-vcloud-director/pull/665)) +* Added `SolutionLandingZone` methods `Refresh`, `RdeId`, `Update`, + `Delete` to help handling of Solution Landing Zones ([#665](https://github.com/vmware/go-vcloud-director/pull/665)) +* Added `VM` methods `GetExtraConfig`, `UpdateExtraConfig`, `DeleteExtraConfig` to manage VM extra-configuration ([#666](https://github.com/vmware/go-vcloud-director/pull/666), [#691](https://github.com/vmware/go-vcloud-director/pull/691)) +* Added `Client` method `GetSite` to retrieve generic data about the current site ([#669](https://github.com/vmware/go-vcloud-director/pull/669)) +* Added `Client` methods `GetSiteAssociationData` and `GetSiteRawAssociationData` to retrieve association about from current site ([#669](https://github.com/vmware/go-vcloud-director/pull/669)) +* Added `Client` methods `GetSiteAssociations` and `QueryAllSiteAssociations` to retrieve all site associations from current site ([#669](https://github.com/vmware/go-vcloud-director/pull/669)) +* Added `Client` method `GetSiteAssociationBySiteId` to retrieve a specific site association from current site ([#669](https://github.com/vmware/go-vcloud-director/pull/669)) +* Added `Client` method `CheckSiteAssociation` to check the status of a site association ([#669](https://github.com/vmware/go-vcloud-director/pull/669)) +* Added `Client` methods `SetSiteAssociationAsync` and `SetSiteAssociation` to set a site association with current site ([#669](https://github.com/vmware/go-vcloud-director/pull/669)) +* Added `Client` methods `RemoveSiteAssociationAsync` and `RemoveSiteAssociation` to delete a site association from current site ([#669](https://github.com/vmware/go-vcloud-director/pull/669)) +* Added `Client` method `QueryAllOrgAssociations` and `GetOrgAssociations` to retrieve all org associations visible to current user ([#669](https://github.com/vmware/go-vcloud-director/pull/669)) +* Added `AdminOrg` method `GetOrgAssociationByOrgId` to retrieve a specific organization association from current org ([#669](https://github.com/vmware/go-vcloud-director/pull/669)) +* Added `AdminOrg` methods `GetOrgAssociationData` and `GetOrgRawAssociationData` to retrieve association about from current org ([#669](https://github.com/vmware/go-vcloud-director/pull/669)) +* Added `AdminOrg` method `CheckOrgAssociation` to check the status of an org association ([#669](https://github.com/vmware/go-vcloud-director/pull/669)) +* Added `AdminOrg` methods `SetOrgAssociationAsync` and `SetOrgAssociation` to set an organization association with current org ([#669](https://github.com/vmware/go-vcloud-director/pull/669)) +* Added `AdminOrg` methods `RemoveOrgAssociationAsync` and `RemoveOrgAssociation` to delete an organization association from current org ([#669](https://github.com/vmware/go-vcloud-director/pull/669)) +* Added function `RawDataToStructuredXml` and `ReadXmlDataFromFile` to extract specific data from string or file ([#669](https://github.com/vmware/go-vcloud-director/pull/669)) +* Added `AdminOrg` methods `QueryAllOrgs`, `QueryOrgByName`, `QueryOrgByID` to query organizations ([#612](https://github.com/vmware/go-vcloud-director/pull/612),[#669](https://github.com/vmware/go-vcloud-director/pull/669)) +* Added `AdminOrg` methods `GetAllOrgs` and `GetAllOpenApiOrgVdcNetworks` to retrieve organizations and networks available to current user ([#669](https://github.com/vmware/go-vcloud-director/pull/669)) +* Added types `SolutionAddOn`, `SolutionAddOnConfig` and `types.SolutionAddOn` for Solution Add-on + Landing configuration ([#670](https://github.com/vmware/go-vcloud-director/pull/670)) +* Added `VCDClient` methods `CreateSolutionAddOn`, `GetAllSolutionAddons`, `GetSolutionAddonById`, + `GetSolutionAddonByName` for handling Solution Add-Ons ([#670](https://github.com/vmware/go-vcloud-director/pull/670)) +* Added `SolutionAddOn` methods `Update`, `RdeId`, `Delete` to help handling of Solution Landing + Zones ([#670](https://github.com/vmware/go-vcloud-director/pull/670)) +* Added `VCDClient` method `TrustAddOnImageCertificate` to trust certificate if it is not yet + trusted ([#670](https://github.com/vmware/go-vcloud-director/pull/670)) +* Added `AdminOrg` methods `GetOpenIdConnectSettings`, `SetOpenIdConnectSettings` and `DeleteOpenIdConnectSettings` + to manage OpenID Connect settings ([#671](https://github.com/vmware/go-vcloud-director/pull/671)) +* Added autoscaling capabilities when creating or updating CSE Kubernetes clusters, with `CseWorkerPoolSettings.Autoscaler` + and `CseWorkerPoolUpdateInput.Autoscaler`, that allows to configure this mechanism on specific worker pools ([#678](https://github.com/vmware/go-vcloud-director/pull/678)) +* Added types `SolutionAddOnInstance` and `types.SolutionAddOnInstance` for Solution Add-on Instance + management ([#679](https://github.com/vmware/go-vcloud-director/pull/679)) +* Added `VCDClient` methods `GetAllSolutionAddonInstanceByName`, `GetAllSolutionAddonInstances`, + `GetSolutionAddOnInstanceById` ([#679](https://github.com/vmware/go-vcloud-director/pull/679)) +* Added `SolutionAddOn` methods `CreateSolutionAddOnInstance`, `GetAllInstances`, + `GetInstanceByName`, `ValidateInputs`, `ConvertInputTypes` ([#679](https://github.com/vmware/go-vcloud-director/pull/679)) +* Added `SolutionAddOnInstance` methods `GetParentSolutionAddOn`, `ReadCreationInputValues` + `Delete`, `RdeId`, `Publishing` ([#679](https://github.com/vmware/go-vcloud-director/pull/679)) +* Added methods to create, read, update and delete VDC Templates: `VCDClient.CreateVdcTemplate`, `VCDClient.GetVdcTemplateById`, +`VCDClient.GetVdcTemplateByName`, `VdcTemplate.Update` and `VdcTemplate.Delete` ([#686](https://github.com/vmware/go-vcloud-director/pull/686)) +* Added methods to manage the access settings of VDC Templates: `VdcTemplate.SetAccessControl` and `VdcTemplate.GetAccessControl` ([#686](https://github.com/vmware/go-vcloud-director/pull/686)) +* Added the `VdcTemplate.InstantiateVdcAsync` and `VdcTemplate.InstantiateVdc` methods to instantiate VDC Templates ([#686](https://github.com/vmware/go-vcloud-director/pull/686)) +* Added the `VCDClient.QueryAdminVdcTemplates` and `Org.QueryVdcTemplates` methods to get all VDC Template records ([#686](https://github.com/vmware/go-vcloud-director/pull/686)) +* Added types `DataSolution` and `types.DataSolution` for Data Storage Extension (DSE) management + ([#689](https://github.com/vmware/go-vcloud-director/pull/689)) +* Added `DataSolution` methods `RdeId`, `Name`, `Update`, `Publish`, `Unpublish`, + `PublishRightsBundle`, `UnpublishRightsBundle`, `PublishAccessControls`, + `UnpublishAccessControls`, `GetAllAccessControls`, `GetAllAccessControlsForTenant`, + `GetAllInstanceTemplates`, `PublishAllInstanceTemplates`, `UnPublishAllInstanceTemplates`, + `GetAllDataSolutionOrgConfigs`, `GetDataSolutionOrgConfigForTenant` ([#689](https://github.com/vmware/go-vcloud-director/pull/689)) +* Added `VCDClient` methods `GetAllDataSolutions`, `GetDataSolutionById`, `GetDataSolutionByName`, + `GetAllInstanceTemplates` ([#689](https://github.com/vmware/go-vcloud-director/pull/689)) +* Added types `DataSolutionInstanceTemplate` and `types.DataSolutionInstanceTemplate` for Data + Storage Extension (DSE) Solution Instance Template management ([#689](https://github.com/vmware/go-vcloud-director/pull/689)) +* Added `DataSolutionInstanceTemplate` methods `Name`, `GetAllAccessControls`, + `GetAllAccessControlsForTenant`, `Publish`, `Unpublish`, `RdeId` ([#689](https://github.com/vmware/go-vcloud-director/pull/689)) +* Added types `DataSolutionOrgConfig` and `types.DataSolutionOrgConfig` for Data Storage Extension + (DSE) Solution Instance Org Configuration management ([#689](https://github.com/vmware/go-vcloud-director/pull/689)) +* Added `DataSolutionOrgConfig` methods `CreateDataSolutionOrgConfig`, + `GetAllDataSolutionOrgConfigs`, `Delete`, `RdeId` ([#689](https://github.com/vmware/go-vcloud-director/pull/689)) + +### IMPROVEMENTS +* Improved log traceability by sending `X-VMWARE-VCLOUD-CLIENT-REQUEST-ID` header in requests. The + header will be formatted in such format `162-2024-04-11-08-41-34-171-` where the first number + (`162`) is the API call sequence number in the life of that particular process followed by a + hyphen separated date time with millisecond precision (`2024-04-11-08-41-34-171` for April 11th of + year 2024 at time 08:41:34.171). The trailing hyphen `-` is here to separate response header + `X-Vmware-Vcloud-Request-Id` suffix with double hyphen + `162-2024-04-11-08-41-34-171--40d78874-27a3-4cad-bd43-2764f557226b` ([#656](https://github.com/vmware/go-vcloud-director/pull/656)) +* Fix bug in `Client.GetSpecificApiVersionOnCondition` that could result in using unsupported API + version ([#658](https://github.com/vmware/go-vcloud-director/pull/658)) +* Added fields `NatAndFirewallServiceIntention` and `NetworkRouteAdvertisementIntention` to + `types.ExternalNetworkV2`, which allow users to configure NAT, Firewall and Route Advertisement + intentions for provider gateways in VCD 10.5.1+ ([#660](https://github.com/vmware/go-vcloud-director/pull/660)) +* Added field `ActionValue` to `types.NsxtFirewallRule` instead of `Action` that is deprecated in + VCD API. It allows users to use `REJECT` option ([#661](https://github.com/vmware/go-vcloud-director/pull/661)) +* Added `DetectedGuestOS` to `QueryResultVMRecordType` ([#673](https://github.com/vmware/go-vcloud-director/pull/673)) +* Added convenience method `DefinedEntity.State()` that will automatically check if path to `*State` has no nil pointers ([#679](https://github.com/vmware/go-vcloud-director/pull/679)) +* Added method `NsxtEdgeGateway.GetUsedAndUnusedExternalIPAddressCountWithLimit` to count used + and unused IPs assigned to Edge Gateway. It supports a `limitTo` argument that can prevent + exhausting system resources when counting IPs in assigned subnets ([#682](https://github.com/vmware/go-vcloud-director/pull/682)) +* Added `Message` field to `types.DefinedEntity` that can return a message for an RDE ([#689](https://github.com/vmware/go-vcloud-director/pull/689)) +* Added convenience method `DefinedEntity.State()` that can return string value of state ([#689](https://github.com/vmware/go-vcloud-director/pull/689)) +* Added `DefinedEntity` methods `SetAccessControl`, `GetAllAccessControls`, `GetAccessControlById`, + `DeleteAccessControl` for managing RDE ACLs ([#689](https://github.com/vmware/go-vcloud-director/pull/689)) + +### BUG FIXES +* Fixed a bug that caused CSE Kubernetes cluster creation to fail when the configured Organization VDC Network belongs to + a VDC Group ([#674](https://github.com/vmware/go-vcloud-director/pull/674)) +* Patched `vm.UpdateNetworkConnectionSection` method that could sometimes fail due to Go's XML + library mishandling XML namespaces when VCD returns irregular payload ([#677](https://github.com/vmware/go-vcloud-director/pull/677)) +* Patched `VdcGroup.CreateDistributedFirewallRule` method that returned incorrect single rule when + `optionalAboveRuleId` is specified ([#680](https://github.com/vmware/go-vcloud-director/pull/680)) +* Patched bug in core OpenAPI handling function `getOpenApiHighestElevatedVersion` that could + sometimes choose unsupported API versions in future VCD versions ([#683](https://github.com/vmware/go-vcloud-director/pull/683)) +* Fixed an error that occurred when updating an Edge Gateway configuration, with an Edge cluster configuration section + (`OpenAPIEdgeGatewayEdgeClusterConfig`). If this section was added, the update operation failed in VCD 10.6+ ([#688](https://github.com/vmware/go-vcloud-director/pull/688)) +* Patched `vm.updateExtraConfig` method that could sometimes fail due to random mishandling of XML namespaces in upstream libraries ([#690](https://github.com/vmware/go-vcloud-director/pull/690)) + +### NOTES +* Patched `Test_NsxtL2VpnTunnel` to match PresharedKey of VCD 10.5.1.1+ as it started returning + `******` instead of PSK itself when performing GET ([#659](https://github.com/vmware/go-vcloud-director/pull/659)) +* Amended the test `Test_RdeAndRdeType` to be compatible with VCD 10.6+ ([#681](https://github.com/vmware/go-vcloud-director/pull/681)) +* Amended many tests to set `ResourceGuaranteedMemory` when spawning a `Flex` VDC ([#684](https://github.com/vmware/go-vcloud-director/pull/684), [#685](https://github.com/vmware/go-vcloud-director/pull/685)) + + +## 2.24.0 (April 18, 2024) + +### FEATURES +* Added method `Client.QueryVappNetworks` to retrieve all vApp networks ([#657](https://github.com/vmware/go-vcloud-director/pull/657)) +* Added `VApp` methods `QueryAllVappNetworks`, `QueryVappNetworks`, `QueryVappOrgNetworks` to retrieve various types of vApp networks ([#657](https://github.com/vmware/go-vcloud-director/pull/657)) + +### BUG FIXES +* Fixed an issue that prevented CSE Kubernetes clusters from being upgraded to an OVA with higher Kubernetes version but same TKG version, + and to an OVA with a higher patch version of Kubernetes ([#663](https://github.com/vmware/go-vcloud-director/pull/663)) +* Fixed an issue that prevented CSE Kubernetes clusters from being upgraded to TKG v2.5.0 with Kubernetes v1.26.11 as it + performed an invalid upgrade of CoreDNS ([#663](https://github.com/vmware/go-vcloud-director/pull/663)) +* Fixed an issue that prevented reading the SSH Public Key from provisioned CSE Kubernetes clusters ([#663](https://github.com/vmware/go-vcloud-director/pull/663)) + +## 2.23.0 (March 22, 2024) + +### FEATURES +* Added the type `CseKubernetesCluster` to manage Container Service Extension Kubernetes clusters for versions 4.1.0, 4.1.1, + 4.2.0 and 4.2.1 ([#645](https://github.com/vmware/go-vcloud-director/pull/645), [#653](https://github.com/vmware/go-vcloud-director/pull/653), [#655](https://github.com/vmware/go-vcloud-director/pull/655)) +* Added methods `Org.CseCreateKubernetesCluster` and `Org.CseCreateKubernetesClusterAsync` to create Kubernetes clusters + in a VCD appliance with Container Service Extension installed ([#645](https://github.com/vmware/go-vcloud-director/pull/645), [#653](https://github.com/vmware/go-vcloud-director/pull/653), [#655](https://github.com/vmware/go-vcloud-director/pull/655)) +* Added methods `VCDClient.CseGetKubernetesClusterById` and `Org.CseGetKubernetesClustersByName` to retrieve a + Container Service Extension Kubernetes cluster ([#645](https://github.com/vmware/go-vcloud-director/pull/645), [#653](https://github.com/vmware/go-vcloud-director/pull/653), [#655](https://github.com/vmware/go-vcloud-director/pull/655)) +* Added the method `CseKubernetesCluster.GetKubeconfig` to retrieve the *kubeconfig* of a provisioned Container Service + Extension Kubernetes cluster ([#645](https://github.com/vmware/go-vcloud-director/pull/645), [#653](https://github.com/vmware/go-vcloud-director/pull/653), [#655](https://github.com/vmware/go-vcloud-director/pull/655)) +* Added the method `CseKubernetesCluster.Refresh` to refresh the information and properties of an existing Container + Service Extension Kubernetes cluster ([#645](https://github.com/vmware/go-vcloud-director/pull/645), [#653](https://github.com/vmware/go-vcloud-director/pull/653), [#655](https://github.com/vmware/go-vcloud-director/pull/655)) +* Added methods to update a Container Service Extension Kubernetes cluster: `CseKubernetesCluster.UpdateWorkerPools`, + `CseKubernetesCluster.AddWorkerPools`, `CseKubernetesCluster.UpdateControlPlane`, `CseKubernetesCluster.UpgradeCluster`, + `CseKubernetesCluster.SetNodeHealthCheck` and `CseKubernetesCluster.SetAutoRepairOnErrors` ([#645](https://github.com/vmware/go-vcloud-director/pull/645), [#653](https://github.com/vmware/go-vcloud-director/pull/653), [#655](https://github.com/vmware/go-vcloud-director/pull/655)) +* Added the method `CseKubernetesCluster.GetSupportedUpgrades` to retrieve all the valid TKGm OVAs that a given Container + Service Extension Kubernetes cluster can use to be upgraded ([#645](https://github.com/vmware/go-vcloud-director/pull/645), [#653](https://github.com/vmware/go-vcloud-director/pull/653), [#655](https://github.com/vmware/go-vcloud-director/pull/655)) +* Added the method `CseKubernetesCluster.Delete` to delete a cluster ([#645](https://github.com/vmware/go-vcloud-director/pull/645), [#653](https://github.com/vmware/go-vcloud-director/pull/653), [#655](https://github.com/vmware/go-vcloud-director/pull/655)) +* Added types `CseClusterSettings`, `CseControlPlaneSettings`, `CseWorkerPoolSettings` and `CseDefaultStorageClassSettings` + to configure the Container Service Extension Kubernetes clusters creation process ([#645](https://github.com/vmware/go-vcloud-director/pull/645), [#653](https://github.com/vmware/go-vcloud-director/pull/653), [#655](https://github.com/vmware/go-vcloud-director/pull/655)) +* Added types `CseClusterUpdateInput`, `CseControlPlaneUpdateInput` and `CseWorkerPoolUpdateInput` to configure the + Container Service Extension Kubernetes clusters update process ([#645](https://github.com/vmware/go-vcloud-director/pull/645), [#653](https://github.com/vmware/go-vcloud-director/pull/653), [#655](https://github.com/vmware/go-vcloud-director/pull/655)) +* Added method `client.QueryVmList` to search VMs across VDCs ([#646](https://github.com/vmware/go-vcloud-director/pull/646)) + +### IMPROVEMENTS +* Added missing field `vdcName` to `types.QueryResultVMRecordType` ([#646](https://github.com/vmware/go-vcloud-director/pull/646)) +* Added `VCDClient.GetAllIpSpaceFloatingIpSuggestions` and `types.IpSpaceFloatingIpSuggestion` to + retrieve IP Space IP suggestions ([#648](https://github.com/vmware/go-vcloud-director/pull/648)) +* Added support for VM disk consolidation using `vm.ConsolidateDisksAsync` and `vm.ConsolidateDisks` + ([#650](https://github.com/vmware/go-vcloud-director/pull/650)) +* Added public method `VApp.GetParentVDC` to retrieve parent VDC of vApp (previously it was private) + ([#652](https://github.com/vmware/go-vcloud-director/pull/652)) +* Added methods `Catalog.CaptureVappTemplate`, `Catalog.CaptureVappTemplateAsync` and type + `types.CaptureVAppParams` that add support for creating catalog template from existing vApp + ([#652](https://github.com/vmware/go-vcloud-director/pull/652)) +* Added method `Org.GetVAppByHref` to retrieve a vApp by given HREF ([#652](https://github.com/vmware/go-vcloud-director/pull/652)) +* Added methods `VAppTemplate.GetCatalogItemHref` and `VAppTemplate.GetCatalogItemId` that can return + related catalog item ID and HREF ([#652](https://github.com/vmware/go-vcloud-director/pull/652)) + +### NOTES +* Removed the conditional API call with outdated API version from `Client.GetStorageProfileByHref` so it works + with the newest VCD versions ([#639](https://github.com/vmware/go-vcloud-director/pull/639)) +* Added a delay for all LDAP tests `Test_LDAP` after LDAP configuration, but before using them + ([#643](https://github.com/vmware/go-vcloud-director/pull/643)) + +* Added internal generic functions to handle CRUD operations for inner and outer entities ([#644](https://github.com/vmware/go-vcloud-director/pull/644)) +* Added section about OpenAPI CRUD functions to `CODING_GUIDELINES.md` [[#644](https://github.com/vmware/go-vcloud-director/pull/644)] +* Converted `DefinedEntityType`, `DefinedEntity`, `DefinedInterface`, `IpSpace`, `IpSpaceUplink`, + `DistributedFirewall`, `DistributedFirewallRule`, `NsxtSegmentProfileTemplate`, + `GetAllIpDiscoveryProfiles`, `GetAllMacDiscoveryProfiles`, `GetAllSpoofGuardProfiles`, + `GetAllQoSProfiles`, `GetAllSegmentSecurityProfiles` to use newly introduced generic CRUD + functions ([#644](https://github.com/vmware/go-vcloud-director/pull/644)) + +## 2.22.0 (December 12, 2023) + +### FEATURES +* Added support for VMware Cloud Director **10.5.1** +* Added metadata support to Runtime Defined Entities with methods `rde.GetMetadataByKey`, `rde.GetMetadataById` `rde.GetMetadata`, + `rde.AddMetadata` and generic metadata methods `openApiMetadataEntry.Update` and `openApiMetadataEntry.Delete` ([#557](https://github.com/vmware/go-vcloud-director/pull/557), [#632](https://github.com/vmware/go-vcloud-director/pull/632)) +* Added methods `SetReadOnlyAccessControl` and `IsSharedReadOnly` for `Catalog` and `AdminCatalog`, to handle read-only catalog sharing ([#559](https://github.com/vmware/go-vcloud-director/pull/559)) +* Added `Firmware` field to `VmSpecSection` type and `BootOptions` to `Vm` type ([#607](https://github.com/vmware/go-vcloud-director/pull/607)) +* Added `Vdc` methods `GetHardwareVersion`, `GetHighestHardwareVersion`, + `FindOsFromId` ([#607](https://github.com/vmware/go-vcloud-director/pull/607)) +* Added `VM` methods `UpdateBootOptions`, `UpdateBootOptionsAsync` ([#607](https://github.com/vmware/go-vcloud-director/pull/607)) +* API calls for `AddRawVM`, `CreateStandaloneVmAsync`, `VM.Refresh`, + `VM.UpdateVmSpecSectionAsync`, `addEmptyVmAsyncV10`, `getVMByHrefV10` + and `UpdateBootOptionsAsync` get elevated to API version `37.1` if available, for `VmSpecSection.Firmware` and `BootOptions` support ([#607](https://github.com/vmware/go-vcloud-director/pull/607)) +* Added `VCDClient` methods `CreateNetworkPool`, `CreateStandaloneVmAsync`, `CreateNetworkPoolGeneve`, `CreateNetworkPoolVlan`, `CreateNetworkPoolPortGroup` to create a network pool ([#613](https://github.com/vmware/go-vcloud-director/pull/613)) +* Added method `VCDClient.GetAllNsxtTransportZones` to retrieve all NSX-T transport zones ([#613](https://github.com/vmware/go-vcloud-director/pull/613)) +* Added method `VCDClient.GetAllVcenterDistributedSwitches` to retrieve all distributed switches ([#613](https://github.com/vmware/go-vcloud-director/pull/613)) +* Added method `VCDClient.QueryNsxtManagers` to retrieve all NSX-T managers ([#613](https://github.com/vmware/go-vcloud-director/pull/613)) +* Added `NetworkPool` methods `Update`, `Delete`, `GetOpenApiUrl` to manage a network pool ([#613](https://github.com/vmware/go-vcloud-director/pull/613)) +* Added `NsxtManager` type and function `VCDClient.GetNsxtManagerByName` ([#618](https://github.com/vmware/go-vcloud-director/pull/618)) +* Added support for Segment Profile Template management using new types `NsxtSegmentProfileTemplate` and `types.NsxtSegmentProfileTemplate` ([#618](https://github.com/vmware/go-vcloud-director/pull/618)) +* Added support for reading Segment Profiles provided by NSX-T via functions + `GetAllIpDiscoveryProfiles`, `GetIpDiscoveryProfileByName`, `GetAllMacDiscoveryProfiles`, + `GetMacDiscoveryProfileByName`, `GetAllSpoofGuardProfiles`, `GetSpoofGuardProfileByName`, + `GetAllQoSProfiles`, `GetQoSProfileByName`, `GetAllSegmentSecurityProfiles`, + `GetSegmentSecurityProfileByName` ([#618](https://github.com/vmware/go-vcloud-director/pull/618)) +* Added support for setting default Segment Profiles for NSX-T Org VDC Networks + `OpenApiOrgVdcNetwork.GetSegmentProfile()`, `OpenApiOrgVdcNetwork.UpdateSegmentProfile()` ([#618](https://github.com/vmware/go-vcloud-director/pull/618)) +* Added support for setting global default Segment Profiles + `VCDClient.GetGlobalDefaultSegmentProfileTemplates()`, + `VCDClient.UpdateGlobalDefaultSegmentProfileTemplates()` ([#618](https://github.com/vmware/go-vcloud-director/pull/618)) +* Added new `types` for NSX-T L2 VPN Tunnel session management `NsxtL2VpnTunnel`, `EdgeL2VpnStretchedNetwork`, `types.EdgeL2VpnTunnelStatistics`, `types.EdgeL2VpnTunnelStatus` ([#619](https://github.com/vmware/go-vcloud-director/pull/619)) +* Added new `NsxtEdgeGateway` methods `CreateL2VpnTunnel`, `GetAllL2VpnTunnels`, `GetL2VpnTunnelByName`, `GetL2VpnTunnelById` for creation and retrieval of NSX-T L2 VPN Tunnel sessions ([#619](https://github.com/vmware/go-vcloud-director/pull/619)) +* Added `NsxtL2VpnTunnel` methods `Refresh`, `Update`, `Statistics`, `Status`, `Delete` ([#619](https://github.com/vmware/go-vcloud-director/pull/619)) +* Added method `Catalog.UploadMediaFile` to upload any file as catalog Media ([#621](https://github.com/vmware/go-vcloud-director/pull/621),[#622](https://github.com/vmware/go-vcloud-director/pull/622)) +* Added method `Media.Download` to download a Media item as a byte stream ([#622](https://github.com/vmware/go-vcloud-director/pull/622)) +* Added `VAppTemplate` methods `GetLease` and `RenewLease` to retrieve and change storage lease ([#623](https://github.com/vmware/go-vcloud-director/pull/623)) +* Added `NsxtEdgeGateway` methods `GetDnsConfig` and `UpdateDnsConfig` ([#627](https://github.com/vmware/go-vcloud-director/pull/627)) +* Added types `types.NsxtEdgeGatewayDns`, `types.NsxtDnsForwarderZoneConfig` + for creation and management of DNS forwarder configuration ([#627](https://github.com/vmware/go-vcloud-director/pull/627)) +* Added `NsxtEdgeGatewayDns` methods `Refresh`, `Update` and `Delete` ([#627](https://github.com/vmware/go-vcloud-director/pull/627)) +* Add type `VgpuProfile` and its methods `GetAllVgpuProfiles`, `GetVgpuProfilesByProviderVdc`, `GetVgpuProfileById`, `GetVgpuProfileByName`, `GetVgpuProfileByTenantFacingName`, `Update` and `Refresh` for managing vGPU profiles ([#633](https://github.com/vmware/go-vcloud-director/pull/633)) +* Update `ComputePolicyV2` type with new fields for managing vGPU policies ([#633](https://github.com/vmware/go-vcloud-director/pull/633)) + +### IMPROVEMENTS +* Added catalog parent retrieval to `client.GetCatalogByHref` and `client.GetAdminCatalogByHref` to facilitate tenant context handling ([#559](https://github.com/vmware/go-vcloud-director/pull/559)) +* Add `VdcGroup.ForceDelete` function to optionally force VDC Group removal, which can be used for + removing VDC Group with child elements ([#597](https://github.com/vmware/go-vcloud-director/pull/597)) +* Bumped up minimal API version to 37.0 (drops support for VCD 10.3.x) ([#609](https://github.com/vmware/go-vcloud-director/pull/609)) +* Add struct `IopsResource` to `types.DiskSettings` (in replacement of dropped field `iops`) initially supported in API 37.0 ([#609](https://github.com/vmware/go-vcloud-director/pull/609)) +* Add field `SslEnabled` to struct `types.NsxtAlbPool` initially supported in API 37.0 ([#609](https://github.com/vmware/go-vcloud-director/pull/609)) +* New method `NsxtEdgeGateway.GetAllocatedIpCountByUplinkType` complementing existing + `NsxtEdgeGateway.GetAllocatedIpCount`. It will return allocated IP counts by uplink types (works + with VCD 10.4.1+) ([#610](https://github.com/vmware/go-vcloud-director/pull/610)) +* New method `NsxtEdgeGateway.GetPrimaryNetworkAllocatedIpCount` that will return total allocated IP + count for primary uplink (T0 or T0 VRF) ([#610](https://github.com/vmware/go-vcloud-director/pull/610)) +* New field `types.EdgeGatewayUplinks.BackingType` that defines backing type of NSX-T Edge Gateway + Uplink ([#610](https://github.com/vmware/go-vcloud-director/pull/610)) +* NSX-T Edge Gateway functions `GetNsxtEdgeGatewayById`, `GetNsxtEdgeGatewayByName`, + `GetNsxtEdgeGatewayByNameAndOwnerId`, `GetAllNsxtEdgeGateways`, `CreateNsxtEdgeGateway`, + `Refresh`, `Update` will additionally sort uplinks to ensure that element 0 contains primary + network (T0 or T0 VRF) ([#610](https://github.com/vmware/go-vcloud-director/pull/610)) +* Makes `DefinedEntityType` method `SetBehaviorAccessControls` more robust to avoid NullPointerException errors in VCD + when the input is a nil slice ([#615](https://github.com/vmware/go-vcloud-director/pull/615)) +* `types.IpSpace` support Firewall and NAT rule autocreation configuration using + `types.DefaultGatewayServiceConfig` on VCD 10.5.0+ ([#628](https://github.com/vmware/go-vcloud-director/pull/628)) +* Added metadata ignore support for Runtime Defined Entity metadata methods `rde.GetMetadataByKey`, `rde.GetMetadata`, `rde.AddMetadata`, + `rde.UpdateMetadata` and `rde.DeleteMetadata` ([#632](https://github.com/vmware/go-vcloud-director/pull/632)) + +### BUG FIXES +* Added handling of catalog creation task, which was leaving the catalog not ready for action in some cases ([#590](https://github.com/vmware/go-vcloud-director/pull/590), [#602](https://github.com/vmware/go-vcloud-director/pull/602)) +* Fix nil pointer dereference bug while fetching a NSX-V Backed Edge Gateway ([#594](https://github.com/vmware/go-vcloud-director/pull/594)) +* Fix vApp network related tests ([#595](https://github.com/vmware/go-vcloud-director/pull/595)) +* Fixed [Issue #1098](https://github.com/vmware/terraform-provider-vcd/issues/1098) crash in VDC creation ([#598](https://github.com/vmware/go-vcloud-director/pull/598)) +* Addressed Issue [1134](https://github.com/vmware/terraform-provider-vcd/issues/1134): Can't use SYSTEM `ldap_mode` ([#625](https://github.com/vmware/go-vcloud-director/pull/625)) + +### DEPRECATIONS +* Deprecated `UpdateInternalDisksAsync` in favor of `UpdateVmSpecSectionAsync` ([#607](https://github.com/vmware/go-vcloud-director/pull/607)) + +### NOTES +* Improved the testing configuration to allow customizing the UI Plugin path ([#599](https://github.com/vmware/go-vcloud-director/pull/599)) +* Added a configurable timeout to the testing options available in the Makefile ([#600](https://github.com/vmware/go-vcloud-director/pull/600)) +* Improved test `Test_NsxtApplicationPortProfileTenant` ([#601](https://github.com/vmware/go-vcloud-director/pull/601)) +* Added explicit removal for many resources in tests ([#605](https://github.com/vmware/go-vcloud-director/pull/605)) +* Improved test `Test_InsertOrEjectMedia` ([#608](https://github.com/vmware/go-vcloud-director/pull/608)) +* Addressed `gosec` 2.17.0 errors ([#608](https://github.com/vmware/go-vcloud-director/pull/608)) + +* Changed `Test_AddNewVMFromMultiVmTemplate` to use preloaded vApp template ([#609](https://github.com/vmware/go-vcloud-director/pull/609)) +* Amended `testMetadataIgnore`, used by all metadata tests, to be compatible with VCD 10.5.1 ([#629](https://github.com/vmware/go-vcloud-director/pull/629)) + +### REMOVALS +* Removed field `iops` from `types.DiskSettings` (dropped in API version 37.0) ([#609](https://github.com/vmware/go-vcloud-director/pull/609)) + + +## 2.21.0 (July 20, 2023) + +### FEATURES +* Added NSX-T Edge Gateway DHCP forwarding configuration support `NsxtEdgeGateway.GetDhcpForwarder` and + `NsxtEdgeGateway.UpdateDhcpForwarder` ([#573](https://github.com/vmware/go-vcloud-director/pull/573)) +* Added methods to create, get, publish and delete UI Plugins `VCDClient.AddUIPlugin`, `VCDClient.GetAllUIPlugins`, + `VCDClient.GetUIPluginById`, `VCDClient.GetUIPlugin`, `UIPlugin.Update`, `UIPlugin.GetPublishedTenants`, + `UIPlugin.PublishAll`, `UIPlugin.UnpublishAll`, `UIPlugin.Publish`, `UIPlugin.Unpublish`, `UIPlugin.IsTheSameAs` and `UIPlugin.Delete` ([#575](https://github.com/vmware/go-vcloud-director/pull/575)) +* Added AdminOrg methods `GetFederationSettings`, `SetFederationSettings`, `UnsetFederationSettings` to handle organization SAML settings ([#576](https://github.com/vmware/go-vcloud-director/pull/576)) +* Added AdminOrg methods `GetServiceProviderSamlMetadata` and `RetrieveServiceProviderSamlMetadata` to retrieve service provider metadata for current organization ([#576](https://github.com/vmware/go-vcloud-director/pull/576)) +* Added method `Client.RetrieveRemoteDocument` to download a document from a URL ([#576](https://github.com/vmware/go-vcloud-director/pull/576)) +* Added function `ValidateSamlServiceProviderMetadata` to validate service oprovider metadata ([#576](https://github.com/vmware/go-vcloud-director/pull/576)) +* Added function `GetErrorMessageFromErrorSlice` to return a single string from a slice of errors ([#576](https://github.com/vmware/go-vcloud-director/pull/576)) +* Added Service Account CRUD support via `ServiceAccount` and `types.ServiceAccount`: `VCDClient.CreateServiceAccount`, + `Org.GetServiceAccountById`, `Org.GetAllServiceAccounts`, `Org.GetServiceAccountByName`, + `ServiceAccount.Update`, `ServiceAccount.Authorize`, `ServiceAccount.Grant`, `ServiceAccount.Refresh`, + `ServiceAccount.Revoke`, `ServiceAccount.Delete`, `ServiceAccount.GetInitialApiToken` ([#577](https://github.com/vmware/go-vcloud-director/pull/577)) +* Added API Token CRUD support via `Token` and `types.Token`: `VCDClient.CreateToken`,`VCDClient.GetTokenById`, +`VCDClient.GetAllTokens`,`VCDClient.GetTokenByNameAndUsername`, `VCDClient.RegisterToken` , `Token.GetInitialApiToken`, `Token.Delete`, `Client.GetApiToken` ([#577](https://github.com/vmware/go-vcloud-director/pull/577)) +* Added IP Space CRUD support via `IpSpace` and `types.IpSpace` and `VCDClient.CreateIpSpace`, + `VCDClient.GetAllIpSpaceSummaries`, `VCDClient.GetIpSpaceById`, `VCDClient.GetIpSpaceByName`, + `VCDClient.GetIpSpaceByNameAndOrgId`, `IpSpace.Update`, `IpSpace.Delete` ([#578](https://github.com/vmware/go-vcloud-director/pull/578)) +* Added IP Space Uplink CRUD support via `IpSpaceUplink` and `types.IpSpaceUplink` and + `VCDClient.CreateIpSpaceUplink`, `VCDClient.GetAllIpSpaceUplinks`, + `VCDClient.GetIpSpaceUplinkById`, `VCDClient.GetIpSpaceUplinkByName`, `IpSpaceUplink.Update`, + `IpSpaceUplink.Delete` ([#579](https://github.com/vmware/go-vcloud-director/pull/579)) +* Added IP Space Allocation CRUD support via `IpSpaceIpAllocation`, `types.IpSpaceIpAllocation`, + `types.IpSpaceIpAllocationRequest`, `types.IpSpaceIpAllocationRequestResult`. Methods + `IpSpace.AllocateIp`, `Org.IpSpaceAllocateIp`, `Org.GetIpSpaceAllocationByTypeAndValue`, + `IpSpace.GetAllIpSpaceAllocations`, `Org.GetIpSpaceAllocationById`, `IpSpaceIpAllocation.Update`, + `IpSpaceIpAllocation.Delete` ([#579](https://github.com/vmware/go-vcloud-director/pull/579)) +* Added IP Space Org assignment to support Custom Quotas via `IpSpaceOrgAssignment`, + `types.IpSpaceOrgAssignment`, `IpSpace.GetAllOrgAssignments`, `IpSpace.GetOrgAssignmentById`, + `IpSpace.GetOrgAssignmentByOrgName`, `IpSpace.GetOrgAssignmentByOrgId`, + `IpSpaceOrgAssignment.Update` ([#579](https://github.com/vmware/go-vcloud-director/pull/579)) +* Added method `VCDClient.QueryNsxtManagerByHref` to retrieve a NSX-T manager by its ID/HREF ([#580](https://github.com/vmware/go-vcloud-director/pull/580)) +* Added method `VCDClient.CreateProviderVdc` to create a Provider VDC ([#580](https://github.com/vmware/go-vcloud-director/pull/580)) +* Added method `VCDClient.ResourcePoolsFromIds` to convert list of IDs to resource pools ([#580](https://github.com/vmware/go-vcloud-director/pull/580)) +* Added `ProviderVdcExtended` methods `AddResourcePools`, `AddStorageProfiles`, `Delete`, `DeleteResourcePools`,`DeleteStorageProfiles`,`Disable`,`Enable`,`GetResourcePools`,`IsEnabled`,`Rename`,`Update` to fully manage a provider VDC ([#580](https://github.com/vmware/go-vcloud-director/pull/580)) +* Added method `NetworkPool.GetOpenApiUrl` to generate the full URL of a network pool ([#580](https://github.com/vmware/go-vcloud-director/pull/580)) +* Added `ResourcePool` methods `GetAvailableHardwareVersions` and `GetDefaultHardwareVersion` to get hardware versions ([#580](https://github.com/vmware/go-vcloud-director/pull/580)) +* Added `VCDClient` method `GetAllResourcePools` to retrieve all resource pools regardless of vCenter affiliation ([#580](https://github.com/vmware/go-vcloud-director/pull/580)) +* Added `VCDClient` method `GetAllVcenters` to retrieve all vCenters ([#580](https://github.com/vmware/go-vcloud-director/pull/580)) +* Added `VCDClient` methods `GetNetworkPoolById`,`GetNetworkPoolByName`,`GetNetworkPoolSummaries` to retrieve network pools ([#580](https://github.com/vmware/go-vcloud-director/pull/580)) +* Added `VCDClient` methods `GetVcenterById`,`GetVcenterByName` to retrieve vCenters ([#580](https://github.com/vmware/go-vcloud-director/pull/580)) +* Added `VCenter` methods `GetAllResourcePools`,`VCenter.GetResourcePoolById`,`VCenter.GetResourcePoolByName` to retrieve resource pools ([#580](https://github.com/vmware/go-vcloud-director/pull/580)) +* Added `VCenter` methods `GetAllStorageProfiles`,`GetStorageProfileById`,`GetStorageProfileByName` to retrieve storage profiles ([#580](https://github.com/vmware/go-vcloud-director/pull/580)) +* Added method `VCenter.GetVimServerUrl` to retrieve the full URL of a vCenter within a VCD ([#580](https://github.com/vmware/go-vcloud-director/pull/580)) +* Added NSX-T Edge Gateway SLAAC Profile (DHCPv6) configuration support + `NsxtEdgeGateway.GetSlaacProfile` and `NsxtEdgeGateway.UpdateSlaacProfile` ([#582](https://github.com/vmware/go-vcloud-director/pull/582)) +* Added RDE Defined Interface Behaviors support with methods `DefinedInterface.AddBehavior`, `DefinedInterface.GetAllBehaviors`, + `DefinedInterface.GetBehaviorById` `DefinedInterface.GetBehaviorByName`, `DefinedInterface.UpdateBehavior` and + `DefinedInterface.DeleteBehavior` ([#584](https://github.com/vmware/go-vcloud-director/pull/584)) +* Added RDE Defined Entity Type Behaviors support with methods `DefinedEntityType.GetAllBehaviors`, + `DefinedEntityType.GetBehaviorById` `DefinedEntityType.GetBehaviorByName`, `DefinedEntityType.UpdateBehaviorOverride` and + `DefinedEntityType.DeleteBehaviorOverride` ([#584](https://github.com/vmware/go-vcloud-director/pull/584)) +* Added RDE Defined Entity Type Behavior Access Controls support with methods `DefinedEntityType.GetAllBehaviorsAccessControls` and + `DefinedEntityType.SetBehaviorAccessControls` ([#584](https://github.com/vmware/go-vcloud-director/pull/584)) +* Added method to invoke Behaviors on Defined Entities `DefinedEntity.InvokeBehavior` and `DefinedEntity.InvokeBehaviorAndMarshal` ([#584](https://github.com/vmware/go-vcloud-director/pull/584)) +* Added support for NSX-T Edge Gateway Static Route configuration via types + `NsxtEdgeGatewayStaticRoute`, `types.NsxtEdgeGatewayStaticRoute` and methods + `NsxtEdgeGateway.CreateStaticRoute`, `NsxtEdgeGateway.GetAllStaticRoutes`, + `NsxtEdgeGateway.GetStaticRouteByNetworkCidr`, `NsxtEdgeGateway.GetStaticRouteByName`, + `NsxtEdgeGateway.GetStaticRouteById`, `NsxtEdgeGatewayStaticRoute.Update`, + `NsxtEdgeGatewayStaticRoute.Delete` ([#586](https://github.com/vmware/go-vcloud-director/pull/586)) +* Added types and methods `DistributedFirewallRule`, `VdcGroup.CreateDistributedFirewallRule`, + `DistributedFirewallRule.Update`, `.DistributedFirewallRuleDelete` to manage NSX-T Distributed + Firewall Rules one by one (opposed to managing all at once using `DistributedFirewall`) ([#587](https://github.com/vmware/go-vcloud-director/pull/587)) +* Added method `Vdc.CreateVappFromTemplate` to create a vApp from a vApp template containing one or more VMs ([#588](https://github.com/vmware/go-vcloud-director/pull/588)) +* Added method `Vdc.CloneVapp` to create a vApp from another vApp ([#588](https://github.com/vmware/go-vcloud-director/pull/588)) +* Added method `VApp.DiscardSuspendedState` to take a vApp out of suspended state ([#588](https://github.com/vmware/go-vcloud-director/pull/588)) + +### IMPROVEMENTS +* `ExternalNetworkV2` now supports IP Spaces on VCD 10.4.1+ with new fields `UsingIpSpace` and + `DedicatedOrg` ([#579](https://github.com/vmware/go-vcloud-director/pull/579)) +* Added a new function `WithIgnoredMetadata` to configure the `Client` to ignore specific metadata entries + in all non-deprecated metadata CRUD methods ([#581](https://github.com/vmware/go-vcloud-director/pull/581)) +* NSX-T ALB Virtual Service supports IPv6 Virtual Service using field`IPv6VirtualIpAddress` in + `types.NsxtAlbVirtualService` for VCD 10.4.0+ ([#582](https://github.com/vmware/go-vcloud-director/pull/582)) +* Added field `EnableDualSubnetNetwork` to enable Dual-Stack mode for Org VDC networks in + `types.OpenApiOrgVdcNetwork` ([#582](https://github.com/vmware/go-vcloud-director/pull/582)) + +### BUG FIXES +* Fixed [Issue #1066](https://github.com/vmware/terraform-provider-vcd/issues/1066) - Not possible to handle more than 128 storage profiles ([#580](https://github.com/vmware/go-vcloud-director/pull/580)) +* Fixed a bug that caused `Client.GetCertificateFromLibraryByName` and `AdminOrg.GetCertificateFromLibraryByName` to fail + retrieving certificates with `:` character in the name ([#589](https://github.com/vmware/go-vcloud-director/pull/589)) + +### DEPRECATIONS +* Deprecated method `Vdc.InstantiateVAppTemplate` (wrong implementation and result) in favor of `Vdc.CreateVappFromTemplate` ([#588](https://github.com/vmware/go-vcloud-director/pull/588)) + +### NOTES +* Internal - replaced 'takeStringPointer', 'takeIntAddress', 'takeBoolPointer' with generic 'addrOf' + ([#571](https://github.com/vmware/go-vcloud-director/pull/571)) +* Changed Org enablement status during tests for VCD 10.4.2, to circumvent a VCD bug that prevents creation of disabled Orgs ([#572](https://github.com/vmware/go-vcloud-director/pull/572)) +* Skipped test `Test_VdcDuplicatedVmPlacementPolicyGetsACleanError` in 10.4.2 as the relevant bug we check for is fixed in that version ([#574](https://github.com/vmware/go-vcloud-director/pull/574)) +* Added `unit` step to GitHub Actions ([#583](https://github.com/vmware/go-vcloud-director/pull/583)) + +## 2.20.0 (April 27, 2023) + +### FEATURES +* Added method `AdminVdc.IsNsxv` to detect whether an Admin VDC is NSX-V ([#521](https://github.com/vmware/go-vcloud-director/pull/521)) +* Added function `NewNsxvDistributedFirewall` to create a new NSX-V distributed firewall ([#521](https://github.com/vmware/go-vcloud-director/pull/521)) +* Added `NsxvDistributedFirewall` methods `GetConfiguration`, `IsEnabled`, `Enable`, `Disable`, `UpdateConfiguration`, `Refresh` to handle CRUD operations with NSX-V distributed firewalls ([#521](https://github.com/vmware/go-vcloud-director/pull/521)) +* Added `NsxvDistributedFirewall` methods `GetServices`, `GetServiceGroups`, `GetServiceById`, `GetServiceByName`, `GetServiceGroupById`, `GetServiceGroupByName` to retrieve specific services or service groups ([#521](https://github.com/vmware/go-vcloud-director/pull/521)) +* Added `NsxvDistributedFirewall` methods `GetServicesByRegex` and `GetServiceGroupsByRegex` to search services or service groups by regular expression ([#521](https://github.com/vmware/go-vcloud-director/pull/521)) +* Added support for Runtime Defined Entity Interfaces with client methods `VCDClient.CreateDefinedInterface`, `VCDClient.GetAllDefinedInterfaces`, + `VCDClient.GetDefinedInterface`, `VCDClient.GetDefinedInterfaceById` and methods to manipulate them `DefinedInterface.Update`, + `DefinedInterface.Delete` ([#527](https://github.com/vmware/go-vcloud-director/pull/527), [#566](https://github.com/vmware/go-vcloud-director/pull/566)) +* Added method `VM.GetEnvironment` to retrieve OVF Environment ([#528](https://github.com/vmware/go-vcloud-director/pull/528)) +* Added `NsxtEdgeGateway.Refresh` method to reload NSX-T Edge Gateway structure ([#532](https://github.com/vmware/go-vcloud-director/pull/532)) +* Added `NsxtEdgeGateway.GetUsedIpAddresses` method to fetch used IP addresses in NSX-T Edge + Gateway ([#532](https://github.com/vmware/go-vcloud-director/pull/532)) +* Added `NsxtEdgeGateway.GetUsedIpAddressSlice` method to fetch used IP addresses in a slice + ([#532](https://github.com/vmware/go-vcloud-director/pull/532)) +* Added `NsxtEdgeGateway.GetUnusedExternalIPAddresses` method that can help to find an unused + IP address in an Edge Gateway by given constraints ([#532](https://github.com/vmware/go-vcloud-director/pull/532),[#567](https://github.com/vmware/go-vcloud-director/pull/567)) +* Added `NsxtEdgeGateway.GetAllUnusedExternalIPAddresses` method that can return all unused IP + addresses in an Edge Gateway ([#532](https://github.com/vmware/go-vcloud-director/pull/532),[#567](https://github.com/vmware/go-vcloud-director/pull/567)) +* Added `NsxtEdgeGateway.GetAllocatedIpCount` method that sums up `TotalIPCount` fields in all + subnets ([#532](https://github.com/vmware/go-vcloud-director/pull/532)) +* Added `NsxtEdgeGateway.QuickDeallocateIpCount` and `NsxtEdgeGateway.DeallocateIpCount` + methods to manually alter Edge Gateway body for IP deallocation ([#532](https://github.com/vmware/go-vcloud-director/pull/532)) +* Added support for Runtime Defined Entity instances with methods `DefinedEntityType.GetAllRdes`, `DefinedEntityType.GetRdeByName`, + `DefinedEntityType.GetRdeById`, `DefinedEntityType.CreateRde` and methods to manipulate them `DefinedEntity.Resolve`, + `DefinedEntity.Update`, `DefinedEntity.Delete` ([#544](https://github.com/vmware/go-vcloud-director/pull/544)) +* Added generic `Client` methods `OpenApiPostItemAndGetHeaders` and `OpenApiGetItemAndHeaders` to be able to retrieve the + response headers when performing a POST or GET operation to an OpenAPI endpoint ([#544](https://github.com/vmware/go-vcloud-director/pull/544)) +* Added support for Runtime Defined Entity Types with client methods `VCDClient.CreateRdeType`, `VCDClient.GetAllRdeTypes`, + `VCDClient.GetRdeType`, `VCDClient.GetRdeTypeById` and methods to manipulate them `DefinedEntityType.Update`, + `DefinedEntityType.Delete` ([#545](https://github.com/vmware/go-vcloud-director/pull/545), [#566](https://github.com/vmware/go-vcloud-director/pull/566)) +* Added support for NSX-T DHCP Bindings via `OpenApiOrgVdcNetworkDhcpBinding`, + `types.OpenApiOrgVdcNetworkDhcpBinding` and functions + `OpenApiOrgVdcNetwork.CreateOpenApiOrgVdcNetworkDhcpBinding`, + `OpenApiOrgVdcNetwork.GetAllOpenApiOrgVdcNetworkDhcpBindings`, + `OpenApiOrgVdcNetwork.GetOpenApiOrgVdcNetworkDhcpBindingById`, + `OpenApiOrgVdcNetwork.GetOpenApiOrgVdcNetworkDhcpBindingByName`, + `OpenApiOrgVdcNetworkDhcpBinding.Update`, `OpenApiOrgVdcNetworkDhcpBinding.Refresh`, + `OpenApiOrgVdcNetworkDhcpBinding.Delete` ([#561](https://github.com/vmware/go-vcloud-director/pull/561)) +* Added QoS Profile lookup functions `GetAllNsxtEdgeGatewayQosProfiles` and + `GetNsxtEdgeGatewayQosProfileByDisplayName` ([#563](https://github.com/vmware/go-vcloud-director/pull/563)) +* Added NSX-T Edge Gateway QoS (Rate Limiting) configuration support `NsxtEdgeGateway.GetQoS` and + `NsxtEdgeGateway.UpdateQoS` ([#563](https://github.com/vmware/go-vcloud-director/pull/563)) +* Added 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` ([#564](https://github.com/vmware/go-vcloud-director/pull/564)) + +### IMPROVEMENTS +* NSX-T ALB settings for Edge Gateway gained support for IPv6 service network definition (VCD 10.4.0+) + and Transparent mode (VCD 10.4.1+) by adding new fields to `types.NsxtAlbConfig` and automatically + elevating API up to 37.1 ([#549](https://github.com/vmware/go-vcloud-director/pull/549)) +* Added support for using subnet prefix length while creating vApp networks ([#550](https://github.com/vmware/go-vcloud-director/pull/550)) +* Improve NSX-T IPSec VPN type `types.NsxtIpSecVpnTunnel` to support 'Certificate' Authentication + mode ([#553](https://github.com/vmware/go-vcloud-director/pull/553)) +* Added new field `TransparentModeEnabled` to `types.NsxtAlbVirtualService` which allows to preserve + client IP for NSX-T ALB Virtual Service (VCD 10.4.1+) ([#560](https://github.com/vmware/go-vcloud-director/pull/560)) +* Added new field `MemberGroupRef` to `types.NsxtAlbPool` which allows to define NSX-T ALB Pool + membership by using Edge Firewall Group (`NsxtFirewallGroup`) instead of plain IPs (VCD 10.4.1+) + ([#560](https://github.com/vmware/go-vcloud-director/pull/560)) +* `types.OpenApiOrgVdcNetwork` got a new read only field `OrgVdcIsNsxTBacked` (available since API + 36.0) which indicates if an Org Network is backed by NSX-T and a function + `OpenApiOrgVdcNetwork.IsNsxt()` ([#561](https://github.com/vmware/go-vcloud-director/pull/561)) +* Added `SetServiceAccountApiToken` method of `VCDClient` that allows + authenticating using a service account token file and handles the refresh token rotation ([#562](https://github.com/vmware/go-vcloud-director/pull/562)) + +### BUG FIXES +* Fixed a bug that prevented returning a specific error while authenticating client with invalid + password ([#536](https://github.com/vmware/go-vcloud-director/pull/536)) +* Fixed accessing uninitialized `Features` field while updating a vApp network ([#550](https://github.com/vmware/go-vcloud-director/pull/550)) + +### NOTES +* Created `Test_RenameCatalog` for making sure the contents of the Catalog don't change after rename ([#546](https://github.com/vmware/go-vcloud-director/pull/546)) + +## 2.19.0 (January 12, 2023) + +### FEATURES +* Added client methods `GetCatalogByHref`, `GetCatalogById`, `GetCatalogByName` to retrieve Catalogs without an Org object ([#537](https://github.com/vmware/go-vcloud-director/pull/537)) +* Added client methods `GetAdminCatalogByHref`, `GetAdminCatalogById`, `GetAdminCatalogByName` to retrieve AdminCatalogs without an AdminOrg object ([#537](https://github.com/vmware/go-vcloud-director/pull/537)) +* Added method `VAppTemplate.GetVappTemplateRecord` to retrieve a VAppTemplate query record ([#537](https://github.com/vmware/go-vcloud-director/pull/537)) + +### BUG FIXES +* Removed URL checks from `CreateCatalogFromSubscriptionAsync` to allow catalog creation from non-VCD entities, such as vSphere shared library ([#537](https://github.com/vmware/go-vcloud-director/pull/537)) +* Fixed flaky test `Test_CatalogAccessAsOrgUsers` that failed randomly for timing issues ([#540](https://github.com/vmware/go-vcloud-director/pull/540)) + +### NOTES +* Amended a quirky test `Test_CreateOrgVdcWithFlex` that failed randomly due to recovered VDC Storage Profiles being unordered ([#538](https://github.com/vmware/go-vcloud-director/pull/538)) +* Amended a quirky test `Test_VMPowerOnPowerOff` that failed due to the testing VM not being powered off fast enough ([#538](https://github.com/vmware/go-vcloud-director/pull/538)) + +## 2.18.0 (December 14, 2022) + +### FEATURES +* Added `VCDClient.GetAllAssignedVdcComputePoliciesV2` to retrieve Compute Policies without the need of an `AdminVdc` receiver ([#530](https://github.com/vmware/go-vcloud-director/pull/530)) +* Added `client` methods `QueryCatalogRecords` and `GetCatalogByHref` ([#531](https://github.com/vmware/go-vcloud-director/pull/531)) + +### BUG FIXES +* Fixed issue that caused VM Group retrieval to fail if the Provider VDC had more than one Resource Pool ([#530](https://github.com/vmware/go-vcloud-director/pull/530)) +* Fixed issue that prevented Org update because of wrong field position in LDAP settings ([#533](https://github.com/vmware/go-vcloud-director/pull/533)) + +## 2.17.0 (November 25, 2022) + +### FEATURES +* Added new functions to get vApp Templates `Catalog.GetVAppTemplateByName`, `Catalog.GetVAppTemplateById`, `Catalog.GetVAppTemplateByNameOrId`, + `Vdc.GetVAppTemplateByName`, `VCDClient.GetVAppTemplateByHref` and `VCDClient.GetVAppTemplateById` ([#495](https://github.com/vmware/go-vcloud-director/pull/495), [#520](https://github.com/vmware/go-vcloud-director/pull/520)) +* Added new functions to query vApp Templates by name `Catalog.QueryVappTemplateWithName`, `Vdc.QueryVappTemplateWithName`, `AdminVdc.QueryVappTemplateWithName` ([#495](https://github.com/vmware/go-vcloud-director/pull/495)) +* Added new functions to delete vApp Templates `VAppTemplate.DeleteAsync` and `VAppTemplate.Delete` ([#495](https://github.com/vmware/go-vcloud-director/pull/495)) +* Added new functions to extract information from vApp Templates `VAppTemplate.GetCatalogName` and `VAppTemplate.GetVdcName` ([#495](https://github.com/vmware/go-vcloud-director/pull/495)) +* Added `Client.TestConnection` method to check remote VCD endpoints ([#447](https://github.com/vmware/go-vcloud-director/pull/447), [#501](https://github.com/vmware/go-vcloud-director/pull/501)) +* Added `Client.TestConnectionWithDefaults` method that uses `Client.TestConnection` with some default + values ([#447](https://github.com/vmware/go-vcloud-director/pull/447), [#501](https://github.com/vmware/go-vcloud-director/pull/501)) +* Changed behavior of `Client.OpenApiPostItem` and `Client.OpenApiPostItemSync` so they accept + response code 200 OK as valid. The reason is `TestConnection` endpoint requires a POST request and + returns a 200OK when successful ([#447](https://github.com/vmware/go-vcloud-director/pull/447), [#501](https://github.com/vmware/go-vcloud-director/pull/501)) +* Added new methods `VCDClient.GetProviderVdcByHref`, `VCDClient.GetProviderVdcById`, `VCDClient.GetProviderVdcByName` and `ProviderVdc.Refresh` to retrieve Provider VDCs ([#502](https://github.com/vmware/go-vcloud-director/pull/502)) +* Added new methods `VCDClient.GetProviderVdcExtendedByHref`, `VCDClient.GetProviderVdcExtendedById`, `VCDClient.GetProviderVdcExtendedByName` and `ProviderVdcExtended.Refresh` to retrieve the extended flavor of Provider VDCs ([#502](https://github.com/vmware/go-vcloud-director/pull/502)) +* Added new methods `ProviderVdcExtended.ToProviderVdc`, to convert from an extended Provider VDC to a regular one ([#502](https://github.com/vmware/go-vcloud-director/pull/502)) +* Added new methods `ProviderVdc.GetMetadata`, `ProviderVdc.AddMetadataEntry`, `ProviderVdc.AddMetadataEntryAsync`, `ProviderVdc.MergeMetadataAsync`, `ProviderVdc.MergeMetadata`, `ProviderVdc.DeleteMetadataEntry` and `ProviderVdc.DeleteMetadataEntryAsync` to manage Provider VDCs metadata ([#502](https://github.com/vmware/go-vcloud-director/pull/502)) +* Added new methods `VCDClient.GetVmGroupById`, `VCDClient.GetVmGroupByNamedVmGroupIdAndProviderVdcUrn` and `VCDClient.GetVmGroupByNameAndProviderVdcUrn` to retrieve VM Groups. These are useful to create VM Placement Policies ([#504](https://github.com/vmware/go-vcloud-director/pull/504)) +* Added new methods `VCDClient.GetLogicalVmGroupById`, `VCDClient.CreateLogicalVmGroup` and `LogicalVmGroup.Delete` to manage Logical VM Groups. These are useful to create VM Placement Policies ([#504](https://github.com/vmware/go-vcloud-director/pull/504)) +* Added the function `VCDClient.GetMetadataByKeyAndHref` to get a specific metadata value using an entity reference ([#510](https://github.com/vmware/go-vcloud-director/pull/510)) +* Added the functions `VCDClient.AddMetadataEntryWithVisibilityByHrefAsync` + and `VCDClient.AddMetadataEntryWithVisibilityByHref` to add metadata with both visibility and domain + to any entity by using its reference ([#510](https://github.com/vmware/go-vcloud-director/pull/510)) +* Added the functions `VCDClient.MergeMetadataWithVisibilityByHrefAsync` + and `VCDClient.MergeMetadataWithVisibilityByHref` to merge metadata data supporting also visibility and domain ([#510](https://github.com/vmware/go-vcloud-director/pull/510)) +* Added the functions `VCDClient.DeleteMetadataEntryWithDomainByHrefAsync` + and `VCDClient.DeleteMetadataEntryWithDomainByHref` to delete metadata from an entity using its reference ([#510](https://github.com/vmware/go-vcloud-director/pull/510)) +* Added the function `GetMetadataByKey` to the following entities: + `VM`, `Vdc`, `AdminVdc`, `ProviderVdc`, `VApp`, `VAppTemplate`, `MediaRecord`, `Media`, `Catalog`, `AdminCatalog`, + `Org`, `AdminOrg`, `Disk`, `OrgVDCNetwork`, `CatalogItem`, `OpenApiOrgVdcNetwork` to get a specific metadata value ([#510](https://github.com/vmware/go-vcloud-director/pull/510)) +* Added the functions `AddMetadataEntryWithVisibilityAsync` and `AddMetadataEntryWithVisibility` to the following entities: + `VM`, `AdminVdc`, `ProviderVdc`, `VApp`, `VAppTemplate`, `MediaRecord`, `Media`, `AdminCatalog`, `AdminOrg`, `Disk`, + `OrgVDCNetwork`, `CatalogItem`, `OpenApiOrgVdcNetwork` to add metadata with both visibility and domain to them ([#510](https://github.com/vmware/go-vcloud-director/pull/510)) +* Added the functions `MergeMetadataWithMetadataValuesAsync` and `MergeMetadataWithMetadataValues` to the following entities: + `VM`, `AdminVdc`, `ProviderVdc`, `VApp`, `VAppTemplate`, `MediaRecord`, `Media`, `AdminCatalog`, `AdminOrg`, `Disk`, + `OrgVDCNetwork`, `CatalogItem`, `OpenApiOrgVdcNetwork` to merge metadata data supporting also visibility and domain ([#510](https://github.com/vmware/go-vcloud-director/pull/510)) +* Added the functions `DeleteMetadataEntryWithDomainAsync` and `DeleteMetadataEntryWithDomain` to the following entities: + `VM`, `AdminVdc`, `ProviderVdc`, `VApp`, `VAppTemplate`, `MediaRecord`, `Media`, `AdminCatalog`, `AdminOrg`, `Disk`, + `OrgVDCNetwork`, `CatalogItem`, `OpenApiOrgVdcNetwork` to delete metadata with the domain, that allows deleting metadata present in SYSTEM ([#510](https://github.com/vmware/go-vcloud-director/pull/510)) +* Added `AdminOrg` methods `CreateCatalogFromSubscriptionAsync` and `CreateCatalogFromSubscription` to create a + subscribed catalog ([#511](https://github.com/vmware/go-vcloud-director/pull/511)) +* Added method `AdminCatalog.FullSubscriptionUrl` to return the subscription URL of a published catalog ([#511](https://github.com/vmware/go-vcloud-director/pull/511)) +* Added method `AdminCatalog.WaitForTasks` to wait for catalog tasks to complete ([#511](https://github.com/vmware/go-vcloud-director/pull/511)) +* Added method `AdminCatalog.UpdateSubscriptionParams` to modify the terms of an existing subscription ([#511](https://github.com/vmware/go-vcloud-director/pull/511)) +* Added methods `Catalog.QueryTaskList` and `AdminCatalog.QueryTaskList` to retrieve the tasks associated with a catalog ([#511](https://github.com/vmware/go-vcloud-director/pull/511)) +* Added function `IsValidUrl` to determine if a URL is valid ([#511](https://github.com/vmware/go-vcloud-director/pull/511)) +* Added `AdminCatalog` methods `Sync` and `LaunchSync` to synchronise a subscribed catalog ([#511](https://github.com/vmware/go-vcloud-director/pull/511)) +* Added method `AdminCatalog.GetCatalogHref` to retrieve the HREF of a regular catalog ([#511](https://github.com/vmware/go-vcloud-director/pull/511)) +* Added `AdminCatalog` methods `QueryCatalogItemList`, `QueryVappTemplateList`, and `QueryMediaList` to retrieve lists of + dependent items ([#511](https://github.com/vmware/go-vcloud-director/pull/511)) +* Added `AdminCatalog` methods `LaunchSynchronisationVappTemplates`, `LaunchSynchronisationAllVappTemplates`, + `LaunchSynchronisationMediaItems`, and `LaunchSynchronisationAllMediaItems` to start synchronisation of dependent + items ([#511](https://github.com/vmware/go-vcloud-director/pull/511)) +* Added `AdminCatalog` methods `GetCatalogItemByHref` and `QueryCatalogItem` to retrieve a single Catalog Item ([#511](https://github.com/vmware/go-vcloud-director/pull/511)) +* Added method `CatalogItem.LaunchSync` to start synchronisation of a catalog item ([#511](https://github.com/vmware/go-vcloud-director/pull/511)) +* Added method `CatalogItem.Refresh` to get fresh contents for a catalog item ([#511](https://github.com/vmware/go-vcloud-director/pull/511)) +* Added function `WaitResource` to wait for tasks associated to a gioven resource ([#511](https://github.com/vmware/go-vcloud-director/pull/511)) +* Added function `MinimalShowTask` to display task progress with minimal info ([#511](https://github.com/vmware/go-vcloud-director/pull/511)) +* Added functions `ResourceInProgress` and `ResourceComplete` to check on task activity for a given entity ([#511](https://github.com/vmware/go-vcloud-director/pull/511)) +* Added functions `SkimTasksList`, `SkimTasksListMonitor`, `WaitTaskListCompletion`, `WaitTaskListCompletionMonitor` to + process lists of tasks and lists of task IDs ([#511](https://github.com/vmware/go-vcloud-director/pull/511)) +* Added `Client` methods `GetTaskByHREF` and `GetTaskById` to retrieve individual tasks ([#511](https://github.com/vmware/go-vcloud-director/pull/511)) +* Implemented `QueryItem` for `Task` and `AdminTask` (`GetHref`, `GetName`, `GetType`, `GetParentId`, `GetParentName`, `GetMetadataValue`, `GetDate`) ([#511](https://github.com/vmware/go-vcloud-director/pull/511)) +* Added `VCDClient.QueryMediaById` to query a media record using a media ID ([#520](https://github.com/vmware/go-vcloud-director/pull/520)) +* Added `Vdc.QueryVappSynchronizedVmTemplate` to query a VM inside a vApp Template that must be synchronized in the catalog [[#520](https://github.com/vmware/go-vcloud-director/pull/520)] +* Added `VCDClient.QueryVmInVAppTemplateByHref` and `VCDClient.QuerySynchronizedVmInVAppTemplateByHref` to query a VM + inside a vApp Template by using the latter's hyper-reference ([#520](https://github.com/vmware/go-vcloud-director/pull/520)) +* Added `VCDClient.QuerySynchronizedVAppTemplateById` to get a synchronized vApp Template query record from a vApp Template ID ([#520](https://github.com/vmware/go-vcloud-director/pull/520)) + +### IMPROVEMENTS +* Bumped Default API Version to V36.0 (VCD 10.3+) [#500](https://github.com/vmware/go-vcloud-director/pull/500) +* Added method `VM.Shutdown` to shut down guest OS ([#413](https://github.com/vmware/go-vcloud-director/pull/413), [#496](https://github.com/vmware/go-vcloud-director/pull/496)) +* Added support for MoRef ID on VM record type. Using the MoRef ID, we can then correlate that back to vCenter Server and find the VM with matching MoRef ID ([#491](https://github.com/vmware/go-vcloud-director/pull/491)) +* Added support for querying VdcStorageProfile: + - functions `QueryAdminOrgVdcStorageProfileByID` and `QueryOrgVdcStorageProfileByID` + - query types `QtOrgVdcStorageProfile` and `QtAdminOrgVdcStorageProfile` + - data struct `QueryResultAdminOrgVdcStorageProfileRecordType` (non admin struct already was here) + ([#499](https://github.com/vmware/go-vcloud-director/pull/499)) +* Created new VDC Compute Policies CRUD methods using OpenAPI v2.0.0: + `VCDClient.GetVdcComputePolicyV2ById`, `VCDClient.GetAllVdcComputePoliciesV2`, `VCDClient.CreateVdcComputePolicyV2`, + `VdcComputePolicyV2.Update`, `VdcComputePolicyV2.Delete` and `AdminVdc.GetAllAssignedVdcComputePoliciesV2`. + This version supports more filtering options like `isVgpuPolicy` ([#502](https://github.com/vmware/go-vcloud-director/pull/502), [#504](https://github.com/vmware/go-vcloud-director/pull/504), [#507](https://github.com/vmware/go-vcloud-director/pull/507)) +* Simplified `Test_LDAP` by using a pre-configured LDAP server ([#505](https://github.com/vmware/go-vcloud-director/pull/505)) +* Added VCDClient.GetAllNsxtEdgeClusters for lookup of NSX-T Edge Clusters in wider scopes - + Provider VDC, VDC Group or VDC ([#512](https://github.com/vmware/go-vcloud-director/pull/512)) +* Switched VDC.GetAllNsxtEdgeClusters to use 'orgVdcId' filter instead of '_context' (now deprecated) + ([#512](https://github.com/vmware/go-vcloud-director/pull/512)) +* Created `VM.UpdateComputePolicyV2` and `VM.UpdateComputePolicyV2Async` that uses v2.0.0 of VDC Compute Policy endpoint + of OpenAPI and allows updating VM Sizing Policies and also VM Placement Policies for a given VM ([#513](https://github.com/vmware/go-vcloud-director/pull/513)) +* Added `[]tenant` structure to simplify org user testing ([#515](https://github.com/vmware/go-vcloud-director/pull/515)) +* Improved `Vdc.QueryVappVmTemplate` to avoid querying VMs in vApp Templates that are not synchronized in the catalog ([#520](https://github.com/vmware/go-vcloud-director/pull/520)) + +### BUG FIXES +* Changed `VdcComputePolicy.Description` to a non-omitempty pointer, to be able to send null values to VCD to set empty descriptions. ([#504](https://github.com/vmware/go-vcloud-director/pull/504)) +* Fixed issue [#514](https://github.com/vmware/go-vcloud-director/issues/514) "ignoring pagination in network queries" ([#518](https://github.com/vmware/go-vcloud-director/pull/518)) +* Fixed type `types.AdminVdc.ResourcePoolRefs` to make unmarshaling work (read-only) ([#494](https://github.com/vmware/go-vcloud-director/pull/494)) +* Fixed Test_NsxtSecurityGroupGetAssociatedVms which had name clash ([#498](https://github.com/vmware/go-vcloud-director/pull/498)) + +### DEPRECATIONS +* Deprecated OpenAPI v1.0.0 VDC Compute Policies CRUD methods in favor of v2.0.0 ones: + `Client.GetVdcComputePolicyById`, `Client.GetAllVdcComputePolicies`, `Client.CreateVdcComputePolicy` + `VdcComputePolicy.Update`, `VdcComputePolicy.Delete` and `AdminVdc.GetAllAssignedVdcComputePolicies` ([#504](https://github.com/vmware/go-vcloud-director/pull/504)) +* Deprecated the functions `VCDClient.AddMetadataEntryByHrefAsync` + and `VCDClient.AddMetadataEntryByHref` in favor of `VCDClient.AddMetadataEntryWithVisibilityByHrefAsync` + and `VCDClient.AddMetadataEntryWithVisibilityByHref` ([#510](https://github.com/vmware/go-vcloud-director/pull/510)) +* Deprecated the functions `VCDClient.MergeMetadataByHrefAsync` + and `VCDClient.MergeMetadataByHref` in favor of `VCDClient.MergeMetadataWithVisibilityByHrefAsync` + and `VCDClient.MergeMetadataWithVisibilityByHref` ([#510](https://github.com/vmware/go-vcloud-director/pull/510)) +* Deprecated the functions `AddMetadataEntryAsync` and `AddMetadataEntry` from the following entities: + `VM`, `Vdc`, `AdminVdc`, `ProviderVdc`, `VApp`, `VAppTemplate`, `MediaRecord`, `Media`, `AdminCatalog`, `AdminOrg`, `Disk`, + `OrgVDCNetwork`, `CatalogItem` in favor of their `AddMetadataEntryWithVisibilityAsync` and `AddMetadataEntryWithVisibility` + counterparts ([#510](https://github.com/vmware/go-vcloud-director/pull/510)) +* Deprecated the functions `MergeMetadataAsync` and `MergeMetadataAsync` from the following entities: + `VM`, `Vdc`, `AdminVdc`, `ProviderVdc`, `VApp`, `VAppTemplate`, `MediaRecord`, `Media`, `AdminCatalog`, `AdminOrg`, `Disk`, + `OrgVDCNetwork`, `CatalogItem` in favor of their `MergeMetadataWithMetadataValuesAsync` and `MergeMetadataWithMetadataValues` + counterparts ([#510](https://github.com/vmware/go-vcloud-director/pull/510)) +* Deprecated the functions `DeleteMetadata` and `DeleteMetadataAsync` from the following entities: + `VM`, `Vdc`, `AdminVdc`, `ProviderVdc`, `VApp`, `VAppTemplate`, `MediaRecord`, `Media`, `AdminCatalog`, `AdminOrg`, `Disk`, + `OrgVDCNetwork`, `CatalogItem` in favor of their `DeleteMetadataWithDomainAsync` and `DeleteMetadataWithDomain` + counterparts ([#510](https://github.com/vmware/go-vcloud-director/pull/510)) + +### NOTES +* Switched `go.mod` to use Go 1.19 ([#511](https://github.com/vmware/go-vcloud-director/pull/511)) +* Ran `make fmt` using Go 1.19 release (`fmt` automatically changes doc comment structure). This + will prevent `make static` errors when running tests in pipeline using Go 1.19 ([#497](https://github.com/vmware/go-vcloud-director/pull/497)) +* Updated branding `vCloud Director` -> `VMware Cloud Director` ([#497](https://github.com/vmware/go-vcloud-director/pull/497)) +* package `io/ioutil` is deprecated as of Go 1.16. `staticcheck` started complaining about usage of + deprecated packages. As a result packages `io` or `os` are used (still the same + functions are used) ([#497](https://github.com/vmware/go-vcloud-director/pull/497)) +* Adjusted `staticcheck` version naming to new format (from `2021.1.2` to `v0.3.3`) ([#497](https://github.com/vmware/go-vcloud-director/pull/497)] +* Added a new GitHub Action to run `gosec` on every push and pull request [[#516](https://github.com/vmware/go-vcloud-director/pull/516)) +* Improved documentation for `types.OpenApiOrgVdcNetworkDhcp` ([#517](https://github.com/vmware/go-vcloud-director/pull/517)) + + + +## 2.16.0 (August 2, 2022) + +### FEATURES +* Added support for `DnsServers` on `OpenApiOrgVdcNetworkDhcp` struct ([#465](https://github.com/vmware/go-vcloud-director/pull/465)) +* Added new methods `Org.GetAllSecurityTaggedEntities`, `Org.GetAllSecurityTaggedEntitiesByName`, `Org.GetAllSecurityTagValues`, `VM.GetVMSecurityTags`, `Org.UpdateSecurityTag` and `VM.UpdateVMSecurityTags` to deal with security tags ([#467](https://github.com/vmware/go-vcloud-director/pull/467)) +* Added new structs `types.SecurityTag`, `types.SecurityTaggedEntity`, `types.SecurityTagValue` and `types.EntitySecurityTags` ([#467](https://github.com/vmware/go-vcloud-director/pull/467)) +* Added `Vdc.GetControlAccess`, `Vdc.SetControlAccess` and `Vdc.DeleteControlAccess` to get, set and delete control access capabilities to VDCs ([#470](https://github.com/vmware/go-vcloud-director/pull/470)) +* Added support to set, get and delete metadata to CatalogItem with the methods + `CatalogItem.AddMetadataEntry`, `CatalogItem.AddMetadataEntryAsync`, `CatalogItem.GetMetadata`, + `CatalogItem.DeleteMetadataEntry` and `CatalogItem.DeleteMetadataEntryAsync`. ([#471](https://github.com/vmware/go-vcloud-director/pull/471)) +* Added `AdminCatalog.MergeMetadata`,`AdminCatalog.MergeMetadataAsync`, `AdminOrg.MergeMetadata`, `AdminOrg.MergeMetadataAsync`, +`CatalogItem.MergeMetadata`, `CatalogItem.MergeMetadataAsync`, `Disk.MergeMetadata`, `Disk.MergeMetadataAsync`, `Media.MergeMetadata`, +`Media.MergeMetadataAsync`, `MediaRecord.MergeMetadata`, `MediaRecord.MergeMetadataAsync`, `OpenAPIOrgVdcNetwork.MergeMetadata`, +`OpenAPIOrgVdcNetwork.MergeMetadataAsync`, `OrgVDCNetwork.MergeMetadata`, `OrgVDCNetwork.MergeMetadataAsync`, +`VApp.MergeMetadata`, `VApp.MergeMetadataAsync`, `VAppTemplate.MergeMetadata`, `VAppTemplate.MergeMetadataAsync`, +`VM.MergeMetadata`, `VM.MergeMetadataAsync`, `Vdc.MergeMetadata`, `Vdc.MergeMetadataAsync` to merge metadata, +which both updates existing metadata with same key and adds new entries for the non-existent ones ([#473](https://github.com/vmware/go-vcloud-director/pull/473)) +* Added NSX-T Edge Gateway methods `NsxtEdgeGateway.GetNsxtRouteAdvertisement`, + `NsxtEdgeGateway.GetNsxtRouteAdvertisementWithContext`, + `NsxtEdgeGateway.UpdateNsxtRouteAdvertisement`, + `NsxtEdgeGateway.UpdateNsxtRouteAdvertisementWithContext`, + `NsxtEdgeGateway.DeleteNsxtRouteAdvertisement` and + `NsxtEdgeGateway.DeleteNsxtRouteAdvertisementWithContext` that allow to manage NSX-T Route + Advertisement ([#478](https://github.com/vmware/go-vcloud-director/pull/478), [#480](https://github.com/vmware/go-vcloud-director/pull/480)) +* Added new methods `NsxtEdgeGateway.GetBgpConfiguration`, `NsxtEdgeGateway.UpdateBgpConfiguration`, + `NsxtEdgeGateway.DisableBgpConfiguration` for BGP Configuration management on NSX-T Edge Gateway + ([#480](https://github.com/vmware/go-vcloud-director/pull/480)) +* Added new structs `types.EdgeBgpConfig`, `types.EdgeBgpGracefulRestartConfig`, + `types.EdgeBgpConfigVersion` for BGP Configuration management on NSX-T Edge Gateway ([#480](https://github.com/vmware/go-vcloud-director/pull/480)) +* Added support for Dynamic Security Groups in VCD 10.3 by expanding `types.NsxtFirewallGroup` to + accommodate fields required for Dynamic Security Groups, implemented automatic API elevation to + v36.0. Added New functions `VdcGroup.CreateNsxtFirewallGroup`, + `NsxtFirewallGroup.IsDynamicSecurityGroup` ([#487](https://github.com/vmware/go-vcloud-director/pull/487)) +* Added support for managing NSX-T Edge Gateway BGP IP Prefix Lists. It is done by adding types `EdgeBgpIpPrefixList` and +`types.EdgeBgpIpPrefixList` with functions `CreateBgpIpPrefixList`, `GetAllBgpIpPrefixLists`, +`GetBgpIpPrefixListByName`, `GetBgpIpPrefixListById`, `Update` and `Delete` ([#488](https://github.com/vmware/go-vcloud-director/pull/488)) +* Added support for managing NSX-T Edge Gateway BGP Neighbor. It is done by adding types `EdgeBgpNeighbor` and + `types.EdgeBgpNeighbor` with functions `CreateBgpNeighbor`, `GetAllBgpNeighbors`, + `GetBgpNeighborByIp`, `GetBgpNeighborById`, `Update` and `Delete` ([#489](https://github.com/vmware/go-vcloud-director/pull/489)) + +### IMPROVEMENTS +* Added methods `client.CreateVdcComputePolicy`, `client.GetVdcComputePolicyById`, `client.GetAllVdcComputePolicies` ([#468](https://github.com/vmware/go-vcloud-director/pull/468)) +* Added additional methods for convenience of NSX-T Org Network DHCP handling + `OpenApiOrgVdcNetwork.GetOpenApiOrgVdcNetworkDhcp`, `OpenApiOrgVdcNetwork.DeletNetworkDhcp` + `OpenApiOrgVdcNetwork.UpdateDhcp` ([#469](https://github.com/vmware/go-vcloud-director/pull/469)) +* Added additional support for UDF type ISO files in `catalog.UploadMediaImage` ([#479](https://github.com/vmware/go-vcloud-director/pull/479)) +* Added `SupportedFeatureSet` attribute to `NsxtAlbServiceEngineGroup` and `NsxtAlbConfig` to +support v37.0 license management for AVI Load Balancer and replace `LicenseType` from +`NsxtAlbController` ([#485](https://github.com/vmware/go-vcloud-director/pull/485)) + +### BUG FIXES +* Fixed method `adminOrg.FindCatalogRecords` to escape name in query URL ([#466](https://github.com/vmware/go-vcloud-director/pull/466)) +* Fixed method `vm.WaitForDhcpIpByNicIndexes` to ignore not found Edge Gateway ([#481](https://github.com/vmware/go-vcloud-director/pull/481)) + +### DEPRECATIONS +* Deprecated `org.GetVdcComputePolicyById`, `adminOrg.GetVdcComputePolicyById` ([#468](https://github.com/vmware/go-vcloud-director/pull/468)) +* Deprecated `org.GetAllVdcComputePolicies`, `adminOrg.GetAllVdcComputePolicies`, `org.CreateVdcComputePolicy` ([#468](https://github.com/vmware/go-vcloud-director/pull/468)) + + +## 2.15.0 (April 14, 2022) + +### FEATURES +* Added support for Shareable disks, i.e., independent disks that can be attached to multiple VMs which is available from + API v35.0 onwards. Also added UUID to the Disk structure which is a new member that is returned from v36.0 onwards. This + member holds a UUID that can be used to correlate the disk that is attached to a particular VM from the VCD side and the + VM host side. ([#383](https://github.com/vmware/go-vcloud-director/pull/383)) +* Added support for uploading OVF using URL `catalog.UploadOvfByLink` ([#422](https://github.com/vmware/go-vcloud-director/pull/422), [#426](https://github.com/vmware/go-vcloud-director/pull/426)) +* Added support for updating vApp template `vAppTemplate.UpdateAsync` and `vAppTemplate.Update` ([#422](https://github.com/vmware/go-vcloud-director/pull/422)) +* Added methods `catalog.PublishToExternalOrganizations` and `adminCatalog.PublishToExternalOrganizations` ([#424](https://github.com/vmware/go-vcloud-director/pull/424)) +* Added types `types.MetadataStringValue`, `types.MetadataNumberValue`, `types.MetadataDateTimeValue` and `types.MetadataBooleanValue` + for adding different kind of metadata to entities ([#430](https://github.com/vmware/go-vcloud-director/pull/430)) +* Added support to set, get and delete metadata to AdminCatalog with the methods + `AdminCatalog.AddMetadataEntry`, `AdminCatalog.AddMetadataEntryAsync`, `AdminCatalog.GetMetadata`, + `AdminCatalog.DeleteMetadataEntry` and `AdminCatalog.DeleteMetadataEntryAsync`. ([#430](https://github.com/vmware/go-vcloud-director/pull/430)) +* Added support to get metadata from Catalog with method `Catalog.GetMetadata` ([#430](https://github.com/vmware/go-vcloud-director/pull/430)) +* Added to `VM` and `VApp` the methods `DeleteMetadataEntry`, `DeleteMetadataEntryAsync`, `AddMetadataEntry` and `AddMetadataEntryAsync` + so it follows the same convention as the rest of entities that uses metadata. ([#430](https://github.com/vmware/go-vcloud-director/pull/430)) +* Added methods `vm.ChangeCPU` and `vm.ChangeMemory` which uses the latest API structure instead of deprecated ones ([#432](https://github.com/vmware/go-vcloud-director/pull/432)) +* Added environment variable `GOVCD_API_VERSION` so API version can be set manually ([#434](https://github.com/vmware/go-vcloud-director/pull/434)) +* Added support to set, get and delete metadata to AdminOrg with the methods + `AdminOrg.AddMetadataEntry`, `AdminOrg.AddMetadataEntryAsync`, `AdminOrg.GetMetadata`, + `AdminOrg.DeleteMetadataEntry` and `AdminOrg.DeleteMetadataEntryAsync`. ([#438](https://github.com/vmware/go-vcloud-director/pull/438)) +* Added support to get metadata to Org with the method + `Org.GetMetadata`. ([#438](https://github.com/vmware/go-vcloud-director/pull/438)) +* Added support to set, get and delete metadata to Disk with the methods + `Disk.AddMetadataEntry`, `Disk.AddMetadataEntryAsync`, `Disk.GetMetadata`, + `Disk.DeleteMetadataEntry` and `Disk.DeleteMetadataEntryAsync`. ([#438](https://github.com/vmware/go-vcloud-director/pull/438)) +* Added new structure `AnyTypeEdgeGateway` which supports retreving both types of Edge Gateways + (NSX-V and NSX-T) with methods `AdminOrg.GetAnyTypeEdgeGatewayById`, + `Org.GetAnyTypeEdgeGatewayById`, `AnyTypeEdgeGateway.IsNsxt`, `AnyTypeEdgeGateway.IsNsxv`, + `AnyTypeEdgeGateway.GetNsxtEdgeGateway` ([#443](https://github.com/vmware/go-vcloud-director/pull/443)) +* Added functions `VdcGroup.GetCapabilities`, `VdcGroup.IsNsxt`, + `VdcGroup.GetOpenApiOrgVdcNetworkByName`, `VdcGroup.GetAllOpenApiOrgVdcNetworks`, + `Org.GetOpenApiOrgVdcNetworkByNameAndOwnerId` ([#443](https://github.com/vmware/go-vcloud-director/pull/443)) +* Added method `AdminOrg.FindCatalogRecords` that allows to query `types.CatalogRecord` by their catalog name. ([#450](https://github.com/vmware/go-vcloud-director/pull/450)) +* Added methods `Client.QueryWithNotEncodedParamsWithHeaders` and `Client.QueryWithNotEncodedParamsWithApiVersionWithHeaders` so HTTP headers can be added now when doing API queries. ([#450](https://github.com/vmware/go-vcloud-director/pull/450)) +* Added functions `VdcGroup.GetNsxtFirewallGroupByName` and `VdcGroup.GetNsxtFirewallGroupById` ([#451](https://github.com/vmware/go-vcloud-director/pull/451)) +* Added support for for Network Context Profile lookup using `GetAllNetworkContextProfiles` and + `GetNetworkContextProfilesByNameScopeAndContext` functions ([#452](https://github.com/vmware/go-vcloud-director/pull/452)) +* Added support for NSX-T Distributed Firewall rule management using type `DistributedFirewall` and +`VdcGroup.GetDistributedFirewall`, `VdcGroup.UpdateDistributedFirewall`, +`VdcGroup.DeleteAllDistributedFirewallRules`, `DistributedFirewall.DeleteAllRules` ([#452](https://github.com/vmware/go-vcloud-director/pull/452)) +* Added support to set, get and delete metadata to the following resources via its HREF: + `catalog`, `catalog item`, `edge gateway`, `independent disk`, `media`, `network`, `org`, `PVDC`, `PVDC storage profile`, `vApp`, `vApp template`,`VDC` and `VDC storage profile`; + with the methods + `VCDClient.GetMetadataByHref`, `VCDClient.AddMetadataEntryByHref`, `VCDClient.AddMetadataEntryByHrefAsync`, + `VCDClient.DeleteMetadataEntryByHref` and `VCDClient.DeleteMetadataEntryByHrefAsync` ([#454](https://github.com/vmware/go-vcloud-director/pull/454)) +* Added functions `VdcGroup.GetOpenApiOrgVdcNetworkById` and `VdcGroup.CreateOpenApiOrgVdcNetwork` ([#456](https://github.com/vmware/go-vcloud-director/pull/456)) +* New method added `Disk.GetAttachedVmsHrefs` ([#436](https://github.com/vmware/go-vcloud-director/pull/436)) + +### IMPROVEMENTS +* Bumped Default API Version to V35.0 ([#434](https://github.com/vmware/go-vcloud-director/pull/434)) +* Disk methods have now the ability to access new properties from API version 36.0. They are: `DiskRecordType.SharingType`, `DiskRecordType.UUID`, `DiskRecordType.Encrypted`, `Disk.SharingType`, `Disk.UUID` and `Disk.Encrypted` ([#436](https://github.com/vmware/go-vcloud-director/pull/436)) +* Added support for `User` entities imported from LDAP, with `IsExternal` property ([#439](https://github.com/vmware/go-vcloud-director/pull/439)) +* Added support for users list attribute for `Group` ([#439](https://github.com/vmware/go-vcloud-director/pull/439)) +* Improved `group.Update()` to avoid sending the users list to VCD to avoid unwanted errors ([#449](https://github.com/vmware/go-vcloud-director/pull/449)) +* NSX-T Edge Gateway now supports VDC Groups by switching from `OrgVdc` to `OwnerRef` field. + Additional methods `NsxtEdgeGateway.MoveToVdcOrVdcGroup()`, + `Org.GetNsxtEdgeGatewayByNameAndOwnerId()`, `VdcGroup.GetNsxtEdgeGatewayByName()`, + `VdcGroup.GetAllNsxtEdgeGateways()`, `org.GetVdcGroupById` ([#440](https://github.com/vmware/go-vcloud-director/pull/440)) +* Added additional helper functions `OwnerIsVdcGroup()`, `OwnerIsVdc()`, `VdcGroup.GetCapabilities()`, + `VdcGroup.IsNsxt()` ([#440](https://github.com/vmware/go-vcloud-director/pull/440)) +* Added support to set, get and delete metadata to VDC Networks with the methods + `OrgVDCNetwork.AddMetadataEntry`, `OrgVDCNetwork.AddMetadataEntryAsync`, `OrgVDCNetwork.GetMetadata`, + `OrgVDCNetwork.DeleteMetadataEntry` and `OrgVDCNetwork.DeleteMetadataEntryAsync` ([#442](https://github.com/vmware/go-vcloud-director/pull/442)) +* Added `CanPublishExternally` and `CanSubscribe` attributes to `OrgGeneralSettings` struct. ([#444](https://github.com/vmware/go-vcloud-director/pull/444)) +* Added workaround to tests for Org Catalog publishing bug when dealing with LDAP ([#458](https://github.com/vmware/go-vcloud-director/pull/458)) +* Added clean-up actions to some tests that were uploading vAppTemplates/medias to catalogs ([#458](https://github.com/vmware/go-vcloud-director/pull/458)) +* Added support to set, get and delete metadata to OpenAPI VDC Networks through XML with the methods + `OpenApiOrgVdcNetwork.AddMetadataEntry`, `OpenApiOrgVdcNetwork.GetMetadata`, + `OpenApiOrgVdcNetwork.DeleteMetadataEntry` ([#459](https://github.com/vmware/go-vcloud-director/pull/459)) +* Added `Vdc.GetNsxtAppPortProfileByName` and `VdcGroup.GetNsxtAppPortProfileByName` for NSX-T + Application Port Profile lookup ([#460](https://github.com/vmware/go-vcloud-director/pull/460)) + +### BUG FIXES +* Fixed Issue #431 "Wrong order in Task structure" ([#433](https://github.com/vmware/go-vcloud-director/pull/433)) +* Fixed Issue where VDC creation with storage profile `enabled=false` wasn't working. `VdcStorageProfile.enabled` and `VdcStorageProfileConfiguration.enabled` changed to pointers ([#433](https://github.com/vmware/go-vcloud-director/pull/433)) +* Fixed method `client.GetStorageProfileByHref` to return IOPS `IopsSettings` ([#435](https://github.com/vmware/go-vcloud-director/pull/435)) +* `Vms.VmReference` changed to array to fix incorrect deserialization ([#436](https://github.com/vmware/go-vcloud-director/pull/436)) +* `Catalog.QueryMediaList` method was not working because `fmt.Sprintf` was being misused ([#441](https://github.com/vmware/go-vcloud-director/pull/441)) + +### DEPRECATIONS +* Deprecated `vm.DeleteMetadata` in favor of `vm.DeleteMetadataEntry` ([#430](https://github.com/vmware/go-vcloud-director/pull/430)) +* Deprecated `vm.AddMetadata` in favor of `vm.AddMetadataEntry` ([#430](https://github.com/vmware/go-vcloud-director/pull/430)) +* Deprecated `vdc.DeleteMetadata` in favor of `vdc.DeleteMetadataEntry` ([#430](https://github.com/vmware/go-vcloud-director/pull/430)) +* Deprecated `vdc.AddMetadata` in favor of `vdc.AddMetadataEntry` ([#430](https://github.com/vmware/go-vcloud-director/pull/430)) +* Deprecated `vdc.AddMetadataAsync` in favor of `vdc.AddMetadataEntryAsync` ([#430](https://github.com/vmware/go-vcloud-director/pull/430)) +* Deprecated `vdc.DeleteMetadataAsync` in favor of `vdc.DeleteMetadataEntryAsync` ([#430](https://github.com/vmware/go-vcloud-director/pull/430)) +* Deprecated `vApp.DeleteMetadata` in favor of `vApp.DeleteMetadataEntry` ([#430](https://github.com/vmware/go-vcloud-director/pull/430)) +* Deprecated `vApp.AddMetadata` in favor of `vApp.AddMetadataEntry` ([#430](https://github.com/vmware/go-vcloud-director/pull/430)) +* Deprecated `vAppTemplate.AddMetadata` in favor of `vAppTemplate.AddMetadataEntry` ([#430](https://github.com/vmware/go-vcloud-director/pull/430)) +* Deprecated `vAppTemplate.AddMetadataAsync` in favor of `vAppTemplate.AddMetadataEntryAsync` ([#430](https://github.com/vmware/go-vcloud-director/pull/430)) +* Deprecated `vAppTemplate.DeleteMetadata` in favor of `vAppTemplate.DeleteMetadataEntry` ([#430](https://github.com/vmware/go-vcloud-director/pull/430)) +* Deprecated `vAppTemplate.DeleteMetadataAsync` in favor of `vAppTemplate.DeleteMetadataEntryAsync` ([#430](https://github.com/vmware/go-vcloud-director/pull/430)) +* Deprecated `mediaRecord.AddMetadata` in favor of `mediaRecord.AddMetadataEntry` ([#430](https://github.com/vmware/go-vcloud-director/pull/430)) +* Deprecated `mediaRecord.AddMetadataAsync` in favor of `mediaRecord.AddMetadataEntryAsync` ([#430](https://github.com/vmware/go-vcloud-director/pull/430)) +* Deprecated `mediaRecord.DeleteMetadata` in favor of `mediaRecord.DeleteMetadataEntry` ([#430](https://github.com/vmware/go-vcloud-director/pull/430)) +* Deprecated `mediaRecord.DeleteMetadataAsync` in favor of `mediaRecord.DeleteMetadataEntryAsync` ([#430](https://github.com/vmware/go-vcloud-director/pull/430)) +* Deprecated `media.AddMetadata` in favor of `media.AddMetadataEntry` ([#430](https://github.com/vmware/go-vcloud-director/pull/430)) +* Deprecated `media.AddMetadataAsync` in favor of `media.AddMetadataEntryAsync` ([#430](https://github.com/vmware/go-vcloud-director/pull/430)) +* Deprecated `media.DeleteMetadata` in favor of `media.DeleteMetadataEntry` ([#430](https://github.com/vmware/go-vcloud-director/pull/430)) +* Deprecated `media.DeleteMetadataAsync` in favor of `media.DeleteMetadataEntryAsync` ([#430](https://github.com/vmware/go-vcloud-director/pull/430)) +* Deprecated `vm.ChangeMemorySize` in favor of `vm.ChangeMemory` ([#432](https://github.com/vmware/go-vcloud-director/pull/432)) +* Deprecated `vm.ChangeCPUCount` and `vm.ChangeCPUCountWithCore` in favor of `vm.ChangeCPU` ([#432](https://github.com/vmware/go-vcloud-director/pull/432)) + +### NOTES +* Bumped `staticcheck` version to 2022.1 with Go 1.18 support ([#457](https://github.com/vmware/go-vcloud-director/pull/457)) + +## 2.14.0 (January 7, 2022) + +### FEATURES +* Added type `NsxtAlbConfig` and functions `NsxtEdgeGateway.UpdateAlbSettings`, `NsxtEdgeGateway.GetAlbSettings`, + `NsxtEdgeGateway.DisableAlb` ([#403](https://github.com/vmware/go-vcloud-director/pull/403)) +* Added types `Certificate` and `types.CertificateLibraryItem` for handling Certificates in Certificate Library with corresponding + methods `client.GetCertificateFromLibraryById`, `client.AddCertificateToLibrary`, `client.GetAllCertificatesFromLibrary`, `client.GetCertificateFromLibraryByName`, `adminOrg.GetCertificateFromLibraryById`, `adminOrg.AddCertificateToLibrary`, `adminOrg.GetAllCertificatesFromLibrary`, `adminOrg.GetCertificateFromLibraryByName`, + `certificate.Update`, `certificate.Delete` ([#404](https://github.com/vmware/go-vcloud-director/pull/404)) +* Added support for ALB Service Engine Group Assignment to NSX-T Edge Gateway via type + `NsxtAlbServiceEngineGroupAssignment` and functions `GetAllAlbServiceEngineGroupAssignments`, + `GetAlbServiceEngineGroupAssignmentById`, `GetAlbServiceEngineGroupAssignmentByName`, + `CreateAlbServiceEngineGroupAssignment`, `Update`, `Delete` ([#405](https://github.com/vmware/go-vcloud-director/pull/405)) +* Added type `types.ApiTokenRefresh` to contain data from API token refresh ([#406](https://github.com/vmware/go-vcloud-director/pull/406)) +* Added method `VCDClient.GetBearerTokenFromApiToken` to get a bearer token from an API token ([#406](https://github.com/vmware/go-vcloud-director/pull/406)) +* Added method `VCDClient.SetApiToken` to set a token and get a bearer token using and API token and get token details in return ([#406](https://github.com/vmware/go-vcloud-director/pull/406)) +* Added types `VdcGroup`, `types.VdcGroup`, `types.ParticipatingOrgVdcs`, `types.CandidateVdc`, `types.DfwPolicies` and `types.DefaultPolicy` for handling VDC groups with corresponding + methods `adminOrg.CreateNsxtVdcGroup`, `adminOrg.CreateVdcGroup`, `adminOrg.GetAllNsxtVdcGroupCandidates`, `adminOrg.GetAllVdcGroupCandidates`, `adminOrg.GetAllVdcGroups`, `adminOrg.GetVdcGroupByName`, `adminOrg.GetVdcGroupById`, `vdcGroup.Update`, `vdcGroup.GenericUpdate`, `vdcGroup.Delete`, `vdcGroup.DisableDefaultPolicy`, `vdcGroup.EnableDefaultPolicy`, `vdcGroup.GetDfwPolicies`, `vdcGroup.DeActivateDfw`, `vdcGroup.ActivateDfw`, `vdcGroup.UpdateDefaultDfwPolicies`, `vdcGroup.UpdateDfwPolicies` ([#410](https://github.com/vmware/go-vcloud-director/pull/410)) +* Added support for ALB Pool to NSX-T Edge Gateway via type `NsxtAlbPool` and functions `GetAllAlbPools`, + `GetAllAlbPoolSummaries`, `GetAlbPoolByName`, `GetAlbPoolById`, `CreateNsxtAlbPool`, `nsxtAlbPool.Update`, + `nsxtAlbPool.Delete` ([#414](https://github.com/vmware/go-vcloud-director/pull/414)) +* Added support for ALB Virtual Services to NSX-T Edge Gateway via type `NsxtAlbVirtualService` and functions `GetAllAlbVirtualServices`, + `GetAllAlbGetAllAlbVirtualServiceSummaries`, `GetAlbVirtualServiceByName`, `GetAlbVirtualServiceById`, + `CreateNsxtAlbVirtualService`, `NsxtAlbVirtualService.Update`, `NsxtAlbVirtualService.Delete` ([#417](https://github.com/vmware/go-vcloud-director/pull/417)) + +### IMPROVEMENTS +* `VCDClient.SetToken` has now the ability of transparently setting a bearer token when receiving an API token ([#406](https://github.com/vmware/go-vcloud-director/pull/406)) +* Removed Coverity warnings from code ([#408](https://github.com/vmware/go-vcloud-director/pull/408), [#412](https://github.com/vmware/go-vcloud-director/pull/412)) +* Added session info to go-vcloud-director logs ([#409](https://github.com/vmware/go-vcloud-director/pull/409)) +* Added type `types.UpdateLeaseSettingsSection` to handle vApp lease settings. ([#420](https://github.com/vmware/go-vcloud-director/pull/420)) +* Added methods `vApp.GetLease` and `vApp.RenewLease`, to query the state of the vApp lease and eventually modify it. ([#420](https://github.com/vmware/go-vcloud-director/pull/420)) +* Added `LeaseSettingsSection` to `types.VApp` structure. ([#420](https://github.com/vmware/go-vcloud-director/pull/420)) + +### BUG FIXES +* Fixed Issue #728: `vm.UpdateInternalDisksAsync()` didn't send VM description and as a result would delete VM description ([#418](https://github.com/vmware/go-vcloud-director/pull/418)) +* Removed hardcoded 0 value for Weight field in `ChangeCPUCountWithCore` function to avoid overriding shares ([#419](https://github.com/vmware/go-vcloud-director/pull/419)) +* Fixed issue #421 "Wrong xml name in SourcedVmTemplateParams" ([#420](https://github.com/vmware/go-vcloud-director/pull/420)) + + +## 2.13.0 (September 30, 2021) + +### FEATURES +* Added method `AdminVdc.AddStorageProfile` ([#393](https://github.com/vmware/go-vcloud-director/pull/393)) +* Added method `AdminVdc.AddStorageProfileWait` ([#393](https://github.com/vmware/go-vcloud-director/pull/393)) +* Added method `AdminVdc.RemoveStorageProfile` ([#393](https://github.com/vmware/go-vcloud-director/pull/393)) +* Added method `AdminVdc.RemoveStorageProfileWait` ([#393](https://github.com/vmware/go-vcloud-director/pull/393)) +* Added method `AdminVdc.SetDefaultStorageProfile` ([#393](https://github.com/vmware/go-vcloud-director/pull/393)) +* Added method `AdminVdc.GetDefaultStorageProfileReference` ([#393](https://github.com/vmware/go-vcloud-director/pull/393)) +* Added method `VCDClient.GetStorageProfileByHref` ([#393](https://github.com/vmware/go-vcloud-director/pull/393)) +* Added method `Client.GetStorageProfileByHref` ([#393](https://github.com/vmware/go-vcloud-director/pull/393)) +* Added method `VCDClient.QueryProviderVdcStorageProfileByName` ([#393](https://github.com/vmware/go-vcloud-director/pull/393)) +* Added method `Client.QueryAllProviderVdcStorageProfiles` ([#393](https://github.com/vmware/go-vcloud-director/pull/393)) +* Added method `Client.QueryProviderVdcStorageProfiles` ([#393](https://github.com/vmware/go-vcloud-director/pull/393)) +* Added types `NsxtAlbController` and `types.NsxtAlbController` for handling NSX-T ALB Controllers with corresponding + functions `GetAllAlbControllers`, `GetAlbControllerByName`, `GetAlbControllerById`, `GetAlbControllerByUrl`, + `CreateNsxtAlbController`, `Update`, `Delete` ([#398](https://github.com/vmware/go-vcloud-director/pull/398)) +* Added types `NsxtAlbCloud` and `types.NsxtAlbCloud` for handling NSX-T ALB Clouds with corresponding functions + `GetAllAlbClouds`, `GetAlbCloudByName`, `GetAlbCloudById`, `CreateAlbCloud`, `Delete` ([#398](https://github.com/vmware/go-vcloud-director/pull/398)) +* Added type `NsxtAlbImportableCloud` and `types.NsxtAlbImportableCloud` for listing NSX-T ALB Importable Clouds with + corresponding functions `GetAllAlbImportableClouds`, `GetAlbImportableCloudByName`, `GetAlbImportableCloudById` + ([#398](https://github.com/vmware/go-vcloud-director/pull/398)) +* Added types `NsxtAlbServiceEngineGroup` and `types.NsxtAlbServiceEngineGroup` for handling NSX-T ALB Service Engine + Groups with corresponding functions `GetAllNsxtAlbServiceEngineGroups`, `GetAlbServiceEngineGroupByName`, + `GetAlbServiceEngineGroupById`, `CreateNsxtAlbServiceEngineGroup`, `Update`, `Delete`, `Sync` ([#398](https://github.com/vmware/go-vcloud-director/pull/398)) +* Added types `NsxtAlbImportableServiceEngineGroups` and `types.NsxtAlbImportableServiceEngineGroups` for listing NSX-T + ALB Importable Service Engine Groups with corresponding functions `GetAllAlbImportableServiceEngineGroups`, + `GetAlbImportableServiceEngineGroupByName`, `GetAlbImportableServiceEngineGroupById` ([#398](https://github.com/vmware/go-vcloud-director/pull/398)) + +### IMPROVEMENTS +* External network type ExternalNetworkV2 automatically elevates API version to maximum available out of 33.0, 35.0 and + 36.0, so that new functionality can be consumed. It uses a controlled version elevation mechanism to consume the newer + features, but at the same time remain tested by not choosing the latest untested version blindly (more information in + openapi_endpoints.go) ([#399](https://github.com/vmware/go-vcloud-director/pull/399)) +* Added new field BackingTypeValue in favor of deprecated BackingType to types.ExternalNetworkV2Backing ([#399](https://github.com/vmware/go-vcloud-director/pull/399)) +* Added new function `GetFilteredNsxtImportableSwitches` to query NSX-T Importable Switches (Segments) ([#399](https://github.com/vmware/go-vcloud-director/pull/399)) +* Added `.changes` directory for changelog items ([#391](https://github.com/vmware/go-vcloud-director/pull/391)) + +* Aligned build tags to match go fmt with Go 1.17 ([#396](https://github.com/vmware/go-vcloud-director/pull/396)) +* Improved `test-tags.sh` script to handle new build tag format ([#396](https://github.com/vmware/go-vcloud-director/pull/396)) + +### BUG FIXES +* Fixed handling of `staticcheck` in GitGub Actions ([#391](https://github.com/vmware/go-vcloud-director/pull/391)) + +* Fixed Issue #390: `catalog.Delete()` ignores returned task and responds immediately which could have caused failures ([#392](https://github.com/vmware/go-vcloud-director/pull/392)) + +* Fixed Issue #395 "BUG: can't update EGW - there is no ownerRef field" ([#397](https://github.com/vmware/go-vcloud-director/pull/397)) + +### DEPRECATIONS +* Deprecated `GetStorageProfileByHref` in favor of either `client.GetStorageProfileByHref` or `vcdClient.GetStorageProfileByHref` ([#393](https://github.com/vmware/go-vcloud-director/pull/393)) +* Deprecated `QueryProviderVdcStorageProfileByName` in favor of `VCDClient.QueryProviderVdcStorageProfileByName` ([#393](https://github.com/vmware/go-vcloud-director/pull/393)) +* Deprecated `VCDClient.QueryProviderVdcStorageProfiles` in favor of either `client.QueryProviderVdcStorageProfiles` or `client.QueryAllProviderVdcStorageProfiles` ([#393](https://github.com/vmware/go-vcloud-director/pull/393)) +* Deprecated `Vdc.GetDefaultStorageProfileReference` in favor of `adminVdc.GetDefaultStorageProfileReference` ([#393](https://github.com/vmware/go-vcloud-director/pull/393)) + +## 2.12.1 (July 5, 2021) + +BUGS FIXED: +* org.GetCatalogByName and org.GetCatalogById could not retrieve shared catalogs from different Orgs + [#389](https://github.com/vmware/go-vcloud-director/pull/389) + + +## 2.12.0 (June 30, 2021) + +* Improved error handling and function receiver name in client + [#379](https://github.com/vmware/go-vcloud-director/pull/379) * Added method `vdc.QueryEdgeGateway` [#364](https://github.com/vmware/go-vcloud-director/pull/364) * Deprecated `vdc.GetEdgeGatewayRecordsType` [#364](https://github.com/vmware/go-vcloud-director/pull/364) +* Dropped support for VCD 9.7 which is EOL now [#371](https://github.com/vmware/go-vcloud-director/pull/371) +* Bumped Default API Version to V33.0 [#371](https://github.com/vmware/go-vcloud-director/pull/371) +* Methods `GetVDCById` and `GetVDCByName` for `Org` now use queries behind the scenes because Org + structure does not list child VDCs anymore [#371](https://github.com/vmware/go-vcloud-director/pull/371), + [#376](https://github.com/vmware/go-vcloud-director/pull/376), [#382](https://github.com/vmware/go-vcloud-director/pull/382) +* Methods `GetCatalogById` and `GetCatalogByName` for `Org` now use queries behind the scenes because Org + structure does not list child Catalogs anymore [#371](https://github.com/vmware/go-vcloud-director/pull/371), + [#376](https://github.com/vmware/go-vcloud-director/pull/376) +* Drop legacy authentication mechanism (vcdAuthorize) and use only new Cloud API provided (vcdCloudApiAuthorize) as + API V33.0 is sufficient for it [#371](https://github.com/vmware/go-vcloud-director/pull/371) +* Added NSX-T Firewall Group type (which represents a Security Group or an IP Set) support by using + structures `NsxtFirewallGroup` and `NsxtFirewallGroupMemberVms`. The following methods are + introduced for managing Security Groups and Ip Sets: `Vdc.CreateNsxtFirewallGroup`, + `NsxtEdgeGateway.CreateNsxtFirewallGroup`, `Org.GetAllNsxtFirewallGroups`, + `Vdc.GetAllNsxtFirewallGroups`, `Org.GetNsxtFirewallGroupByName`, + `Vdc.GetNsxtFirewallGroupByName`, `NsxtEdgeGateway.GetNsxtFirewallGroupByName`, + `Org.GetNsxtFirewallGroupById`, `Vdc.GetNsxtFirewallGroupById`, + `NsxtEdgeGateway.GetNsxtFirewallGroupById`, `NsxtFirewallGroup.Update`, + `NsxtFirewallGroup.Delete`, `NsxtFirewallGroup.GetAssociatedVms`, + `NsxtFirewallGroup.IsSecurityGroup`, `NsxtFirewallGroup.IsIpSet` + [#368](https://github.com/vmware/go-vcloud-director/pull/368) +* Added methods Org.QueryVmList and Org.QueryVmById to find VM by ID in an Org + [#368](https://github.com/vmware/go-vcloud-director/pull/368) +* Added `NsxtAppPortProfile` and `types.NsxtAppPortProfile` for NSX-T Application Port Profile management + [#378](https://github.com/vmware/go-vcloud-director/pull/378) +* Deprecated methods `vdc.ComposeRawVApp` and `vdc.ComposeVApp` [#387](https://github.com/vmware/go-vcloud-director/pull/387) +* Added method `vdc.CreateRawVApp` [#387](https://github.com/vmware/go-vcloud-director/pull/387) +* Removed deprecated method `adminOrg.GetRole` +* Added Tenant Context management functions `Client.RemoveCustomHeader`, `Client.SetCustomHeader`, `WithHttpHeader`, + and many private methods to retrieve tenant context down the hierarchy. More details in `CODING_GUIDELINES.md` + [#380](https://github.com/vmware/go-vcloud-director/pull/380) +* Added Rights management methods `AdminOrg.GetAllRights`, `AdminOrg.GetAllRightsCategories`, `AdminOrg.GetRightById`, + `AdminOrg.GetRightByName`, `Client.GetAllRights`, `Client.GetAllRightsCategories`, `Client.GetRightById`, + `Client.GetRightByName`, `client.GetRightsCategoryById`, `AdminOrg.GetRightsCategoryById` [#380](https://github.com/vmware/go-vcloud-director/pull/380) +* Added Global Role management methods `Client.GetAllGlobalRoles`, `Client.CreateGlobalRole`, `Client.GetGlobalRoleById`, + `Client.GetGlobalRoleByName`, `GlobalRole.AddRights`, `GlobalRole.Delete`, `GlobalRole.GetRights`, + `GlobalRole.GetTenants`, `GlobalRole.PublishAllTenants`, `GlobalRole.PublishTenants`, `GlobalRole.RemoveAllRights`, + `GlobalRole.RemoveRights`, `GlobalRole.ReplacePublishedTenants`, `GlobalRole.UnpublishAllTenants`, + `GlobalRole.UnpublishTenants`, `GlobalRole.Update`, `GlobalRole.UpdateRights` [#380](https://github.com/vmware/go-vcloud-director/pull/380) +* Added Rights Bundle management methods `Client.CreateRightsBundle`, `Client.GetAllRightsBundles`, + `Client.GetRightsBundleById`, `Client.GetRightsBundleByName`, `RightsBundle.AddRights`, `RightsBundle.Delete`, + `RightsBundle.GetRights`, `RightsBundle.GetTenants`, `RightsBundle.PublishAllTenants`, `RightsBundle.PublishTenants`, + `RightsBundle.RemoveAllRights`, `RightsBundle.RemoveRights`, `RightsBundle.ReplacePublishedTenants`, + `RightsBundle.UnpublishAllTenants`, `RightsBundle.UnpublishTenants`, `RightsBundle.Update`, `RightsBundle.UpdateRights` + [#380](https://github.com/vmware/go-vcloud-director/pull/380) +* Added Role managemnt methods `AdminOrg.GetAllRoles`, `AdminOrg.GetRoleById`, `AdminOrg.GetRoleByName`, + `Client.GetAllRoles`, `Role.AddRights`, `Role.GetRights`, `Role.RemoveAllRights`, `Role.RemoveRights`, `Role.UpdateRights` + [#380](https://github.com/vmware/go-vcloud-director/pull/380) +* Added convenience function `FindMissingImpliedRights` [#380](https://github.com/vmware/go-vcloud-director/pull/380) +* Added methods `NsxtEdgeGateway.UpdateNsxtFirewall()`, `NsxtEdgeGateway.GetNsxtFirewall()`, `nsxtFirewall.DeleteAllRules()`, + `nsxtFirewall.DeleteRuleById` [#381](https://github.com/vmware/go-vcloud-director/pull/381) +* Added NSX-T NAT support with types `NsxtNatRule` and `types.NsxtNatRule` as well as methods `edge.GetAllNsxtNatRules`, + `edge.GetNsxtNatRuleByName`, `edge.GetNsxtNatRuleById`, `edge.CreateNatRule`, `nsxtNatRule.Update`, `nsxtNatRule.Delete`, + `nsxtNatRule.IsEqualTo` [#382](https://github.com/vmware/go-vcloud-director/pull/382) +* Added `NsxtIpSecVpnTunnel` and `types.NsxtIpSecVpnTunnel` for NSX-T IPsec VPN Tunnel configuration + [#385](https://github.com/vmware/go-vcloud-director/pull/385) + +BREAKING CHANGES: +* Added parameter `description` to method `vdc.ComposeRawVapp` [#372](https://github.com/vmware/go-vcloud-director/pull/372) +* Added methods `vapp.Rename`, `vapp.UpdateDescription`, `vapp.UpdateNameDescription` [#372](https://github.com/vmware/go-vcloud-director/pull/372) +* Field `types.Disk.Size` is replaced with `types.Disk.SizeMb` as size in Kilobytes is not supported in V33.0 + [#371](https://github.com/vmware/go-vcloud-director/pull/371) +* Field `types.DiskRecordType.SizeB` is replaced with `types.DiskRecordType.SizeMb` as size in Kilobytes is not + supported in V33.0 [#371](https://github.com/vmware/go-vcloud-director/pull/371) +* Added parameter `additionalHeader map[string]string` to functions `Client.OpenApiDeleteItem`, `Client.OpenApiGetAllItems`, + `Client.OpenApiGetItem`, `Client.OpenApiPostItem`, `Client.OpenApiPutItem`, `Client.OpenApiPutItemAsync`, + `Client.OpenApiPutItemSync` [#380](https://github.com/vmware/go-vcloud-director/pull/380) +* Renamed functions `GetOpenApiRoleById` -> `GetRoleById`, `GetOpenApiRoleByName` -> `GetRoleByName`, + `GetAllOpenApiRoles` -> `GetAllRoles` [#380](https://github.com/vmware/go-vcloud-director/pull/380) + +IMPROVEMENTS: +* Only send xml.Header when payload is not empty (some WAFs block empty requests with XML header) + [#367](https://github.com/vmware/go-vcloud-director/pull/367) +* Improved test entity cleanup to find standalone VMs in any VDC (not only default NSX-V one) + [#368](https://github.com/vmware/go-vcloud-director/pull/368) +* Improved test entity cleanup to allow specifying parent VDC for vApp removals + [#368](https://github.com/vmware/go-vcloud-director/pull/368) +* Cleanup a few unnecessary type conversions detected by new staticcheck version + [#381](https://github.com/vmware/go-vcloud-director/pull/381) +* Improved `OpenApiGetAllItems` to still follow pages in VCD endpoints with BUG which don't return 'nextPage' link for + pagination [#378](https://github.com/vmware/go-vcloud-director/pull/378) +* Improved LDAP container related tests to use correct port mapping for latest LDAP container version + [#378](https://github.com/vmware/go-vcloud-director/pull/378) + ## 2.11.0 (March 10, 2021) @@ -167,12 +1129,12 @@ BUGS FIXED: * Added method `VCDClient.SetToken` * Added method `VCDClient.GetAuthResponse` * Added script `scripts/get_token.sh` -* Increment vCD API version used from 27.0 to 29.0 +* Incremented vCD API version used from 27.0 to 29.0 * Remove fields `VdcEnabled`, `VAppParentHREF`, `VAppParentName`, `HighestSupportedVersion`, `VmToolsVersion`, `TaskHREF`, `TaskStatusName`, `TaskDetails`, `TaskStatus` from `QueryResultVMRecordType` - * Add fields `ID, Type, ContainerName, ContainerID, OwnerName, Owner, NetworkHref, IpAddress, CatalogName, VmToolsStatus, GcStatus, AutoUndeployDate, AutoDeleteDate, AutoUndeployNotified, AutoDeleteNotified, Link, MetaData` to `QueryResultVMRecordType`, `DistributedInterface` to `NetworkConfiguration` and `RegenerateBiosUuid` to `VMGeneralParams` + * Added fields `ID, Type, ContainerName, ContainerID, OwnerName, Owner, NetworkHref, IpAddress, CatalogName, VmToolsStatus, GcStatus, AutoUndeployDate, AutoDeleteDate, AutoUndeployNotified, AutoDeleteNotified, Link, MetaData` to `QueryResultVMRecordType`, `DistributedInterface` to `NetworkConfiguration` and `RegenerateBiosUuid` to `VMGeneralParams` * Change to pointers `DistributedRoutingEnabled` in `GatewayConfiguration` and `DistributedInterface` in `NetworkConfiguration` -* Add new field to type `GatewayConfiguration`: `FipsModeEnabled` - +* Added new field to type `GatewayConfiguration`: `FipsModeEnabled` - [#267](https://github.com/vmware/go-vcloud-director/pull/267) * Change bool to bool pointer for fields in type `GatewayConfiguration`: `HaEnabled`, `UseDefaultRouteForDNSRelay`, `AdvancedNetworkingEnabled` - @@ -302,7 +1264,7 @@ BREAKING CHANGES: IMPROVEMENTS: * Refactored code by introducing helper function to handle API calls. New functions ExecuteRequest, ExecuteTaskRequest, ExecuteRequestWithoutResponse -* Add authorization request header for media file and catalog item upload +* Added authorization request header for media file and catalog item upload * Tests files are now all tagged. Running them through Makefile works as before, but manual execution requires specific tags. Run `go test -v .` for tags list. ## 2.1.0 (March 21, 2019) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index ba4ac56ac..33978b201 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -30,4 +30,4 @@ an issue or PR. **Attribution** -This Code of Conduct is adapted from the VMware Clarity project, available at this page: https://github.com/vmware/clarity/blob/master/CODE_OF_CONDUCT.md +This Code of Conduct is adapted from the VMware Clarity project, available at this page: https://github.com/vmware/clarity/blob/next/docs/CODE_OF_CONDUCT.md diff --git a/CODING_GUIDELINES.md b/CODING_GUIDELINES.md index 3f300dbb5..6b3c9bd02 100644 --- a/CODING_GUIDELINES.md +++ b/CODING_GUIDELINES.md @@ -115,7 +115,7 @@ func (client *Client) ExecuteRequestWithCustomError(pathURL, requestType, conten In addition to saving code and time by reducing the boilerplate, these functions also trigger debugging calls that make the code easier to monitor. Using any of the above calls will result in the standard log i -(See [LOGGING.md](https://github.com/vmware/go-vcloud-director/blob/master/util/LOGGING.md)) recording all the requests and responses +(See [LOGGING.md](https://github.com/vmware/go-vcloud-director/blob/main/util/LOGGING.md)) recording all the requests and responses on demand, and also triggering debug output for specific calls (see `enableDebugShowRequest` and `enableDebugShowResponse` and the corresponding `disable*` in `api.go`). @@ -348,7 +348,7 @@ To add a type to the search engine, we need the following: 2. Add the list of supported fields to `queryFieldsOnDemand` (`query_metadata.go`) 3. Implement the interface `QueryItem` (`filter_interface.go`), which requires a type localization (such as `type QueryMedia types.MediaRecordType`) -4. Add a clause to `resultsToQueryItems` (`filter_interface.go`) +4. Add a clause to `resultToQueryItems` (`filter_interface.go`) ## Data inspection checkpoints @@ -365,6 +365,245 @@ they will be "NET1", "NET2", etc, and then activate them using In the code, we use the function `dataInspectionRequested(code)` that will check whether the environment variable contains the given code. +## Tenant Context + +Tenant context is a mechanism in the VCD API to run calls as a tenant when connected as a system administrator. +It is used, for example, in the UI, to start a session as tenant administrator without having credentials for such a user, +or even when there is no such user yet. +The context change works by adding a header to the API call, containing these fields: + +``` +X-Vmware-Vcloud-Tenant-Context: [604cf889-b01e-408b-95ae-67b02a0ecf33] +X-Vmware-Vcloud-Auth-Context: [org-name] +``` + +The field `X-Vmware-Vcloud-Tenant-Context` contains the bare ID of the organization (it's just the UUID, without the +prefix `urn:vcloud:org:`). +The field `X-Vmware-Vcloud-Auth-Context` contains the organization name. + +### tenant context: data availability + +From the SDK standpoint, finding the data needed to put together the tenant context is relatively easy when the originator +of the API call is the organization itself (such as `org.GetSomeEntityByName`). +When we deal with objects down the hierarchy, however, things are more difficult. Running a call from a VDC means that +we need to retrieve the parent organization, and extract ID and name. The ID is available through the `Link` structure +of the VDC, but for the name we need to retrieve the organization itself. + +The approach taken in the SDK is to save the tenant context (or a pointer to the parent) in the object that we have just +created. For example, when we create a VDC, we save the organization as a pointer in the `parent` field, and the organization +itself has a field `TenantContext` with the needed information. + +Here are the types that are needed for tenant context manipulation +```go + +// tenant_context.go +type TenantContext struct { + OrgId string // The bare ID (without prefix) of an organization + OrgName string // The organization name +} + +// tenant_context.go +type organization interface { + orgId() string + orgName() string + tenantContext() (*TenantContext, error) + fullObject() interface{} +} + +// org.go +type Org struct { + Org *types.Org + client *Client + TenantContext *TenantContext +} + +// adminorg.go +type AdminOrg struct { + AdminOrg *types.AdminOrg + client *Client + TenantContext *TenantContext +} + +// vdc.go +type Vdc struct { + Vdc *types.Vdc + client *Client + parent organization +} +``` + +The `organization` type is an abstraction to include both `Org` and `AdminOrg`. Thus, the VDC object has a pointer to its +parent that is only needed to get the tenant context quickly. + +Each object has a way to get the tenant context by means of a `entity.getTenantContext()`. The information +trickles down from the hierarchy: + +* a VDC gets the tenant context directly from its `parent` field, which has a method `tenantContext()` +* similarly, a Catalog has a `parent` field with the same functionality. +* a vApp will get the tenant context by first retrieving its parent (`vapp.getParentVdc()`) and then asking the parent +for the tenant context. + +### tenant context: usage + +Once we have the tenant context, we need to pass the information along to the HTTP request that builds the request header, +so that our API call will run in the desired context. + +The basic OpenAPI methods (`Client.OpenApiDeleteItem`, `Client.OpenApiGetAllItems`, `Client.OpenApiGetItem`, +`Client.OpenApiPostItem`, `Client.OpenApiPutItem`, `Client.OpenApiPutItemAsync`, `Client.OpenApiPutItemSync`) all include +a parameter `additionalHeader map[string]string` containing the information needed to build the tenant context header elements. + +Inside the function where we want to use tenant context, we do these two steps: + +1. retrieve the tenant context +2. add the additional header to the API call. + +For example: + +```go +func (adminOrg *AdminOrg) GetAllRoles(queryParameters url.Values) ([]*Role, error) { + tenantContext, err := adminOrg.getTenantContext() + if err != nil { + return nil, err + } + return getAllRoles(adminOrg.client, queryParameters, getTenantContextHeader(tenantContext)) +} +``` +The function `getTenantContextHeader` takes a tenant context and returns a map of strings containing the right header +keys. In the example above, the header is passed to `getAllRoles`, which in turn calls `Client.OpenApiGetAllItems`, +which passes the additional header until it reaches `newOpenApiRequest`, where the tenent context data is inserted in +the request header. + +When the tenant context is not needed (system administration calls), we just pass `nil` as `additionalHeader`. + +## Generic CRUD functions for OpenAPI entity implementation + +Generic CRUD functions are used to minimize boilerplate for entity implementation in the SDK. They +might not always be the way to go when there are very specific operation needs as it is not worth +having a generic function for single use case. In such cases, low level API client function set, +that is located in `openapi.go` can help to perform such operations. + +### Terminology + +#### inner vs outer types + +For the context of generic CRUD function implementation (mainly in files +`govcd/openapi_generic_outer_entities.go`, `govcd/openapi_generic_inner_entities.go`), such terms +are commonly used: + +* `inner` type is the type that is responsible for marshaling/unmarshaling API + request payload and is usually inside `types` package. (e.g. `types.IpSpace`, + `types.NsxtAlbPoolMember`, etc.) +* `outer` (type) - this is the type that wraps `inner` type and possibly any other entities that are + required to perform operations for a particular VCD entity. It will almost always include some + reference to client (`VCDClient` or `Client`), which is required to perform API operations. It may + contain additional fields. + +Here are the entities mapped in the example below: + +* `DistributedFirewall` is the **`outer`** type +* `types.DistributedFirewallRules` is the **`inner`** type (specified in + `DistributedFirewall.DistributedFirewallRuleContainer` field) +* `client` field contains the client that is required for perfoming API operations +* `VdcGroup` field contains additional data (VDC Group reference) that is required for +implementation of this particular entity + +```go +type DistributedFirewall struct { + DistributedFirewallRuleContainer *types.DistributedFirewallRules + client *Client + VdcGroup *VdcGroup +} +``` + +#### crudConfig + +A special type `govcd.crudConfig` is used for passing configuration to both - `inner` and `outer` +generic CRUD functions. It also has an internal `validate()` method, which is called upon execution +of any `inner` and `outer` CRUD functions. + +See documentation of `govcd.crudConfig` for the options it provides. + +### Use cases + +The main consideration when to use which functions depends on whether one is dealing with `inner` +types or `outer` types. Both types can be used for quicker development. + +Usually, `outer` type is used for a full featured entity (e.g. `IpSpace`, `NsxtEdgeGateway`), while +`inner` suits cases where one needs to perform operations on an already existing or a read-only +entity. + +**Hint:** return value of your entity method will always hint whether it is `inner` or `outer` one: + +`inner` type function signature example (returns `*types.VdcNetworkProfile`): + +``` +func (adminVdc *AdminVdc) UpdateVdcNetworkProfile(vdcNetworkProfileConfig *types.VdcNetworkProfile) (*types.VdcNetworkProfile, error) { +``` + +`outer` type function signature example (returns `*IpSpace`): + +``` +func (vcdClient *VCDClient) CreateIpSpace(ipSpaceConfig *types.IpSpace) (*IpSpace, error) { +``` + +#### inner CRUD functions + +The entities that match below criteria are usually going to use `inner` crud functions: +* API property manipulation with separate API endpoints for an already existing entity (e.g. VDC + Network Profiles `Vdc.UpdateVdcNetworkProfile`) +* Read only entities (e.g. NSX-T Segment Profiles `VCDClient.GetAllIpDiscoveryProfiles`) + +Inner types are more simple as they can be directly used without any additional overhead. There are +7 functions that can be used: + +* `createInnerEntity` +* `updateInnerEntity` +* `updateInnerEntityWithHeaders` +* `getInnerEntity` +* `getInnerEntityWithHeaders` +* `deleteEntityById` +* `getAllInnerEntities` + +Existing examples of the implementation are: + +* `Vdc.GetVdcNetworkProfile` +* `Vdc.UpdateVdcNetworkProfile` +* `Vdc.DeleteVdcNetworkProfile` +* `VCDClient.GetAllIpDiscoveryProfiles` + +#### outer CRUD functions + +The entities, that implement complete management of a VCD entity will usually rely on `outer` CRUD +functions. Any `outer` type *must* implement `wrap` method (example signature provided below). It is +required to satisfy generic interface constraint (so that generic functions are able to wrap `inner` +type into `outer` type) + +```go +func (o OuterEntity) wrap(inner *InnerEntity) *OuterEntity { + o.OuterEntity = inner + return &o +} +``` +There are 5 functions for handling CRU(D). +* `createOuterEntity` +* `updateOuterEntity` +* `getOuterEntity` +* `getOuterEntityWithHeaders` +* `getAllOuterEntities` + +*Note*: `D` (deletion) in `CRUD` is a simple operation that does not additionally handle data and +`deleteEntityById` is sufficient. + +Existing examples of the implementation are: +* `IpSpace` +* `IpSpaceUplink` +* `DistributedFirewall` +* `DistributedFirewallRule` +* `NsxtSegmentProfileTemplate` +* `DefinedEntityType` +* `DefinedInterface` +* `DefinedEntity` + ## Testing -Every feature in the library must include testing. See [TESTING.md](https://github.com/vmware/go-vcloud-director/blob/master/TESTING.md) for more info. +Every feature in the library must include testing. See [TESTING.md](https://github.com/vmware/go-vcloud-director/blob/main/TESTING.md) for more info. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 40dd8c9c9..d9669040a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,8 +8,8 @@ request. For any questions about the CLA process, please refer to our ## Community -vCloud Director Golang and Terraform contributors can be found here: -https://vmwarecode.slack.com, vcd-terraform-dev# +VMware Cloud Director Go(lang) and Terraform contributors can be found here: +https://vmwarecode.slack.com, #vcd-terraform-dev ## Logging Bugs @@ -31,7 +31,7 @@ contributors. Typical contribution flow steps are: - Update Go modules files `go.mod` and `go.sum` if you're changing dependencies. - Fetch changes from upstream and resolve any merge conflicts so that your topic branch is up-to-date - Push all commits to the topic branch in your forked repo -- Submit a pull request to merge topic branch commits to upstream master +- Submit a pull request to merge topic branch commits to upstream main If this process sounds unfamiliar have a look at the excellent [overview of collaboration via pull requests on @@ -83,10 +83,7 @@ of the repo for pull requests back to go-vcloud-director. ### Clone and Set Upstream Remote Make a local clone of the forked repo and add the base go-vcloud-director -repo as the upstream remote repository. - -The project uses Go modules so the path is up to you, but do not forget -to set `GO111MODULE=on` if you are in `GOPATH` +repo as the upstream remote repository. The project uses Go modules so the path is up to you. ``` shell @@ -102,11 +99,11 @@ the fork up to date. More on that shortly. ### Make Changes and Commit -Start a new topic branch from the current HEAD position on master and +Start a new topic branch from the current HEAD position on main and commit your feature changes into that branch. ``` shell -git checkout -b foo-api-fix-22 master +git checkout -b foo-api-fix-22 main # (Make feature changes) git commit -a --signoff git push origin foo-api-fix-22 @@ -131,7 +128,7 @@ them now](https://stackoverflow.com/questions/161813/how-to-resolve-merge-confli ``` shell git checkout foo-api-fix-22 git fetch -a -git pull --rebase upstream master --tags +git pull --rebase upstream main --tags git push --force-with-lease origin foo-api-fix-22 ``` @@ -159,7 +156,7 @@ To contribute your feature, create a pull request by going to the [go-vcloud-dir Select 'compare across forks' and select imahacker/go-vcloud-director as 'head fork' and foo-api-fix-22 as the 'compare' branch. Leave the base fork as -vmware/go-vcloud-director and master. +vmware/go-vcloud-director and main. ### Wait... @@ -193,7 +190,7 @@ If you need to squash changes into an earlier commit, you can use: ``` shell git add . git commit --fixup -git rebase -i --autosquash master +git rebase -i --autosquash main git push --force-with-lease origin foo-api-fix-22 ``` diff --git a/Makefile b/Makefile index 69232a345..fd8f55219 100644 --- a/Makefile +++ b/Makefile @@ -1,16 +1,20 @@ TEST?=./... GOFMT_FILES?=$$(find . -name '*.go') maindir=$(PWD) +timeout=0 +ifdef VCD_TIMEOUT +timeout="$(VCD_TIMEOUT)" +endif -default: fmtcheck vet static build +default: fmtcheck vet static security build # test runs the test suite and vets the code test: testunit tagverify @echo "==> Running Functional Tests" - cd govcd && go test -tags "functional" -timeout=650m -check.vv + cd govcd && go test -tags "functional" -timeout=$(timeout) -check.vv # tagverify checks that each tag can run independently -tagverify: fmtcheck +tagverify: fmtcheck @echo "==> Running Tags Tests" @./scripts/test-tags.sh @@ -26,42 +30,46 @@ testrace: # This will include tests guarded by build tag concurrent with race detector testconcurrent: - cd govcd && go test -race -tags "api concurrent" -timeout 15m -check.vv -check.f "Test.*Concurrent" + cd govcd && go test -race -tags "api concurrent" -timeout $(timeout) -check.vv -check.f "Test.*Concurrent" # tests only catalog related features testcatalog: - cd govcd && go test -tags "catalog" -timeout 15m -check.vv + cd govcd && go test -tags "catalog" -timeout $(timeout) -check.vv # tests only vapp and vm features testvapp: - cd govcd && go test -tags "vapp vm" -timeout 25m -check.vv + cd govcd && go test -tags "vapp vm" -timeout $(timeout) -check.vv # tests only edge gateway features testgateway: - cd govcd && go test -tags "gateway" -timeout 15m -check.vv + cd govcd && go test -tags "gateway" -timeout $(timeout) -check.vv # tests only networking features testnetwork: - cd govcd && go test -tags "network" -timeout 15m -check.vv + cd govcd && go test -tags "network" -timeout $(timeout) -check.vv # tests only load balancer features testlb: - cd govcd && go test -tags "lb" -timeout 15m -check.vv + cd govcd && go test -tags "lb" -timeout $(timeout) -check.vv # tests only NSXV related features testnsxv: - cd govcd && go test -tags "nsxv" -timeout 15m -check.vv + cd govcd && go test -tags "nsxv" -timeout $(timeout) -check.vv # vet runs the Go source code static analysis tool `vet` to find # any common errors. vet: @echo "==> Running Go Vet" - @go vet ./... ; if [ $$? -ne 0 ] ; then echo "vet error!" ; exit 1 ; fi + @go vet -tags ALL ./... ; if [ $$? -ne 0 ] ; then echo "vet error!" ; exit 1 ; fi # static runs the source code static analysis tool `staticcheck` static: fmtcheck @./scripts/staticcheck.sh +# security runs the source code security analysis tool `gosec` +security: fmtcheck + @./scripts/gosec.sh + get-deps: @echo "==> Fetching dependencies" @go get -v $(TEST) diff --git a/README.md b/README.md index c17e3400e..a3e52ae24 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# go-vcloud-director [![Build Status](https://travis-ci.org/vmware/go-vcloud-director.svg?branch=master)](https://travis-ci.org/vmware/go-vcloud-director) [![Coverage Status](https://coveralls.io/repos/vmware/go-vcloud-director/badge.svg?branch=master&service=github)](https://coveralls.io/github/vmware/go-vcloud-director?branch=master) [![GoDoc](https://godoc.org/github.com/vmware/go-vcloud-director?status.svg)](http://godoc.org/github.com/vmware/go-vcloud-director) [![Chat](https://img.shields.io/badge/chat-on%20slack-brightgreen.svg)](https://vmwarecode.slack.com/messages/CBBBXVB16) +# go-vcloud-director [![GoDoc](https://godoc.org/github.com/vmware/go-vcloud-director?status.svg)](http://godoc.org/github.com/vmware/go-vcloud-director) [![Chat](https://img.shields.io/badge/chat-on%20slack-brightgreen.svg)](https://vmwarecode.slack.com/messages/CBBBXVB16) This repo contains the `go-vcloud-director` package which implements -an SDK for vCloud Director. The project serves the needs of Golang -developers who need to integrate with vCloud Director. It is also the +an SDK for VMware Cloud Director. The project serves the needs of Golang +developers who need to integrate with VMware Cloud Director. It is also the basis of the [vCD Terraform Provider](https://github.com/vmware/terraform-provider-vcd). @@ -37,7 +37,7 @@ To show the SDK in action run the example: ``` mkdir ~/govcd_example go mod init govcd_example -go get github.com/vmware/go-vcloud-director/v2@master +go get github.com/vmware/go-vcloud-director/v2@main go build -o example ./example user_name "password" org_name vcd_IP vdc_name ``` @@ -114,7 +114,7 @@ func main() { ## Authentication -You can authenticate to the vCD in four ways: +You can authenticate to the vCD in five ways: * With a System Administration user and password (`administrator@system`) * With an Organization user and password (`tenant-admin@org-name`) @@ -133,6 +133,11 @@ For the above two methods, you use: The file `scripts/get_token.sh` provides a handy method of extracting the token (`x-vcloud-authorization` value) for future use. +* With a service account token (the file needs to have `r+w` rights) +```go + err := vcdClient.SetServiceAccountApiToken(Org, "tokenfile.json") +``` + * SAML user and password (works with ADFS as IdP using WS-TRUST endpoint "/adfs/services/trust/13/usernamemixed"). One must pass `govcd.WithSamlAdfs(true,customAdfsRptId)` and username must be formatted so that ADFS understands it ('user@contoso.com' or diff --git a/TESTING.md b/TESTING.md index 0c3f31e19..f61975741 100644 --- a/TESTING.md +++ b/TESTING.md @@ -342,6 +342,10 @@ While running tests, the following environment variables can be used: * `VCD_TOKEN` : specifies the authorization token to use instead of username/password (Use `./scripts/get_token.sh` to retrieve one) * `GOVCD_KEEP_TEST_OBJECTS` will skip deletion of objects created during tests. +* `GOVCD_API_VERSION` allows to select the API version to use. This must be used **for testing purposes only** as the SDK + has been tested to use certain version of the API. Using this environment variable may lead to unexpected failures. +* `GOVCD_SKIP_LOG_TRACING` can disable sending 'X-VMWARE-VCLOUD-CLIENT-REQUEST-ID' header that is + used for easier log correlation When both the environment variable and the command line option are possible, the environment variable gets evaluated first. diff --git a/go.mod b/go.mod index 749933f69..73af1609f 100644 --- a/go.mod +++ b/go.mod @@ -1,18 +1,25 @@ module github.com/vmware/go-vcloud-director/v2 -go 1.13 +go 1.22 require ( github.com/araddon/dateparse v0.0.0-20190622164848-0fb0a474d195 github.com/hashicorp/go-version v1.2.0 github.com/kr/pretty v0.2.1 github.com/peterhellberg/link v1.1.0 - github.com/stretchr/testify v1.5.1 // indirect + golang.org/x/exp v0.0.0-20240119083558-1b970713d09a + golang.org/x/text v0.14.0 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 - gopkg.in/yaml.v2 v2.2.2 + gopkg.in/yaml.v2 v2.4.0 + sigs.k8s.io/yaml v1.4.0 +) + +require ( + github.com/kr/text v0.1.0 // indirect + github.com/stretchr/testify v1.9.0 // indirect ) replace ( gopkg.in/check.v1 => github.com/go-check/check v0.0.0-20201130134442-10cb98267c6c - gopkg.in/yaml.v2 => github.com/go-yaml/yaml/v2 v2.2.2 + gopkg.in/yaml.v2 => github.com/go-yaml/yaml/v2 v2.4.0 ) diff --git a/go.sum b/go.sum index 187972327..1070a96a7 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,13 @@ github.com/araddon/dateparse v0.0.0-20190622164848-0fb0a474d195 h1:c4mLfegoDw6OhSJXTd2jUEQgZUQuJWtocudb97Qn9EM= github.com/araddon/dateparse v0.0.0-20190622164848-0fb0a474d195/go.mod h1:SLqhdZcd+dF3TEVL2RMoob5bBP5R1P1qkox+HtCBgGI= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-check/check v0.0.0-20201130134442-10cb98267c6c h1:3LdnoQiW6yLkxRIwSU3pbYp3zqW1daDgoOcOD09OzJs= github.com/go-check/check v0.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -github.com/go-yaml/yaml/v2 v2.2.2 h1:uw2m9KuKRscWGAkuyoBGQcZSdibhmuXKSJ3+9Tj3zXc= -github.com/go-yaml/yaml/v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +github.com/go-yaml/yaml/v2 v2.4.0 h1:FNqNkD8zxfgoQ6pSknwk+CnijAT6ijXMqcUg7FXN3LU= +github.com/go-yaml/yaml/v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= @@ -17,6 +19,13 @@ github.com/peterhellberg/link v1.1.0 h1:s2+RH8EGuI/mI4QwrWGSYQCRz7uNgip9BaM04HKu github.com/peterhellberg/link v1.1.0/go.mod h1:gtSlOT4jmkY8P47hbTc8PTgiDDWpdPbFYl75keYyBB8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA= +golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/govcd/access_control.go b/govcd/access_control.go index e83dff108..a573d50bf 100644 --- a/govcd/access_control.go +++ b/govcd/access_control.go @@ -15,15 +15,9 @@ import ( "github.com/vmware/go-vcloud-director/v2/types/v56" ) -// orgInfoType is the basic information about an organization (needed for tenant context) -type orgInfoType struct { - id string - name string -} - // orgInfoCache is a cache to save org information, avoid repeated calls to compute the same result. // The keys to this map are the requesting objects IDs. -var orgInfoCache = make(map[string]orgInfoType) +var orgInfoCache = make(map[string]*TenantContext) // GetAccessControl retrieves the access control information for the requested entity func (client Client) GetAccessControl(href, entityType, entityName string, headerValues map[string]string) (*types.ControlAccessParams, error) { @@ -74,7 +68,13 @@ func (client Client) GetAccessControl(href, entityType, entityName string, heade // * The subject (HREF and Type are mandatory) // * The access level (one of ReadOnly, Change, FullControl) func (client *Client) SetAccessControl(accessControl *types.ControlAccessParams, href, entityType, entityName string, headerValues map[string]string) error { + return client.setAccessControlWithHttpMethod(http.MethodPost, accessControl, href, entityType, entityName, headerValues) +} +// setAccessControlWithMethod is the same as Client.SetAccessControl but allowing passing a different HTTP method. +// This method has been created since VDC accessControl endpoint works with PUT and SetAccessControl method worked +// exclusively with POST. This private method gives the flexibility to use both POST and PUT passing it as httpMethod parameter. +func (client *Client) setAccessControlWithHttpMethod(httpMethod string, accessControl *types.ControlAccessParams, href, entityType, entityName string, headerValues map[string]string) error { href += "/action/controlAccess" // Make sure that subjects in the setting list are used only once if accessControl.AccessSettings != nil && len(accessControl.AccessSettings.AccessSetting) > 0 { @@ -88,7 +88,7 @@ func (client *Client) SetAccessControl(accessControl *types.ControlAccessParams, return fmt.Errorf("[client.SetAccessControl] subject %s (%s) used more than once", setting.Subject.Name, setting.Subject.HREF) } used[setting.Subject.HREF] = true - if setting.Subject.Type == "" { + if setting.Subject.Type == "" && !strings.Contains(strings.ToLower(href), "vdctemplate") { // VDC Templates must not send subject type, otherwise calls fail return fmt.Errorf("[client.SetAccessControl] subject %s (%s) has no type defined", setting.Subject.Name, setting.Subject.HREF) } } @@ -116,7 +116,7 @@ func (client *Client) SetAccessControl(accessControl *types.ControlAccessParams, req := client.newRequest( nil, // params nil, // notEncodedParams - http.MethodPost, // method + httpMethod, // method *queryUrl, // reqUrl body, // body client.APIVersion, // apiVersion @@ -329,7 +329,7 @@ func (vapp *VApp) getAccessControlHeader(useTenantContext bool) (map[string]stri if err != nil { return nil, err } - return map[string]string{types.HeaderTenantContext: orgInfo.id, types.HeaderAuthContext: orgInfo.name}, nil + return map[string]string{types.HeaderTenantContext: orgInfo.OrgId, types.HeaderAuthContext: orgInfo.OrgName}, nil } // getAccessControlHeader builds the data needed to set the header when tenant context is required. @@ -343,7 +343,7 @@ func (catalog *Catalog) getAccessControlHeader(useTenantContext bool) (map[strin if err != nil { return nil, err } - return map[string]string{types.HeaderTenantContext: orgInfo.id, types.HeaderAuthContext: orgInfo.name}, nil + return map[string]string{types.HeaderTenantContext: orgInfo.OrgId, types.HeaderAuthContext: orgInfo.OrgName}, nil } // getAccessControlHeader builds the data needed to set the header when tenant context is required. @@ -358,5 +358,243 @@ func (adminCatalog *AdminCatalog) getAccessControlHeader(useTenantContext bool) if err != nil { return nil, err } - return map[string]string{types.HeaderTenantContext: orgInfo.id, types.HeaderAuthContext: orgInfo.name}, nil + return map[string]string{types.HeaderTenantContext: orgInfo.OrgId, types.HeaderAuthContext: orgInfo.OrgName}, nil +} + +// GetControlAccess read and returns the control access parameters from a VDC +func (vdc *Vdc) GetControlAccess(useTenantContext bool) (*types.ControlAccessParams, error) { + err := checkSanityVdcControlAccess(vdc) + if err != nil { + return nil, err + } + + var tenantContextHeaders map[string]string + + if useTenantContext { + tenantContext, err := vdc.getTenantContext() + if err != nil { + return nil, fmt.Errorf("error getting the tenant context - %s", err) + } + + tenantContextHeaders = getTenantContextHeader(tenantContext) + } + + controlAccessParams, err := vdc.client.GetAccessControl(vdc.Vdc.HREF, "vdc", vdc.Vdc.Name, tenantContextHeaders) + if err != nil { + return nil, fmt.Errorf("there was an error when retrieving VDC control access params - %s", err) + } + + return controlAccessParams, nil +} + +// SetControlAccess sets VDC control access parameters for everybody or individual users/groups. +// This method either sets control for everybody, passing isSharedToEveryOne true, and everyoneAccessLevel (currently only ReadOnly is supported for VDC) and nil for accessSettings, +// or can set access control for specific users/groups, passing isSharedToEveryOne false, everyoneAccessLevel "" and accessSettings filled as desired. +// The method will fail if tries to configure access control for everybody and passes individual users/groups to configure. +// It returns the control access parameters that are read from the API (using Vdc.GetControlAccess). +func (vdc *Vdc) SetControlAccess(isSharedToEveryOne bool, everyoneAccessLevel string, accessSettings []*types.AccessSetting, useTenantContext bool) (*types.ControlAccessParams, error) { + err := checkSanityVdcControlAccess(vdc) + if err != nil { + return nil, err + } + + if (isSharedToEveryOne && accessSettings != nil) && len(accessSettings) > 0 { + return nil, fmt.Errorf("either configure access for everybody or individual users, not both at the same time") + } + + var tenantContextHeaders map[string]string + var accessControl = &types.ControlAccessParams{ + Xmlns: types.XMLNamespaceVCloud, + } + + if isSharedToEveryOne { // Do configuration for everyone + if everyoneAccessLevel == "" { + return nil, fmt.Errorf("everyoneAccessLevel needs to be set if isSharedToEveryOne is true") + } + + accessControl.IsSharedToEveryone = true + accessControl.EveryoneAccessLevel = &everyoneAccessLevel + + } else { // Do configuration for individual users/groups + if len(accessSettings) > 0 { + accessControl.AccessSettings = &types.AccessSettingList{ + AccessSetting: accessSettings, + } + } + } + + if useTenantContext { + tenantContext, err := vdc.getTenantContext() + if err != nil { + return nil, fmt.Errorf("error getting the tenant context - %s", err) + } + + tenantContextHeaders = getTenantContextHeader(tenantContext) + } + + err = vdc.client.setAccessControlWithHttpMethod(http.MethodPut, accessControl, vdc.Vdc.HREF, "vdc", vdc.Vdc.Name, tenantContextHeaders) + if err != nil { + return nil, fmt.Errorf("there was an error when setting VDC control access params - %s", err) + } + + return vdc.GetControlAccess(useTenantContext) +} + +// DeleteControlAccess makes stop sharing VDC with anyone +func (vdc *Vdc) DeleteControlAccess(useTenantContext bool) (*types.ControlAccessParams, error) { + return vdc.SetControlAccess(false, "", nil, useTenantContext) +} + +// checkSanityVdcControlAccess is a function that check some Vdc attributes and returns error if any is missing. It is useful for +// checking sanity of Vdc struct before running controlAccess methods. +func checkSanityVdcControlAccess(vdc *Vdc) error { + if vdc.client == nil { + return fmt.Errorf("client has not been set up on Vdc struct. Please initialize it before using this method") + } + if vdc.Vdc == nil || vdc.Vdc.Name == "" { + return fmt.Errorf("types.Vdc struct has not been set up on Vdc struct or Vdc.Vdc.Name is missing. Please initialize it before using this method ") + } + return nil +} + +func publishCatalog(client *Client, catalogUrl string, tenantContext *TenantContext, publishCatalog types.PublishCatalogParams) error { + catalogUrl = catalogUrl + "/action/publish" + + publishCatalog.Xmlns = types.XMLNamespaceVCloud + + if tenantContext != nil { + client.SetCustomHeader(getTenantContextHeader(tenantContext)) + } + + err := client.ExecuteRequestWithoutResponse(catalogUrl, http.MethodPost, + types.PublishCatalog, "error setting catalog publishing state: %s", publishCatalog) + + if tenantContext != nil { + client.RemoveProvidedCustomHeaders(getTenantContextHeader(tenantContext)) + } + + return err +} + +// IsSharedReadOnly returns the state of the catalog read-only sharing to all organizations +func (cat *Catalog) IsSharedReadOnly() (bool, error) { + accessControl, err := cat.GetAccessControl(true) + if err != nil { + return false, err + } + if accessControl.AccessSettings != nil || accessControl.IsSharedToEveryone { + return false, nil + } + err = cat.Refresh() + if err != nil { + return false, err + } + return cat.Catalog.IsPublished, nil +} + +// IsSharedReadOnly returns the state of the catalog read-only sharing to all organizations +func (cat *AdminCatalog) IsSharedReadOnly() (bool, error) { + accessControl, err := cat.GetAccessControl(true) + if err != nil { + return false, err + } + if accessControl.AccessSettings != nil || accessControl.IsSharedToEveryone { + return false, nil + } + err = cat.Refresh() + if err != nil { + return false, err + } + return cat.AdminCatalog.IsPublished, nil +} + +// publish publishes a catalog read-only access control to all organizations. +// This operation is usually the second step for a read-only sharing to all Orgs +func (cat *Catalog) publish(isPublished bool) error { + if cat.Catalog == nil { + return fmt.Errorf("cannot publish catalog, Object is empty") + } + + catalogUrl := cat.Catalog.HREF + if catalogUrl == "nil" || catalogUrl == "" { + return fmt.Errorf("cannot publish catalog, HREF is empty") + } + + tenantContext, err := cat.getTenantContext() + if err != nil { + return fmt.Errorf("cannot publish catalog, tenant context error: %s", err) + } + + publishParameters := types.PublishCatalogParams{ + IsPublished: &isPublished, + } + err = publishCatalog(cat.client, catalogUrl, tenantContext, publishParameters) + if err != nil { + return err + } + + return cat.Refresh() +} + +// publish publishes a catalog read-only access control to all organizations. +// This operation is usually the second step for a read-only sharing to all Orgs +func (cat *AdminCatalog) publish(isPublished bool) error { + if cat.AdminCatalog == nil { + return fmt.Errorf("cannot publish catalog, Object is empty") + } + + catalogUrl := cat.AdminCatalog.HREF + if catalogUrl == "nil" || catalogUrl == "" { + return fmt.Errorf("cannot publish catalog, HREF is empty") + } + + tenantContext, err := cat.getTenantContext() + if err != nil { + return fmt.Errorf("cannot publish catalog, tenant context error: %s", err) + } + + publishParameters := types.PublishCatalogParams{ + IsPublished: &isPublished, + } + err = publishCatalog(cat.client, catalogUrl, tenantContext, publishParameters) + if err != nil { + return err + } + + err = cat.Refresh() + if err != nil { + return err + } + + return err +} + +// SetReadOnlyAccessControl will create or rescind the read-only catalog sharing to all organizations +func (cat *Catalog) SetReadOnlyAccessControl(isPublished bool) error { + if cat.Catalog == nil { + return fmt.Errorf("cannot set access control, Object is empty") + } + err := cat.SetAccessControl(&types.ControlAccessParams{ + IsSharedToEveryone: false, + EveryoneAccessLevel: addrOf(types.ControlAccessReadOnly), + }, true) + if err != nil { + return fmt.Errorf("error resetting access control record for catalog %s: %s", cat.Catalog.Name, err) + } + return cat.publish(isPublished) +} + +// SetReadOnlyAccessControl will create or rescind the read-only AdminCatalog sharing to all organizations +func (cat *AdminCatalog) SetReadOnlyAccessControl(isPublished bool) error { + if cat.AdminCatalog == nil { + return fmt.Errorf("cannot set access control, Object is empty") + } + err := cat.SetAccessControl(&types.ControlAccessParams{ + IsSharedToEveryone: false, + EveryoneAccessLevel: addrOf(types.ControlAccessReadOnly), + }, true) + if err != nil { + return err + } + return cat.publish(isPublished) } diff --git a/govcd/access_control_catalog_test.go b/govcd/access_control_catalog_test.go index 12191dd1b..a310db786 100644 --- a/govcd/access_control_catalog_test.go +++ b/govcd/access_control_catalog_test.go @@ -1,4 +1,4 @@ -// +build functional catalog ALL +//go:build functional || catalog || ALL /* * Copyright 2020 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -34,6 +34,7 @@ func (vcd *TestVCD) Test_AdminCatalogAccessControl(check *C) { check.Skip("Test_AdminCatalogAccessControl: Org name not given.") return } + vcd.checkSkipWhenApiToken(check) org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) check.Assert(err, IsNil) check.Assert(org, NotNil) @@ -51,8 +52,8 @@ func (vcd *TestVCD) Test_AdminCatalogAccessControl(check *C) { orgInfo, err := adminCatalog.getOrgInfo() check.Assert(err, IsNil) - check.Assert(orgInfo.id, Equals, extractUuid(adminorg.AdminOrg.ID)) - check.Assert(orgInfo.name, Equals, adminorg.AdminOrg.Name) + check.Assert(orgInfo.OrgId, Equals, extractUuid(adminorg.AdminOrg.ID)) + check.Assert(orgInfo.OrgName, Equals, adminorg.AdminOrg.Name) err = adminCatalog.Delete(true, true) check.Assert(err, IsNil) @@ -63,6 +64,7 @@ func (vcd *TestVCD) Test_CatalogAccessControl(check *C) { check.Skip("Test_CatalogAccessControl: Org name not given.") return } + vcd.checkSkipWhenApiToken(check) org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) check.Assert(err, IsNil) check.Assert(org, NotNil) @@ -82,8 +84,8 @@ func (vcd *TestVCD) Test_CatalogAccessControl(check *C) { orgInfo, err := catalog.getOrgInfo() check.Assert(err, IsNil) - check.Assert(orgInfo.id, Equals, extractUuid(adminorg.AdminOrg.ID)) - check.Assert(orgInfo.name, Equals, adminorg.AdminOrg.Name) + check.Assert(orgInfo.OrgId, Equals, extractUuid(adminorg.AdminOrg.ID)) + check.Assert(orgInfo.OrgName, Equals, adminorg.AdminOrg.Name) err = catalog.Delete(true, true) check.Assert(err, IsNil) @@ -153,7 +155,7 @@ func (vcd *TestVCD) testCatalogAccessControl(adminOrg *AdminOrg, catalog accessC checkEmpty() //globalSettings := types.ControlAccessParams{ // IsSharedToEveryone: true, - // EveryoneAccessLevel: takeStringPointer(types.ControlAccessReadWrite), + // EveryoneAccessLevel: addrOf(types.ControlAccessReadWrite), // AccessSettings: nil, //} //err = testAccessControl(catalogName+" catalog global", catalog, globalSettings, globalSettings, true, check) @@ -164,8 +166,8 @@ func (vcd *TestVCD) testCatalogAccessControl(adminOrg *AdminOrg, catalog accessC IsSharedToEveryone: false, EveryoneAccessLevel: nil, AccessSettings: &types.AccessSettingList{ - []*types.AccessSetting{ - &types.AccessSetting{ + AccessSetting: []*types.AccessSetting{ + { Subject: &types.LocalSubject{ HREF: users[0].user.User.Href, Name: users[0].user.User.Name, @@ -196,8 +198,8 @@ func (vcd *TestVCD) testCatalogAccessControl(adminOrg *AdminOrg, catalog accessC IsSharedToEveryone: false, EveryoneAccessLevel: nil, AccessSettings: &types.AccessSettingList{ - []*types.AccessSetting{ - &types.AccessSetting{ + AccessSetting: []*types.AccessSetting{ + { Subject: &types.LocalSubject{ HREF: users[0].user.User.Href, //Name: users[0].user.User.Name, // Pass info without name for one of the subjects @@ -206,7 +208,7 @@ func (vcd *TestVCD) testCatalogAccessControl(adminOrg *AdminOrg, catalog accessC ExternalSubject: nil, AccessLevel: types.ControlAccessReadOnly, }, - &types.AccessSetting{ + { Subject: &types.LocalSubject{ HREF: users[1].user.User.Href, Name: users[1].user.User.Name, @@ -231,8 +233,8 @@ func (vcd *TestVCD) testCatalogAccessControl(adminOrg *AdminOrg, catalog accessC IsSharedToEveryone: false, EveryoneAccessLevel: nil, AccessSettings: &types.AccessSettingList{ - []*types.AccessSetting{ - &types.AccessSetting{ + AccessSetting: []*types.AccessSetting{ + { Subject: &types.LocalSubject{ HREF: users[0].user.User.Href, Name: users[0].user.User.Name, @@ -241,7 +243,7 @@ func (vcd *TestVCD) testCatalogAccessControl(adminOrg *AdminOrg, catalog accessC ExternalSubject: nil, AccessLevel: types.ControlAccessReadOnly, }, - &types.AccessSetting{ + { Subject: &types.LocalSubject{ HREF: users[1].user.User.Href, //Name: users[1].user.User.Name,// Pass info without name for one of the subjects @@ -250,7 +252,7 @@ func (vcd *TestVCD) testCatalogAccessControl(adminOrg *AdminOrg, catalog accessC ExternalSubject: nil, AccessLevel: types.ControlAccessFullControl, }, - &types.AccessSetting{ + { Subject: &types.LocalSubject{ HREF: users[2].user.User.Href, Name: users[2].user.User.Name, @@ -272,8 +274,8 @@ func (vcd *TestVCD) testCatalogAccessControl(adminOrg *AdminOrg, catalog accessC IsSharedToEveryone: false, EveryoneAccessLevel: nil, AccessSettings: &types.AccessSettingList{ - []*types.AccessSetting{ - &types.AccessSetting{ + AccessSetting: []*types.AccessSetting{ + { Subject: &types.LocalSubject{ HREF: users[0].user.User.Href, Name: users[0].user.User.Name, @@ -282,7 +284,7 @@ func (vcd *TestVCD) testCatalogAccessControl(adminOrg *AdminOrg, catalog accessC ExternalSubject: nil, AccessLevel: types.ControlAccessReadOnly, }, - &types.AccessSetting{ + { Subject: &types.LocalSubject{ HREF: users[1].user.User.Href, //Name: users[1].user.User.Name,// Pass info without name for one of the subjects @@ -291,7 +293,7 @@ func (vcd *TestVCD) testCatalogAccessControl(adminOrg *AdminOrg, catalog accessC ExternalSubject: nil, AccessLevel: types.ControlAccessFullControl, }, - &types.AccessSetting{ + { Subject: &types.LocalSubject{ HREF: users[2].user.User.Href, Name: users[2].user.User.Name, @@ -300,7 +302,7 @@ func (vcd *TestVCD) testCatalogAccessControl(adminOrg *AdminOrg, catalog accessC ExternalSubject: nil, AccessLevel: types.ControlAccessReadWrite, }, - &types.AccessSetting{ + { Subject: &types.LocalSubject{ HREF: newOrg.AdminOrg.HREF, Name: newOrg.AdminOrg.Name, @@ -324,8 +326,8 @@ func (vcd *TestVCD) testCatalogAccessControl(adminOrg *AdminOrg, catalog accessC IsSharedToEveryone: false, EveryoneAccessLevel: nil, AccessSettings: &types.AccessSettingList{ - []*types.AccessSetting{ - &types.AccessSetting{ + AccessSetting: []*types.AccessSetting{ + { Subject: &types.LocalSubject{ HREF: adminOrg.AdminOrg.HREF, Name: adminOrg.AdminOrg.Name, @@ -334,7 +336,7 @@ func (vcd *TestVCD) testCatalogAccessControl(adminOrg *AdminOrg, catalog accessC ExternalSubject: nil, AccessLevel: types.ControlAccessFullControl, }, - &types.AccessSetting{ + { Subject: &types.LocalSubject{ HREF: newOrg.AdminOrg.HREF, Name: newOrg.AdminOrg.Name, @@ -348,6 +350,12 @@ func (vcd *TestVCD) testCatalogAccessControl(adminOrg *AdminOrg, catalog accessC } err = testAccessControl(catalogName+" catalog two org", catalog, twoOrgsSettings, twoOrgsSettings, true, catalogTenantContext, check) check.Assert(err, IsNil) + catalogs, err := vcd.client.Client.QueryCatalogRecords(catalogName, TenantContext{newOrg.AdminOrg.ID, newOrg.AdminOrg.Name}) + check.Assert(err, IsNil) + check.Assert(len(catalogs), Equals, 1) + foundCatalog, err := vcd.client.Client.GetAdminCatalogByHref(catalogs[0].HREF) + check.Assert(err, IsNil) + check.Assert(foundCatalog.AdminCatalog.ID, Equals, catalog.GetId()) } // Set empty settings explicitly diff --git a/govcd/access_control_test.go b/govcd/access_control_test.go index 1e89bc40e..8ff8bb277 100644 --- a/govcd/access_control_test.go +++ b/govcd/access_control_test.go @@ -1,4 +1,4 @@ -// +build functional vapp catalog ALL +//go:build functional || vapp || catalog || ALL /* * Copyright 2020 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. diff --git a/govcd/access_control_vapp_test.go b/govcd/access_control_vapp_test.go index b3866227b..8a255a772 100644 --- a/govcd/access_control_vapp_test.go +++ b/govcd/access_control_vapp_test.go @@ -1,4 +1,4 @@ -// +build functional vapp ALL +//go:build functional || vapp || ALL /* * Copyright 2020 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -34,6 +34,7 @@ func (vcd *TestVCD) Test_VappAccessControl(check *C) { check.Skip("Test_VappAccessControl: VDC name not given.") return } + vcd.checkSkipWhenApiToken(check) org, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) check.Assert(err, IsNil) check.Assert(org, NotNil) @@ -54,10 +55,10 @@ func (vcd *TestVCD) Test_VappAccessControl(check *C) { } // Create a new vApp - vapp, err := makeEmptyVapp(vdc, vappName) + vapp, err := makeEmptyVapp(vdc, vappName, "") check.Assert(err, IsNil) check.Assert(vapp, NotNil) - AddToCleanupList(vappName, "vapp", vcd.config.VCD.Org+"|"+vcd.config.VCD.Vdc, "Test_VappAccessControl") + AddToCleanupList(vappName, "vapp", vcd.config.VCD.Vdc, "Test_VappAccessControl") checkEmpty := func() { settings, err := vapp.GetAccessControl(vappTenantContext) @@ -97,7 +98,7 @@ func (vcd *TestVCD) Test_VappAccessControl(check *C) { // Set access control to every user and group allUsersSettings := types.ControlAccessParams{ - EveryoneAccessLevel: takeStringPointer(types.ControlAccessReadOnly), + EveryoneAccessLevel: addrOf(types.ControlAccessReadOnly), IsSharedToEveryone: true, } @@ -106,7 +107,7 @@ func (vcd *TestVCD) Test_VappAccessControl(check *C) { check.Assert(err, IsNil) allUsersSettings = types.ControlAccessParams{ - EveryoneAccessLevel: takeStringPointer(types.ControlAccessReadWrite), + EveryoneAccessLevel: addrOf(types.ControlAccessReadWrite), IsSharedToEveryone: true, } err = testAccessControl("vapp all users R/W", vapp, allUsersSettings, allUsersSettings, true, vappTenantContext, check) @@ -117,8 +118,8 @@ func (vcd *TestVCD) Test_VappAccessControl(check *C) { IsSharedToEveryone: false, EveryoneAccessLevel: nil, AccessSettings: &types.AccessSettingList{ - []*types.AccessSetting{ - &types.AccessSetting{ + AccessSetting: []*types.AccessSetting{ + { Subject: &types.LocalSubject{ HREF: users[0].user.User.Href, Name: users[0].user.User.Name, @@ -149,8 +150,8 @@ func (vcd *TestVCD) Test_VappAccessControl(check *C) { IsSharedToEveryone: false, EveryoneAccessLevel: nil, AccessSettings: &types.AccessSettingList{ - []*types.AccessSetting{ - &types.AccessSetting{ + AccessSetting: []*types.AccessSetting{ + { Subject: &types.LocalSubject{ HREF: users[0].user.User.Href, //Name: users[0].user.User.Name, // Pass info without name for one of the subjects @@ -159,7 +160,7 @@ func (vcd *TestVCD) Test_VappAccessControl(check *C) { ExternalSubject: nil, AccessLevel: types.ControlAccessReadOnly, }, - &types.AccessSetting{ + { Subject: &types.LocalSubject{ HREF: users[1].user.User.Href, Name: users[1].user.User.Name, @@ -184,8 +185,8 @@ func (vcd *TestVCD) Test_VappAccessControl(check *C) { IsSharedToEveryone: false, EveryoneAccessLevel: nil, AccessSettings: &types.AccessSettingList{ - []*types.AccessSetting{ - &types.AccessSetting{ + AccessSetting: []*types.AccessSetting{ + { Subject: &types.LocalSubject{ HREF: users[0].user.User.Href, Name: users[0].user.User.Name, @@ -194,7 +195,7 @@ func (vcd *TestVCD) Test_VappAccessControl(check *C) { ExternalSubject: nil, AccessLevel: types.ControlAccessReadOnly, }, - &types.AccessSetting{ + { Subject: &types.LocalSubject{ HREF: users[1].user.User.Href, //Name: users[1].user.User.Name,// Pass info without name for one of the subjects @@ -203,7 +204,7 @@ func (vcd *TestVCD) Test_VappAccessControl(check *C) { ExternalSubject: nil, AccessLevel: types.ControlAccessFullControl, }, - &types.AccessSetting{ + { Subject: &types.LocalSubject{ HREF: users[2].user.User.Href, Name: users[2].user.User.Name, @@ -229,6 +230,6 @@ func (vcd *TestVCD) Test_VappAccessControl(check *C) { orgInfo, err := vapp.getOrgInfo() check.Assert(err, IsNil) - check.Assert(orgInfo.id, Equals, extractUuid(org.AdminOrg.ID)) - check.Assert(orgInfo.name, Equals, org.AdminOrg.Name) + check.Assert(orgInfo.OrgId, Equals, extractUuid(org.AdminOrg.ID)) + check.Assert(orgInfo.OrgName, Equals, org.AdminOrg.Name) } diff --git a/govcd/admincatalog.go b/govcd/admincatalog.go index 7cf2034c9..9585249c5 100644 --- a/govcd/admincatalog.go +++ b/govcd/admincatalog.go @@ -1,5 +1,5 @@ /* - * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + * Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. */ package govcd @@ -7,11 +7,15 @@ package govcd import ( "fmt" "net/http" + "net/url" + "strings" + "time" "github.com/vmware/go-vcloud-director/v2/types/v56" + "github.com/vmware/go-vcloud-director/v2/util" ) -// AdminCatalog is a admin view of a vCloud Director Catalog +// AdminCatalog is a admin view of a VMware Cloud Director Catalog // To be able to get an AdminCatalog representation, users must have // admin credentials to the System org. AdminCatalog is used // for creating, updating, and deleting a Catalog. @@ -19,6 +23,7 @@ import ( type AdminCatalog struct { AdminCatalog *types.AdminCatalog client *Client + parent organization } func NewAdminCatalog(client *Client) *AdminCatalog { @@ -28,7 +33,15 @@ func NewAdminCatalog(client *Client) *AdminCatalog { } } -// Deletes the Catalog, returning an error if the vCD call fails. +func NewAdminCatalogWithParent(client *Client, parent organization) *AdminCatalog { + return &AdminCatalog{ + AdminCatalog: new(types.AdminCatalog), + client: client, + parent: parent, + } +} + +// Delete deletes the Catalog, returning an error if the vCD call fails. // Link to API call: https://code.vmware.com/apis/220/vcloud#/doc/doc/operations/DELETE-Catalog.html func (adminCatalog *AdminCatalog) Delete(force, recursive bool) error { catalog := NewCatalog(adminCatalog.client) @@ -36,7 +49,7 @@ func (adminCatalog *AdminCatalog) Delete(force, recursive bool) error { return catalog.Delete(force, recursive) } -// Updates the Catalog definition from current Catalog struct contents. +// Update updates the Catalog definition from current Catalog struct contents. // Any differences that may be legally applied will be updated. // Returns an error if the call to vCD fails. Update automatically performs // a refresh with the admin catalog it gets back from the rest api @@ -59,16 +72,18 @@ func (adminCatalog *AdminCatalog) Update() error { return err } -// Uploads an ova file to a catalog. This method only uploads bits to vCD spool area. +// UploadOvf uploads an ova file to a catalog. This method only uploads bits to vCD spool area. // Returns errors if any occur during upload from vCD or upload process. On upload fail client may need to // remove vCD catalog item which waits for files to be uploaded. Files from ova are extracted to system // temp folder "govcd+random number" and left for inspection on error. func (adminCatalog *AdminCatalog) UploadOvf(ovaFileName, itemName, description string, uploadPieceSize int64) (UploadTask, error) { catalog := NewCatalog(adminCatalog.client) catalog.Catalog = &adminCatalog.AdminCatalog.Catalog + catalog.parent = adminCatalog.parent return catalog.UploadOvf(ovaFileName, itemName, description, uploadPieceSize) } +// Refresh fetches a fresh copy of the Admin Catalog func (adminCatalog *AdminCatalog) Refresh() error { if *adminCatalog == (AdminCatalog{}) || adminCatalog.AdminCatalog.HREF == "" { return fmt.Errorf("cannot refresh, Object is empty or HREF is empty") @@ -87,6 +102,609 @@ func (adminCatalog *AdminCatalog) Refresh() error { } // getOrgInfo finds the organization to which the admin catalog belongs, and returns its name and ID -func (adminCatalog *AdminCatalog) getOrgInfo() (orgInfoType, error) { - return getOrgInfo(adminCatalog.client, adminCatalog.AdminCatalog.Link, adminCatalog.AdminCatalog.ID, adminCatalog.AdminCatalog.Name, "AdminCatalog") +func (adminCatalog *AdminCatalog) getOrgInfo() (*TenantContext, error) { + return adminCatalog.getTenantContext() +} + +// PublishToExternalOrganizations publishes a catalog to external organizations. +func (cat *AdminCatalog) PublishToExternalOrganizations(publishExternalCatalog types.PublishExternalCatalogParams) error { + if cat.AdminCatalog == nil { + return fmt.Errorf("cannot publish to external organization, Object is empty") + } + + url := cat.AdminCatalog.HREF + if url == "nil" || url == "" { + return fmt.Errorf("cannot publish to external organization, HREF is empty") + } + + tenantContext, err := cat.getTenantContext() + if err != nil { + return fmt.Errorf("cannot publish to external organization, tenant context error: %s", err) + } + + err = publishToExternalOrganizations(cat.client, url, tenantContext, publishExternalCatalog) + if err != nil { + return err + } + + err = cat.Refresh() + if err != nil { + return err + } + + return err +} + +// CreateCatalogFromSubscriptionAsync creates a new catalog by subscribing to a published catalog +// Parameter subscription needs to be filled manually +func (org *AdminOrg) CreateCatalogFromSubscriptionAsync(subscription types.ExternalCatalogSubscription, + storageProfiles *types.CatalogStorageProfiles, + catalogName, password string, localCopy bool) (*AdminCatalog, error) { + + // If the receiving Org doesn't have any VDCs, it means that there is no storage that can be used + // by a catalog + if len(org.AdminOrg.Vdcs.Vdcs) == 0 { + return nil, fmt.Errorf("org %s does not have any storage to support a catalog", org.AdminOrg.Name) + } + href := "" + + // The subscribed catalog creation is like a regular catalog creation, with the + // difference that the subscription details are filled in + for _, link := range org.AdminOrg.Link { + if link.Rel == "add" && link.Type == types.MimeAdminCatalog { + href = link.HREF + break + } + } + if href == "" { + return nil, fmt.Errorf("catalog creation link not found for org %s", org.AdminOrg.Name) + } + adminCatalog := NewAdminCatalog(org.client) + reqCatalog := &types.Catalog{ + Name: catalogName, + } + adminCatalog.AdminCatalog = &types.AdminCatalog{ + Xmlns: types.XMLNamespaceVCloud, + Catalog: *reqCatalog, + CatalogStorageProfiles: storageProfiles, + ExternalCatalogSubscription: &types.ExternalCatalogSubscription{ + LocalCopy: localCopy, + Password: password, + Location: subscription.Location, + SubscribeToExternalFeeds: true, + }, + } + + adminCatalog.AdminCatalog.ExternalCatalogSubscription.Password = password + adminCatalog.AdminCatalog.ExternalCatalogSubscription.LocalCopy = localCopy + _, err := org.client.ExecuteRequest(href, http.MethodPost, types.MimeAdminCatalog, + "error subscribing to catalog: %s", adminCatalog.AdminCatalog, adminCatalog.AdminCatalog) + if err != nil { + return nil, err + } + // Before returning, check that there are no failing tasks + err = adminCatalog.Refresh() + if err != nil { + return nil, fmt.Errorf("error refreshing subscribed catalog %s: %s", catalogName, err) + } + if adminCatalog.AdminCatalog.Tasks != nil { + msg := "" + for _, task := range adminCatalog.AdminCatalog.Tasks.Task { + if task.Status == "error" { + if task.Error != nil { + msg = task.Error.Error() + } + return nil, fmt.Errorf("error while subscribing catalog %s (task %s): %s", catalogName, task.Name, msg) + } + if task.Tasks != nil { + for _, subTask := range task.Tasks.Task { + if subTask.Status == "error" { + if subTask.Error != nil { + msg = subTask.Error.Error() + } + return nil, fmt.Errorf("error while subscribing catalog %s (subTask %s): %s", catalogName, subTask.Name, msg) + } + + } + } + } + } + return adminCatalog, nil +} + +// FullSubscriptionUrl returns the subscription URL from a publishing catalog +// adding the HOST if needed +func (cat *AdminCatalog) FullSubscriptionUrl() (string, error) { + err := cat.Refresh() + if err != nil { + return "", err + } + if cat.AdminCatalog.PublishExternalCatalogParams == nil { + return "", fmt.Errorf("AdminCatalog %s has no publishing parameters", cat.AdminCatalog.Name) + } + subscriptionUrl, err := buildFullUrl(cat.AdminCatalog.PublishExternalCatalogParams.CatalogPublishedUrl, cat.AdminCatalog.HREF) + if err != nil { + return "", err + } + return subscriptionUrl, nil +} + +// buildFullUrl gets a (possibly incomplete) URL and returns it completed, using the provided HREF as basis +func buildFullUrl(subscriptionUrl, href string) (string, error) { + var err error + if !IsValidUrl(subscriptionUrl) { + // Get the entity base URL + cutPosition := strings.Index(href, "/api") + host := href[:cutPosition] + subscriptionUrl, err = url.JoinPath(host, subscriptionUrl) + if err != nil { + return "", err + } + } + return subscriptionUrl, nil +} + +// IsValidUrl returns true if the given URL is complete and usable +func IsValidUrl(str string) bool { + u, err := url.Parse(str) + return err == nil && u.Scheme != "" && u.Host != "" +} + +// CreateCatalogFromSubscription is a wrapper around CreateCatalogFromSubscriptionAsync +// After catalog creation, it waits for the import tasks to complete within a given timeout +func (org *AdminOrg) CreateCatalogFromSubscription(subscription types.ExternalCatalogSubscription, + storageProfiles *types.CatalogStorageProfiles, + catalogName, password string, localCopy bool, timeout time.Duration) (*AdminCatalog, error) { + noTimeout := timeout == 0 + adminCatalog, err := org.CreateCatalogFromSubscriptionAsync(subscription, storageProfiles, catalogName, password, localCopy) + if err != nil { + return nil, err + } + start := time.Now() + for noTimeout || time.Since(start) < timeout { + if noTimeout { + util.Logger.Printf("[TRACE] [CreateCatalogFromSubscription] no timeout given - Elapsed %s", time.Since(start)) + } + err = adminCatalog.Refresh() + if err != nil { + return nil, err + } + if ResourceComplete(adminCatalog.AdminCatalog.Tasks) { + return adminCatalog, nil + } + } + return nil, fmt.Errorf("adminCatalog %s still not complete after %s", adminCatalog.AdminCatalog.Name, timeout) +} + +// WaitForTasks waits for the catalog's tasks to complete +func (cat *AdminCatalog) WaitForTasks() error { + if ResourceInProgress(cat.AdminCatalog.Tasks) { + err := WaitResource(func() (*types.TasksInProgress, error) { + err := cat.Refresh() + if err != nil { + return nil, err + } + return cat.AdminCatalog.Tasks, nil + }) + return err + } + return nil +} + +// Sync synchronises a subscribed AdminCatalog +func (cat *AdminCatalog) Sync() error { + // if the catalog was not subscribed, return + if cat.AdminCatalog.ExternalCatalogSubscription == nil || cat.AdminCatalog.ExternalCatalogSubscription.Location == "" { + return nil + } + // The sync operation is only available for Catalog, not AdminCatalog. + // We use the embedded Catalog object for this purpose + catalogHref, err := cat.GetCatalogHref() + if err != nil || catalogHref == "" { + return fmt.Errorf("empty catalog HREF for admin catalog %s", cat.AdminCatalog.Name) + } + err = cat.WaitForTasks() + if err != nil { + return err + } + return elementSync(cat.client, catalogHref, "admin catalog") +} + +// LaunchSync starts synchronisation of a subscribed AdminCatalog +func (cat *AdminCatalog) LaunchSync() (*Task, error) { + err := checkIfSubscribedCatalog(cat) + if err != nil { + return nil, err + } + // The sync operation is only available for Catalog, not AdminCatalog. + // We use the embedded Catalog object for this purpose + catalogHref, err := cat.GetCatalogHref() + if err != nil || catalogHref == "" { + return nil, fmt.Errorf("empty catalog HREF for admin catalog %s", cat.AdminCatalog.Name) + } + err = cat.WaitForTasks() + if err != nil { + return nil, err + } + return elementLaunchSync(cat.client, catalogHref, "admin catalog") +} + +// GetCatalogHref retrieves the regular catalog HREF from an admin catalog +func (cat *AdminCatalog) GetCatalogHref() (string, error) { + href := "" + for _, link := range cat.AdminCatalog.Link { + if link.Rel == "alternate" && link.Type == types.MimeCatalog { + href = link.HREF + break + } + } + if href == "" { + return "", fmt.Errorf("no regular Catalog HREF found for admin Catalog %s", cat.AdminCatalog.Name) + } + return href, nil +} + +// QueryVappTemplateList returns a list of vApp templates for the given catalog +func (catalog *AdminCatalog) QueryVappTemplateList() ([]*types.QueryResultVappTemplateType, error) { + return queryVappTemplateListWithFilter(catalog.client, map[string]string{"catalogName": catalog.AdminCatalog.Name}) +} + +// QueryMediaList retrieves a list of media items for the Admin Catalog +func (catalog *AdminCatalog) QueryMediaList() ([]*types.MediaRecordType, error) { + return queryMediaList(catalog.client, catalog.AdminCatalog.HREF) +} + +// LaunchSynchronisationVappTemplates starts synchronisation of a list of vApp templates +func (cat *AdminCatalog) LaunchSynchronisationVappTemplates(nameList []string) ([]*Task, error) { + return launchSynchronisationVappTemplates(cat, nameList, true) +} + +// launchSynchronisationVappTemplates waits for existing tasks to complete and then starts synchronisation for a list of vApp templates +// optionally checking for running tasks +// TODO: re-implement without the undocumented task-related fields +func launchSynchronisationVappTemplates(cat *AdminCatalog, nameList []string, checkForRunningTasks bool) ([]*Task, error) { + err := checkIfSubscribedCatalog(cat) + if err != nil { + return nil, err + } + util.Logger.Printf("[TRACE] launchSynchronisationVappTemplates - AdminCatalog '%s' - 'make_local_copy=%v]\n", cat.AdminCatalog.Name, cat.AdminCatalog.ExternalCatalogSubscription.LocalCopy) + var taskList []*Task + + for _, element := range nameList { + var queryResultCatalogItem *types.QueryResultCatalogItemType + + if checkForRunningTasks { + queryResultVappTemplate, err := cat.QueryVappTemplateWithName(element) + if err != nil { + return nil, err + } + err = checkIfTaskComplete(cat.client, queryResultVappTemplate.Task, queryResultVappTemplate.TaskStatus) + if err != nil { + return nil, err + } + queryResultCatalogItem = &types.QueryResultCatalogItemType{ + HREF: queryResultVappTemplate.CatalogItem, + ID: extractUuid(queryResultVappTemplate.CatalogItem), + Type: types.MimeCatalogItem, + Entity: queryResultVappTemplate.HREF, + EntityName: queryResultVappTemplate.Name, + EntityType: "vapptemplate", + Catalog: cat.AdminCatalog.HREF, + CatalogName: cat.AdminCatalog.Name, + Status: queryResultVappTemplate.Status, + Name: queryResultVappTemplate.Name, + } + } else { + queryResultCatalogItem, err = cat.QueryCatalogItem(element) + if err != nil { + return nil, fmt.Errorf("error retrieving catalog item %s: %s", element, err) + } + } + task, err := queryResultCatalogItemToCatalogItem(cat.client, queryResultCatalogItem).LaunchSync() + if err != nil { + return nil, err + } + if task != nil { + taskList = append(taskList, task) + } + } + return taskList, nil +} + +// LaunchSynchronisationAllVappTemplates waits for existing tasks to complete and then starts synchronisation of all vApp templates for a given catalog +// TODO: re-implement without the undocumented task-related fields +func (cat *AdminCatalog) LaunchSynchronisationAllVappTemplates() ([]*Task, error) { + err := checkIfSubscribedCatalog(cat) + if err != nil { + return nil, err + } + util.Logger.Printf("[TRACE] AdminCatalog '%s' LaunchSynchronisationAllVappTemplates - 'make_local_copy=%v]\n", cat.AdminCatalog.Name, cat.AdminCatalog.ExternalCatalogSubscription.LocalCopy) + vappTemplatesList, err := cat.QueryVappTemplateList() + if err != nil { + return nil, err + } + var nameList []string + for _, element := range vappTemplatesList { + err = checkIfTaskComplete(cat.client, element.Task, element.TaskStatus) + if err != nil { + return nil, err + } + nameList = append(nameList, element.Name) + } + // Launch synchronisation for each item, without checking for running tasks, as it was already done in this function + return launchSynchronisationVappTemplates(cat, nameList, false) +} + +func checkIfTaskComplete(client *Client, taskHref, taskStatus string) error { + complete := taskStatus == "" || isTaskCompleteOrError(taskStatus) + if !complete { + task, err := client.GetTaskById(taskHref) + if err != nil { + return err + } + err = task.WaitTaskCompletion() + if err != nil { + return err + } + } + return nil +} + +func checkIfSubscribedCatalog(catalog *AdminCatalog) error { + err := catalog.Refresh() + if err != nil { + return err + } + if catalog.AdminCatalog.ExternalCatalogSubscription == nil || catalog.AdminCatalog.ExternalCatalogSubscription.Location == "" { + return fmt.Errorf("catalog '%s' is not subscribed", catalog.AdminCatalog.Name) + } + return nil +} + +// LaunchSynchronisationMediaItems waits for existing tasks to complete and then starts synchronisation of a list of media items +// TODO: re-implement without the undocumented task-related fields +func (cat *AdminCatalog) LaunchSynchronisationMediaItems(nameList []string) ([]*Task, error) { + err := checkIfSubscribedCatalog(cat) + if err != nil { + return nil, err + } + util.Logger.Printf("[TRACE] AdminCatalog '%s' LaunchSynchronisationMediaItems\n", cat.AdminCatalog.Name) + var taskList []*Task + mediaList, err := cat.QueryMediaList() + if err != nil { + return nil, err + } + var actionList []string + + var found = make(map[string]string) + for _, element := range mediaList { + if contains(element.Name, nameList) { + complete := element.TaskStatus == "" || isTaskCompleteOrError(element.TaskStatus) + if !complete { + if element.Task != "" { + task, err := cat.client.GetTaskById(element.Task) + if err != nil { + return nil, err + } + err = task.WaitTaskCompletion() + if err != nil { + return nil, err + } + } + } + util.Logger.Printf("scheduling for synchronisation Media item %s with catalog item HREF %s\n", element.Name, element.CatalogItem) + actionList = append(actionList, element.CatalogItem) + found[element.Name] = element.CatalogItem + } + } + if len(actionList) < len(nameList) { + var foundList []string + for k := range found { + foundList = append(foundList, k) + } + return nil, fmt.Errorf("%d names provided [%v] but %d actions scheduled [%v]", len(nameList), nameList, len(actionList), foundList) + } + for _, element := range actionList { + util.Logger.Printf("synchronising Media catalog item HREF %s\n", element) + catalogItem, err := cat.GetCatalogItemByHref(element) + if err != nil { + return nil, err + } + task, err := catalogItem.LaunchSync() + if err != nil { + return nil, err + } + if task != nil { + taskList = append(taskList, task) + } + } + return taskList, nil +} + +// LaunchSynchronisationAllMediaItems waits for existing tasks to complete and then starts synchronisation of all media items for a given catalog +// TODO re-implement without the non-documented task-related fields +func (cat *AdminCatalog) LaunchSynchronisationAllMediaItems() ([]*Task, error) { + err := checkIfSubscribedCatalog(cat) + if err != nil { + return nil, err + } + util.Logger.Printf("[TRACE] AdminCatalog '%s' LaunchSynchronisationAllMediaItems\n", cat.AdminCatalog.Name) + var taskList []*Task + mediaList, err := cat.QueryMediaList() + if err != nil { + return nil, err + } + for _, element := range mediaList { + if isTaskRunning(element.TaskStatus) { + task, err := cat.client.GetTaskByHREF(element.Task) + if err != nil { + return nil, err + } + err = task.WaitTaskCompletion() + if err != nil { + return nil, err + } + } + catalogItem, err := cat.GetCatalogItemByHref(element.CatalogItem) + if err != nil { + return nil, err + } + task, err := catalogItem.LaunchSync() + if err != nil { + return nil, err + } + if task != nil { + taskList = append(taskList, task) + } + } + return taskList, nil +} + +// GetCatalogItemByHref finds a CatalogItem by HREF +// On success, returns a pointer to the CatalogItem structure and a nil error +// On failure, returns a nil pointer and an error +func (cat *AdminCatalog) GetCatalogItemByHref(catalogItemHref string) (*CatalogItem, error) { + catItem := NewCatalogItem(cat.client) + + _, err := cat.client.ExecuteRequest(catalogItemHref, http.MethodGet, + "", "error retrieving catalog item: %s", nil, catItem.CatalogItem) + if err != nil { + return nil, err + } + return catItem, nil +} + +// UpdateSubscriptionParams modifies the subscription parameters of an already subscribed catalog +func (catalog *AdminCatalog) UpdateSubscriptionParams(params types.ExternalCatalogSubscription) error { + err := checkIfSubscribedCatalog(catalog) + if err != nil { + return err + } + var href string + for _, link := range catalog.AdminCatalog.Link { + if link.Rel == "subscribeToExternalCatalog" && link.Type == types.MimeSubscribeToExternalCatalog { + href = link.HREF + break + } + } + if href == "" { + return fmt.Errorf("catalog subscription link not found for catalog %s", catalog.AdminCatalog.Name) + } + _, err = catalog.client.ExecuteRequest(href, http.MethodPost, types.MimeAdminCatalog, + "error subscribing to catalog: %s", params, nil) + if err != nil { + return err + } + return catalog.Refresh() +} + +// QueryTaskList retrieves a list of tasks associated to the Admin Catalog +func (catalog *AdminCatalog) QueryTaskList(filter map[string]string) ([]*types.QueryResultTaskRecordType, error) { + catalogHref, err := catalog.GetCatalogHref() + if err != nil { + return nil, err + } + if filter == nil { + filter = make(map[string]string) + } + filter["object"] = catalogHref + return catalog.client.QueryTaskList(filter) +} + +// GetAdminCatalogByHref allows retrieving a catalog from HREF, without a fully qualified AdminOrg object +func (client *Client) GetAdminCatalogByHref(catalogHref string) (*AdminCatalog, error) { + catalogHref = strings.Replace(catalogHref, "/api/catalog", "/api/admin/catalog", 1) + + cat := NewAdminCatalog(client) + + _, err := client.ExecuteRequest(catalogHref, http.MethodGet, + "", "error retrieving catalog: %s", nil, cat.AdminCatalog) + + if err != nil { + return nil, err + } + + // Setting the catalog parent, necessary to handle the tenant context + org := NewAdminOrg(client) + for _, link := range cat.AdminCatalog.Link { + if link.Rel == "up" && link.Type == types.MimeAdminOrg { + _, err = client.ExecuteRequest(link.HREF, http.MethodGet, + "", "error retrieving parent Org: %s", nil, org.AdminOrg) + if err != nil { + return nil, fmt.Errorf("error retrieving catalog parent: %s", err) + } + break + } + } + + cat.parent = org + return cat, nil +} + +// QueryCatalogRecords given a catalog name, retrieves the catalogRecords that match its name +// Returns a list of catalog records for such name, empty list if none was found +func (client *Client) QueryCatalogRecords(name string, ctx TenantContext) ([]*types.CatalogRecord, error) { + util.Logger.Printf("[DEBUG] QueryCatalogRecords") + + var filter string + if name != "" { + filter = fmt.Sprintf("name==%s", url.QueryEscape(name)) + } + + var tenantHeaders map[string]string + + if client.IsSysAdmin && ctx.OrgId != "" && ctx.OrgName != "" { + // Set tenant context headers just for the query + tenantHeaders = map[string]string{ + types.HeaderAuthContext: ctx.OrgName, + types.HeaderTenantContext: ctx.OrgId, + } + } + + queryType := types.QtCatalog + + results, err := client.cumulativeQueryWithHeaders(queryType, nil, map[string]string{ + "type": queryType, + "filter": filter, + "filterEncoded": "true", + }, tenantHeaders) + if err != nil { + return nil, err + } + + catalogs := results.Results.CatalogRecord + + util.Logger.Printf("[DEBUG] QueryCatalogRecords returned with : %#v (%d) and error: %v", catalogs, len(catalogs), err) + return catalogs, nil +} + +// GetAdminCatalogById allows retrieving a catalog from ID, without a fully qualified AdminOrg object +func (client *Client) GetAdminCatalogById(catalogId string) (*AdminCatalog, error) { + href, err := url.JoinPath(client.VCDHREF.String(), "admin", "catalog", extractUuid(catalogId)) + if err != nil { + return nil, err + } + return client.GetAdminCatalogByHref(href) +} + +// GetAdminCatalogByName allows retrieving a catalog from name, without a fully qualified AdminOrg object +func (client *Client) GetAdminCatalogByName(parentOrg, catalogName string) (*AdminCatalog, error) { + catalogs, err := queryCatalogList(client, nil) + if err != nil { + return nil, err + } + var parentOrgs []string + for _, cat := range catalogs { + if cat.Name == catalogName && cat.OrgName == parentOrg { + return client.GetAdminCatalogByHref(cat.HREF) + } + if cat.Name == catalogName { + parentOrgs = append(parentOrgs, cat.OrgName) + } + } + parents := "" + if len(parentOrgs) > 0 { + parents = fmt.Sprintf(" - Found catalog %s in Orgs %v", catalogName, parentOrgs) + } + return nil, fmt.Errorf("no catalog '%s' found in Org %s%s", catalogName, parentOrg, parents) } diff --git a/govcd/adminorg.go b/govcd/adminorg.go index 9dedc6883..943bcea30 100644 --- a/govcd/adminorg.go +++ b/govcd/adminorg.go @@ -1,5 +1,5 @@ /* - * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + * Copyright 2020 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. */ package govcd @@ -23,8 +23,9 @@ import ( // elements that can be viewed and modified only by system administrators. // Definition: https://code.vmware.com/apis/220/vcloud#/doc/doc/types/AdminOrgType.html type AdminOrg struct { - AdminOrg *types.AdminOrg - client *Client + AdminOrg *types.AdminOrg + client *Client + TenantContext *TenantContext } func NewAdminOrg(cli *Client) *AdminOrg { @@ -34,17 +35,42 @@ func NewAdminOrg(cli *Client) *AdminOrg { } } -// CreateCatalog creates a catalog with given name and description under the +// CreateCatalog creates a catalog with given name and description under // the given organization. Returns an AdminCatalog that contains a creation // task. // API Documentation: https://code.vmware.com/apis/220/vcloud#/doc/doc/operations/POST-CreateCatalog.html func (adminOrg *AdminOrg) CreateCatalog(name, description string) (AdminCatalog, error) { - return CreateCatalog(adminOrg.client, adminOrg.AdminOrg.Link, name, description) + adminCatalog, err := adminOrg.CreateCatalogWithStorageProfile(name, description, nil) + if err != nil { + return AdminCatalog{}, err + } + adminCatalog.parent = adminOrg + + err = adminCatalog.Refresh() + if err != nil { + return AdminCatalog{}, err + } + // Make sure that the creation task is finished + err = adminCatalog.WaitForTasks() + if err != nil { + return AdminCatalog{}, err + } + err = adminCatalog.WaitForTasks() + if err != nil { + return AdminCatalog{}, err + } + return *adminCatalog, nil } // CreateCatalogWithStorageProfile is like CreateCatalog, but allows to specify storage profile func (adminOrg *AdminOrg) CreateCatalogWithStorageProfile(name, description string, storageProfiles *types.CatalogStorageProfiles) (*AdminCatalog, error) { - return CreateCatalogWithStorageProfile(adminOrg.client, adminOrg.AdminOrg.Link, name, description, storageProfiles) + adminCatalog, err := CreateCatalogWithStorageProfile(adminOrg.client, adminOrg.AdminOrg.Link, name, description, storageProfiles) + if err != nil { + return nil, err + } + adminCatalogWithParent := NewAdminCatalogWithParent(adminOrg.client, adminOrg) + adminCatalogWithParent.AdminCatalog = adminCatalog.AdminCatalog + return adminCatalogWithParent, nil } // GetAllVDCs returns all depending VDCs for a particular Org @@ -111,8 +137,8 @@ func (adminOrg *AdminOrg) GetStorageProfileReferenceById(id string, refresh bool ErrorEntityNotFound, id, adminOrg.AdminOrg.Name) } -// Deletes the org, returning an error if the vCD call fails. -// API Documentation: https://code.vmware.com/apis/220/vcloud#/doc/doc/operations/DELETE-Organization.html +// Deletes the org, returning an error if the vCD call fails. +// API Documentation: https://code.vmware.com/apis/220/vcloud#/doc/doc/operations/DELETE-Organization.html func (adminOrg *AdminOrg) Delete(force bool, recursive bool) error { if force && recursive { //undeploys vapps @@ -179,10 +205,10 @@ func (adminOrg *AdminOrg) Disable() error { return adminOrg.client.ExecuteRequestWithoutResponse(orgHREF.String(), http.MethodPost, "", "error disabling organization: %s", nil) } -// Updates the Org definition from current org struct contents. -// Any differences that may be legally applied will be updated. -// Returns an error if the call to vCD fails. -// API Documentation: https://code.vmware.com/apis/220/vcloud#/doc/doc/operations/PUT-Organization.html +// Updates the Org definition from current org struct contents. +// Any differences that may be legally applied will be updated. +// Returns an error if the call to vCD fails. +// API Documentation: https://code.vmware.com/apis/220/vcloud#/doc/doc/operations/PUT-Organization.html func (adminOrg *AdminOrg) Update() (Task, error) { vcomp := &types.AdminOrg{ Xmlns: types.XMLNamespaceVCloud, @@ -274,6 +300,8 @@ func (adminOrg *AdminOrg) getVdcByAdminHREF(adminVdcUrl *url.URL) (*Vdc, error) vdc := NewVdc(adminOrg.client) + vdc.parent = adminOrg + _, err := adminOrg.client.ExecuteRequest(vdcURL.String(), http.MethodGet, "", "error retrieving vdc: %s", nil, vdc.Vdc) @@ -470,6 +498,7 @@ func (adminOrg *AdminOrg) GetCatalogByHref(catalogHref string) (*Catalog, error) if err != nil { return nil, err } + cat.parent = adminOrg // The request was successful return cat, nil } @@ -494,13 +523,13 @@ func (adminOrg *AdminOrg) GetCatalogByName(catalogName string, refresh bool) (*C return nil, ErrorEntityNotFound } -// Extracts an UUID from a string, regardless of surrounding text +// Extracts an UUID from a string, regardless of surrounding text, returns the last found occurrence // Returns an empty string if no UUID was found func extractUuid(input string) string { reGetID := regexp.MustCompile(`([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})`) matchListId := reGetID.FindAllStringSubmatch(input, -1) if len(matchListId) > 0 && len(matchListId[0]) > 0 { - return matchListId[0][1] + return matchListId[len(matchListId)-1][1] } return "" } @@ -577,6 +606,7 @@ func (adminOrg *AdminOrg) GetAdminCatalogByHref(catalogHref string) (*AdminCatal return nil, err } + adminCatalog.parent = adminOrg // The request was successful return adminCatalog, nil } @@ -656,6 +686,7 @@ func (adminOrg *AdminOrg) GetVDCByHref(vdcHref string) (*Vdc, error) { if err != nil { return nil, err } + vdc.parent = adminOrg return vdc, nil } @@ -740,27 +771,43 @@ func (adminOrg *AdminOrg) GetVdcByName(vdcname string) (Vdc, error) { // QueryCatalogList returns a list of catalogs for this organization func (adminOrg *AdminOrg) QueryCatalogList() ([]*types.CatalogRecord, error) { + return adminOrg.FindCatalogRecords("") +} + +// FindCatalogRecords given a catalog name, retrieves the catalogRecords for a given organization +func (adminOrg *AdminOrg) FindCatalogRecords(name string) ([]*types.CatalogRecord, error) { util.Logger.Printf("[DEBUG] QueryCatalogList with org name %s", adminOrg.AdminOrg.Name) - queryType := types.QtCatalog + + var tenantHeaders map[string]string + if adminOrg.client.IsSysAdmin { - queryType = types.QtAdminCatalog + // Set tenant context headers just for the query + tenantHeaders = map[string]string{ + types.HeaderAuthContext: adminOrg.TenantContext.OrgName, + types.HeaderTenantContext: adminOrg.TenantContext.OrgId, + } + } + + var filter string + filter = fmt.Sprintf("orgName==%s", url.QueryEscape(adminOrg.AdminOrg.Name)) + if name != "" { + filter = fmt.Sprintf("%s;name==%s", filter, url.QueryEscape(name)) } - results, err := adminOrg.client.cumulativeQuery(queryType, nil, map[string]string{ - "type": queryType, - "filter": fmt.Sprintf("orgName==%s", url.QueryEscape(adminOrg.AdminOrg.Name)), + + results, err := adminOrg.client.cumulativeQueryWithHeaders(types.QtCatalog, nil, map[string]string{ + "type": types.QtCatalog, + "filter": filter, "filterEncoded": "true", - }) + }, tenantHeaders) if err != nil { return nil, err } - var catalogs []*types.CatalogRecord - - if adminOrg.client.IsSysAdmin { - catalogs = results.Results.AdminCatalogRecord - } else { - catalogs = results.Results.CatalogRecord + catalogs := results.Results.CatalogRecord + if catalogs == nil { + return nil, ErrorEntityNotFound } + util.Logger.Printf("[DEBUG] QueryCatalogList returned with : %#v and error: %s", catalogs, err) return catalogs, nil } diff --git a/govcd/adminorg_administration_test.go b/govcd/adminorg_administration_test.go index 1c90de54c..79e6ad613 100644 --- a/govcd/adminorg_administration_test.go +++ b/govcd/adminorg_administration_test.go @@ -1,4 +1,4 @@ -// +build org functional ALL +//go:build org || functional || ALL /* * Copyright 2020 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. diff --git a/govcd/adminorg_ldap_test.go b/govcd/adminorg_ldap_test.go index 0e754cbb6..19dee2f14 100644 --- a/govcd/adminorg_ldap_test.go +++ b/govcd/adminorg_ldap_test.go @@ -1,121 +1,118 @@ -// +build user functional ALL -// +build !skipLong +//go:build user || functional || ALL /* - * Copyright 2020 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + * Copyright 2022 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. */ package govcd import ( "fmt" - "time" "github.com/vmware/go-vcloud-director/v2/types/v56" . "gopkg.in/check.v1" ) // Test_LDAP serves as a "subtest" framework for tests requiring LDAP configuration. It sets up LDAP -// server and configuration for Org and cleans up this test run. +// configuration for Org and cleans up this test run. // // Prerequisites: -// * External network subnet must have access to internet -// * Correct DNS servers must be set for external network so that guest VM can resolve DNS records +// * LDAP server already installed +// * LDAP server IP set in TestConfig.VCD.LdapServer func (vcd *TestVCD) Test_LDAP(check *C) { if vcd.skipAdminTests { check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) } + vcd.checkSkipWhenApiToken(check) - if !catalogItemIsPhotonOs(vcd) { - check.Skip(fmt.Sprintf("Catalog item '%s' is not Photon OS", vcd.config.VCD.Catalog.CatalogItem)) + ldapHostIp := vcd.config.VCD.LdapServer + if ldapHostIp == "" { + check.Skip("[" + check.TestName() + "] LDAP server IP not provided in configuration") } + // Due to a bug in VCD, when configuring LDAP service, Org publishing catalog settings `Publish external catalogs` and + // `Subscribe to external catalogs ` gets disabled. For that reason we are getting the current values from those vars + // to set them at the end of the test, to avoid interference with other tests. + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.org.Org.Name) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) - if vcd.config.VCD.ExternalNetwork == "" { - check.Skip("[" + check.TestName() + "] external network not provided") - } + publishExternalCatalogs := adminOrg.AdminOrg.OrgSettings.OrgGeneralSettings.CanPublishExternally + subscribeToExternalCatalogs := adminOrg.AdminOrg.OrgSettings.OrgGeneralSettings.CanSubscribe - fmt.Println("Setting up LDAP") - networkName, vappName, vmName := vcd.configureLdap(check) + fmt.Printf("Setting up LDAP (IP: %s)\n", ldapHostIp) + err = configureLdapForOrg(vcd, adminOrg, ldapHostIp, check.TestName()) + check.Assert(err, IsNil) defer func() { fmt.Println("Unconfiguring LDAP") - vcd.unconfigureLdap(check, networkName, vappName, vmName) - }() - - // Run tests requiring LDAP from here. - vcd.test_GroupCRUD(check) - vcd.test_GroupFinderGetGenericEntity(check) - -} + // Clear LDAP configuration + err = adminOrg.LdapDisable() + check.Assert(err, IsNil) -// configureLdap creates direct network, spawns Photon OS VM with LDAP server and configures vCD to -// use LDAP server -func (vcd *TestVCD) configureLdap(check *C) (string, string, string) { + // Due to the VCD bug mentioned above, we need to set the previous state from the publishing settings vars + check.Assert(adminOrg.Refresh(), IsNil) - // Create direct network to expose LDAP server on external network - directNetworkName := createDirectNetwork(vcd, check) + adminOrg.AdminOrg.OrgSettings.OrgGeneralSettings.CanPublishExternally = publishExternalCatalogs + adminOrg.AdminOrg.OrgSettings.OrgGeneralSettings.CanSubscribe = subscribeToExternalCatalogs - // Launch LDAP server on external network - ldapHostIp, vappName, vmName := createLdapServer(vcd, check, directNetworkName) + task, err := adminOrg.Update() + check.Assert(err, IsNil) - // Configure vCD to use new LDAP server - configureLdapForOrg(vcd, check, ldapHostIp) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) + }() - return directNetworkName, vappName, vmName + // Run tests requiring LDAP from here. + vcd.test_GroupCRUD(check) + vcd.test_GroupFinderGetGenericEntity(check) + vcd.test_GroupUserListIsPopulated(check) } -// unconfigureLdap cleans up LDAP configuration created by `configureLdap` immediately to reduce -// resource usage -func (vcd *TestVCD) unconfigureLdap(check *C, networkName, vAppName, vmName string) { - - // Get Org Vdc - org, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) - check.Assert(err, IsNil) - vdc, err := org.GetVDCByName(vcd.config.VCD.Vdc, false) - check.Assert(err, IsNil) - check.Assert(vdc, NotNil) - - vapp, err := vdc.GetVAppByName(vAppName, false) - check.Assert(err, IsNil) - - vm, err := vapp.GetVMByName(vmName, false) - check.Assert(err, IsNil) +func (vcd *TestVCD) Test_LDAPSystem(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + vcd.checkSkipWhenApiToken(check) - // Remove VM - task, err := vm.Undeploy() - check.Assert(err, IsNil) - err = task.WaitTaskCompletion() + // Due to a bug in VCD, when configuring LDAP service, Org publishing catalog settings `Publish external catalogs` and + // `Subscribe to external catalogs ` gets disabled. For that reason we are getting the current values from those vars + // to set them at the end of the test, to avoid interference with other tests. + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.org.Org.Name) check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) - err = vapp.RemoveVM(*vm) - check.Assert(err, IsNil) + publishExternalCatalogs := adminOrg.AdminOrg.OrgSettings.OrgGeneralSettings.CanPublishExternally + subscribeToExternalCatalogs := adminOrg.AdminOrg.OrgSettings.OrgGeneralSettings.CanSubscribe + ldapSettings := types.OrgLdapSettingsType{ + OrgLdapMode: "SYSTEM", + CustomUsersOu: "ou=Foo,dc=domain,dc=local base DN", + } - // undeploy and remove vApp - task, err = vapp.Undeploy() - check.Assert(err, IsNil) - err = task.WaitTaskCompletion() + _, err = adminOrg.LdapConfigure(&ldapSettings) check.Assert(err, IsNil) + defer func() { + fmt.Println("Unconfiguring LDAP") + // Clear LDAP configuration + err = adminOrg.LdapDisable() + check.Assert(err, IsNil) - task, err = vapp.Delete() - check.Assert(err, IsNil) - err = task.WaitTaskCompletion() - check.Assert(err, IsNil) + // Due to the VCD bug mentioned above, we need to set the previous state from the publishing settings vars + check.Assert(adminOrg.Refresh(), IsNil) - // Remove network - err = RemoveOrgVdcNetworkIfExists(*vcd.vdc, networkName) - check.Assert(err, IsNil) + adminOrg.AdminOrg.OrgSettings.OrgGeneralSettings.CanPublishExternally = publishExternalCatalogs + adminOrg.AdminOrg.OrgSettings.OrgGeneralSettings.CanSubscribe = subscribeToExternalCatalogs - // Clear LDAP configuration - err = org.LdapDisable() - check.Assert(err, IsNil) + task, err := adminOrg.Update() + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) + }() } -// orgConfigureLdap sets up LDAP configuration in vCD org specified by vcd.config.VCD.Org variable -func configureLdapForOrg(vcd *TestVCD, check *C, ldapHostIp string) { +// configureLdapForOrg sets up LDAP configuration in vCD org +func configureLdapForOrg(vcd *TestVCD, adminOrg *AdminOrg, ldapHostIp, testName string) error { fmt.Printf("# Configuring LDAP settings for Org '%s'", vcd.config.VCD.Org) - org, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) - check.Assert(err, IsNil) // The below settings are tailored for LDAP docker testing image // https://github.com/rroemhild/docker-test-openldap ldapSettings := &types.OrgLdapSettingsType{ @@ -149,157 +146,11 @@ func configureLdapForOrg(vcd *TestVCD, check *C, ldapHostIp string) { }, } - _, err = org.LdapConfigure(ldapSettings) - check.Assert(err, IsNil) - - fmt.Println(" Done") - AddToCleanupList("LDAP-configuration", "orgLdapSettings", org.AdminOrg.Name, check.TestName()) -} - -// createLdapServer spawns a vApp and photon OS VM. Using customization script it starts a testing -// LDAP server in docker container which has a few users and groups defined. -// In essence it creates two groups - "admin_staff" and "ship_crew" and a few users. -// More information about users and groups in: https://github.com/rroemhild/docker-test-openldap -func createLdapServer(vcd *TestVCD, check *C, directNetworkName string) (string, string, string) { - vAppName := "ldap" - // The customization script waits until IP address is set on the NIC because Guest tools run - // script and network configuration together. If the script runs too quick - there is a risk - // that network card is not yet configured and it will not be able to pull docker image from - // remote. Guest tools could also be interrupted if the script below failed before NICs are - // configured therefore it is run in background. - // It waits until "inet" (not "inet6") is set and then runs docker container - const ldapCustomizationScript = ` - { - until ip a show eth0 | grep "inet " - do - sleep 1 - done - systemctl enable docker - systemctl start docker - docker run --name ldap-server --restart=always --privileged -d -p 389:389 rroemhild/test-openldap - } & - ` - // Get Org, Vdc - org, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) - check.Assert(err, IsNil) - vdc, err := org.GetVDCByName(vcd.config.VCD.Vdc, false) - check.Assert(err, IsNil) - check.Assert(vdc, NotNil) - - // Find catalog and catalog item - catalog, err := org.GetCatalogByName(vcd.config.VCD.Catalog.Name, false) - check.Assert(err, IsNil) - check.Assert(catalog, NotNil) - catalogItem, err := catalog.GetCatalogItemByName(vcd.config.VCD.Catalog.CatalogItem, false) - check.Assert(err, IsNil) - - fmt.Printf("# Creating RAW vApp '%s'", vAppName) - vappTemplate, err := catalogItem.GetVAppTemplate() - check.Assert(err, IsNil) - // Compose Raw vApp - err = vdc.ComposeRawVApp(vAppName) - check.Assert(err, IsNil) - vapp, err := vdc.GetVAppByName(vAppName, true) - check.Assert(err, IsNil) - // vApp was created - adding it to cleanup list (using prepend to remove it before direct - // network removal) - PrependToCleanupList(vAppName, "vapp", "", check.TestName()) - // Wait until vApp becomes configurable - initialVappStatus, err := vapp.GetStatus() - check.Assert(err, IsNil) - if initialVappStatus != "RESOLVED" { // RESOLVED vApp is ready to accept operations - err = vapp.BlockWhileStatus(initialVappStatus, vapp.client.MaxRetryTimeout) - check.Assert(err, IsNil) - } - fmt.Printf(". Done\n") - - // Attach VDC network to vApp so that VMs can use it - fmt.Printf("# Attaching network '%s'", directNetworkName) - net, err := vdc.GetOrgVdcNetworkByName(directNetworkName, false) - check.Assert(err, IsNil) - task, err := vapp.AddRAWNetworkConfig([]*types.OrgVDCNetwork{net.OrgVDCNetwork}) - check.Assert(err, IsNil) - err = task.WaitTaskCompletion() - check.Assert(err, IsNil) - fmt.Printf(". Done\n") - - // Create VM - desiredNetConfig := types.NetworkConnectionSection{} - desiredNetConfig.PrimaryNetworkConnectionIndex = 0 - desiredNetConfig.NetworkConnection = append(desiredNetConfig.NetworkConnection, - &types.NetworkConnection{ - IsConnected: true, - IPAddressAllocationMode: types.IPAllocationModePool, - Network: directNetworkName, - NetworkConnectionIndex: 0, - }) - - // LDAP docker container does not start if Photon OS VM does not have at least 1024 of RAM - ldapVm, err := spawnVM("ldap-vm", 1024, *vdc, *vapp, desiredNetConfig, vappTemplate, check, ldapCustomizationScript, true) - check.Assert(err, IsNil) - - // Must be deleted before vApp to avoid IP leak - PrependToCleanupList(ldapVm.VM.Name, "vm", vAppName, check.TestName()) - - // Got VM - ensure that TCP port for ldap service is open and reachable - ldapHostIp := ldapVm.VM.NetworkConnectionSection.NetworkConnection[0].IPAddress - fmt.Printf("# Waiting for server %s to respond on port 389: ", ldapHostIp) - timerStart := time.Now() - isLdapServiceUp := isTcpPortOpen(ldapHostIp, "389", vapp.client.MaxRetryTimeout) - check.Assert(isLdapServiceUp, Equals, true) - fmt.Printf("# Time taken to start LDAP container: %s\n", time.Since(timerStart)) - - return ldapHostIp, vAppName, ldapVm.VM.Name -} - -// createDirectNetwork creates a direct network attached to existing external network -func createDirectNetwork(vcd *TestVCD, check *C) string { - networkName := check.TestName() - fmt.Printf("# Creating direct network %s.", networkName) - - err := RemoveOrgVdcNetworkIfExists(*vcd.vdc, networkName) + _, err := adminOrg.LdapConfigure(ldapSettings) if err != nil { - check.Skip(fmt.Sprintf("Error deleting network : %s", err)) - } - - externalNetwork, err := vcd.client.GetExternalNetworkByName(vcd.config.VCD.ExternalNetwork) - check.Assert(err, IsNil) - // Note that there is no IPScope for this type of network - description := "Created by govcd test" - var networkConfig = types.OrgVDCNetwork{ - Xmlns: types.XMLNamespaceVCloud, - Name: networkName, - Description: description, - Configuration: &types.NetworkConfiguration{ - FenceMode: types.FenceModeBridged, - ParentNetwork: &types.Reference{ - HREF: externalNetwork.ExternalNetwork.HREF, - Name: externalNetwork.ExternalNetwork.Name, - Type: externalNetwork.ExternalNetwork.Type, - }, - BackwardCompatibilityMode: true, - }, - IsShared: false, + return err } - LogNetwork(networkConfig) - - task, err := vcd.vdc.CreateOrgVDCNetwork(&networkConfig) - if err != nil { - fmt.Printf("error creating the network: %s", err) - } - check.Assert(err, IsNil) - if task == (Task{}) { - fmt.Printf("NULL task retrieved after network creation") - } - check.Assert(task.Task.HREF, Not(Equals), "") - - AddToCleanupList(networkName, "network", vcd.org.Org.Name+"|"+vcd.vdc.Vdc.Name, check.TestName()) - - err = task.WaitInspectTaskCompletion(LogTask, 10) - if err != nil { - fmt.Printf("error performing task: %s", err) - } - check.Assert(err, IsNil) fmt.Println(" Done") - return networkName + AddToCleanupList("LDAP-configuration", "orgLdapSettings", adminOrg.AdminOrg.Name, testName) + return nil } diff --git a/govcd/adminorg_test.go b/govcd/adminorg_test.go index 380297d58..48604a62a 100644 --- a/govcd/adminorg_test.go +++ b/govcd/adminorg_test.go @@ -1,4 +1,4 @@ -// +build org functional ALL +//go:build org || functional || ALL /* * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -117,17 +117,16 @@ func (vcd *TestVCD) TestAdminOrg_SetLease(check *C) { }, } - for _, info := range leaseData { - + for infoIndex, info := range leaseData { fmt.Printf("update lease params %v\n", info) // Change the lease parameters for both vapp and vApp template - adminOrg.AdminOrg.OrgSettings.OrgVAppLeaseSettings.StorageLeaseSeconds = &info.vappStorageLease - adminOrg.AdminOrg.OrgSettings.OrgVAppLeaseSettings.DeploymentLeaseSeconds = &info.deploymentLeaseSeconds - adminOrg.AdminOrg.OrgSettings.OrgVAppLeaseSettings.PowerOffOnRuntimeLeaseExpiration = &info.powerOffOnRuntimeLeaseExpiration - adminOrg.AdminOrg.OrgSettings.OrgVAppLeaseSettings.DeleteOnStorageLeaseExpiration = &info.vappDeleteOnStorageLeaseExpiration + adminOrg.AdminOrg.OrgSettings.OrgVAppLeaseSettings.StorageLeaseSeconds = &leaseData[infoIndex].vappStorageLease + adminOrg.AdminOrg.OrgSettings.OrgVAppLeaseSettings.DeploymentLeaseSeconds = &leaseData[infoIndex].deploymentLeaseSeconds + adminOrg.AdminOrg.OrgSettings.OrgVAppLeaseSettings.PowerOffOnRuntimeLeaseExpiration = &leaseData[infoIndex].powerOffOnRuntimeLeaseExpiration + adminOrg.AdminOrg.OrgSettings.OrgVAppLeaseSettings.DeleteOnStorageLeaseExpiration = &leaseData[infoIndex].vappDeleteOnStorageLeaseExpiration - adminOrg.AdminOrg.OrgSettings.OrgVAppTemplateSettings.StorageLeaseSeconds = &info.vappTemplateStorageLease - adminOrg.AdminOrg.OrgSettings.OrgVAppTemplateSettings.DeleteOnStorageLeaseExpiration = &info.vappTemplateDeleteOnStorageLeaseExpiration + adminOrg.AdminOrg.OrgSettings.OrgVAppTemplateSettings.StorageLeaseSeconds = &leaseData[infoIndex].vappTemplateStorageLease + adminOrg.AdminOrg.OrgSettings.OrgVAppTemplateSettings.DeleteOnStorageLeaseExpiration = &leaseData[infoIndex].vappTemplateDeleteOnStorageLeaseExpiration task, err := adminOrg.Update() check.Assert(err, IsNil) @@ -189,6 +188,22 @@ func (vcd *TestVCD) TestOrg_AdminOrg_QueryCatalogList(check *C) { catalogsInAdminOrg, err := adminOrg.QueryCatalogList() check.Assert(err, IsNil) + // gets a specific catalog as an adminOrg + singleCatalogInAdminOrg, err := adminOrg.FindCatalogRecords(vcd.config.VCD.Catalog.Name) + check.Assert(err, IsNil) + check.Assert(singleCatalogInAdminOrg, NotNil) + check.Assert(len(singleCatalogInAdminOrg), Equals, 1) + + // try to get a non-existent catalog + nonExistentCatalog, err := adminOrg.FindCatalogRecords("iCompletelyMadeThisUp") + check.Assert(nonExistentCatalog, IsNil) + check.Assert(err, Equals, ErrorEntityNotFound) + + // try to get a non-existent catalog with space + spaceTestCatalog, err := adminOrg.FindCatalogRecords("space test catalog name") + check.Assert(spaceTestCatalog, IsNil) + check.Assert(err, Equals, ErrorEntityNotFound) + // gets the catalog list as an Org catalogsInOrg, err := org.QueryCatalogList() check.Assert(err, IsNil) @@ -227,7 +242,7 @@ func (vcd *TestVCD) TestOrg_AdminOrg_QueryCatalogList(check *C) { } // Test_GetAllVDCs checks that adminOrg.GetAllVDCs returns at least one VDC -func (vcd *TestVCD) Test_GetAllVDCs(check *C) { +func (vcd *TestVCD) Test_AdminOrgGetAllVDCs(check *C) { if vcd.skipAdminTests { check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) } @@ -239,6 +254,11 @@ func (vcd *TestVCD) Test_GetAllVDCs(check *C) { vdcs, err := adminOrg.GetAllVDCs(true) check.Assert(err, IsNil) check.Assert(len(vdcs) > 0, Equals, true) + + // If NSX-T VDC is configured we expect to see at least 2 VDCs (NSX-V and NSX-T) + if vcd.config.VCD.Nsxt.Vdc != "" { + check.Assert(len(vdcs) >= 2, Equals, true) + } } // Test_GetAllStorageProfileReferences checks that adminOrg.GetAllStorageProfileReferences returns at least one storage diff --git a/govcd/adminorg_unit_test.go b/govcd/adminorg_unit_test.go index 102078300..68b2e3b5b 100644 --- a/govcd/adminorg_unit_test.go +++ b/govcd/adminorg_unit_test.go @@ -1,4 +1,4 @@ -// +build unit ALL +//go:build unit || ALL /* * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -102,6 +102,15 @@ func Test_equalIds(t *testing.T) { }, expected: false, }, + { + // URL has ID also case + wanted: "urn:vcloud:catalogitem:97384890-180c-4563-b9b7-0dc50a2430b0", + reference: types.Reference{ + Name: "url_with_id", + HREF: "https://vcd-a8bbe9be-13f2-4ce7-9187-d0d075c42531.cds.cloud.vmware.com/api/entity/97384890-180c-4563-b9b7-0dc50a2430b0", + }, + expected: true, + }, } for _, item := range testItems { result := equalIds(item.wanted, item.reference.ID, item.reference.HREF) diff --git a/govcd/adminvdc.go b/govcd/adminvdc.go index 08bc88e09..09087a9c8 100644 --- a/govcd/adminvdc.go +++ b/govcd/adminvdc.go @@ -1,5 +1,5 @@ /* - * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + * Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. */ package govcd @@ -7,15 +7,17 @@ package govcd import ( "errors" "fmt" - "github.com/vmware/go-vcloud-director/v2/types/v56" - "github.com/vmware/go-vcloud-director/v2/util" "net/http" "net/url" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + "github.com/vmware/go-vcloud-director/v2/util" ) type AdminVdc struct { AdminVdc *types.AdminVdc client *Client + parent organization } func NewAdminVdc(cli *Client) *AdminVdc { @@ -82,6 +84,7 @@ func (adminOrg *AdminOrg) GetAdminVdcByName(vdcname string) (AdminVdc, error) { // GetAdminVDCByHref retrieves a VDC using a direct call with the HREF func (adminOrg *AdminOrg) GetAdminVDCByHref(vdcHref string) (*AdminVdc, error) { adminVdc := NewAdminVdc(adminOrg.client) + adminVdc.parent = adminOrg _, err := adminOrg.client.ExecuteRequest(vdcHref, http.MethodGet, "", "error getting vdc: %s", nil, adminVdc.AdminVdc) @@ -170,6 +173,9 @@ func (adminOrg *AdminOrg) CreateVdc(vdcConfiguration *types.VdcConfiguration) (T // Return the task task := NewTask(adminOrg.client) + if adminVdc.AdminVdc.Tasks == nil || len(adminVdc.AdminVdc.Tasks.Task) == 0 { + return Task{}, fmt.Errorf("no task found after VDC %s creation", vdcConfiguration.Name) + } task.Task = adminVdc.AdminVdc.Tasks.Task[0] return *task, nil } @@ -248,6 +254,10 @@ func (adminVdc *AdminVdc) Update() (AdminVdc, error) { util.Logger.Printf("[DEBUG] Update call function for version %s", vdcFunctions.SupportedVersion) + // Explicitly remove ResourcePoolRefs because it cannot be set and breaks Go marshaling bug + // https://github.com/golang/go/issues/9519 + adminVdc.AdminVdc.ResourcePoolRefs = nil + updatedAdminVdc, err := vdcFunctions.UpdateVdc(adminVdc) if err != nil { return AdminVdc{}, err @@ -369,6 +379,9 @@ func createVdcAsyncV97(adminOrg *AdminOrg, vdcConfiguration *types.VdcConfigurat // Return the task task := NewTask(adminOrg.client) + if adminVdc.AdminVdc.Tasks == nil || len(adminVdc.AdminVdc.Tasks.Task) == 0 { + return Task{}, fmt.Errorf("no task found after VDC %s creation", vdcConfiguration.Name) + } task.Task = adminVdc.AdminVdc.Tasks.Task[0] return *task, nil } @@ -421,3 +434,179 @@ func (vdc *AdminVdc) UpdateStorageProfile(storageProfileId string, storageProfil return updateAdminVdcStorageProfile, err } + +// AddStorageProfile adds a storage profile to a VDC +func (vdc *AdminVdc) AddStorageProfile(storageProfile *types.VdcStorageProfileConfiguration, description string) (Task, error) { + if vdc.client.VCDHREF.String() == "" { + return Task{}, fmt.Errorf("cannot add VDC storage profile, VCD HREF is unset") + } + + href := vdc.AdminVdc.HREF + "/vdcStorageProfiles" + + var updateStorageProfile = types.UpdateVdcStorageProfiles{ + Xmlns: types.XMLNamespaceVCloud, + Name: storageProfile.ProviderVdcStorageProfile.Name, + Description: description, + AddStorageProfile: storageProfile, + RemoveStorageProfile: nil, + } + + task, err := vdc.client.ExecuteTaskRequest(href, http.MethodPost, + types.MimeUpdateVdcStorageProfiles, "error adding VDC storage profile: %s", &updateStorageProfile) + if err != nil { + return Task{}, fmt.Errorf("cannot add VDC storage profile, error: %s", err) + } + + return task, nil +} + +// AddStorageProfileWait adds a storage profile to a VDC and return a refreshed VDC +func (vdc *AdminVdc) AddStorageProfileWait(storageProfile *types.VdcStorageProfileConfiguration, description string) error { + task, err := vdc.AddStorageProfile(storageProfile, description) + if err != nil { + return err + } + err = task.WaitTaskCompletion() + if err != nil { + return err + } + return vdc.Refresh() +} + +// RemoveStorageProfile remove a storage profile from a VDC +func (vdc *AdminVdc) RemoveStorageProfile(storageProfileName string) (Task, error) { + if vdc.client.VCDHREF.String() == "" { + return Task{}, fmt.Errorf("cannot remove VDC storage profile: VCD HREF is unset") + } + + var storageProfile *types.Reference + for _, sp := range vdc.AdminVdc.VdcStorageProfiles.VdcStorageProfile { + if sp.Name == storageProfileName { + storageProfile = sp + } + } + if storageProfile == nil { + return Task{}, fmt.Errorf("cannot remove VDC storage profile: storage profile '%s' not found in VDC", storageProfileName) + } + + vdcStorageProfileDetails, err := vdc.client.GetStorageProfileByHref(storageProfile.HREF) + if err != nil { + return Task{}, fmt.Errorf("cannot retrieve VDC storage profile '%s' details: %s", storageProfileName, err) + } + if vdcStorageProfileDetails.Enabled != nil && *vdcStorageProfileDetails.Enabled { + _, err = vdc.UpdateStorageProfile(extractUuid(storageProfile.HREF), &types.AdminVdcStorageProfile{ + Name: vdcStorageProfileDetails.Name, + Units: vdcStorageProfileDetails.Units, + Limit: vdcStorageProfileDetails.Limit, + Default: false, + Enabled: addrOf(false), + ProviderVdcStorageProfile: &types.Reference{ + HREF: vdcStorageProfileDetails.ProviderVdcStorageProfile.HREF, + }, + }, + ) + if err != nil { + return Task{}, fmt.Errorf("cannot disable VDC storage profile '%s': %s", storageProfileName, err) + } + } + + href := vdc.AdminVdc.HREF + "/vdcStorageProfiles" + + var updateStorageProfile = types.UpdateVdcStorageProfiles{ + Xmlns: types.XMLNamespaceVCloud, + Name: vdcStorageProfileDetails.Name, + Description: "", + RemoveStorageProfile: storageProfile, + } + + task, err := vdc.client.ExecuteTaskRequest(href, http.MethodPost, + types.MimeUpdateVdcStorageProfiles, "error removing VDC storage profile: %s", &updateStorageProfile) + if err != nil { + return Task{}, fmt.Errorf("cannot remove VDC storage profile, error: %s", err) + } + + return task, nil +} + +// RemoveStorageProfileWait removes a storege profile from a VDC and returns a refreshed VDC or an error +func (vdc *AdminVdc) RemoveStorageProfileWait(storageProfileName string) error { + task, err := vdc.RemoveStorageProfile(storageProfileName) + if err != nil { + return err + } + err = task.WaitTaskCompletion() + if err != nil { + return err + } + return vdc.Refresh() +} + +// SetDefaultStorageProfile sets a given storage profile as default +// This operation will automatically unset the previous default storage profile. +func (vdc *AdminVdc) SetDefaultStorageProfile(storageProfileName string) error { + if vdc.client.VCDHREF.String() == "" { + return fmt.Errorf("cannot set VDC default storage profile: VCD HREF is unset") + } + + var storageProfile *types.Reference + for _, sp := range vdc.AdminVdc.VdcStorageProfiles.VdcStorageProfile { + if sp.Name == storageProfileName { + storageProfile = sp + } + } + if storageProfile == nil { + return fmt.Errorf("cannot set VDC default storage profile: storage profile '%s' not found in VDC", storageProfileName) + } + + vdcStorageProfileDetails, err := vdc.client.GetStorageProfileByHref(storageProfile.HREF) + if err != nil { + return fmt.Errorf("cannot retrieve VDC storage profile '%s' details: %s", storageProfileName, err) + } + _, err = vdc.UpdateStorageProfile(extractUuid(storageProfile.HREF), &types.AdminVdcStorageProfile{ + Name: vdcStorageProfileDetails.Name, + Units: vdcStorageProfileDetails.Units, + Limit: vdcStorageProfileDetails.Limit, + Default: true, + Enabled: addrOf(true), + ProviderVdcStorageProfile: &types.Reference{ + HREF: vdcStorageProfileDetails.ProviderVdcStorageProfile.HREF, + }, + }, + ) + if err != nil { + return fmt.Errorf("cannot set VDC default storage profile '%s': %s", storageProfileName, err) + } + return vdc.Refresh() +} + +// GetDefaultStorageProfileReference finds the default storage profile for the VDC +func (adminVdc *AdminVdc) GetDefaultStorageProfileReference() (*types.Reference, error) { + var defaultSp *types.Reference + if adminVdc.AdminVdc.VdcStorageProfiles == nil || adminVdc.AdminVdc.VdcStorageProfiles.VdcStorageProfile == nil { + return nil, fmt.Errorf("no storage profiles found in VDC %s", adminVdc.AdminVdc.Name) + } + for _, sp := range adminVdc.AdminVdc.VdcStorageProfiles.VdcStorageProfile { + fullSp, err := adminVdc.client.GetStorageProfileByHref(sp.HREF) + if err != nil { + return nil, fmt.Errorf("error retrieving storage profile %s for VDC %s: %s", sp.Name, adminVdc.AdminVdc.Name, err) + } + if fullSp.Default { + if defaultSp != nil { + return nil, fmt.Errorf("more than one default storage profile found for VDC %s: '%s' and '%s'", adminVdc.AdminVdc.Name, sp.Name, defaultSp.Name) + } + defaultSp = sp + } + } + if defaultSp != nil { + return defaultSp, nil + } + return nil, fmt.Errorf("no default storage profile found for VDC %s", adminVdc.AdminVdc.Name) +} + +// IsNsxv is a convenience function to check if the Admin VDC is backed by NSX-V Provider VDC +func (adminVdc *AdminVdc) IsNsxv() bool { + vdc := NewVdc(adminVdc.client) + vdc.Vdc = &adminVdc.AdminVdc.Vdc + vdc.parent = adminVdc.parent + return vdc.IsNsxv() +} diff --git a/govcd/adminvdc_nsxt_test.go b/govcd/adminvdc_nsxt_test.go index f289634c3..47d045089 100644 --- a/govcd/adminvdc_nsxt_test.go +++ b/govcd/adminvdc_nsxt_test.go @@ -1,4 +1,4 @@ -// +build org functional nsxt ALL +//go:build org || functional || nsxt || ALL /* * Copyright 2020 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -34,13 +34,10 @@ func (vcd *TestVCD) Test_CreateNsxtOrgVdc(check *C) { } providerVdcHref := pVdcs[0].HREF - pvdcStorageProfiles, err := QueryProviderVdcStorageProfileByName(vcd.client, vcd.config.VCD.NsxtProviderVdc.StorageProfile) + pvdcStorageProfile, err := vcd.client.QueryProviderVdcStorageProfileByName(vcd.config.VCD.NsxtProviderVdc.StorageProfile, providerVdcHref) check.Assert(err, IsNil) - if len(pvdcStorageProfiles) == 0 { - check.Skip(fmt.Sprintf("No storage profile found with name '%s'", vcd.config.VCD.NsxtProviderVdc.StorageProfile)) - } - providerVdcStorageProfileHref := pvdcStorageProfiles[0].HREF + providerVdcStorageProfileHref := pvdcStorageProfile.HREF networkPools, err := QueryNetworkPoolByName(vcd.client, vcd.config.VCD.NsxtProviderVdc.NetworkPool) check.Assert(err, IsNil) @@ -71,7 +68,7 @@ func (vcd *TestVCD) Test_CreateNsxtOrgVdc(check *C) { }, }, VdcStorageProfile: []*types.VdcStorageProfileConfiguration{&types.VdcStorageProfileConfiguration{ - Enabled: true, + Enabled: addrOf(true), Units: "MB", Limit: 1024, Default: true, @@ -94,6 +91,8 @@ func (vcd *TestVCD) Test_CreateNsxtOrgVdc(check *C) { if allocationModel == "Flex" { vdcConfiguration.IsElastic = &trueValue vdcConfiguration.IncludeMemoryOverhead = &trueValue + // Memory guaranteed percentage is required when IncludeMemoryOverhead is true in VCD 10.6+ + vdcConfiguration.ResourceGuaranteedMemory = addrOf(1.00) } vdc, _ := adminOrg.GetVDCByName(vdcConfiguration.Name, false) @@ -123,6 +122,7 @@ func (vcd *TestVCD) Test_CreateNsxtOrgVdc(check *C) { check.Assert(vdc.Vdc.Name, Equals, vdcConfiguration.Name) check.Assert(vdc.Vdc.IsEnabled, Equals, vdcConfiguration.IsEnabled) check.Assert(vdc.Vdc.AllocationModel, Equals, vdcConfiguration.AllocationModel) + check.Assert(len(adminVdc.AdminVdc.ResourcePoolRefs.VimObjectRef) > 0, Equals, true) // Test update adminVdc.AdminVdc.Description = "updated-description" + check.TestName() diff --git a/govcd/adminvdc_test.go b/govcd/adminvdc_test.go index fd7994acf..5ad1dff6e 100644 --- a/govcd/adminvdc_test.go +++ b/govcd/adminvdc_test.go @@ -1,4 +1,4 @@ -// +build org functional ALL +//go:build org || functional || ALL /* * Copyright 2020 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -38,35 +38,20 @@ func (vcd *TestVCD) Test_CreateOrgVdcWithFlex(check *C) { check.Assert(err, IsNil) check.Assert(adminOrg, NotNil) - results, err := vcd.client.QueryWithNotEncodedParams(nil, map[string]string{ - "type": "providerVdc", - "filter": fmt.Sprintf("name==%s", vcd.config.VCD.ProviderVdc.Name), - }) - check.Assert(err, IsNil) - if len(results.Results.VMWProviderVdcRecord) == 0 { - check.Skip(fmt.Sprintf("No Provider VDC found with name '%s'", vcd.config.VCD.ProviderVdc.Name)) - } - providerVdcHref := results.Results.VMWProviderVdcRecord[0].HREF + providerVdcHref := getVdcProviderVdcHref(vcd, check) - results, err = vcd.client.QueryWithNotEncodedParams(nil, map[string]string{ - "type": "providerVdcStorageProfile", - "filter": fmt.Sprintf("name==%s", vcd.config.VCD.ProviderVdc.StorageProfile), - }) + storageProfile, err := vcd.client.QueryProviderVdcStorageProfileByName(vcd.config.VCD.StorageProfile.SP1, providerVdcHref) check.Assert(err, IsNil) - if len(results.Results.ProviderVdcStorageProfileRecord) == 0 { - check.Skip(fmt.Sprintf("No storage profile found with name '%s'", vcd.config.VCD.ProviderVdc.StorageProfile)) - } - providerVdcStorageProfileHref := results.Results.ProviderVdcStorageProfileRecord[0].HREF + firstStorageProfileHref := storageProfile.HREF + networkPoolHref := getVdcNetworkPoolHref(vcd, check) - results, err = vcd.client.QueryWithNotEncodedParams(nil, map[string]string{ - "type": "networkPool", - "filter": fmt.Sprintf("name==%s", vcd.config.VCD.ProviderVdc.NetworkPool), - }) - check.Assert(err, IsNil) - if len(results.Results.NetworkPoolRecord) == 0 { - check.Skip(fmt.Sprintf("No network pool found with name '%s'", vcd.config.VCD.ProviderVdc.NetworkPool)) + secondStorageProfileHref := "" + // Make test more robust and tests additionally disabled storage profile + if vcd.config.VCD.StorageProfile.SP2 != "" { + storageProfile, err := vcd.client.QueryProviderVdcStorageProfileByName(vcd.config.VCD.StorageProfile.SP2, providerVdcHref) + check.Assert(err, IsNil) + secondStorageProfileHref = storageProfile.HREF } - networkPoolHref := results.Results.NetworkPoolRecord[0].HREF allocationModels := []string{"AllocationVApp", "AllocationPool", "ReservationPool", "Flex"} trueValue := true @@ -76,7 +61,7 @@ func (vcd *TestVCD) Test_CreateOrgVdcWithFlex(check *C) { Xmlns: types.XMLNamespaceVCloud, AllocationModel: allocationModel, ComputeCapacity: []*types.ComputeCapacity{ - &types.ComputeCapacity{ + { CPU: &types.CapacityWithUsage{ Units: "MHz", Allocated: 1024, @@ -88,13 +73,13 @@ func (vcd *TestVCD) Test_CreateOrgVdcWithFlex(check *C) { }, }, }, - VdcStorageProfile: []*types.VdcStorageProfileConfiguration{&types.VdcStorageProfileConfiguration{ - Enabled: true, + VdcStorageProfile: []*types.VdcStorageProfileConfiguration{{ + Enabled: addrOf(true), Units: "MB", Limit: 1024, Default: true, ProviderVdcStorageProfile: &types.Reference{ - HREF: providerVdcStorageProfileHref, + HREF: firstStorageProfileHref, }, }, }, @@ -112,6 +97,19 @@ func (vcd *TestVCD) Test_CreateOrgVdcWithFlex(check *C) { if allocationModel == "Flex" { vdcConfiguration.IsElastic = &trueValue vdcConfiguration.IncludeMemoryOverhead = &trueValue + vdcConfiguration.ResourceGuaranteedMemory = addrOf(1.00) + } + + if secondStorageProfileHref != "" { + vdcConfiguration.VdcStorageProfile = append(vdcConfiguration.VdcStorageProfile, &types.VdcStorageProfileConfiguration{ + Enabled: addrOf(false), + Units: "MB", + Limit: 1024, + Default: false, + ProviderVdcStorageProfile: &types.Reference{ + HREF: secondStorageProfileHref, + }, + }) } vdc, _ := adminOrg.GetVDCByName(vdcConfiguration.Name, false) @@ -130,8 +128,8 @@ func (vcd *TestVCD) Test_CreateOrgVdcWithFlex(check *C) { vdcConfiguration.ComputeCapacity[0].Memory.Units = "MB" vdc, err = adminOrg.CreateOrgVdc(vdcConfiguration) - check.Assert(vdc, NotNil) check.Assert(err, IsNil) + check.Assert(vdc, NotNil) AddToCleanupList(vdcConfiguration.Name, "vdc", vcd.org.Org.Name, "Test_CreateVdcWithFlex") @@ -141,6 +139,28 @@ func (vcd *TestVCD) Test_CreateOrgVdcWithFlex(check *C) { check.Assert(vdc.Vdc.Name, Equals, vdcConfiguration.Name) check.Assert(vdc.Vdc.IsEnabled, Equals, vdcConfiguration.IsEnabled) check.Assert(vdc.Vdc.AllocationModel, Equals, vdcConfiguration.AllocationModel) + check.Assert(vdc.Vdc.VdcStorageProfiles, NotNil) + check.Assert(vdc.Vdc.VdcStorageProfiles.VdcStorageProfile, NotNil) + if secondStorageProfileHref == "" { + check.Assert(len(vdc.Vdc.VdcStorageProfiles.VdcStorageProfile), Equals, 1) + } else { + check.Assert(len(vdc.Vdc.VdcStorageProfiles.VdcStorageProfile), Equals, 2) + } + + // As storage profiles may come unordered, we check them in a generic way with a loop + for _, spReference := range vdc.Vdc.VdcStorageProfiles.VdcStorageProfile { + check.Assert(spReference, NotNil) + vdcStorageProfileDetails, err := adminOrg.client.GetStorageProfileByHref(spReference.HREF) + check.Assert(err, IsNil) + switch spReference.Name { + case vcd.config.VCD.StorageProfile.SP1: + check.Assert(*vdcStorageProfileDetails.Enabled, Equals, true) + case vcd.config.VCD.StorageProfile.SP2: + check.Assert(*vdcStorageProfileDetails.Enabled, Equals, false) + default: + check.Errorf("didn't expect a storage profile with ID '%s' and name '%s'", spReference.ID, spReference.Name) + } + } err = vdc.DeleteWait(true, true) check.Assert(err, IsNil) @@ -169,6 +189,41 @@ func (vcd *TestVCD) Test_UpdateVdcFlex(check *C) { check.Assert(adminVdc.AdminVdc.IsEnabled, Equals, vdcConfiguration.IsEnabled) check.Assert(adminVdc.AdminVdc.AllocationModel, Equals, vdcConfiguration.AllocationModel) + // test part to reproduce https://github.com/vmware/go-vcloud-director/issues/431 + // this part manages to create task error which later on VDC update fails if type properties order is bad + providerVdcHref := getVdcProviderVdcHref(vcd, check) + pvdcStorageProfile, err := vcd.client.QueryProviderVdcStorageProfileByName(vcd.config.VCD.StorageProfile.SP2, providerVdcHref) + check.Assert(err, IsNil) + + err = adminVdc.AddStorageProfileWait(&types.VdcStorageProfileConfiguration{ + Enabled: addrOf(true), + Default: false, + Units: "MB", + ProviderVdcStorageProfile: &types.Reference{HREF: pvdcStorageProfile.HREF}, + }, + "") + check.Assert(err, IsNil) + + vdc, err := adminOrg.GetVDCByName(vdcConfiguration.Name, true) + check.Assert(err, IsNil) + + vappName := check.TestName() + vmName := check.TestName() + vapp, err := makeEmptyVapp(vdc, vappName, "") + check.Assert(err, IsNil) + _, err = makeEmptyVm(vapp, vmName) + check.Assert(err, IsNil) + AddToCleanupList(vappName, "vapp", "", vappName) + + err = adminVdc.SetDefaultStorageProfile(vcd.config.VCD.StorageProfile.SP2) + check.Assert(err, IsNil) + err = adminVdc.RemoveStorageProfileWait(vcd.config.VCD.StorageProfile.SP1) + // fails with error in task which stays referenced in VDC as `history` element + check.Assert(err, NotNil) + err = adminVdc.Refresh() + check.Assert(err, IsNil) + // end + updateDescription := "updateDescription" computeCapacity := []*types.ComputeCapacity{ &types.ComputeCapacity{ @@ -219,6 +274,12 @@ func (vcd *TestVCD) Test_UpdateVdcFlex(check *C) { check.Assert(math.Abs(*updatedVdc.AdminVdc.ResourceGuaranteedMemory-guaranteed) < 0.001, Equals, true) check.Assert(*updatedVdc.AdminVdc.IsElastic, Equals, true) check.Assert(*updatedVdc.AdminVdc.IncludeMemoryOverhead, Equals, false) + vdc, err = adminOrg.GetVDCByName(updatedVdc.AdminVdc.Name, true) + check.Assert(err, IsNil) + task, err := vdc.Delete(true, true) + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) } // Tests VDC storage profile update @@ -233,8 +294,11 @@ func (vcd *TestVCD) Test_VdcUpdateStorageProfile(check *C) { adminVdc, err := adminOrg.GetAdminVDCByName(vdcConfiguration.Name, true) check.Assert(err, IsNil) check.Assert(adminVdc, NotNil) + vdc, err := adminOrg.GetVDCByName(vdcConfiguration.Name, true) + check.Assert(err, IsNil) + check.Assert(adminVdc, NotNil) - foundStorageProfile, err := GetStorageProfileByHref(vcd.client, adminVdc.AdminVdc.VdcStorageProfiles.VdcStorageProfile[0].HREF) + foundStorageProfile, err := vcd.client.Client.GetStorageProfileByHref(adminVdc.AdminVdc.VdcStorageProfiles.VdcStorageProfile[0].HREF) check.Assert(err, IsNil) check.Assert(foundStorageProfile, Not(Equals), types.VdcStorageProfile{}) check.Assert(foundStorageProfile, NotNil) @@ -244,10 +308,10 @@ func (vcd *TestVCD) Test_VdcUpdateStorageProfile(check *C) { check.Assert(storageProfileId, NotNil) updatedVdc, err := adminVdc.UpdateStorageProfile(storageProfileId, &types.AdminVdcStorageProfile{ - Name: foundStorageProfile.ProviderVdcStorageProfile.Name, + Name: foundStorageProfile.Name, Default: true, Limit: 9081, - Enabled: takeBoolPointer(true), + Enabled: addrOf(true), Units: "MB", IopsSettings: nil, ProviderVdcStorageProfile: &types.Reference{HREF: foundStorageProfile.ProviderVdcStorageProfile.HREF}, @@ -255,13 +319,17 @@ func (vcd *TestVCD) Test_VdcUpdateStorageProfile(check *C) { check.Assert(err, IsNil) check.Assert(updatedVdc, Not(IsNil)) - updatedStorageProfile, err := GetStorageProfileByHref(vcd.client, adminVdc.AdminVdc.VdcStorageProfiles.VdcStorageProfile[0].HREF) + updatedStorageProfile, err := vcd.client.Client.GetStorageProfileByHref(adminVdc.AdminVdc.VdcStorageProfiles.VdcStorageProfile[0].HREF) check.Assert(err, IsNil) check.Assert(updatedStorageProfile, Not(Equals), types.VdcStorageProfile{}) check.Assert(updatedStorageProfile, NotNil) - check.Assert(updatedStorageProfile.Enabled, Equals, true) + check.Assert(*updatedStorageProfile.Enabled, Equals, true) check.Assert(updatedStorageProfile.Limit, Equals, int64(9081)) check.Assert(updatedStorageProfile.Default, Equals, true) check.Assert(updatedStorageProfile.Units, Equals, "MB") + task, err := vdc.Delete(true, true) + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) } diff --git a/govcd/anytypeedgegateway.go b/govcd/anytypeedgegateway.go new file mode 100644 index 000000000..cfddc3f8c --- /dev/null +++ b/govcd/anytypeedgegateway.go @@ -0,0 +1,89 @@ +/* + * Copyright 2022 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" +) + +// AnyTypeEdgeGateway is a common structure which fetches any type of Edge Gateway (NSX-T or NSX-V) +// using OpenAPI endpoint. It can be useful to identify type of Edge Gateway or just retrieve common +// fields - like OwnerRef. There is also a function GetNsxtEdgeGateway to convert it to +// NsxtEdgeGateway (if it is an NSX-T one) +type AnyTypeEdgeGateway struct { + EdgeGateway *types.OpenAPIEdgeGateway + client *Client +} + +// GetNsxtEdgeGatewayById allows retrieving NSX-T or NSX-V Edge Gateway by ID for Org admins +func (adminOrg *AdminOrg) GetAnyTypeEdgeGatewayById(id string) (*AnyTypeEdgeGateway, error) { + return getAnyTypeApiEdgeGatewayById(adminOrg.client, id, nil) +} + +// GetNsxtEdgeGatewayById allows retrieving NSX-T or NSX-V Edge Gateway by ID for Org users +func (org *Org) GetAnyTypeEdgeGatewayById(id string) (*AnyTypeEdgeGateway, error) { + return getAnyTypeApiEdgeGatewayById(org.client, id, nil) +} + +// getNsxtEdgeGatewayById is a private parent for wrapped functions: +// func (adminOrg *AdminOrg) GetAnyTypeEdgeGatewayById(id string) (*AnyTypeEdgeGateway, error) +// func (org *Org) GetAnyTypeEdgeGatewayById(id string) (*AnyTypeEdgeGateway, error) +func getAnyTypeApiEdgeGatewayById(client *Client, id string, queryParameters url.Values) (*AnyTypeEdgeGateway, error) { + if id == "" { + return nil, fmt.Errorf("empty Edge Gateway ID") + } + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGateways + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, id) + if err != nil { + return nil, err + } + + egw := &AnyTypeEdgeGateway{ + EdgeGateway: &types.OpenAPIEdgeGateway{}, + client: client, + } + + err = client.OpenApiGetItem(apiVersion, urlRef, queryParameters, egw.EdgeGateway, nil) + if err != nil { + return nil, err + } + return egw, nil +} + +// IsNsxt checks if Edge Gateways is NSX-T backed +func (anyTypeGateway *AnyTypeEdgeGateway) IsNsxt() bool { + if anyTypeGateway != nil && anyTypeGateway.EdgeGateway != nil && anyTypeGateway.EdgeGateway.GatewayBacking != nil { + return anyTypeGateway.EdgeGateway.GatewayBacking.GatewayType == "NSXT_BACKED" + } + return false +} + +// IsNsxv checks if Edge Gateways is NSX-V backed +func (anyTypeGateway *AnyTypeEdgeGateway) IsNsxv() bool { + return !anyTypeGateway.IsNsxt() +} + +// GetNsxtEdgeGateway converts `AnyTypeEdgeGateway` to `NsxtEdgeGateway` if it is an NSX-T one +func (anyTypeGateway *AnyTypeEdgeGateway) GetNsxtEdgeGateway() (*NsxtEdgeGateway, error) { + if !anyTypeGateway.IsNsxt() { + return nil, fmt.Errorf("this is not an NSX-T backed Edge Gateway") + } + + nsxtEdgeGateway := &NsxtEdgeGateway{ + EdgeGateway: anyTypeGateway.EdgeGateway, + client: anyTypeGateway.client, + } + + return nsxtEdgeGateway, nil +} diff --git a/govcd/anytypeedgegateway_test.go b/govcd/anytypeedgegateway_test.go new file mode 100644 index 000000000..efe4b4f52 --- /dev/null +++ b/govcd/anytypeedgegateway_test.go @@ -0,0 +1,65 @@ +//go:build network || nsxt || functional || openapi || ALL + +package govcd + +import ( + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_AnyTypeEdgeGateway(check *C) { + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointEdgeGateways) + + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + + org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(org, NotNil) + + nsxtEdge, err := adminOrg.GetNsxtEdgeGatewayByName(vcd.config.VCD.Nsxt.EdgeGateway) + check.Assert(err, IsNil) + check.Assert(nsxtEdge, NotNil) + + nsxvEdge, err := vcd.vdc.GetEdgeGatewayByName(vcd.config.VCD.EdgeGateway, false) + check.Assert(err, IsNil) + check.Assert(nsxvEdge, NotNil) + + // Retrieve both types of Edge Gateways using adminOrg structure (NSX-T and NSX-V) using the + // common type AnyTypeEdgeGateway + nsxtAnyTypeEdgeGateway, err := adminOrg.GetAnyTypeEdgeGatewayById(nsxtEdge.EdgeGateway.ID) + check.Assert(err, IsNil) + check.Assert(nsxtAnyTypeEdgeGateway, NotNil) + check.Assert(nsxtAnyTypeEdgeGateway.EdgeGateway, DeepEquals, nsxtEdge.EdgeGateway) + + nsxvAnyTypeEdgeGateway, err := adminOrg.GetAnyTypeEdgeGatewayById(nsxvEdge.EdgeGateway.ID) + check.Assert(err, IsNil) + check.Assert(nsxvAnyTypeEdgeGateway, NotNil) + + // Structures for NSX-V Edge Gateway differ (because it uses XML API) therefore all fields + // cannot be compared + check.Assert(nsxvAnyTypeEdgeGateway.EdgeGateway.ID, DeepEquals, nsxvEdge.EdgeGateway.ID) + + // Retrieve both types of Edge Gateways using Org structure (NSX-T and NSX-V) using the + // common type AnyTypeEdgeGateway + nsxtOrgAnyTypeEdgeGateway, err := org.GetAnyTypeEdgeGatewayById(nsxtEdge.EdgeGateway.ID) + check.Assert(err, IsNil) + check.Assert(nsxtOrgAnyTypeEdgeGateway, NotNil) + check.Assert(nsxtOrgAnyTypeEdgeGateway.EdgeGateway, DeepEquals, nsxtEdge.EdgeGateway) + + // Convert NSX-T backed AnyTypeEdgeGateway to NsxtEdgeGateway + convertedGw, err := nsxtOrgAnyTypeEdgeGateway.GetNsxtEdgeGateway() + check.Assert(err, IsNil) + check.Assert(convertedGw, NotNil) + check.Assert(convertedGw.EdgeGateway, DeepEquals, nsxtOrgAnyTypeEdgeGateway.EdgeGateway) + + // Structures for NSX-V Edge Gateway differ (because it uses XML API) therefore all fields + // cannot be compared + nsxvOrgAnyTypeEdgeGateway, err := org.GetAnyTypeEdgeGatewayById(nsxvEdge.EdgeGateway.ID) + check.Assert(err, IsNil) + check.Assert(nsxvOrgAnyTypeEdgeGateway, NotNil) + check.Assert(nsxvOrgAnyTypeEdgeGateway.EdgeGateway.ID, DeepEquals, nsxvEdge.EdgeGateway.ID) + +} diff --git a/govcd/api.go b/govcd/api.go index 449d5cfa9..e4aaa87f9 100644 --- a/govcd/api.go +++ b/govcd/api.go @@ -2,7 +2,7 @@ * Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. */ -// Package govcd provides a simple binding for vCloud Director REST APIs. +// Package govcd provides a simple binding for VMware Cloud Director REST APIs. package govcd import ( @@ -11,11 +11,11 @@ import ( "encoding/xml" "fmt" "io" - "io/ioutil" "net/http" "net/url" "os" "regexp" + "strconv" "strings" "time" @@ -23,17 +23,19 @@ import ( "github.com/vmware/go-vcloud-director/v2/util" ) -// Client provides a client to vCloud Director, values can be populated automatically using the Authenticate method. +// Client provides a client to VMware Cloud Director, values can be populated automatically using the Authenticate method. type Client struct { - APIVersion string // The API version required - VCDToken string // Access Token (authorization header) - VCDAuthHeader string // Authorization header - VCDHREF url.URL // VCD API ENDPOINT - Http http.Client // HttpClient is the client to use. Default will be used if not provided. - IsSysAdmin bool // flag if client is connected as system administrator + APIVersion string // The API version required + VCDToken string // Access Token (authorization header) + VCDAuthHeader string // Authorization header + VCDHREF url.URL // VCD API ENDPOINT + Http http.Client // HttpClient is the client to use. Default will be used if not provided. + IsSysAdmin bool // flag if client is connected as system administrator + UsingBearerToken bool // flag if client is using a bearer token + UsingAccessToken bool // flag if client is using an API token // MaxRetryTimeout specifies a time limit (in seconds) for retrying requests made by the SDK - // where vCloud director may take time to respond and retry mechanism is needed. + // where VMware Cloud Director may take time to respond and retry mechanism is needed. // This must be >0 to avoid instant timeout errors. MaxRetryTimeout int @@ -51,21 +53,34 @@ type Client struct { // "User-Agent: / " UserAgent string + // RequestIdFunc is a function that would return an unique string to be used for + // 'X-Vmware-Vcloud-Client-Request-Id' which helps log tracing + // Function `WithVcloudRequestIdFunc` contains more details + RequestIdFunc func() string + + // IgnoredMetadata allows to ignore metadata entries when using the methods defined in metadata_v2.go + IgnoredMetadata []IgnoredMetadata + supportedVersions SupportedVersions // Versions from /api/versions endpoint + customHeader http.Header } // AuthorizationHeader header key used by default to set the authorization token. const AuthorizationHeader = "X-Vcloud-Authorization" // BearerTokenHeader is the header key containing a bearer token +// #nosec G101 -- This is not a credential, it's just the header key const BearerTokenHeader = "X-Vmware-Vcloud-Access-Token" +const ApiTokenHeader = "API-token" + // General purpose error to be used whenever an entity is not found from a "GET" request // Allows a simpler checking of the call result // such as -// if err == ErrorEntityNotFound { -// // do what is needed in case of not found -// } +// +// if err == ErrorEntityNotFound { +// // do what is needed in case of not found +// } var errorEntityNotFoundMessage = "[ENF] entity not found" var ErrorEntityNotFound = fmt.Errorf(errorEntityNotFoundMessage) @@ -74,29 +89,39 @@ var debugShowRequestEnabled = os.Getenv("GOVCD_SHOW_REQ") != "" var debugShowResponseEnabled = os.Getenv("GOVCD_SHOW_RESP") != "" // Enables the debugging hook to show requests as they are processed. +// //lint:ignore U1000 this function is used on request for debugging purposes func enableDebugShowRequest() { debugShowRequestEnabled = true } // Disables the debugging hook to show requests as they are processed. +// //lint:ignore U1000 this function is used on request for debugging purposes func disableDebugShowRequest() { debugShowRequestEnabled = false - _ = os.Setenv("GOVCD_SHOW_REQ", "") + err := os.Setenv("GOVCD_SHOW_REQ", "") + if err != nil { + util.Logger.Printf("[DEBUG - disableDebugShowRequest] error setting environment variable: %s", err) + } } // Enables the debugging hook to show responses as they are processed. +// //lint:ignore U1000 this function is used on request for debugging purposes func enableDebugShowResponse() { debugShowResponseEnabled = true } // Disables the debugging hook to show responses as they are processed. +// //lint:ignore U1000 this function is used on request for debugging purposes func disableDebugShowResponse() { debugShowResponseEnabled = false - _ = os.Setenv("GOVCD_SHOW_RESP", "") + err := os.Setenv("GOVCD_SHOW_RESP", "") + if err != nil { + util.Logger.Printf("[DEBUG - disableDebugShowResponse] error setting environment variable: %s", err) + } } // On-the-fly debug hook. If either debugShowRequestEnabled or the environment @@ -136,11 +161,12 @@ func debugShowResponse(resp *http.Response, body []byte) { } } -// Convenience function, similar to os.IsNotExist that checks whether a given error +// IsNotFound is a convenience function, similar to os.IsNotExist that checks whether a given error // is a "Not found" error, such as -// if isNotFound(err) { -// // do what is needed in case of not found -// } +// +// if isNotFound(err) { +// // do what is needed in case of not found +// } func IsNotFound(err error) bool { return err != nil && err == ErrorEntityNotFound } @@ -153,8 +179,8 @@ func ContainsNotFound(err error) bool { } // NewRequestWitNotEncodedParams allows passing complex values params that shouldn't be encoded like for queries. e.g. /query?filter=name=foo -func (cli *Client) NewRequestWitNotEncodedParams(params map[string]string, notEncodedParams map[string]string, method string, reqUrl url.URL, body io.Reader) *http.Request { - return cli.NewRequestWitNotEncodedParamsWithApiVersion(params, notEncodedParams, method, reqUrl, body, cli.APIVersion) +func (client *Client) NewRequestWitNotEncodedParams(params map[string]string, notEncodedParams map[string]string, method string, reqUrl url.URL, body io.Reader) *http.Request { + return client.NewRequestWitNotEncodedParamsWithApiVersion(params, notEncodedParams, method, reqUrl, body, client.APIVersion) } // NewRequestWitNotEncodedParamsWithApiVersion allows passing complex values params that shouldn't be encoded like for queries. e.g. /query?filter=name=foo @@ -164,13 +190,13 @@ func (cli *Client) NewRequestWitNotEncodedParams(params map[string]string, notEn // * reqUrl - request url // * body - request body // * apiVersion - provided Api version overrides default Api version value used in request parameter -func (cli *Client) NewRequestWitNotEncodedParamsWithApiVersion(params map[string]string, notEncodedParams map[string]string, method string, reqUrl url.URL, body io.Reader, apiVersion string) *http.Request { - return cli.newRequest(params, notEncodedParams, method, reqUrl, body, apiVersion, nil) +func (client *Client) NewRequestWitNotEncodedParamsWithApiVersion(params map[string]string, notEncodedParams map[string]string, method string, reqUrl url.URL, body io.Reader, apiVersion string) *http.Request { + return client.newRequest(params, notEncodedParams, method, reqUrl, body, apiVersion, nil) } // newRequest is the parent of many "specific" "NewRequest" functions. // Note. It is kept private to avoid breaking public API on every new field addition. -func (cli *Client) newRequest(params map[string]string, notEncodedParams map[string]string, method string, reqUrl url.URL, body io.Reader, apiVersion string, additionalHeader http.Header) *http.Request { +func (client *Client) newRequest(params map[string]string, notEncodedParams map[string]string, method string, reqUrl url.URL, body io.Reader, apiVersion string, additionalHeader http.Header) *http.Request { reqValues := url.Values{} // Build up our request parameters @@ -190,43 +216,55 @@ func (cli *Client) newRequest(params map[string]string, notEncodedParams map[str // If the body contains data - try to read all contents for logging and re-create another // io.Reader with all contents to use it down the line var readBody []byte + var err error if body != nil { - readBody, _ = ioutil.ReadAll(body) + readBody, err = io.ReadAll(body) + if err != nil { + util.Logger.Printf("[DEBUG - newRequest] error reading body: %s", err) + } body = bytes.NewReader(readBody) } - // Build the request, no point in checking for errors here as we're just - // passing a string version of an url.URL struct and http.NewRequest returns - // error only if can't process an url.ParseRequestURI(). - req, _ := http.NewRequest(method, reqUrl.String(), body) + req, err := http.NewRequest(method, reqUrl.String(), body) + if err != nil { + util.Logger.Printf("[DEBUG - newRequest] error getting new request: %s", err) + } - if cli.VCDAuthHeader != "" && cli.VCDToken != "" { + if client.VCDAuthHeader != "" && client.VCDToken != "" { // Add the authorization header - req.Header.Add(cli.VCDAuthHeader, cli.VCDToken) + req.Header.Add(client.VCDAuthHeader, client.VCDToken) } - if (cli.VCDAuthHeader != "" && cli.VCDToken != "") || + if (client.VCDAuthHeader != "" && client.VCDToken != "") || (additionalHeader != nil && additionalHeader.Get("Authorization") != "") { // Add the Accept header for VCD req.Header.Add("Accept", "application/*+xml;version="+apiVersion) } // The deprecated authorization token is 32 characters long // The bearer token is 612 characters long - if len(cli.VCDToken) > 32 { + if len(client.VCDToken) > 32 { req.Header.Add("X-Vmware-Vcloud-Token-Type", "Bearer") - req.Header.Add("Authorization", "bearer "+cli.VCDToken) + req.Header.Add("Authorization", "bearer "+client.VCDToken) } - // Merge in additional headers before logging if any where specified in additionalHeader + // Merge in additional headers before logging if anywhere specified in additionalHeader // parameter - if additionalHeader != nil && len(additionalHeader) > 0 { + if len(additionalHeader) > 0 { for headerName, headerValueSlice := range additionalHeader { for _, singleHeaderValue := range headerValueSlice { - req.Header.Add(headerName, singleHeaderValue) + req.Header.Set(headerName, singleHeaderValue) + } + } + } + if client.customHeader != nil { + for k, v := range client.customHeader { + for _, v1 := range v { + req.Header.Add(k, v1) } } } - setHttpUserAgent(cli.UserAgent, req) + setHttpUserAgent(client.UserAgent, req) + setVcloudClientRequestId(client.RequestIdFunc, req) // Avoids passing data if the logging of requests is disabled if util.LogHttpRequest { @@ -243,14 +281,14 @@ func (cli *Client) newRequest(params map[string]string, notEncodedParams map[str } // NewRequest creates a new HTTP request and applies necessary auth headers if set. -func (cli *Client) NewRequest(params map[string]string, method string, reqUrl url.URL, body io.Reader) *http.Request { - return cli.NewRequestWitNotEncodedParams(params, nil, method, reqUrl, body) +func (client *Client) NewRequest(params map[string]string, method string, reqUrl url.URL, body io.Reader) *http.Request { + return client.NewRequestWitNotEncodedParams(params, nil, method, reqUrl, body) } // NewRequestWithApiVersion creates a new HTTP request and applies necessary auth headers if set. // Allows to override default request API Version -func (cli *Client) NewRequestWithApiVersion(params map[string]string, method string, reqUrl url.URL, body io.Reader, apiVersion string) *http.Request { - return cli.NewRequestWitNotEncodedParamsWithApiVersion(params, nil, method, reqUrl, body, apiVersion) +func (client *Client) NewRequestWithApiVersion(params map[string]string, method string, reqUrl url.URL, body io.Reader, apiVersion string) *http.Request { + return client.NewRequestWitNotEncodedParamsWithApiVersion(params, nil, method, reqUrl, body, apiVersion) } // ParseErr takes an error XML resp, error interface for unmarshalling and returns a single string for @@ -272,7 +310,7 @@ func ParseErr(bodyType types.BodyType, resp *http.Response, errType error) error // decodeBody is used to decode a response body of types.BodyType func decodeBody(bodyType types.BodyType, resp *http.Response, out interface{}) error { - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) // In case of JSON, body does not have indents in response therefore it must be indented if bodyType == types.BodyTypeJSON { @@ -282,7 +320,7 @@ func decodeBody(bodyType types.BodyType, resp *http.Response, out interface{}) e } } - util.ProcessResponseOutput(util.FuncNameCallStack(), resp, fmt.Sprintf("%s", body)) + util.ProcessResponseOutput(util.FuncNameCallStack(), resp, string(body)) if err != nil { return err } @@ -379,7 +417,7 @@ func checkRespWithErrType(bodyType types.BodyType, resp *http.Response, err, err } } -// Helper function creates request, runs it, checks response and parses task from response. +// ExecuteTaskRequest helper function creates request, runs it, checks response and parses task from response. // pathURL - request URL // requestType - HTTP method type // contentType - value to set for "Content-Type" @@ -390,7 +428,7 @@ func (client *Client) ExecuteTaskRequest(pathURL, requestType, contentType, erro return client.executeTaskRequest(pathURL, requestType, contentType, errorMessage, payload, client.APIVersion) } -// Helper function creates request, runs it, checks response and parses task from response. +// ExecuteTaskRequestWithApiVersion helper function creates request, runs it, checks response and parses task from response. // pathURL - request URL // requestType - HTTP method type // contentType - value to set for "Content-Type" @@ -436,7 +474,7 @@ func (client *Client) executeTaskRequest(pathURL, requestType, contentType, erro return *task, nil } -// Helper function creates request, runs it, checks response and do not expect any values from it. +// ExecuteRequestWithoutResponse helper function creates request, runs it, checks response and do not expect any values from it. // pathURL - request URL // requestType - HTTP method type // contentType - value to set for "Content-Type" @@ -447,7 +485,7 @@ func (client *Client) ExecuteRequestWithoutResponse(pathURL, requestType, conten return client.executeRequestWithoutResponse(pathURL, requestType, contentType, errorMessage, payload, client.APIVersion) } -// Helper function creates request, runs it, checks response and do not expect any values from it. +// ExecuteRequestWithoutResponseWithApiVersion helper function creates request, runs it, checks response and do not expect any values from it. // pathURL - request URL // requestType - HTTP method type // contentType - value to set for "Content-Type" @@ -491,7 +529,7 @@ func (client *Client) executeRequestWithoutResponse(pathURL, requestType, conten return nil } -// Helper function creates request, runs it, check responses and parses out interface from response. +// ExecuteRequest helper function creates request, runs it, check responses and parses out interface from response. // pathURL - request URL // requestType - HTTP method type // contentType - value to set for "Content-Type" @@ -504,7 +542,7 @@ func (client *Client) ExecuteRequest(pathURL, requestType, contentType, errorMes return client.executeRequest(pathURL, requestType, contentType, errorMessage, payload, out, client.APIVersion) } -// Helper function creates request, runs it, check responses and parses out interface from response. +// ExecuteRequestWithApiVersion helper function creates request, runs it, check responses and parses out interface from response. // pathURL - request URL // requestType - HTTP method type // contentType - value to set for "Content-Type" @@ -577,12 +615,12 @@ func (client *Client) ExecuteParamRequestWithCustomError(pathURL string, params // read from resp.Body io.Reader for debug output if it has body var bodyBytes []byte if resp.Body != nil { - bodyBytes, err = ioutil.ReadAll(resp.Body) + bodyBytes, err = io.ReadAll(resp.Body) if err != nil { return &http.Response{}, fmt.Errorf("could not read response body: %s", err) } // Restore the io.ReadCloser to its original state with no-op closer - resp.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes)) + resp.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) } util.ProcessResponseOutput(util.FuncNameCallStack(), resp, string(bodyBytes)) @@ -598,30 +636,32 @@ func executeRequestWithApiVersion(pathURL, requestType, contentType string, payl // executeRequestCustomErr performs request and unmarshals API error to errType if not 2xx status was returned func executeRequestCustomErr(pathURL string, params map[string]string, requestType, contentType string, payload interface{}, client *Client, errType error, apiVersion string) (*http.Response, error) { - url, _ := url.ParseRequestURI(pathURL) + requestURI, err := url.ParseRequestURI(pathURL) + if err != nil { + return nil, fmt.Errorf("couldn't parse path request URI '%s': %s", pathURL, err) + } var req *http.Request - switch requestType { - case http.MethodPost, http.MethodPut: - + switch { + // Only send data (and xml.Header) if the payload is actually provided to avoid sending empty body with XML header + // (some Web Application Firewalls block requests when empty XML header is set but not body provided) + case payload != nil: marshaledXml, err := xml.MarshalIndent(payload, " ", " ") if err != nil { return &http.Response{}, fmt.Errorf("error marshalling xml data %s", err) } body := bytes.NewBufferString(xml.Header + string(marshaledXml)) - req = client.NewRequestWithApiVersion(params, requestType, *url, body, apiVersion) + req = client.NewRequestWithApiVersion(params, requestType, *requestURI, body, apiVersion) default: - req = client.NewRequestWithApiVersion(params, requestType, *url, nil, apiVersion) + req = client.NewRequestWithApiVersion(params, requestType, *requestURI, nil, apiVersion) } if contentType != "" { req.Header.Add("Content-Type", contentType) } - setHttpUserAgent(client.UserAgent, req) - resp, err := client.Http.Do(req) if err != nil { return resp, err @@ -637,6 +677,24 @@ func setHttpUserAgent(userAgent string, req *http.Request) { } } +// The X-VMWARE-VCLOUD-CLIENT-REQUEST-ID header must contain only alpha-numeric characters or +// dashes. The header must contain at least one alpha-numeric character, and VMware Cloud Director +// shortens it if it's longer than 128 characters long. The X-VMWARE-VCLOUD-REQUEST-ID response +// header is formed from the first 128 characters of X-VMWARE-VCLOUD-CLIENT-REQUEST-ID, followed by +// a dash and a random UUID that the server generates. If the X-VMWARE-VCLOUD-CLIENT-REQUEST-ID +// header is invalid, null, or empty, the X-VMWARE-VCLOUD-REQUEST-ID is a random UUID. VMware Cloud +// Director adds this value to every VMware Cloud Director, vCenter Server, and ESXi log message +// related to processing the request, and provides a way to correlate the processing of a request +// across all participating systems. If a request does not supply a +// X-VMWARE-VCLOUD-CLIENT-REQUEST-ID header, the response contains an X-VMWARE-VCLOUD-REQUEST-ID +// header with a generated value that cannot be used for log correlation. +func setVcloudClientRequestId(requestBuilder func() string, req *http.Request) { + if requestBuilder != nil { + requestId := requestBuilder() + req.Header.Set("X-VMWARE-VCLOUD-CLIENT-REQUEST-ID", requestId) + } +} + func isMessageWithPlaceHolder(message string) bool { err := fmt.Errorf(message, "test error") return !strings.Contains(err.Error(), "%!(EXTRA") @@ -654,18 +712,11 @@ func combinedTaskErrorMessage(task *types.Task, err error) string { return extendedError } -func takeBoolPointer(value bool) *bool { - return &value -} - -// takeIntAddress is a helper that returns the address of an `int` -func takeIntAddress(x int) *int { - return &x -} - -// takeStringPointer is a helper that returns the address of a `string` -func takeStringPointer(x string) *string { - return &x +// addrOf is a generic function to return the address of a variable +// Note. It is mainly meant for converting literal values to pointers (e.g. `addrOf(true)`) +// and not getting the address of a variable (e.g. `addrOf(variable)`) +func addrOf[T any](variable T) *T { + return &variable } // IsUuid returns true if the identifier is a bare UUID @@ -715,3 +766,180 @@ func BuildUrnWithUuid(urnPrefix, uuid string) (string, error) { func takeFloatAddress(x float64) *float64 { return &x } + +// SetCustomHeader adds custom HTTP header values to a client +func (client *Client) SetCustomHeader(values map[string]string) { + if len(client.customHeader) == 0 { + client.customHeader = make(http.Header) + } + for k, v := range values { + client.customHeader.Add(k, v) + } +} + +// RemoveCustomHeader remove custom header values from the client +func (client *Client) RemoveCustomHeader() { + if client.customHeader != nil { + client.customHeader = nil + } +} + +// RemoveProvidedCustomHeaders removes custom header values from the client +func (client *Client) RemoveProvidedCustomHeaders(values map[string]string) { + if client.customHeader != nil { + for k := range values { + client.customHeader.Del(k) + } + } +} + +// Retrieves the administrator URL of a given HREF +func getAdminURL(href string) string { + adminApi := "/api/admin/" + if strings.Contains(href, adminApi) { + return href + } + return strings.ReplaceAll(href, "/api/", adminApi) +} + +// Retrieves the admin extension URL of a given HREF +func getAdminExtensionURL(href string) string { + adminExtensionApi := "/api/admin/extension/" + if strings.Contains(href, adminExtensionApi) { + return href + } + return strings.ReplaceAll(getAdminURL(href), "/api/admin/", adminExtensionApi) +} + +// TestConnection calls API to test a connection against a VCD, including SSL handshake and hostname verification. +func (client *Client) TestConnection(testConnection types.TestConnection) (*types.TestConnectionResult, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointTestConnection + + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + returnTestConnectionResult := &types.TestConnectionResult{ + TargetProbe: &types.ProbeResult{}, + ProxyProbe: &types.ProbeResult{}, + } + + err = client.OpenApiPostItem(apiVersion, urlRef, nil, testConnection, returnTestConnectionResult, nil) + if err != nil { + return nil, fmt.Errorf("error performing test connection: %s", err) + } + + return returnTestConnectionResult, nil +} + +// TestConnectionWithDefaults calls TestConnection given a subscriptionURL. The rest of parameters are set as default. +// It returns whether it could reach the server and establish SSL connection or not. +func (client *Client) TestConnectionWithDefaults(subscriptionURL string) (bool, error) { + if subscriptionURL == "" { + return false, fmt.Errorf("TestConnectionWithDefaults needs to be passed a host. i.e. my-host.vmware.com") + } + + url, err := url.Parse(subscriptionURL) + if err != nil { + return false, fmt.Errorf("unable to parse URL - %s", err) + } + + // Get port + var port int + if v := url.Port(); v != "" { + port, err = strconv.Atoi(v) + if err != nil { + return false, fmt.Errorf("couldn't parse port provided - %s", err) + } + } else { + switch url.Scheme { + case "http": + port = 80 + case "https": + port = 443 + } + } + + testConnectionConfig := types.TestConnection{ + Host: url.Hostname(), + Port: port, + Secure: addrOf(true), // Default value used by VCD UI + Timeout: 30, // Default value used by VCD UI + } + + testConnectionResult, err := client.TestConnection(testConnectionConfig) + if err != nil { + return false, err + } + + if !testConnectionResult.TargetProbe.CanConnect { + return false, fmt.Errorf("the remote host is not reachable") + } + + if !testConnectionResult.TargetProbe.SSLHandshake { + return true, fmt.Errorf("unsupported or unrecognized SSL message") + } + + return true, nil +} + +// buildUrl uses the Client base URL to create a customised URL +func (client *Client) buildUrl(elements ...string) (string, error) { + baseUrl := client.VCDHREF.String() + if !IsValidUrl(baseUrl) { + return "", fmt.Errorf("incorrect URL %s", client.VCDHREF.String()) + } + if strings.HasSuffix(baseUrl, "/") { + baseUrl = strings.TrimRight(baseUrl, "/") + } + if strings.HasSuffix(baseUrl, "/api") { + baseUrl = strings.TrimRight(baseUrl, "/api") + } + return url.JoinPath(baseUrl, elements...) +} + +// --------------------------------------------------------------------- +// The following functions are needed to avoid strict Coverity warnings +// --------------------------------------------------------------------- + +// urlParseRequestURI returns a URL, discarding the error +func urlParseRequestURI(href string) *url.URL { + apiEndpoint, err := url.ParseRequestURI(href) + if err != nil { + util.Logger.Printf("[DEBUG - urlParseRequestURI] error parsing request URI: %s", err) + } + return apiEndpoint +} + +// safeClose closes a file and logs the error, if any. This can be used instead of file.Close() +func safeClose(file *os.File) { + if err := file.Close(); err != nil { + util.Logger.Printf("Error closing file: %s\n", err) + } +} + +// isSuccessStatus returns true if the given status code is between 200 and 300 +func isSuccessStatus(statusCode int) bool { + if statusCode >= http.StatusOK && // 200 + statusCode < http.StatusMultipleChoices { // 300 + return true + } + return false +} + +// convertSliceOfStringsToOpenApiReferenceIds converts []string to []types.OpenApiReference by filling +// types.OpenApiReference.ID fields +func convertSliceOfStringsToOpenApiReferenceIds(ids []string) []types.OpenApiReference { + resultReferences := make([]types.OpenApiReference, len(ids)) + for i, v := range ids { + resultReferences[i].ID = v + } + + return resultReferences +} diff --git a/govcd/api_json.go b/govcd/api_json.go new file mode 100644 index 000000000..e8b3023a4 --- /dev/null +++ b/govcd/api_json.go @@ -0,0 +1,69 @@ +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "encoding/json" + "fmt" + "github.com/vmware/go-vcloud-director/v2/types/v56" + "github.com/vmware/go-vcloud-director/v2/util" + "io" + "net/http" + "net/url" + "strings" +) + +// executeJsonRequest is a wrapper around regular API call operations, similar to client.ExecuteRequest, but with JSON payback +// Returns a http.Response object, which, in case of success, has its body still unread +// Caller function has the responsibility for closing the response body +func (client Client) executeJsonRequest(href, httpMethod string, inputStructure any, errorMessage string) (*http.Response, error) { + + text, err := json.MarshalIndent(inputStructure, " ", " ") + if err != nil { + return nil, err + } + requestHref, err := url.Parse(href) + if err != nil { + return nil, err + } + + var resp *http.Response + body := strings.NewReader(string(text)) + apiVersion := client.APIVersion + headAccept := http.Header{} + headAccept.Set("Accept", fmt.Sprintf("application/*+json;version=%s", apiVersion)) + headAccept.Set("Content-Type", "application/*+json") + request := client.newRequest(nil, nil, httpMethod, *requestHref, body, apiVersion, headAccept) + resp, err = client.Http.Do(request) + if err != nil { + return nil, fmt.Errorf(errorMessage, err) + } + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + body, _ := io.ReadAll(resp.Body) + util.ProcessResponseOutput(util.CallFuncName(), resp, string(body)) + var jsonError types.OpenApiError + err = json.Unmarshal(body, &jsonError) + // By default, we return the whole response body as error message. This may also contain the stack trace + message := string(body) + // if the body contains a valid JSON representation of the error, we return a more agile message, using the + // exposed fields, and hiding the stack trace from view + if err == nil { + message = fmt.Sprintf("%s - %s", jsonError.MinorErrorCode, jsonError.Message) + } + util.ProcessResponseOutput(util.CallFuncName(), resp, string(body)) + return resp, fmt.Errorf(errorMessage, message) + } + + return checkRespWithErrType(types.BodyTypeJSON, resp, err, &types.Error{}) +} + +// closeBody is a wrapper function that should be used with "defer" after calling executeJsonRequest +func closeBody(resp *http.Response) { + err := resp.Body.Close() + if err != nil { + util.Logger.Printf("error closing response body - Called by %s: %s\n", util.CallFuncName(), err) + } +} diff --git a/govcd/api_test.go b/govcd/api_test.go index 53f3728c8..859e98dc4 100644 --- a/govcd/api_test.go +++ b/govcd/api_test.go @@ -22,7 +22,7 @@ var INVALID_NAME = `*******************************************INVALID ************************` // This ID won't be found by lookup in any entity -var invalidEntityId = "one:two:three:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" +var invalidEntityId = "urn:vcloud:three:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" func tagsHelp(t *testing.T) { @@ -51,6 +51,8 @@ At least one of the following tags should be defined: * user: Runs user related tests * vapp: Runs vapp related tests * vdc: Runs vdc related tests + * vdcGroup: Runs vdc group related tests + * certificate Runs certificate related tests * vm: Runs vm related tests Examples: diff --git a/govcd/api_token.go b/govcd/api_token.go new file mode 100644 index 000000000..f253e9295 --- /dev/null +++ b/govcd/api_token.go @@ -0,0 +1,336 @@ +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "encoding/json" + "fmt" + "io/fs" + "net/url" + "os" + "path" + "strings" + "time" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +// TODO Have distinct names for API and Refresh tokens +// Token is a struct that handles two methods: Delete() and GetInitialRefreshToken() +type Token struct { + Token *types.Token + client *Client +} + +// CreateToken is used for creating API tokens and works in two steps: +// 1. Register the token through the `register` endpoint +// 2. Fetch it using GetTokenById(tokenID) +// The user then can use *Token.GetInitialRefreshToken to get the API token +func (vcdClient *VCDClient) CreateToken(org, tokenName string) (*Token, error) { + apiTokenParams := &types.ApiTokenParams{ + ClientName: tokenName, + } + + newTokenParams, err := vcdClient.RegisterToken(org, apiTokenParams) + if err != nil { + return nil, fmt.Errorf("failed to register API token: %s", err) + } + + tokenUrn, err := BuildUrnWithUuid("urn:vcloud:token:", newTokenParams.ClientID) + if err != nil { + return nil, fmt.Errorf("failed to build URN: %s", err) + } + + token, err := vcdClient.GetTokenById(tokenUrn) + if err != nil { + return nil, fmt.Errorf("failed to get token: %s", err) + } + + return token, nil +} + +// GetTokenById retrieves a Token by ID +func (vcdClient *VCDClient) GetTokenById(tokenId string) (*Token, error) { + client := vcdClient.Client + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointTokens + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, tokenId) + if err != nil { + return nil, err + } + + apiToken := &Token{ + Token: &types.Token{}, + client: &client, + } + + err = client.OpenApiGetItem(apiVersion, urlRef, nil, apiToken.Token, nil) + if err != nil { + return nil, fmt.Errorf("failed to get token: %s", err) + } + + return apiToken, nil +} + +// GetAllTokens gets all tokens with the specified query parameters +func (vcdClient *VCDClient) GetAllTokens(queryParameters url.Values) ([]*Token, error) { + client := vcdClient.Client + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointTokens + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + typeResponses := []*types.Token{{}} + err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParameters, &typeResponses, nil) + if err != nil { + return nil, fmt.Errorf("failed to get tokens: %s", err) + } + + results := make([]*Token, len(typeResponses)) + for sliceIndex := range typeResponses { + results[sliceIndex] = &Token{ + Token: typeResponses[sliceIndex], + client: &client, + } + } + + return results, nil +} + +// GetTokenByNameAndUsername retrieves a Token by name and username +func (vcdClient *VCDClient) GetTokenByNameAndUsername(tokenName, userName string) (*Token, error) { + queryParameters := url.Values{} + queryParameters.Add("filter", fmt.Sprintf("(name==%s;owner.name==%s;(type==PROXY,type==REFRESH))", tokenName, userName)) + + tokens, err := vcdClient.GetAllTokens(queryParameters) + if err != nil { + return nil, fmt.Errorf("failed to get token by name and owner: %s", err) + } + + token, err := oneOrError("name", tokenName, tokens) + if err != nil { + return nil, err + } + + return token, nil +} + +// RegisterToken registers an API token with the given name. The access token can still be fetched for the API +// token using token.GetInitialApiToken() +func (vcdClient *VCDClient) RegisterToken(org string, tokenParams *types.ApiTokenParams) (*types.ApiTokenParams, error) { + client := vcdClient.Client + + if client.APIVCDMaxVersionIs("< 36.1") { + version, err := client.GetVcdFullVersion() + if err == nil { + return nil, fmt.Errorf("minimum version for Token registration is 10.3.1 - Version detected: %s", version.Version) + } + // If we can't get the VCD version, we return API version info + return nil, fmt.Errorf("minimum API version for Token registration is 36.1 - Version detected: %s", client.APIVersion) + } + + // If the client is a user of an org, the endpoint is oauth/tenant/orgName/register + // if the client is a user of sysorg, the endpoint is oauth/provider/register + userDef := "tenant/" + org + if strings.EqualFold(org, "system") { + userDef = "provider" + } + + // Create the URL for the register endpoint + urlRef, err := url.ParseRequestURI(fmt.Sprintf("%s://%s/oauth/%s/%s", client.VCDHREF.Scheme, client.VCDHREF.Host, userDef, "register")) + if err != nil { + return nil, fmt.Errorf("error getting request URL from %s : %s", urlRef.String(), err) + } + + newTokenParams := &types.ApiTokenParams{} + + // oauth/{tenantcontext}/register isn't an OpenAPI endpoint, so it doesn't have a defined + // API version + err = client.OpenApiPostItemSync("", urlRef, nil, tokenParams, newTokenParams) + if err != nil { + return nil, fmt.Errorf("error registering token: %s", err) + } + + return newTokenParams, nil +} + +// getAccessToken gets the access token structure containing the bearer token +func (client *Client) getAccessToken(org, funcName string, payloadMap map[string]string) (*types.ApiTokenRefresh, error) { + userDef := "tenant/" + org + if strings.EqualFold(org, "system") { + userDef = "provider" + } + + endpoint := fmt.Sprintf("%s://%s/oauth/%s/token", client.VCDHREF.Scheme, client.VCDHREF.Host, userDef) + urlRef, err := url.ParseRequestURI(endpoint) + if err != nil { + return nil, fmt.Errorf("error getting request url from %s: %s", urlRef.String(), err) + } + + newToken := &types.ApiTokenRefresh{} + + // Not an OpenAPI endpoint so hardcoding the API token minimal version + err = client.OpenApiPostUrlEncoded("36.1", urlRef, nil, payloadMap, &newToken, nil) + if err != nil { + return nil, fmt.Errorf("error authorizing service account: %s", err) + } + + return newToken, nil +} + +// GetInitialApiToken gets the initial API token, usable only once per token. +func (token *Token) GetInitialApiToken() (*types.ApiTokenRefresh, error) { + client := token.client + uuid := extractUuid(token.Token.ID) + data := map[string]string{ + "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", + "assertion": client.VCDToken, + "client_id": uuid, + } + + refreshToken, err := client.getAccessToken(token.Token.Org.Name, "CreateApiToken", data) + if err != nil { + return nil, fmt.Errorf("error getting token: %s", err) + } + + return refreshToken, nil +} + +// DeleteTokenByID deletes an existing token by its' URN ID +func (token *Token) Delete() error { + client := token.client + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointTokens + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, token.Token.ID) + if err != nil { + return err + } + + err = client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) + if err != nil { + return err + } + + return nil +} + +// SetApiToken behaves similarly to SetToken, with the difference that it will +// return full information about the bearer token, so that the caller can make decisions about token expiration +func (vcdClient *VCDClient) SetApiToken(org, apiToken string) (*types.ApiTokenRefresh, error) { + tokenRefresh, err := vcdClient.GetBearerTokenFromApiToken(org, apiToken) + if err != nil { + return nil, err + } + err = vcdClient.SetToken(org, BearerTokenHeader, tokenRefresh.AccessToken) + if err != nil { + return nil, err + } + return tokenRefresh, nil +} + +// GetBearerTokenFromApiToken uses an API token to retrieve a bearer token +// using the refresh token operation. +func (vcdClient *VCDClient) GetBearerTokenFromApiToken(org, token string) (*types.ApiTokenRefresh, error) { + data := map[string]string{ + "grant_type": "refresh_token", + "refresh_token": token, + } + tokenDef, err := vcdClient.Client.getAccessToken(org, "GetBearerTokenFromApiToken", data) + if err != nil { + return nil, fmt.Errorf("error getting bearer token: %s", err) + } + + return tokenDef, nil +} + +// SetApiTokenFile reads the API token file, sets the client's bearer +// token and fetches a new API token for next authentication request +// using SetApiToken +func (vcdClient *VCDClient) SetApiTokenFromFile(org, apiTokenFile string) (*types.ApiTokenRefresh, error) { + apiToken, err := GetTokenFromFile(apiTokenFile) + if err != nil { + return nil, err + } + + return vcdClient.SetApiToken(org, apiToken.RefreshToken) +} + +func SaveApiTokenToFile(filename, userAgent string, apiToken *types.ApiTokenRefresh) error { + return saveTokenToFile(filename, "API Token", userAgent, apiToken) +} + +// GetTokenFromFile reads an API token from a given file +func GetTokenFromFile(tokenFilename string) (*types.ApiTokenRefresh, error) { + apiToken := &types.ApiTokenRefresh{} + // Read file contents and unmarshal them to apiToken + err := readFileAndUnmarshalJSON(tokenFilename, apiToken) + if err != nil { + return nil, err + } + + return apiToken, nil +} + +func saveTokenToFile(filename, tokenType, userAgent string, token *types.ApiTokenRefresh) error { + token = &types.ApiTokenRefresh{ + RefreshToken: token.RefreshToken, + TokenType: tokenType, + UpdatedBy: userAgent, + UpdatedOn: time.Now().Format(time.RFC3339), + } + err := marshalJSONAndWriteToFile(filename, token, 0600) + if err != nil { + return err + } + return nil +} + +// readFileAndUnmarshalJSON reads a file and unmarshals it to the given variable +func readFileAndUnmarshalJSON(filename string, object any) error { + data, err := os.ReadFile(path.Clean(filename)) + if err != nil { + return fmt.Errorf("failed to read from file: %s", err) + } + + err = json.Unmarshal(data, object) + if err != nil { + return fmt.Errorf("failed to unmarshal file contents to the object: %s", err) + } + + return nil +} + +// marshalJSONAndWriteToFile marshalls the given object into JSON and writes +// to a file with the given permissions in octal format (e.g 0600) +func marshalJSONAndWriteToFile(filename string, object any, permissions int) error { + data, err := json.MarshalIndent(object, " ", " ") + if err != nil { + return fmt.Errorf("error marshalling object to JSON: %s", err) + } + + err = os.WriteFile(filename, data, fs.FileMode(permissions)) + if err != nil { + return fmt.Errorf("error writing to the file: %s", err) + } + + return nil +} diff --git a/govcd/api_token_test.go b/govcd/api_token_test.go new file mode 100644 index 000000000..7fca4b041 --- /dev/null +++ b/govcd/api_token_test.go @@ -0,0 +1,174 @@ +//go:build api || functional || ALL + +/* + * Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "net/url" + "os" + + "github.com/kr/pretty" + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +// TestVCDClient_GetBearerTokenFromApiToken tests the token refresh operation +// To make it work, we need the following, or the test is skipped: +// - VCD version 10.3.1 or greater +// - environment variable TEST_VCD_API_TOKEN filled with a valid API token for that VCD +// - If the API token was not set for the Organization defined in vcd.config.VCD.Org, the variable +// TEST_VCD_ORG should be filled with the name of the Org for which the API token was set. +func (vcd *TestVCD) TestVCDClient_GetBearerTokenFromApiToken(check *C) { + apiToken := os.Getenv("TEST_VCD_API_TOKEN") + + orgName := os.Getenv("TEST_VCD_ORG") + if orgName == "" { + orgName = vcd.config.VCD.Org + } + if orgName == "" { + check.Skip("orgName not set") + } + if apiToken == "" { + check.Skip(fmt.Sprintf("API token not set. Use TEST_VCD_API_TOKEN to indicate an API token for Org '%s'", orgName)) + } + + isApiTokenEnabled, err := vcd.client.Client.VersionEqualOrGreater("10.3.1", 3) + check.Assert(err, IsNil) + if !isApiTokenEnabled { + check.Skip("This test requires VCD 10.3.1 or greater") + } + + tokenInfo, err := vcd.client.GetBearerTokenFromApiToken(orgName, apiToken) + check.Assert(err, IsNil) + check.Assert(tokenInfo, NotNil) + check.Assert(tokenInfo.AccessToken, Not(Equals), "") + if testVerbose { + fmt.Printf("%# v\n", pretty.Formatter(tokenInfo)) + } + check.Assert(tokenInfo.ExpiresIn, Not(Equals), 0) + check.Assert(tokenInfo.TokenType, Equals, "Bearer") +} + +func (vcd *TestVCD) Test_ApiTokenCreation(check *C) { + isApiTokenEnabled, err := vcd.client.Client.VersionEqualOrGreater("10.3.1", 3) + check.Assert(err, IsNil) + if !isApiTokenEnabled { + check.Skip("This test requires VCD 10.3.1 or greater") + } + client := vcd.client + + token, err := client.CreateToken(vcd.config.Provider.SysOrg, check.TestName()) + check.Assert(err, IsNil) + check.Assert(token, NotNil) + check.Assert(token.Token.Type, Equals, "REFRESH") + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointTokens + token.Token.ID + AddToCleanupListOpenApi(token.Token.Name, check.TestName(), endpoint) + + tokenInfo, err := token.GetInitialApiToken() + check.Assert(err, IsNil) + check.Assert(tokenInfo.AccessToken, Not(Equals), "") + check.Assert(tokenInfo.TokenType, Equals, "Bearer") + + tokenInfo, err = token.GetInitialApiToken() + check.Assert(err, NotNil) + check.Assert(tokenInfo, IsNil) + + err = token.Delete() + check.Assert(err, IsNil) + + notFound, err := client.GetTokenById(token.Token.ID) + check.Assert(ContainsNotFound(err), Equals, true) + check.Assert(notFound, IsNil) +} + +func (vcd *TestVCD) Test_GetFilteredTokensSysOrg(check *C) { + isApiTokenEnabled, err := vcd.client.Client.VersionEqualOrGreater("10.3.1", 3) + check.Assert(err, IsNil) + if !isApiTokenEnabled { + check.Skip("This test requires VCD 10.3.1 or greater") + } + client := vcd.client + if !client.Client.IsSysAdmin { + check.Skip("This test requires to be run by a SysAdmin") + } + + token, err := client.CreateToken(vcd.config.Provider.SysOrg, check.TestName()) + check.Assert(err, IsNil) + check.Assert(token, NotNil) + check.Assert(token.Token.Type, Equals, "REFRESH") + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointTokens + AddToCleanupListOpenApi(token.Token.Name, check.TestName(), endpoint+token.Token.ID) + + queryParameters := &url.Values{} + queryParameters.Add("filter", fmt.Sprintf("(name==%s;owner.name==%s;(type==PROXY,type==REFRESH))", check.TestName(), "administrator")) + + tokens, err := client.GetAllTokens(*queryParameters) + check.Assert(err, IsNil) + check.Assert(len(tokens), Equals, 1) + check.Assert(tokens[0].Token.Name, Equals, check.TestName()) + check.Assert(tokens[0].Token.Owner.Name, Equals, "administrator") + + newToken, err := client.GetTokenByNameAndUsername(check.TestName(), "administrator") + check.Assert(err, IsNil) + check.Assert(newToken.Token.Name, Equals, check.TestName()) + check.Assert(newToken.Token.Owner.Name, Equals, "administrator") + + err = newToken.Delete() + check.Assert(err, IsNil) + + newToken, err = client.GetTokenByNameAndUsername(check.TestName(), "administrator") + check.Assert(ContainsNotFound(err), Equals, true) + check.Assert(newToken, IsNil) +} + +func (vcd *TestVCD) Test_GetFilteredTokensOrg(check *C) { + isApiTokenEnabled, err := vcd.client.Client.VersionEqualOrGreater("10.3.1", 3) + check.Assert(err, IsNil) + if !isApiTokenEnabled { + check.Skip("This test requires VCD 10.3.1 or greater") + } + + if vcd.config.Tenants == nil || len(vcd.config.Tenants) < 2 { + check.Skip("no tenants found in configuration") + } + + orgName := vcd.config.Tenants[0].SysOrg + userName := vcd.config.Tenants[0].User + password := vcd.config.Tenants[0].Password + + vcdClient1 := NewVCDClient(vcd.client.Client.VCDHREF, true) + err = vcdClient1.Authenticate(userName, password, orgName) + check.Assert(err, IsNil) + + token, err := vcdClient1.CreateToken(vcd.config.Tenants[0].SysOrg, check.TestName()) + check.Assert(err, IsNil) + check.Assert(token, NotNil) + check.Assert(token.Token.Type, Equals, "REFRESH") + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointTokens + AddToCleanupListOpenApi(token.Token.Name, check.TestName(), endpoint+token.Token.ID) + + queryParameters := &url.Values{} + queryParameters.Add("filter", fmt.Sprintf("(name==%s;owner.name==%s;(type==PROXY,type==REFRESH))", check.TestName(), userName)) + + tokens, err := vcdClient1.GetAllTokens(*queryParameters) + check.Assert(err, IsNil) + check.Assert(len(tokens), Equals, 1) + check.Assert(tokens[0].Token.Name, Equals, check.TestName()) + check.Assert(tokens[0].Token.Owner.Name, Equals, userName) + + newToken, err := vcdClient1.GetTokenByNameAndUsername(check.TestName(), userName) + check.Assert(err, IsNil) + check.Assert(newToken.Token.Name, Equals, check.TestName()) + check.Assert(newToken.Token.Owner.Name, Equals, userName) + + err = newToken.Delete() + check.Assert(err, IsNil) + + newToken, err = vcdClient1.GetTokenByNameAndUsername(check.TestName(), userName) + check.Assert(ContainsNotFound(err), Equals, true) + check.Assert(newToken, IsNil) +} diff --git a/govcd/api_token_unit_test.go b/govcd/api_token_unit_test.go new file mode 100644 index 000000000..dbbd87af4 --- /dev/null +++ b/govcd/api_token_unit_test.go @@ -0,0 +1,75 @@ +//go:build unit || ALL + +/* +* Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "reflect" + "testing" +) + +func Test_readFileAndUnmarshalJSON(t *testing.T) { + type args struct { + filename string + object *testEntity + } + tests := []struct { + name string + args args + want *testEntity + wantErr bool + }{ + { + name: "simpleCase", + args: args{ + filename: "test-resources/test.json", + object: &testEntity{}, + }, + want: &testEntity{Name: "test"}, + wantErr: false, + }, + { + name: "emptyFile", + args: args{ + filename: "test-resources/test_empty.json", + object: &testEntity{}, + }, + want: &testEntity{}, + wantErr: true, + }, + { + name: "emptyJSON", + args: args{ + filename: "test-resources/test_emptyJSON.json", + object: &testEntity{}, + }, + want: &testEntity{}, + wantErr: false, + }, + { + name: "nonexistentFile", + args: args{ + filename: "thisfiledoesntexist.json", + object: &testEntity{}, + }, + want: &testEntity{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := readFileAndUnmarshalJSON(tt.args.filename, tt.args.object) + if (err != nil) != tt.wantErr { + t.Errorf("readFileAndUnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if !reflect.DeepEqual(tt.args.object, tt.want) { + t.Errorf("readFileAndUnmarshalJSON() = %v, want %v", tt.args.object, tt.want) + } + }) + } +} diff --git a/govcd/api_vcd.go b/govcd/api_vcd.go index 325add38f..ef1e072ec 100644 --- a/govcd/api_vcd.go +++ b/govcd/api_vcd.go @@ -5,17 +5,32 @@ package govcd import ( + "bytes" "crypto/tls" "fmt" + "io" + "math" "net/http" "net/url" + "os" "strings" + "sync/atomic" "time" + semver "github.com/hashicorp/go-version" + "github.com/vmware/go-vcloud-director/v2/types/v56" "github.com/vmware/go-vcloud-director/v2/util" ) +func init() { + // Initialize global API request counter that is used by VcloudRequestIdBuilderFunc + counter := apiRequestCount(0) + requestCounter = &counter +} + +var minVcdApiVersion = "37.0" // supported by 10.4+ + // VCDClientOption defines signature for customizing VCDClient using // functional options pattern. type VCDClientOption func(*VCDClient) error @@ -26,15 +41,15 @@ type VCDClient struct { QueryHREF url.URL // HREF for the query API } -func (vcdCli *VCDClient) vcdloginurl() error { - if err := vcdCli.Client.validateAPIVersion(); err != nil { +func (vcdClient *VCDClient) vcdloginurl() error { + if err := vcdClient.Client.validateAPIVersion(); err != nil { return fmt.Errorf("could not find valid version for login: %s", err) } // find login address matching the API version var neededVersion VersionInfo - for _, versionInfo := range vcdCli.Client.supportedVersions.VersionInfos { - if versionInfo.Version == vcdCli.Client.APIVersion { + for _, versionInfo := range vcdClient.Client.supportedVersions.VersionInfos { + if versionInfo.Version == vcdClient.Client.APIVersion { neededVersion = versionInfo break } @@ -42,18 +57,31 @@ func (vcdCli *VCDClient) vcdloginurl() error { loginUrl, err := url.Parse(neededVersion.LoginUrl) if err != nil { - return fmt.Errorf("couldn't find a LoginUrl for version %s", vcdCli.Client.APIVersion) + return fmt.Errorf("couldn't find a LoginUrl for version %s", vcdClient.Client.APIVersion) } - vcdCli.sessionHREF = *loginUrl + vcdClient.sessionHREF = *loginUrl return nil } // vcdCloudApiAuthorize performs the authorization to VCD using open API -func (vcdCli *VCDClient) vcdCloudApiAuthorize(user, pass, org string) (*http.Response, error) { +func (vcdClient *VCDClient) vcdCloudApiAuthorize(user, pass, org string) (*http.Response, error) { + var missingItems []string + if user == "" { + missingItems = append(missingItems, "user") + } + if pass == "" { + missingItems = append(missingItems, "password") + } + if org == "" { + missingItems = append(missingItems, "org") + } + if len(missingItems) > 0 { + return nil, fmt.Errorf("authorization is not possible because of these missing items: %v", missingItems) + } util.Logger.Println("[TRACE] Connecting to VCD using cloudapi") // This call can only be used by tenants - rawUrl := vcdCli.sessionHREF.Scheme + "://" + vcdCli.sessionHREF.Host + "/cloudapi/1.0.0/sessions" + rawUrl := vcdClient.sessionHREF.Scheme + "://" + vcdClient.sessionHREF.Host + "/cloudapi/1.0.0/sessions" // If we are connecting as provider, we need to qualify the request. if strings.EqualFold(org, "system") { @@ -64,72 +92,68 @@ func (vcdCli *VCDClient) vcdCloudApiAuthorize(user, pass, org string) (*http.Res if err != nil { return nil, fmt.Errorf("error parsing URL %s", rawUrl) } - vcdCli.sessionHREF = *loginUrl - req := vcdCli.Client.NewRequest(map[string]string{}, http.MethodPost, *loginUrl, nil) + vcdClient.sessionHREF = *loginUrl + req := vcdClient.Client.NewRequest(map[string]string{}, http.MethodPost, *loginUrl, nil) // Set Basic Authentication Header req.SetBasicAuth(user+"@"+org, pass) // Add the Accept header. The version must be at least 33.0 for cloudapi to work - req.Header.Add("Accept", "application/*;version=33.0") - return vcdCli.Client.Http.Do(req) -} - -// vcdAuthorize authorizes the client and returns a http response -func (vcdCli *VCDClient) vcdAuthorize(user, pass, org string) (*http.Response, error) { - var missingItems []string - if user == "" { - missingItems = append(missingItems, "user") - } - if pass == "" { - missingItems = append(missingItems, "password") - } - if org == "" { - missingItems = append(missingItems, "org") - } - if len(missingItems) > 0 { - return nil, fmt.Errorf("authorization is not possible because of these missing items: %v", missingItems) + req.Header.Add("Accept", "application/*;version="+vcdClient.Client.APIVersion) + resp, err := vcdClient.Client.Http.Do(req) + if err != nil { + return nil, err } - // No point in checking for errors here - req := vcdCli.Client.NewRequest(map[string]string{}, http.MethodPost, vcdCli.sessionHREF, nil) - // Set Basic Authentication Header - req.SetBasicAuth(user+"@"+org, pass) - // Add the Accept header for vCA - req.Header.Add("Accept", "application/*+xml;version="+vcdCli.Client.APIVersion) - resp, err := vcdCli.Client.Http.Do(req) - - // If the VCD has disabled the call to /api/sessions, the attempt will fail with error 401 (unauthorized) - // https://docs.vmware.com/en/VMware-Cloud-Director/10.0/com.vmware.vcloud.install.doc/GUID-84390C8F-E8C5-4137-A1A5-53EC27FE0024.html - // TODO: convert this method to main once we drop support for 9.7 - if resp.StatusCode == 401 { - resp, err = vcdCli.vcdCloudApiAuthorize(user, pass, org) + + defer func(Body io.ReadCloser) { + err := Body.Close() if err != nil { - return nil, err + util.Logger.Printf("error closing response Body [vcdCloudApiAuthorize]: %s", err) } - resp, err = checkRespWithErrType(types.BodyTypeJSON, resp, err, &types.Error{}) - } else { - resp, err = checkResp(resp, err) - } + }(resp.Body) + // read from resp.Body io.Reader for debug output if it has body + bodyBytes, err := rewrapRespBodyNoopCloser(resp) if err != nil { - return nil, err + return resp, err } - defer resp.Body.Close() + util.ProcessResponseOutput(util.FuncNameCallStack(), resp, string(bodyBytes)) + debugShowResponse(resp, bodyBytes) + + // Catch HTTP 401 (Status Unauthorized) to return an error as otherwise this library would return + // odd errors while doing lookup of resources and confuse user. + if resp.StatusCode == http.StatusUnauthorized { + return nil, fmt.Errorf("received response HTTP %d (Unauthorized). Please check if your credentials are valid", + resp.StatusCode) + } + // Store the authorization header - vcdCli.Client.VCDToken = resp.Header.Get(BearerTokenHeader) - vcdCli.Client.VCDAuthHeader = BearerTokenHeader - vcdCli.Client.IsSysAdmin = strings.EqualFold(org, "system") + vcdClient.Client.VCDToken = resp.Header.Get(BearerTokenHeader) + vcdClient.Client.VCDAuthHeader = BearerTokenHeader + vcdClient.Client.IsSysAdmin = strings.EqualFold(org, "system") // Get query href - vcdCli.QueryHREF = vcdCli.Client.VCDHREF - vcdCli.QueryHREF.Path += "/query" + vcdClient.QueryHREF = vcdClient.Client.VCDHREF + vcdClient.QueryHREF.Path += "/query" return resp, nil } -// NewVCDClient initializes VMware vCloud Director client with reasonable defaults. +// 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 + } + // Setting defaults + // #nosec G402 -- InsecureSkipVerify: insecure - This allows connecting to VCDs with self-signed certificates vcdClient := &VCDClient{ Client: Client{ - APIVersion: "32.0", // supported by 9.7+ + APIVersion: minVcdApiVersion, // 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", @@ -148,30 +172,36 @@ func NewVCDClient(vcdEndpoint url.URL, insecure bool, options ...VCDClientOption }, } + // Attach function that will generate unique 'X-VMWARE-VCLOUD-CLIENT-REQUEST-ID' headers for + // each request unless it is specifically disabled + if os.Getenv("GOVCD_SKIP_LOG_TRACING") == "" { + vcdClient.Client.RequestIdFunc = VcloudRequestIdBuilderFunc + } + // Override defaults with functional options for _, option := range options { err := option(vcdClient) 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: %s", err)) + panic(fmt.Sprintf("unable to initialize VCD client: %s", err)) } } return vcdClient } -// Authenticate is a helper function that performs a login in vCloud Director. -func (vcdCli *VCDClient) Authenticate(username, password, org string) error { - _, err := vcdCli.GetAuthResponse(username, password, org) +// 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) return err } // GetAuthResponse performs authentication and returns the full HTTP response // The purpose of this function is to preserve information that is useful // for token-based authentication -func (vcdCli *VCDClient) GetAuthResponse(username, password, org string) (*http.Response, error) { +func (vcdClient *VCDClient) GetAuthResponse(username, password, org string) (*http.Response, error) { // LoginUrl - err := vcdCli.vcdloginurl() + err := vcdClient.vcdloginurl() if err != nil { return nil, fmt.Errorf("error finding LoginUrl: %s", err) } @@ -180,70 +210,87 @@ func (vcdCli *VCDClient) GetAuthResponse(username, password, org string) (*http. // for each of the below functions is to set authorization token vcdCli.Client.VCDToken. var resp *http.Response switch { - case vcdCli.Client.UseSamlAdfs: - err = vcdCli.authorizeSamlAdfs(username, password, org, vcdCli.Client.CustomAdfsRptId) + case vcdClient.Client.UseSamlAdfs: + err = vcdClient.authorizeSamlAdfs(username, password, org, vcdClient.Client.CustomAdfsRptId) if err != nil { return nil, fmt.Errorf("error authorizing SAML: %s", err) } default: // Authorize - resp, err = vcdCli.vcdAuthorize(username, password, org) + resp, err = vcdClient.vcdCloudApiAuthorize(username, password, org) if err != nil { return nil, fmt.Errorf("error authorizing: %s", err) } } + vcdClient.LogSessionInfo() return resp, nil } // SetToken will set the authorization token in the client, without using other credentials -// Up to version 29, token authorization uses the the header key x-vcloud-authorization +// Up to version 29, token authorization uses the header key x-vcloud-authorization // In version 30+ it also uses X-Vmware-Vcloud-Access-Token:TOKEN coupled with // X-Vmware-Vcloud-Token-Type:"bearer" -func (vcdCli *VCDClient) SetToken(org, authHeader, token string) error { - vcdCli.Client.VCDAuthHeader = authHeader - vcdCli.Client.VCDToken = token +func (vcdClient *VCDClient) SetToken(org, authHeader, token string) error { + if authHeader == ApiTokenHeader { + util.Logger.Printf("[DEBUG] Attempt authentication using API token") + apiToken, err := vcdClient.GetBearerTokenFromApiToken(org, token) + if err != nil { + util.Logger.Printf("[DEBUG] Authentication using API token was UNSUCCESSFUL: %s", err) + return err + } + token = apiToken.AccessToken + authHeader = BearerTokenHeader + vcdClient.Client.UsingAccessToken = true + util.Logger.Printf("[DEBUG] Authentication using API token was SUCCESSFUL") + } + if !vcdClient.Client.UsingAccessToken { + vcdClient.Client.UsingBearerToken = true + } + vcdClient.Client.VCDAuthHeader = authHeader + vcdClient.Client.VCDToken = token - err := vcdCli.vcdloginurl() + err := vcdClient.vcdloginurl() if err != nil { return fmt.Errorf("error finding LoginUrl: %s", err) } - vcdCli.Client.IsSysAdmin = strings.EqualFold(org, "system") + vcdClient.Client.IsSysAdmin = strings.EqualFold(org, "system") // Get query href - vcdCli.QueryHREF = vcdCli.Client.VCDHREF - vcdCli.QueryHREF.Path += "/query" + vcdClient.QueryHREF = vcdClient.Client.VCDHREF + vcdClient.QueryHREF.Path += "/query" // The client is now ready to connect using the token, but has not communicated with the vCD yet. // To make sure that it is working, we run a request for the org list. // This list should work always: when run as system administrator, it retrieves all organizations. // When run as org user, it only returns the organization the user is authorized to. // In both cases, we discard the list, as we only use it to certify that the token works. - orgListHREF := vcdCli.Client.VCDHREF + orgListHREF := vcdClient.Client.VCDHREF orgListHREF.Path += "/org" orgList := new(types.OrgList) - _, err = vcdCli.Client.ExecuteRequest(orgListHREF.String(), http.MethodGet, + _, err = vcdClient.Client.ExecuteRequest(orgListHREF.String(), http.MethodGet, "", "error connecting to vCD using token: %s", nil, orgList) if err != nil { return err } + vcdClient.LogSessionInfo() return nil } -// Disconnect performs a disconnection from the vCloud Director API endpoint. -func (vcdCli *VCDClient) Disconnect() error { - if vcdCli.Client.VCDToken == "" && vcdCli.Client.VCDAuthHeader == "" { +// Disconnect performs a disconnection from the VMware Cloud Director API endpoint. +func (vcdClient *VCDClient) Disconnect() error { + if vcdClient.Client.VCDToken == "" && vcdClient.Client.VCDAuthHeader == "" { return fmt.Errorf("cannot disconnect, client is not authenticated") } - req := vcdCli.Client.NewRequest(map[string]string{}, http.MethodDelete, vcdCli.sessionHREF, nil) + req := vcdClient.Client.NewRequest(map[string]string{}, http.MethodDelete, vcdClient.sessionHREF, nil) // Add the Accept header for vCA - req.Header.Add("Accept", "application/xml;version="+vcdCli.Client.APIVersion) + req.Header.Add("Accept", "application/xml;version="+vcdClient.Client.APIVersion) // Set Authorization Header - req.Header.Add(vcdCli.Client.VCDAuthHeader, vcdCli.Client.VCDToken) - if _, err := checkResp(vcdCli.Client.Http.Do(req)); err != nil { - return fmt.Errorf("error processing session delete for vCloud Director: %s", err) + req.Header.Add(vcdClient.Client.VCDAuthHeader, vcdClient.Client.VCDToken) + if _, err := checkResp(vcdClient.Client.Http.Do(req)); err != nil { + return fmt.Errorf("error processing session delete for VMware Cloud Director: %s", err) } return nil } @@ -289,10 +336,109 @@ func WithSamlAdfs(useSaml bool, customAdfsRptId string) VCDClientOption { } // WithHttpUserAgent allows to specify HTTP user-agent which can be useful for statistics tracking. -// By default User-Agent is set to "go-vcloud-director". It can be unset by supplying empty value. +// By default User-Agent is set to "go-vcloud-director". It can be unset by supplying an empty value. func WithHttpUserAgent(userAgent string) VCDClientOption { return func(vcdClient *VCDClient) error { vcdClient.Client.UserAgent = userAgent return nil } } + +// WithHttpHeader allows to specify custom HTTP header values. +// Typical usage of this function is to inject a tenant context into the client. +// +// WARNING: Using this function in an environment with concurrent operations may result in negative side effects, +// such as operations as system administrator and as tenant using the same client. +// This setting is justified when we want to start a session where the additional header is always needed. +// For cases where we need system administrator and tenant operations in the same environment we can either +// a) use two separate clients +// or b) use the `additionalHeader` parameter in *newRequest* functions +func WithHttpHeader(options map[string]string) VCDClientOption { + return func(vcdClient *VCDClient) error { + vcdClient.Client.customHeader = make(http.Header) + for k, v := range options { + vcdClient.Client.customHeader.Add(k, v) + } + return nil + } +} + +// WithIgnoredMetadata allows specifying metadata entries to be ignored when using metadata_v2 methods. +// It can be unset by supplying an empty value. +// See the documentation of the IgnoredMetadata structure for more information. +func WithIgnoredMetadata(ignoredMetadata []IgnoredMetadata) VCDClientOption { + return func(vcdClient *VCDClient) error { + vcdClient.Client.IgnoredMetadata = ignoredMetadata + return nil + } +} + +// WithVcloudRequestIdFunc enables sending 'X-VMWARE-VCLOUD-CLIENT-REQUEST-ID' header by supplying a +// function that will return unique value for each time it is executed. The code of this SDK will +// make sure that the header is populated every time. +// +// The X-VMWARE-VCLOUD-CLIENT-REQUEST-ID header must contain only alpha-numeric characters or +// dashes. The header must contain at least one alpha-numeric character, and VMware Cloud Director +// shortens it if it's longer than 128 characters long. The X-VMWARE-VCLOUD-REQUEST-ID response +// header is formed from the first 128 characters of X-VMWARE-VCLOUD-CLIENT-REQUEST-ID, followed by +// a dash and a random UUID that the server generates. If the X-VMWARE-VCLOUD-CLIENT-REQUEST-ID +// header is invalid, null, or empty, the X-VMWARE-VCLOUD-REQUEST-ID is a random UUID. VMware Cloud +// Director adds this value to every VMware Cloud Director, vCenter Server, and ESXi log message +// related to processing the request, and provides a way to correlate the processing of a request +// across all participating systems. If a request does not supply a +// X-VMWARE-VCLOUD-CLIENT-REQUEST-ID header, the response contains an X-VMWARE-VCLOUD-REQUEST-ID +// header with a generated value that cannot be used for log correlation. +// +// There is a builtin function VcloudRequestIdBuilderFunc that can be used to add sequence number +// and time-id for each request +func WithVcloudRequestIdFunc(vcloudRequestItBuilder func() string) VCDClientOption { + return func(vcdClient *VCDClient) error { + vcdClient.Client.RequestIdFunc = vcloudRequestItBuilder + return nil + } +} + +// VcloudRequestIdBuilderFunc can be used in 'WithVcloudRequestIdFunc' +// It would populate 'X-Vmware-Vcloud-Client-Request-Id' formatted so: +// {sequence-number}-{date-time-hyphen-separated} +// (e.g. 1-2024-04-13-01-58-25-733-) +func VcloudRequestIdBuilderFunc() string { + incrementCounter := requestCounter.inc() + + timeNow := time.Now() + // milliseconds include a "." by default that is not allowed in header so it is replaced with hyphen + // Sample time is "2024-04-13-01-58-25-733" + timeString := strings.ReplaceAll(timeNow.Format("2006-01-02-15-04-05.000"), ".", "-") + return fmt.Sprintf("%d-%s-", incrementCounter, timeString) +} + +// requestCounter is used by VcloudRequestIdBuilderFunc +// it is being initalized to 0 in `init` +var requestCounter *apiRequestCount + +// apiRequestCount is a type used to count number of API calls performed in the code when +// VcloudRequestIdBuilderFunc is used +type apiRequestCount uint64 + +// inc increments counter by one and returns new value +func (c *apiRequestCount) inc() uint64 { + // prevent overflowing counter + if *c == math.MaxUint64 { + *c = 0 + } + return atomic.AddUint64((*uint64)(c), 1) +} + +func rewrapRespBodyNoopCloser(resp *http.Response) ([]byte, error) { + var bodyBytes []byte + if resp.Body != nil { + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return bodyBytes, fmt.Errorf("could not read response body: %s", err) + } + // Restore the io.ReadCloser to its original state with no-op closer + resp.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + } + + return bodyBytes, nil +} diff --git a/govcd/api_vcd_test.go b/govcd/api_vcd_test.go index 424af06f4..6d0aa699f 100644 --- a/govcd/api_vcd_test.go +++ b/govcd/api_vcd_test.go @@ -1,7 +1,7 @@ -// +build api openapi functional catalog vapp gateway network org query extnetwork task vm vdc system disk lb lbAppRule lbAppProfile lbServerPool lbServiceMonitor lbVirtualServer user search nsxv nsxt auth affinity ALL +//go:build api || openapi || functional || catalog || vapp || gateway || network || org || query || extnetwork || task || vm || vdc || system || disk || lb || lbAppRule || lbAppProfile || lbServerPool || lbServiceMonitor || lbVirtualServer || user || search || nsxv || nsxt || auth || affinity || role || alb || certificate || vdcGroup || metadata || providervdc || rde || vsphere || uiPlugin || cse || slz || ALL /* - * Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + * Copyright 2022 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. */ package govcd @@ -11,7 +11,6 @@ import ( "encoding/json" "flag" "fmt" - "io/ioutil" "net/http" "net/url" "os" @@ -21,10 +20,12 @@ import ( "strings" "sync" "testing" + "time" - . "gopkg.in/check.v1" "gopkg.in/yaml.v2" + . "gopkg.in/check.v1" + "github.com/vmware/go-vcloud-director/v2/types/v56" "github.com/vmware/go-vcloud-director/v2/util" ) @@ -41,7 +42,9 @@ func init() { setBoolFlag(&ignoreCleanupFile, "vcd-ignore-cleanup-file", "GOVCD_IGNORE_CLEANUP_FILE", "Does not process previous cleanup file") setBoolFlag(&debugShowRequestEnabled, "vcd-show-request", "GOVCD_SHOW_REQ", "Shows API request") setBoolFlag(&debugShowResponseEnabled, "vcd-show-response", "GOVCD_SHOW_RESP", "Shows API response") - + setBoolFlag(&connectAsOrgUser, "vcd-as-org-user", "VCD_TEST_ORG_USER", "Connect as Org user") + setBoolFlag(&connectAsOrgUser, "vcd-test-org-user", "VCD_TEST_ORG_USER", "Connect as Org user") + flag.IntVar(&connectTenantNum, "vcd-connect-tenant", connectTenantNum, "change index of tenant to use (0=first)") } const ( @@ -76,32 +79,44 @@ const ( TestVdcFindDiskByHREF = "TestVdcFindDiskByHREF" TestFindDiskByHREF = "TestFindDiskByHREF" TestDisk = "TestDisk" - TestVMAttachOrDetachDisk = "TestVMAttachOrDetachDisk" - TestVMAttachDisk = "TestVMAttachDisk" - TestVMDetachDisk = "TestVMDetachDisk" - TestCreateExternalNetwork = "TestCreateExternalNetwork" - TestDeleteExternalNetwork = "TestDeleteExternalNetwork" - TestLbServiceMonitor = "TestLbServiceMonitor" - TestLbServerPool = "TestLbServerPool" - TestLbAppProfile = "TestLbAppProfile" - TestLbAppRule = "TestLbAppRule" - TestLbVirtualServer = "TestLbVirtualServer" - TestLb = "TestLb" - TestNsxvSnatRule = "TestNsxvSnatRule" - TestNsxvDnatRule = "TestNsxvDnatRule" + // #nosec G101 -- Not a credential + TestVMAttachOrDetachDisk = "TestVMAttachOrDetachDisk" + TestVMAttachDisk = "TestVMAttachDisk" + TestVMDetachDisk = "TestVMDetachDisk" + TestCreateExternalNetwork = "TestCreateExternalNetwork" + TestDeleteExternalNetwork = "TestDeleteExternalNetwork" + TestLbServiceMonitor = "TestLbServiceMonitor" + TestLbServerPool = "TestLbServerPool" + TestLbAppProfile = "TestLbAppProfile" + TestLbAppRule = "TestLbAppRule" + TestLbVirtualServer = "TestLbVirtualServer" + TestLb = "TestLb" + TestNsxvSnatRule = "TestNsxvSnatRule" + TestNsxvDnatRule = "TestNsxvDnatRule" ) const ( TestRequiresSysAdminPrivileges = "Test %s requires system administrator privileges" ) +type Tenant struct { + User string `yaml:"user,omitempty"` + Password string `yaml:"password,omitempty"` + Token string `yaml:"token,omitempty"` + ApiToken string `yaml:"api_token,omitempty"` + SysOrg string `yaml:"sysOrg,omitempty"` +} + // Struct to get info from a config yaml file that the user // specifies type TestConfig struct { Provider struct { - User string `yaml:"user"` - Password string `yaml:"password"` - Token string `yaml:"token"` + User string `yaml:"user"` + Password string `yaml:"password"` + Token string `yaml:"token"` + ApiToken string `yaml:"api_token"` + VcdVersion string `yaml:"vcdVersion,omitempty"` + ApiVersion string `yaml:"apiVersion,omitempty"` // UseSamlAdfs specifies if SAML auth is used for authenticating vCD instead of local login. // The above `User` and `Password` will be used to authenticate against ADFS IdP when true. @@ -124,7 +139,8 @@ type TestConfig struct { MaxRetryTimeout int `yaml:"maxRetryTimeout,omitempty"` HttpTimeout int64 `yaml:"httpTimeout,omitempty"` } - VCD struct { + Tenants []Tenant `yaml:"tenants,omitempty"` + VCD struct { Org string `yaml:"org"` Vdc string `yaml:"vdc"` ProviderVdc struct { @@ -133,17 +149,23 @@ type TestConfig struct { NetworkPool string `yaml:"network_pool"` } `yaml:"provider_vdc"` NsxtProviderVdc struct { - Name string `yaml:"name"` - StorageProfile string `yaml:"storage_profile"` - NetworkPool string `yaml:"network_pool"` + Name string `yaml:"name"` + StorageProfile string `yaml:"storage_profile"` + StorageProfile2 string `yaml:"storage_profile_2"` + NetworkPool string `yaml:"network_pool"` + PlacementPolicyVmGroup string `yaml:"placementPolicyVmGroup,omitempty"` } `yaml:"nsxt_provider_vdc"` Catalog struct { - Name string `yaml:"name,omitempty"` - Description string `yaml:"description,omitempty"` - CatalogItem string `yaml:"catalogItem,omitempty"` - CatalogItemDescription string `yaml:"catalogItemDescription,omitempty"` - CatalogItemWithMultiVms string `yaml:"catalogItemWithMultiVms,omitempty"` - VmNameInMultiVmItem string `yaml:"vmNameInMultiVmItem,omitempty"` + Name string `yaml:"name,omitempty"` + NsxtBackedCatalogName string `yaml:"nsxtBackedCatalogName,omitempty"` + Description string `yaml:"description,omitempty"` + CatalogItem string `yaml:"catalogItem,omitempty"` + CatalogItemWithEfiSupport string `yaml:"catalogItemWithEfiSupport,omitempty"` + NsxtCatalogItem string `yaml:"nsxtCatalogItem,omitempty"` + NsxtCatalogAddonDse string `yaml:"nsxtCatalogAddonDse,omitempty"` + CatalogItemDescription string `yaml:"catalogItemDescription,omitempty"` + CatalogItemWithMultiVms string `yaml:"catalogItemWithMultiVms,omitempty"` + VmNameInMultiVmItem string `yaml:"vmNameInMultiVmItem,omitempty"` } `yaml:"catalog"` Network struct { Net1 string `yaml:"network1"` @@ -162,20 +184,43 @@ type TestConfig struct { ExternalNetworkPortGroup string `yaml:"externalNetworkPortGroup,omitempty"` ExternalNetworkPortGroupType string `yaml:"externalNetworkPortGroupType,omitempty"` VimServer string `yaml:"vimServer,omitempty"` - Disk struct { - Size int64 `yaml:"size,omitempty"` - SizeForUpdate int64 `yaml:"sizeForUpdate,omitempty"` - } + LdapServer string `yaml:"ldapServer,omitempty"` + OidcServer struct { + Url string `yaml:"url,omitempty"` + WellKnownEndpoint string `yaml:"wellKnownEndpoint,omitempty"` + } `yaml:"oidcServer,omitempty"` Nsxt struct { - Manager string `yaml:"manager"` - Tier0router string `yaml:"tier0router"` - Tier0routerVrf string `yaml:"tier0routerVrf"` - Vdc string `yaml:"vdc"` - ExternalNetwork string `yaml:"externalNetwork"` - EdgeGateway string `yaml:"edgeGateway"` - NsxtImportSegment string `yaml:"nsxtImportSegment"` + 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"` + EdgeGateway string `yaml:"edgeGateway"` + NsxtImportSegment string `yaml:"nsxtImportSegment"` + NsxtImportSegment2 string `yaml:"nsxtImportSegment2"` + VdcGroup string `yaml:"vdcGroup"` + VdcGroupEdgeGateway string `yaml:"vdcGroupEdgeGateway"` + NsxtEdgeCluster string `yaml:"nsxtEdgeCluster"` + RoutedNetwork string `yaml:"routedNetwork"` + IsolatedNetwork string `yaml:"isolatedNetwork"` + NsxtAlbControllerUrl string `yaml:"nsxtAlbControllerUrl"` + NsxtAlbControllerUser string `yaml:"nsxtAlbControllerUser"` + NsxtAlbControllerPassword string `yaml:"nsxtAlbControllerPassword"` + NsxtAlbImportableCloud string `yaml:"nsxtAlbImportableCloud"` + NsxtAlbServiceEngineGroup string `yaml:"nsxtAlbServiceEngineGroup"` + IpDiscoveryProfile string `yaml:"ipDiscoveryProfile"` + MacDiscoveryProfile string `yaml:"macDiscoveryProfile"` + SpoofGuardProfile string `yaml:"spoofGuardProfile"` + QosProfile string `yaml:"qosProfile"` + SegmentSecurityProfile string `yaml:"segmentSecurityProfile"` } `yaml:"nsxt"` } `yaml:"vcd"` + Vsphere struct { + ResourcePoolForVcd1 string `yaml:"resourcePoolForVcd1,omitempty"` + ResourcePoolForVcd2 string `yaml:"resourcePoolForVcd2,omitempty"` + } `yaml:"vsphere,omitempty"` Logging struct { Enabled bool `yaml:"enabled,omitempty"` LogFileName string `yaml:"logFileName,omitempty"` @@ -191,12 +236,40 @@ type TestConfig struct { OvaMultiVmPath string `yaml:"ovaMultiVmPath,omitempty"` OvaWithoutSizePath string `yaml:"ovaWithoutSizePath,omitempty"` OvfPath string `yaml:"ovfPath,omitempty"` + OvfUrl string `yaml:"ovfUrl,omitempty"` } `yaml:"ova"` Media struct { - MediaPath string `yaml:"mediaPath,omitempty"` - Media string `yaml:"mediaName,omitempty"` - PhotonOsOvaPath string `yaml:"photonOsOvaPath,omitempty"` + MediaPath string `yaml:"mediaPath,omitempty"` + Media string `yaml:"mediaName,omitempty"` + NsxtMedia string `yaml:"nsxtBackedMediaName,omitempty"` + PhotonOsOvaPath string `yaml:"photonOsOvaPath,omitempty"` + MediaUdfTypePath string `yaml:"mediaUdfTypePath,omitempty"` + UiPluginPath string `yaml:"uiPluginPath,omitempty"` } `yaml:"media"` + Cse struct { + Version string `yaml:"version,omitempty"` + SolutionsOrg string `yaml:"solutionsOrg,omitempty"` + TenantOrg string `yaml:"tenantOrg,omitempty"` + TenantVdc string `yaml:"tenantVdc,omitempty"` + RoutedNetwork string `yaml:"routedNetwork,omitempty"` + EdgeGateway string `yaml:"edgeGateway,omitempty"` + StorageProfile string `yaml:"storageProfile,omitempty"` + OvaCatalog string `yaml:"ovaCatalog,omitempty"` + OvaName string `yaml:"ovaName,omitempty"` + } `yaml:"cse,omitempty"` + SolutionAddOn struct { + Org string `yaml:"org"` + Vdc string `yaml:"vdc"` + RoutedNetwork string `yaml:"routedNetwork"` + ComputePolicy string `yaml:"computePolicy"` + StoragePolicy string `yaml:"storagePolicy"` + Catalog string `yaml:"catalog"` + AddonImageDse string `yaml:"addonImageDse"` + // DseSolutions contains a nested map of maps. This is done so that the structure is dynamic + // enough to add new entries, yet maintain the flexibility to have different fields for each + // of those entities + DseSolutions map[string]map[string]string `yaml:"dseSolutions,omitempty"` + } `yaml:"solutionAddOn,omitempty"` } // Test struct for vcloud-director. @@ -256,6 +329,10 @@ var enableDebug bool // ignoreCleanupFile prevents processing a previous cleanup file var ignoreCleanupFile bool +// connectAsOrgUser connects as Org user instead of System administrator +var connectAsOrgUser bool +var connectTenantNum int + // Makes the name for the cleanup entities persistent file // Using a name for each vCD allows us to run tests with different servers // and persist the cleanup list for all. @@ -295,7 +372,7 @@ func readCleanupList() ([]CleanupEntity, error) { if os.IsNotExist(err) { return nil, err } - listText, err := ioutil.ReadFile(persistentCleanupListFile) + listText, err := os.ReadFile(filepath.Clean(persistentCleanupListFile)) if err != nil { return nil, err } @@ -322,7 +399,7 @@ func writeCleanupList(cleanupList []CleanupEntity) error { if err != nil { return err } - file, err := os.Create(persistentCleanupListFile) + file, err := os.Create(filepath.Clean(persistentCleanupListFile)) if err != nil { return err } @@ -392,7 +469,6 @@ func AddToCleanupListOpenApi(name, createdBy, openApiEndpoint string) { // PrependToCleanupListOpenApi prepends an OpenAPI entity OpenApi objects `entityType=OpenApiEntity` and // `openApiEndpoint`should be set in format "types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointOrgVdcNetworks + ID" -//lint:ignore U1000 Not yet used func PrependToCleanupListOpenApi(name, createdBy, openApiEndpoint string) { for _, item := range cleanupEntityList { // avoid adding the same item twice @@ -413,7 +489,7 @@ func PrependToCleanupListOpenApi(name, createdBy, openApiEndpoint string) { // yaml file or if it cannot read it. func GetConfigStruct() (TestConfig, error) { config := os.Getenv("GOVCD_CONFIG") - configStruct := TestConfig{} + var configStruct TestConfig if config == "" { // Finds the current directory, through the path of this running test _, currentFilename, _, _ := runtime.Caller(0) @@ -425,7 +501,7 @@ func GetConfigStruct() (TestConfig, error) { if os.IsNotExist(err) { return TestConfig{}, fmt.Errorf("Configuration file %s not found: %s", config, err) } - yamlFile, err := ioutil.ReadFile(config) + yamlFile, err := os.ReadFile(filepath.Clean(config)) if err != nil { return TestConfig{}, fmt.Errorf("could not read config file %s: %s", config, err) } @@ -433,6 +509,20 @@ func GetConfigStruct() (TestConfig, error) { if err != nil { return TestConfig{}, fmt.Errorf("could not unmarshal yaml file: %s", err) } + if connectAsOrgUser { + if len(configStruct.Tenants) == 0 { + return TestConfig{}, fmt.Errorf("org user connection required, but 'tenants[%d]' is empty", connectTenantNum) + } + if connectTenantNum > len(configStruct.Tenants)-1 { + return TestConfig{}, fmt.Errorf("org user connection required, but tenant number %d is higher than the number of tenants ", connectTenantNum) + } + // Change configStruct.Provider, to reuse the global fields, such as URL + configStruct.Provider.User = configStruct.Tenants[connectTenantNum].User + configStruct.Provider.Password = configStruct.Tenants[connectTenantNum].Password + configStruct.Provider.SysOrg = configStruct.Tenants[connectTenantNum].SysOrg + configStruct.Provider.Token = configStruct.Tenants[connectTenantNum].Token + configStruct.Provider.ApiToken = configStruct.Tenants[connectTenantNum].ApiToken + } return configStruct, nil } @@ -476,15 +566,17 @@ func (vcd *TestVCD) SetUpSuite(check *C) { fmt.Println() // Prints only the flags defined in this package flag.CommandLine.VisitAll(func(f *flag.Flag) { - if strings.Contains(f.Name, "vcd-") { + if strings.HasPrefix(f.Name, "vcd-") { fmt.Printf(" -%-40s %s (%v)\n", f.Name, f.Usage, f.Value) } }) fmt.Println() - os.Exit(0) + // This will skip the whole suite. + // Instead, running os.Exit(0) will panic + check.Skip("--- showing help ---") } config, err := GetConfigStruct() - if config == (TestConfig{}) || err != nil { + if config.Provider.Url == "" || err != nil { panic(err) } vcd.config = config @@ -526,12 +618,22 @@ func (vcd *TestVCD) SetUpSuite(check *C) { token = config.Provider.Token } + apiToken := os.Getenv("VCD_API_TOKEN") + if apiToken == "" { + apiToken = config.Provider.ApiToken + } + authenticationMode := "password" - if token != "" { - authenticationMode = "token" - err = vcd.client.SetToken(config.Provider.SysOrg, AuthorizationHeader, token) + if apiToken != "" { + authenticationMode = "API-token" + err = vcd.client.SetToken(config.Provider.SysOrg, ApiTokenHeader, apiToken) } else { - err = vcd.client.Authenticate(config.Provider.User, config.Provider.Password, config.Provider.SysOrg) + if token != "" { + authenticationMode = "token" + err = vcd.client.SetToken(config.Provider.SysOrg, AuthorizationHeader, token) + } else { + err = vcd.client.Authenticate(config.Provider.User, config.Provider.Password, config.Provider.SysOrg) + } } if config.Provider.UseSamlAdfs { authenticationMode = "SAML password" @@ -544,7 +646,7 @@ func (vcd *TestVCD) SetUpSuite(check *C) { if err == nil { versionInfo = fmt.Sprintf("version %s built at %s", version, versionTime) } - fmt.Printf("Running on vCD %s (%s)\nas user %s@%s (using %s)\n", vcd.config.Provider.Url, versionInfo, + fmt.Printf("Running on VCD %s (%s)\nas user %s@%s (using %s)\n", vcd.config.Provider.Url, versionInfo, vcd.config.Provider.User, vcd.config.Provider.SysOrg, authenticationMode) if !vcd.client.Client.IsSysAdmin { vcd.skipAdminTests = true @@ -587,7 +689,7 @@ func (vcd *TestVCD) SetUpSuite(check *C) { // Gets the persistent cleanup list from file, if exists. cleanupList, err := readCleanupList() if len(cleanupList) > 0 && err == nil { - if ignoreCleanupFile { + if !ignoreCleanupFile { // If we found a cleanup file and we want to process it (default) // We proceed to cleanup the leftovers before any other operation fmt.Printf("*** Found cleanup file %s\n", makePersistentCleanupFileName()) @@ -602,7 +704,8 @@ func (vcd *TestVCD) SetUpSuite(check *C) { // creates a new VApp for vapp tests if !skipVappCreation && config.VCD.Network.Net1 != "" && config.VCD.StorageProfile.SP1 != "" && config.VCD.Catalog.Name != "" && config.VCD.Catalog.CatalogItem != "" { - vcd.vapp, err = vcd.createTestVapp(TestSetUpSuite) + // deployVappForTest replaces the old createTestVapp() because it was using bad implemented method vdc.ComposeVApp + vcd.vapp, err = deployVappForTest(vcd, TestSetUpSuite) // If no vApp is created, we skip all vApp tests if err != nil { fmt.Printf("%s\n", err) @@ -626,25 +729,6 @@ func (vcd *TestVCD) infoCleanup(format string, args ...interface{}) { } } -// Gets the two or three components of a "parent" string, as passed to AddToCleanupList -// If the number of split strings is not 2 or 3 it return 3 empty strings -// Example input parent: my-org|my-vdc|my-edge-gw, separator: | -// Output : first: my-org, second: my-vdc, third: my-edge-gw -func splitParent(parent string, separator string) (first, second, third string) { - strList := strings.Split(parent, separator) - if len(strList) < 2 || len(strList) > 3 { - return "", "", "" - } - first = strList[0] - second = strList[1] - - if len(strList) == 3 { - third = strList[2] - } - - return -} - func getOrgVdcByNames(vcd *TestVCD, orgName, vdcName string) (*Org, *Vdc, error) { if orgName == "" || vdcName == "" { return nil, nil, fmt.Errorf("orgName, vdcName cant be empty") @@ -719,15 +803,19 @@ func (vcd *TestVCD) removeLeftoverEntities(entity CleanupEntity) { // openApiEntity can be used to delete any OpenAPI entity due to the API being uniform and allowing the same // low level OpenApiDeleteItem() case "OpenApiEntity": - // entity.OpenApiEndpoint contains "endpoint/{ID}" // (in format types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointOrgVdcNetworks + ID) but // to lookup used API version this ID must not be present therefore below we remove suffix ID. // This is done by splitting whole path by "/" and rebuilding path again without last element in slice (which is // expected to be the ID) + // Sometimes API endpoint path might contain URNs in the middle (e.g. OpenApiEndpointNsxtNatRules). They are + // replaced back to string placeholder %s to match original definitions endpointSlice := strings.Split(entity.OpenApiEndpoint, "/") - endpoint := strings.Join(endpointSlice[:len(endpointSlice)-1], "/") + "/" - apiVersion, _ := vcd.client.Client.checkOpenApiEndpointCompatibility(endpoint) + endpointWithUuid := strings.Join(endpointSlice[:len(endpointSlice)-1], "/") + "/" + // replace any "urns" (e.g. 'urn:vcloud:gateway:64966c36-e805-44e2-980b-c1077ab54956') with '%s' to match API definitions + re := regexp.MustCompile(`urn[^\/]+`) // Regexp matches from 'urn' up to next '/' in the path + endpointRemovedUuids := re.ReplaceAllString(endpointWithUuid, "%s") + apiVersion, _ := vcd.client.Client.checkOpenApiEndpointCompatibility(endpointRemovedUuids) // Build UP complete endpoint address urlRef, err := vcd.client.Client.OpenApiBuildEndpoint(entity.OpenApiEndpoint) @@ -737,7 +825,19 @@ func (vcd *TestVCD) removeLeftoverEntities(entity CleanupEntity) { } // Validate if the resource still exists - err = vcd.client.Client.OpenApiGetItem(apiVersion, urlRef, nil, nil) + err = vcd.client.Client.OpenApiGetItem(apiVersion, urlRef, nil, nil, nil) + + // RDE Framework has a bug in VCD 10.3.0 that causes "not found" errors to return as "400 bad request", + // so we need to amend them + if strings.Contains(entity.OpenApiEndpoint, types.OpenApiEndpointRdeInterfaces) { + err = amendRdeApiError(&vcd.client.Client, err) + } + // UI Plugin has a bug in VCD 10.4.x that causes "not found" errors to return a NullPointerException, + // so we need to amend them + if strings.Contains(entity.OpenApiEndpoint, types.OpenApiEndpointExtensionsUi) { + err = amendUIPluginGetByIdError(entity.Name, err) + } + if ContainsNotFound(err) { vcd.infoCleanup(notFoundMsg, entity.EntityType, entity.Name) return @@ -754,20 +854,99 @@ func (vcd *TestVCD) removeLeftoverEntities(entity CleanupEntity) { } // Attempt to use supplied path in entity.Parent for element deletion - err = vcd.client.Client.OpenApiDeleteItem(apiVersion, urlRef, nil) + err = vcd.client.Client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) + if err != nil { + vcd.infoCleanup(notDeletedMsg, entity.EntityType, entity.Name, err) + return + } + + vcd.infoCleanup(removedMsg, entity.EntityType, entity.Name, entity.CreatedBy) + // OpenApiEntityFirewall has different API structure therefore generic `OpenApiEntity` case does not fit cleanup + case "OpenApiEntityFirewall": + apiVersion, err := vcd.client.Client.checkOpenApiEndpointCompatibility(types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtFirewallRules) + if err != nil { + vcd.infoCleanup(notDeletedMsg, entity.EntityType, entity.Name, err) + return + } + + urlRef, err := vcd.client.Client.OpenApiBuildEndpoint(entity.Name) + if err != nil { + vcd.infoCleanup(notDeletedMsg, entity.EntityType, entity.Name, err) + return + } + + // Attempt to use supplied path in entity.Parent for element deletion + err = vcd.client.Client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) + if err != nil { + vcd.infoCleanup(notDeletedMsg, entity.EntityType, entity.Name, err) + return + } + + vcd.infoCleanup(removedMsg, entity.EntityType, entity.Name, entity.CreatedBy) + case "OpenApiEntityGlobalDefaultSegmentProfileTemplate": + // Check if any default settings are applied + gdSpt, err := vcd.client.GetGlobalDefaultSegmentProfileTemplates() + if err != nil { + vcd.infoCleanup(notDeletedMsg, entity.EntityType, entity.Name, err) + return + } + + if gdSpt.VappNetworkSegmentProfileTemplateRef == nil && gdSpt.VdcNetworkSegmentProfileTemplateRef == nil { + vcd.infoCleanup(notFoundMsg, entity.EntityType, entity.Name) + return + } + + _, err = vcd.client.UpdateGlobalDefaultSegmentProfileTemplates(&types.NsxtGlobalDefaultSegmentProfileTemplate{}) if err != nil { vcd.infoCleanup(notDeletedMsg, entity.EntityType, entity.Name, err) return } vcd.infoCleanup(removedMsg, entity.EntityType, entity.Name, entity.CreatedBy) + // OpenApiEntityAlbSettingsDisable has different API structure therefore generic `OpenApiEntity` case does not fit cleanup + case "OpenApiEntityAlbSettingsDisable": + edge, err := vcd.nsxtVdc.GetNsxtEdgeGatewayByName(entity.Parent) + if err != nil { + vcd.infoCleanup(notDeletedMsg, entity.EntityType, entity.Name, err) + return + } + edgeAlbSettingsConfig, err := edge.GetAlbSettings() + if err != nil { + vcd.infoCleanup(notDeletedMsg, entity.EntityType, entity.Name, err) + return + } + if edgeAlbSettingsConfig.Enabled == false { + vcd.infoCleanup(notFoundMsg, entity.EntityType, entity.Name) + return + } + + err = edge.DisableAlb() + if err != nil { + vcd.infoCleanup(notFoundMsg, entity.EntityType, entity.Name) + return + } + + vcd.infoCleanup(removedMsg, entity.EntityType, entity.Name, entity.CreatedBy) case "vapp": - vapp, err := vcd.vdc.GetVAppByName(entity.Name, true) + vdc := vcd.vdc + var err error + + // Check if parent VDC was specified. If not - use the default NSX-V VDC + if entity.Parent != "" { + vdc, err = vcd.org.GetVDCByName(entity.Parent, true) + if err != nil { + vcd.infoCleanup(notDeletedMsg, entity.EntityType, entity.Name, err) + return + } + } + + vapp, err := vdc.GetVAppByName(entity.Name, true) if err != nil { vcd.infoCleanup(notFoundMsg, entity.EntityType, entity.Name) return } + task, _ := vapp.Undeploy() _ = task.WaitTaskCompletion() // Detach all Org networks during vApp removal because network removal errors if it happens @@ -820,6 +999,29 @@ func (vcd *TestVCD) removeLeftoverEntities(entity CleanupEntity) { } vcd.infoCleanup(removedMsg, entity.EntityType, entity.Name, entity.CreatedBy) return + case "provider_vdc": + pvdc, err := vcd.client.GetProviderVdcExtendedByName(entity.Name) + if err != nil { + vcd.infoCleanup(notFoundMsg, entity.EntityType, entity.Name) + return + } + err = pvdc.Disable() + if err != nil { + vcd.infoCleanup(notDeletedMsg, entity.EntityType, entity.Name, err) + return + } + task, err := pvdc.Delete() + if err != nil { + vcd.infoCleanup(notDeletedMsg, entity.EntityType, entity.Name, err) + return + } + err = task.WaitTaskCompletion() + if err != nil { + vcd.infoCleanup(notDeletedMsg, entity.EntityType, entity.Name, err) + return + } + vcd.infoCleanup(removedMsg, entity.EntityType, entity.Name, entity.CreatedBy) + return case "catalogItem": if entity.Parent == "" { vcd.infoCleanup("removeLeftoverEntries: [ERROR] No Org provided for catalogItem '%s'\n", strings.Split(entity.Parent, "|")[0]) @@ -835,22 +1037,16 @@ func (vcd *TestVCD) removeLeftoverEntities(entity CleanupEntity) { vcd.infoCleanup(notFoundMsg, entity.EntityType, entity.Name) return } - for _, catalogItems := range catalog.Catalog.CatalogItems { - for _, catalogItem := range catalogItems.CatalogItem { - if catalogItem.Name == entity.Name { - catalogItemApi, err := catalog.GetCatalogItemByName(catalogItem.Name, false) - if catalogItemApi == nil || err != nil { - vcd.infoCleanup(notFoundMsg, entity.EntityType, entity.Name) - return - } - err = catalogItemApi.Delete() - vcd.infoCleanup(removedMsg, entity.EntityType, entity.Name, entity.CreatedBy) - if err != nil { - vcd.infoCleanup(notDeletedMsg, entity.EntityType, entity.Name, err) - } - } - } + catalogItem, err := catalog.GetCatalogItemByName(entity.Name, false) + if err != nil { + vcd.infoCleanup(notFoundMsg, entity.EntityType, entity.Name) + return } + err = catalogItem.Delete() + if err != nil { + vcd.infoCleanup(notDeletedMsg, entity.EntityType, entity.Name, err) + } + vcd.infoCleanup(removedMsg, entity.EntityType, entity.Name, entity.CreatedBy) return case "edgegateway": _, vdc, err := vcd.getAdminOrgAndVdcFromCleanupEntity(entity) @@ -960,6 +1156,11 @@ func (vcd *TestVCD) removeLeftoverEntities(entity CleanupEntity) { return } + _, err = adminCatalog.GetMediaByName(entity.Name, true) + if ContainsNotFound(err) { + vcd.infoCleanup(notFoundMsg, entity.EntityType, entity.Name) + return + } err = adminCatalog.RemoveMediaIfExists(entity.Name) if err == nil { vcd.infoCleanup(removedMsg, entity.EntityType, entity.Name, entity.CreatedBy) @@ -1033,8 +1234,40 @@ func (vcd *TestVCD) removeLeftoverEntities(entity CleanupEntity) { vcd.infoCleanup(notDeletedMsg, entity.EntityType, entity.Name, err) } return + case "nsxv_dfw": + if entity.Parent == "" { + vcd.infoCleanup("removeLeftoverEntries: [ERROR] No ORG provided for VDC '%s'\n", entity.Name) + return + } + org, err := vcd.client.GetAdminOrgByName(entity.Parent) + if err != nil { + vcd.infoCleanup(notFoundMsg, "org", entity.Parent) + return + } + vdc, err := org.GetVDCByName(entity.Name, false) + if vdc == nil || err != nil { + vcd.infoCleanup(notFoundMsg, "vdc", entity.Name) + return + } + dfw := NewNsxvDistributedFirewall(vdc.client, vdc.Vdc.ID) + enabled, err := dfw.IsEnabled() + if err != nil { + vcd.infoCleanup("removeLeftoverEntries: [ERROR] checking distributed firewall from VCD '%s': %s", entity.Name, err) + return + } + if !enabled { + vcd.infoCleanup(notFoundMsg, entity.EntityType, entity.Name) + return + } + err = dfw.Disable() + if err == nil { + vcd.infoCleanup(removedMsg, entity.EntityType, entity.Name, entity.CreatedBy) + } else { + vcd.infoCleanup("removeLeftoverEntries: [ERROR] removing distributed firewall from VCD '%s': %s", entity.Name, err) + return + } case "standaloneVm": - vm, err := vcd.vdc.QueryVmById(entity.Name) // The VM ID must be passed as Name + vm, err := vcd.org.QueryVmById(entity.Name) // The VM ID must be passed as Name if IsNotFound(err) { vcd.infoCleanup(notFoundMsg, entity.EntityType, entity.Name) return @@ -1054,16 +1287,14 @@ func (vcd *TestVCD) removeLeftoverEntities(entity CleanupEntity) { case "vm": vapp, err := vcd.vdc.GetVAppByName(entity.Parent, true) if err != nil { - vcd.infoCleanup("removeLeftoverEntries: [ERROR] Deleting VM '%s' in vApp '%s'. Could not find vApp: %s\n", - entity.Name, entity.Parent, err) + vcd.infoCleanup(notFoundMsg, entity.EntityType, entity.Name) return } vm, err := vapp.GetVMByName(entity.Name, false) if err != nil { - vcd.infoCleanup("removeLeftoverEntries: [ERROR] Could not find VM '%s' in vApp '%s': %s\n", - entity.Name, vapp.VApp.Name, err) + vcd.infoCleanup(notFoundMsg, entity.EntityType, entity.Name) return } @@ -1428,26 +1659,30 @@ func (vcd *TestVCD) removeLeftoverEntities(entity CleanupEntity) { entity.Parent, err) return } - err = org.LdapDisable() + ldapConfig, err := org.GetLdapConfiguration() if err != nil { - vcd.infoCleanup("removeLeftoverEntries: [ERROR] Could not clear LDAP settings for Org '%s': %s", + vcd.infoCleanup("removeLeftoverEntries: [ERROR] Couldn't get LDAP settings for Org '%s': %s", entity.Parent, err) return } - vcd.infoCleanup(removedMsg, entity.EntityType, entity.Name, entity.CreatedBy) + + // This is done to avoid calling LdapDisable() if it has been unconfigured, due to bug with Org catalog publish settings + if ldapConfig.OrgLdapMode != types.LdapModeNone { + err = org.LdapDisable() + if err != nil { + vcd.infoCleanup("removeLeftoverEntries: [ERROR] Could not clear LDAP settings for Org '%s': %s", + entity.Parent, err) + vcd.infoCleanup(removedMsg, entity.EntityType, entity.Name, entity.CreatedBy) + return + } + } + + vcd.infoCleanup(notFoundMsg, entity.EntityType, entity.Name) return + case "vdcComputePolicy": - if entity.Parent == "" { - vcd.infoCleanup("removeLeftoverEntries: [ERROR] No ORG provided for vdcComputePolicy '%s'\n", entity.Name) - return - } - org, err := vcd.client.GetAdminOrgByName(entity.Parent) - if err != nil { - vcd.infoCleanup(notFoundMsg, "org", entity.Parent) - return - } - policy, err := org.GetVdcComputePolicyById(entity.Name) + policy, err := vcd.client.GetVdcComputePolicyV2ById(entity.Name) if policy == nil || err != nil { vcd.infoCleanup(notFoundMsg, "vdcComputePolicy", entity.Name) return @@ -1460,6 +1695,80 @@ func (vcd *TestVCD) removeLeftoverEntities(entity CleanupEntity) { } return + case "logicalVmGroup": + logicalVmGroup, err := vcd.client.GetLogicalVmGroupById(entity.Name) + if logicalVmGroup == nil || err != nil { + vcd.infoCleanup(notFoundMsg, "logicalVmGroup", entity.Name) + return + } + err = logicalVmGroup.Delete() + if err == nil { + vcd.infoCleanup(removedMsg, entity.EntityType, entity.Name, entity.CreatedBy) + } else { + vcd.infoCleanup(notDeletedMsg, entity.EntityType, entity.Name, err) + } + return + + case "nsxtDhcpForwarder": + edge, err := vcd.nsxtVdc.GetNsxtEdgeGatewayByName(entity.Name) + if err != nil { + vcd.infoCleanup("removeLeftoverEntries: [ERROR] %s \n", err) + } + + dhcpForwarder, err := edge.GetDhcpForwarder() + if err != nil { + vcd.infoCleanup("removeLeftoverEntries: [ERROR] %s \n", err) + } + + if dhcpForwarder.Enabled == false && len(dhcpForwarder.DhcpServers) == 0 { + vcd.infoCleanup(notFoundMsg, "dhcpForwarder", entity.Name) + return + } + + _, err = edge.UpdateDhcpForwarder(&types.NsxtEdgeGatewayDhcpForwarder{}) + if err != nil { + vcd.infoCleanup(notDeletedMsg, entity.EntityType, entity.Name, err) + } + + vcd.infoCleanup(removedMsg, entity.EntityType, entity.Name, entity.CreatedBy) + return + case "nsxtEdgeGatewayDns": + edge, err := vcd.nsxtVdc.GetNsxtEdgeGatewayByName(entity.Name) + if err != nil { + vcd.infoCleanup("removeLeftoverEntries: [ERROR] %s \n", err) + } + + dns, err := edge.GetDnsConfig() + if err != nil { + vcd.infoCleanup("removeLeftoverEntries: [ERROR] %s \n", err) + } + + if dns.NsxtEdgeGatewayDns.Enabled == false && dns.NsxtEdgeGatewayDns.DefaultForwarderZone == nil { + vcd.infoCleanup(notFoundMsg, entity.EntityType, entity.Name) + return + } + + err = dns.Delete() + if err != nil { + vcd.infoCleanup(notDeletedMsg, entity.EntityType, entity.Name, err) + } + + vcd.infoCleanup(removedMsg, entity.EntityType, entity.Name, entity.CreatedBy) + return + case "slaacProfile": + edge, err := vcd.nsxtVdc.GetNsxtEdgeGatewayByName(entity.Name) + if err != nil { + vcd.infoCleanup("removeLeftoverEntries: [ERROR] %s \n", err) + } + + _, err = edge.UpdateSlaacProfile(&types.NsxtEdgeGatewaySlaacProfile{Enabled: false, Mode: "SLAAC"}) + if err != nil { + vcd.infoCleanup(notDeletedMsg, entity.EntityType, entity.Name, err) + } + + vcd.infoCleanup(removedMsg, entity.EntityType, entity.Name, entity.CreatedBy) + return + default: // If we reach this point, we are trying to clean up an entity that // we aren't prepared for yet. @@ -1481,175 +1790,91 @@ func (vcd *TestVCD) TearDownSuite(check *C) { // Tests getloginurl with the endpoint given // in the config file. -func TestClient_getloginurl(t *testing.T) { +func (vcd *TestVCD) TestClient_getloginurl(check *C) { + if os.Getenv("GOVCD_API_VERSION") != "" { + check.Skip("custom API version is being used") + } config, err := GetConfigStruct() if err != nil { - t.Fatalf("err: %s", err) + check.Fatalf("err: %s", err) } client, err := GetTestVCDFromYaml(config) if err != nil { - t.Fatalf("err: %s", err) + check.Fatalf("err: %s", err) } err = client.vcdloginurl() if err != nil { - t.Fatalf("err: %s", err) + check.Fatalf("err: %s", err) } - if client.sessionHREF.Path != "/api/sessions" { - t.Fatalf("Getting LoginUrl failed, url: %s", client.sessionHREF.Path) + if client.sessionHREF.Path != "/cloudapi/1.0.0/sessions" { + check.Fatalf("Getting LoginUrl failed, url: %s", client.sessionHREF.Path) } } // Tests Authenticate with the vcd credentials (or token) given in the config file -func TestVCDClient_Authenticate(t *testing.T) { +func (vcd *TestVCD) TestVCDClient_Authenticate(check *C) { config, err := GetConfigStruct() if err != nil { - t.Fatalf("err: %s", err) + check.Fatalf("err: %s", err) } client, err := GetTestVCDFromYaml(config) if err != nil { - t.Fatalf("err: %s", err) + check.Fatalf("err: %s", err) } - - token := os.Getenv("VCD_TOKEN") - if token == "" { - token = config.Provider.Token + apiToken := os.Getenv("VCD_API_TOKEN") + if apiToken == "" { + apiToken = config.Provider.ApiToken } - if token != "" { - err = client.SetToken(config.Provider.SysOrg, AuthorizationHeader, token) + if apiToken != "" { + err = client.SetToken(config.Provider.SysOrg, ApiTokenHeader, apiToken) } else { - err = client.Authenticate(config.Provider.User, config.Provider.Password, config.Provider.SysOrg) + token := os.Getenv("VCD_TOKEN") + if token == "" { + token = config.Provider.Token + } + if token != "" { + err = client.SetToken(config.Provider.SysOrg, AuthorizationHeader, token) + } else { + err = client.Authenticate(config.Provider.User, config.Provider.Password, config.Provider.SysOrg) + } } if err != nil { - t.Fatalf("Error authenticating: %s", err) + check.Fatalf("Error authenticating: %s", err) } } -func (vcd *TestVCD) createTestVapp(name string) (*VApp, error) { - // ========================= issue#252 ================================== - // TODO: To be enabled when issue#252 is resolved. - // Allows re-using a pre-created vApp - // existingVapp, err := vcd.vdc.GetVAppByName(name, false) - // if err == nil { - // fmt.Printf("vApp %s already exists. Skipping creation\n",name) - // return existingVapp, nil - // } - // ====================================================================== - // Populate OrgVDCNetwork - var networks []*types.OrgVDCNetwork - net, err := vcd.vdc.GetOrgVdcNetworkByName(vcd.config.VCD.Network.Net1, false) - if err != nil { - return nil, fmt.Errorf("error finding network : %s, err: %s", vcd.config.VCD.Network.Net1, err) - } - networks = append(networks, net.OrgVDCNetwork) - // Populate Catalog - cat, err := vcd.org.GetCatalogByName(vcd.config.VCD.Catalog.Name, false) - if err != nil || cat == nil { - return nil, fmt.Errorf("error finding catalog : %s", err) - } - // Populate Catalog Item - catitem, err := cat.GetCatalogItemByName(vcd.config.VCD.Catalog.CatalogItem, false) - if err != nil { - return nil, fmt.Errorf("error finding catalog item : %s", err) - } - // Get VAppTemplate - vAppTemplate, err := catitem.GetVAppTemplate() - if err != nil { - return nil, fmt.Errorf("error finding vapptemplate : %s", err) - } - // Get StorageProfileReference - storageProfileRef, err := vcd.vdc.FindStorageProfileReference(vcd.config.VCD.StorageProfile.SP1) +func (vcd *TestVCD) TestVCDClient_AuthenticateInvalidPassword(check *C) { + config, err := GetConfigStruct() if err != nil { - return nil, fmt.Errorf("error finding storage profile: %s", err) + check.Fatalf("err: %s", err) } - // Compose VApp - task, err := vcd.vdc.ComposeVApp(networks, vAppTemplate, storageProfileRef, name, "description", true) + client, err := GetTestVCDFromYaml(config) if err != nil { - return nil, fmt.Errorf("error composing vapp: %s", err) + check.Fatalf("error getting client structure: %s", err) } - // After a successful creation, the entity is added to the cleanup list. - // If something fails after this point, the entity will be removed - AddToCleanupList(name, "vapp", "", "createTestVapp") - err = task.WaitTaskCompletion() - if err != nil { - return nil, fmt.Errorf("error composing vapp: %s", err) + + err = client.Authenticate(config.Provider.User, "INVALID-PASSWORD", config.Provider.SysOrg) + if err == nil || !strings.Contains(err.Error(), "401") { + check.Fatalf("expected error for invalid credentials") } - // Get VApp - vapp, err := vcd.vdc.GetVAppByName(name, true) +} + +func (vcd *TestVCD) TestVCDClient_AuthenticateInvalidToken(check *C) { + config, err := GetConfigStruct() if err != nil { - return nil, fmt.Errorf("error getting vapp: %s", err) + check.Fatalf("err: %s", err) } - - err = vapp.BlockWhileStatus("UNRESOLVED", vapp.client.MaxRetryTimeout) + client, err := GetTestVCDFromYaml(config) if err != nil { - return nil, fmt.Errorf("error waiting for created test vApp to have working state: %s", err) + check.Fatalf("error getting client structure: %s", err) } - return vapp, err -} - -func Test_splitParent(t *testing.T) { - type args struct { - parent string - separator string - } - tests := []struct { - name string - args args - wantFirst string - wantSecond string - wantThird string - }{ - { - name: "Empty", - args: args{parent: "", separator: "|"}, - wantFirst: "", - wantSecond: "", - wantThird: "", - }, - { - name: "One", - wantFirst: "", - wantSecond: "", - wantThird: "", - }, - { - name: "Two", - args: args{parent: "first|second", separator: "|"}, - wantFirst: "first", - wantSecond: "second", - wantThird: "", - }, - { - name: "Three", - args: args{parent: "first|second|third", separator: "|"}, - wantFirst: "first", - wantSecond: "second", - wantThird: "third", - }, - { - name: "Four", - args: args{parent: "first|second|third|fourth", separator: "|"}, - wantFirst: "", - wantSecond: "", - wantThird: "", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotFirst, gotSecond, gotThird := splitParent(tt.args.parent, tt.args.separator) - if gotFirst != tt.wantFirst { - t.Errorf("splitParent() gotFirst = %v, want %v", gotFirst, tt.wantFirst) - } - if gotSecond != tt.wantSecond { - t.Errorf("splitParent() gotSecond = %v, want %v", gotSecond, tt.wantSecond) - } - if gotThird != tt.wantThird { - t.Errorf("splitParent() gotThird = %v, want %v", gotThird, tt.wantThird) - } - }) + err = client.SetToken(config.Provider.SysOrg, AuthorizationHeader, "invalid-token") + if err == nil || !strings.Contains(err.Error(), "401") { + check.Fatalf("expected error for invalid credentials") } } @@ -1676,28 +1901,21 @@ func (vcd *TestVCD) findFirstVapp() VApp { return VApp{} } wantedVapp := vcd.vapp.VApp.Name - vappName := "" - for _, res := range vdc.Vdc.ResourceEntities { - for _, item := range res.ResourceEntity { - // Finding a named vApp, if it was defined in config - if wantedVapp != "" { - if item.Name == wantedVapp { - vappName = item.Name - break - } - } else { - // Otherwise, we get the first vApp from the vDC list + if wantedVapp == "" { + // As no vApp is defined in config, we search for one randomly + for _, res := range vdc.Vdc.ResourceEntities { + for _, item := range res.ResourceEntity { if item.Type == "application/vnd.vmware.vcloud.vApp+xml" { - vappName = item.Name + wantedVapp = item.Name break } } } } - if wantedVapp == "" { + vapp, err := vdc.GetVAppByName(wantedVapp, false) + if err != nil { return VApp{} } - vapp, _ := vdc.GetVAppByName(vappName, false) return *vapp } @@ -1720,6 +1938,12 @@ func (vcd *TestVCD) Test_NewRequestWitNotEncodedParamsWithApiVersion(check *C) { check.Assert(resp.Header.Get("Content-Type"), Equals, types.MimeQueryRecords+";version="+apiVersion) + bodyBytes, err := rewrapRespBodyNoopCloser(resp) + check.Assert(err, IsNil) + + util.ProcessResponseOutput(util.FuncNameCallStack(), resp, string(bodyBytes)) + debugShowResponse(resp, bodyBytes) + // Repeats the call without API version change req = vcd.client.Client.NewRequestWitNotEncodedParams(nil, map[string]string{"type": "media", "filter": "name==any"}, http.MethodGet, queryUlr, nil) @@ -1730,6 +1954,11 @@ func (vcd *TestVCD) Test_NewRequestWitNotEncodedParamsWithApiVersion(check *C) { // Checks that the regularAPI version was not affected by the previous call check.Assert(resp.Header.Get("Content-Type"), Equals, types.MimeQueryRecords+";version="+vcd.client.Client.APIVersion) + bodyBytes, err = rewrapRespBodyNoopCloser(resp) + check.Assert(err, IsNil) + util.ProcessResponseOutput(util.FuncNameCallStack(), resp, string(bodyBytes)) + debugShowResponse(resp, bodyBytes) + fmt.Printf("Test: %s run with api Version: %s\n", check.TestName(), apiVersion) } @@ -1798,6 +2027,38 @@ func skipNoNsxtConfiguration(vcd *TestVCD, check *C) { if vcd.config.VCD.Nsxt.EdgeGateway == "" { check.Skip(generalMessage + "No NSX-T Edge Gateway specified in configuration") } + + if vcd.config.VCD.Nsxt.IpDiscoveryProfile == "" || + vcd.config.VCD.Nsxt.MacDiscoveryProfile == "" || + vcd.config.VCD.Nsxt.SpoofGuardProfile == "" || + vcd.config.VCD.Nsxt.QosProfile == "" || + vcd.config.VCD.Nsxt.SegmentSecurityProfile == "" { + check.Skip(generalMessage + "NSX-T Segment Profiles are not specified in configuration") + } +} + +func skipNoNsxtAlbConfiguration(vcd *TestVCD, check *C) { + skipNoNsxtConfiguration(vcd, check) + generalMessage := "Missing NSX-T ALB config: " + + if vcd.config.VCD.Nsxt.NsxtAlbControllerUrl == "" { + check.Skip(generalMessage + "No NSX-T ALB Controller URL specified in configuration") + } + + if vcd.config.VCD.Nsxt.NsxtAlbControllerUser == "" { + check.Skip(generalMessage + "No NSX-T ALB Controller Name specified in configuration") + } + + if vcd.config.VCD.Nsxt.NsxtAlbControllerPassword == "" { + check.Skip(generalMessage + "No NSX-T ALB Controller Password specified in configuration") + } + + if vcd.config.VCD.Nsxt.NsxtAlbImportableCloud == "" { + check.Skip(generalMessage + "No NSX-T ALB Controller Importable Cloud Name") + } + if vcd.config.VCD.Nsxt.NsxtAlbServiceEngineGroup == "" { + check.Skip(generalMessage + "No NSX-T ALB Service Engine Group name specified in configuration") + } } // skipOpenApiEndpointTest is a helper to skip tests for particular unsupported OpenAPI endpoints @@ -1815,3 +2076,82 @@ func skipOpenApiEndpointTest(vcd *TestVCD, check *C, endpoint string) { check.Skip(skipText) } } + +// newUserConnection returns a connection for a given user +func newUserConnection(href, userName, password, orgName string, insecure bool) (*VCDClient, error) { + u, err := url.ParseRequestURI(href) + if err != nil { + return nil, fmt.Errorf("[newUserConnection] unable to pass url: %s", err) + } + vcdClient := NewVCDClient(*u, insecure) + err = vcdClient.Authenticate(userName, password, orgName) + if err != nil { + return nil, fmt.Errorf("[newUserConnection] unable to authenticate: %s", err) + } + return vcdClient, nil +} + +// newOrgUserConnection creates a new Org User and returns a connection to it. +// Attention: Set the user to use only lowercase letters. If you put upper case letters the function fails on waiting +// because VCD creates the user with lowercase letters. +func newOrgUserConnection(adminOrg *AdminOrg, userName, password, href string, insecure bool) (*VCDClient, *OrgUser, error) { + _, err := adminOrg.GetUserByName(userName, false) + if err == nil { + // user exists + return nil, nil, fmt.Errorf("user %s already exists", userName) + } + _, err = adminOrg.CreateUserSimple(OrgUserConfiguration{ + Name: userName, + Password: password, + RoleName: OrgUserRoleOrganizationAdministrator, + ProviderType: OrgUserProviderIntegrated, + IsEnabled: true, + DeployedVmQuota: 0, + StoredVmQuota: 0, + FullName: userName, + Description: "Test user created by newOrgUserConnection", + }) + if err != nil { + return nil, nil, err + } + + AddToCleanupList(userName, "user", adminOrg.AdminOrg.Name, "newOrgUserConnection") + + _ = adminOrg.Refresh() + newUser, err := adminOrg.GetUserByName(userName, false) + if err != nil { + return nil, nil, fmt.Errorf("[newOrgUserConnection] unable to retrieve newly created user: %s", err) + } + + vcdClient, err := newUserConnection(href, userName, password, adminOrg.AdminOrg.Name, insecure) + if err != nil { + return nil, nil, fmt.Errorf("[newOrgUserConnection] error connecting new user: %s", err) + } + + return vcdClient, newUser, nil +} + +func (vcd *TestVCD) skipIfNotSysAdmin(check *C) { + if !vcd.client.Client.IsSysAdmin { + check.Skip(fmt.Sprintf("Skipping %s: requires system administrator privileges", check.TestName())) + } +} + +// retryOnError is a function that will attempt to execute function with signature `func() error` +// multiple times (until maxRetries) and waiting given retryInterval between tries. It will return +// original deletion error for troubleshooting. +func retryOnError(operation func() error, maxRetries int, retryInterval time.Duration) error { + var err error + for attempt := 0; attempt < maxRetries; attempt++ { + err = operation() + if err == nil { + return nil + } + + fmt.Printf("# retrying after %v (Attempt %d/%d)\n", retryInterval, attempt+1, maxRetries) + fmt.Printf("# error was: %s", err) + time.Sleep(retryInterval) + } + + return fmt.Errorf("exceeded maximum retries, final error: %s", err) +} diff --git a/govcd/api_vcd_test_unit.go b/govcd/api_vcd_test_unit.go index 750bac62c..6620c1653 100644 --- a/govcd/api_vcd_test_unit.go +++ b/govcd/api_vcd_test_unit.go @@ -1,4 +1,4 @@ -// +build unit ALL +//go:build unit || ALL /* * Copyright 2020 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -7,11 +7,75 @@ package govcd import ( - "io/ioutil" + "io" "os" + "path/filepath" "testing" ) +func Test_splitParent(t *testing.T) { + type args struct { + parent string + separator string + } + tests := []struct { + name string + args args + wantFirst string + wantSecond string + wantThird string + }{ + { + name: "Empty", + args: args{parent: "", separator: "|"}, + wantFirst: "", + wantSecond: "", + wantThird: "", + }, + { + name: "One", + wantFirst: "", + wantSecond: "", + wantThird: "", + }, + { + name: "Two", + args: args{parent: "first|second", separator: "|"}, + wantFirst: "first", + wantSecond: "second", + wantThird: "", + }, + { + name: "Three", + args: args{parent: "first|second|third", separator: "|"}, + wantFirst: "first", + wantSecond: "second", + wantThird: "third", + }, + { + name: "Four", + args: args{parent: "first|second|third|fourth", separator: "|"}, + wantFirst: "", + wantSecond: "", + wantThird: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotFirst, gotSecond, gotThird := splitParent(tt.args.parent, tt.args.separator) + if gotFirst != tt.wantFirst { + t.Errorf("splitParent() gotFirst = %v, want %v", gotFirst, tt.wantFirst) + } + if gotSecond != tt.wantSecond { + t.Errorf("splitParent() gotSecond = %v, want %v", gotSecond, tt.wantSecond) + } + if gotThird != tt.wantThird { + t.Errorf("splitParent() gotThird = %v, want %v", gotThird, tt.wantThird) + } + }) + } +} + // goldenString is a test helper to manage Golden files. It supports `update` parameter which may be // useful for writing such files (manual or automated way). func goldenString(t *testing.T, goldenFile string, actual string, update bool) string { @@ -19,11 +83,11 @@ func goldenString(t *testing.T, goldenFile string, actual string, update bool) s goldenPath := "../test-resources/golden/" + t.Name() + "_" + goldenFile + ".golden" - f, err := os.OpenFile(goldenPath, os.O_RDWR|os.O_CREATE, 0644) + f, err := os.OpenFile(filepath.Clean(goldenPath), os.O_RDWR|os.O_CREATE, 0600) if err != nil { t.Fatalf("unable to find golden file '%s': %s", goldenPath, err) } - defer f.Close() + defer safeClose(f) if update { _, err := f.WriteString(actual) @@ -34,7 +98,7 @@ func goldenString(t *testing.T, goldenFile string, actual string, update bool) s return actual } - content, err := ioutil.ReadAll(f) + content, err := io.ReadAll(f) if err != nil { t.Fatalf("error opening file %s: %s", goldenPath, err) } diff --git a/govcd/api_vcd_versions.go b/govcd/api_vcd_versions.go index 5c4268462..6e873989d 100644 --- a/govcd/api_vcd_versions.go +++ b/govcd/api_vcd_versions.go @@ -20,9 +20,10 @@ import ( ) type VersionInfo struct { - Version string `xml:"Version"` - LoginUrl string `xml:"LoginUrl"` - Deprecated bool `xml:"deprecated,attr,omitempty"` + Version string `xml:"Version"` + LoginUrl string `xml:"LoginUrl"` + ProviderLoginUrl string `xml:"ProviderLoginUrl"` + Deprecated bool `xml:"deprecated,attr,omitempty"` } type VersionInfos []VersionInfo @@ -45,7 +46,9 @@ var apiVersionToVcdVersion = map[string]string{ "32.0": "9.7", "33.0": "10.0", "34.0": "10.1", - "35.0": "10.2", // Provisional version for non-GA release. It may change later + "35.0": "10.2", + "36.0": "10.3", + "37.0": "10.4", // Provisional version for non-GA release. It may change later } // vcdVersionToApiVersion gets the max supported API version from vCD version @@ -56,7 +59,9 @@ var vcdVersionToApiVersion = map[string]string{ "9.7": "32.0", "10.0": "33.0", "10.1": "34.0", - "10.2": "35.0", // Provisional version for non-GA release. It may change later + "10.2": "35.0", + "10.3": "36.0", + "10.4": "37.0", // Provisional version for non-GA release. It may change later } // to make vcdVersionToApiVersion used @@ -72,21 +77,21 @@ var _ = vcdVersionToApiVersion // Format: ">= 27.0, < 32.0", ">= 30.0", "= 27.0" // // vCD version mapping to API version support https://code.vmware.com/doc/preview?id=8072 -func (cli *Client) APIVCDMaxVersionIs(versionConstraint string) bool { - err := cli.vcdFetchSupportedVersions() +func (client *Client) APIVCDMaxVersionIs(versionConstraint string) bool { + err := client.vcdFetchSupportedVersions() if err != nil { util.Logger.Printf("[ERROR] could not retrieve supported versions: %s", err) return false } util.Logger.Printf("[TRACE] checking max API version against constraints '%s'", versionConstraint) - maxVersion, err := cli.MaxSupportedVersion() + maxVersion, err := client.MaxSupportedVersion() if err != nil { util.Logger.Printf("[ERROR] unable to find max supported version : %s", err) return false } - isSupported, err := cli.apiVersionMatchesConstraint(maxVersion, versionConstraint) + isSupported, err := client.apiVersionMatchesConstraint(maxVersion, versionConstraint) if err != nil { util.Logger.Printf("[ERROR] unable to find max supported version : %s", err) return false @@ -102,13 +107,13 @@ func (cli *Client) APIVCDMaxVersionIs(versionConstraint string) bool { // Format: ">= 27.0, < 32.0", ">= 30.0", "= 27.0" // // vCD version mapping to API version support https://code.vmware.com/doc/preview?id=8072 -func (cli *Client) APIClientVersionIs(versionConstraint string) bool { +func (client *Client) APIClientVersionIs(versionConstraint string) bool { util.Logger.Printf("[TRACE] checking current API version against constraints '%s'", versionConstraint) - isSupported, err := cli.apiVersionMatchesConstraint(cli.APIVersion, versionConstraint) + isSupported, err := client.apiVersionMatchesConstraint(client.APIVersion, versionConstraint) if err != nil { - util.Logger.Printf("[ERROR] unable to find cur supported version : %s", err) + util.Logger.Printf("[ERROR] unable to find supported version : %s", err) return false } @@ -118,26 +123,26 @@ func (cli *Client) APIClientVersionIs(versionConstraint string) bool { // vcdFetchSupportedVersions retrieves list of supported versions from // /api/versions endpoint and stores them in VCDClient for future uses. // It only does it once. -func (cli *Client) vcdFetchSupportedVersions() error { +func (client *Client) vcdFetchSupportedVersions() error { // Only fetch /versions if it is not stored already - numVersions := len(cli.supportedVersions.VersionInfos) + numVersions := len(client.supportedVersions.VersionInfos) if numVersions > 0 { util.Logger.Printf("[TRACE] skipping fetch of versions because %d are stored", numVersions) return nil } - apiEndpoint := cli.VCDHREF + apiEndpoint := client.VCDHREF apiEndpoint.Path += "/versions" suppVersions := new(SupportedVersions) - _, err := cli.ExecuteRequest(apiEndpoint.String(), http.MethodGet, + _, err := client.ExecuteRequest(apiEndpoint.String(), http.MethodGet, "", "error fetching versions: %s", nil, suppVersions) - cli.supportedVersions = *suppVersions + client.supportedVersions = *suppVersions // Log all supported API versions in one line to help identify vCD version from logs - allApiVersions := make([]string, len(cli.supportedVersions.VersionInfos)) - for versionIndex, version := range cli.supportedVersions.VersionInfos { + allApiVersions := make([]string, len(client.supportedVersions.VersionInfos)) + for versionIndex, version := range client.supportedVersions.VersionInfos { allApiVersions[versionIndex] = version.Version } util.Logger.Printf("[DEBUG] supported API versions : %s", strings.Join(allApiVersions, ",")) @@ -146,10 +151,13 @@ func (cli *Client) vcdFetchSupportedVersions() error { } // MaxSupportedVersion parses supported version list and returns the highest version in string format. -func (cli *Client) MaxSupportedVersion() (string, error) { - versions := make([]*semver.Version, len(cli.supportedVersions.VersionInfos)) - for index, versionInfo := range cli.supportedVersions.VersionInfos { - version, _ := semver.NewVersion(versionInfo.Version) +func (client *Client) MaxSupportedVersion() (string, error) { + versions := make([]*semver.Version, len(client.supportedVersions.VersionInfos)) + for index, versionInfo := range client.supportedVersions.VersionInfos { + version, err := semver.NewVersion(versionInfo.Version) + if err != nil { + return "", fmt.Errorf("error parsing version %s: %s", versionInfo.Version, err) + } versions[index] = version } // Sort supported versions in order lowest-highest @@ -167,27 +175,27 @@ func (cli *Client) MaxSupportedVersion() (string, error) { // vcdCheckSupportedVersion checks if there is at least one specified version exactly matching listed ones. // Format example "27.0" -func (cli *Client) vcdCheckSupportedVersion(version string) (bool, error) { - return cli.checkSupportedVersionConstraint(fmt.Sprintf("= %s", version)) +func (client *Client) vcdCheckSupportedVersion(version string) error { + return client.checkSupportedVersionConstraint(fmt.Sprintf("= %s", version)) } // 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 (cli *Client) checkSupportedVersionConstraint(versionConstraint string) (bool, error) { - for _, versionInfo := range cli.supportedVersions.VersionInfos { - versionMatch, err := cli.apiVersionMatchesConstraint(versionInfo.Version, versionConstraint) +func (client *Client) checkSupportedVersionConstraint(versionConstraint string) error { + for _, versionInfo := range client.supportedVersions.VersionInfos { + versionMatch, err := client.apiVersionMatchesConstraint(versionInfo.Version, versionConstraint) if err != nil { - return false, fmt.Errorf("cannot match version: %s", err) + return fmt.Errorf("cannot match version: %s", err) } if versionMatch { - return true, nil + return nil } } - return false, fmt.Errorf("version %s is not supported", versionConstraint) + return fmt.Errorf("version %s is not supported", versionConstraint) } -func (cli *Client) apiVersionMatchesConstraint(version, versionConstraint string) (bool, error) { +func (client *Client) apiVersionMatchesConstraint(version, versionConstraint string) (bool, error) { checkVer, err := semver.NewVersion(version) if err != nil { @@ -208,38 +216,51 @@ func (cli *Client) apiVersionMatchesConstraint(version, versionConstraint string } // validateAPIVersion fetches API versions -func (cli *Client) validateAPIVersion() error { - err := cli.vcdFetchSupportedVersions() +func (client *Client) validateAPIVersion() error { + err := client.vcdFetchSupportedVersions() if err != nil { return fmt.Errorf("could not retrieve supported versions: %s", err) } // Check if version is supported - if ok, err := cli.vcdCheckSupportedVersion(cli.APIVersion); !ok || err != nil { - return fmt.Errorf("API version %s is not supported: %s", cli.APIVersion, err) + err = client.vcdCheckSupportedVersion(client.APIVersion) + if err != nil { + return fmt.Errorf("API version %s is not supported: %s", client.APIVersion, err) } return nil } -// GetSpecificApiVersionOnCondition returns default version or wantedApiVersion if it is connected to version -// described in vcdApiVersionCondition -// f.e. values ">= 32.0", "32.0" returns 32.0 if vCD version is above or 9.7 -func (cli *Client) GetSpecificApiVersionOnCondition(vcdApiVersionCondition, wantedApiVersion string) string { - apiVersion := cli.APIVersion - if cli.APIVCDMaxVersionIs(vcdApiVersionCondition) { - apiVersion = wantedApiVersion +// GetSpecificApiVersionOnCondition returns default version or wantedApiVersion if it is connected +// to version described in vcdApiVersionCondition e.g. values ">= 32.0", "32.0" returns 32.0 if vCD +// version is above or 9.7 +// Note. This function will always respect minimum supported API version which is defined in +// client.APIVersion. If the wantedApiVersionOrMinimumRequired is less than minimum supported +// version, this function will return the minimum supported version. This means that it must be be +// well tested when client.APIVersion is bumped to avoid unexpected errors due to newer API version +// being used. +func (client *Client) GetSpecificApiVersionOnCondition(vcdApiVersionCondition, wantedApiVersionOrMinimumRequired string) string { + apiVersion := client.APIVersion + if client.APIVCDMaxVersionIs(vcdApiVersionCondition) { + versionConstraint := fmt.Sprintf(">= %s", apiVersion) + // only if the version is not less than minimum supported version 'client.APIVersion' we can + // specify 'wantedApiVersionOrMinimumRequired' + if matches, err := client.apiVersionMatchesConstraint(wantedApiVersionOrMinimumRequired, versionConstraint); err == nil && matches { + return wantedApiVersionOrMinimumRequired + } + util.Logger.Printf("[TRACE] API version %s does not satisfy constraints '%s'. Will use minimum supported version '%s'.", + wantedApiVersionOrMinimumRequired, versionConstraint, apiVersion) } return apiVersion } // GetVcdVersion finds the VCD version and the time of build -func (cli *Client) GetVcdVersion() (string, time.Time, error) { +func (client *Client) GetVcdVersion() (string, time.Time, error) { - path := cli.VCDHREF + path := client.VCDHREF path.Path += "/admin" var admin types.VCloud - _, err := cli.ExecuteRequest(path.String(), http.MethodGet, + _, err := client.ExecuteRequest(path.String(), http.MethodGet, "", "error retrieving admin info: %s", nil, &admin) if err != nil { return "", time.Time{}, err @@ -267,9 +288,9 @@ func (cli *Client) GetVcdVersion() (string, time.Time, error) { } // GetVcdShortVersion returns the VCD version (three digits, no build info) -func (cli *Client) GetVcdShortVersion() (string, error) { +func (client *Client) GetVcdShortVersion() (string, error) { - vcdVersion, err := cli.GetVcdFullVersion() + vcdVersion, err := client.GetVcdFullVersion() if err != nil { return "", fmt.Errorf("error getting version digits: %s", err) } @@ -278,9 +299,9 @@ func (cli *Client) GetVcdShortVersion() (string, error) { } // GetVcdFullVersion returns the full VCD version information as a structure -func (cli *Client) GetVcdFullVersion() (VcdVersion, error) { +func (client *Client) GetVcdFullVersion() (VcdVersion, error) { var vcdVersion VcdVersion - version, versionTime, err := cli.GetVcdVersion() + version, versionTime, err := client.GetVcdVersion() if err != nil { return VcdVersion{}, err } @@ -315,16 +336,19 @@ func intListToVersion(digits []int, atMost int) string { // VersionEqualOrGreater return true if the current version is the same or greater than the one being compared. // If howManyDigits is > 3, the comparison includes the build. // Examples: -// client version is 1.2.3.1234 -// compare version is 1.2.3.2000 +// +// client version is 1.2.3.1234 +// compare version is 1.2.3.2000 +// // function return true if howManyDigits is <= 3, but false if howManyDigits is > 3 // -// client version is 1.2.3.1234 -// compare version is 1.1.1.0 +// client version is 1.2.3.1234 +// compare version is 1.1.1.0 +// // function returns true regardless of value of howManyDigits -func (cli *Client) VersionEqualOrGreater(compareTo string, howManyDigits int) (bool, error) { +func (client *Client) VersionEqualOrGreater(compareTo string, howManyDigits int) (bool, error) { - fullVersion, err := cli.GetVcdFullVersion() + fullVersion, err := client.GetVcdFullVersion() if err != nil { return false, err } diff --git a/govcd/api_vcd_versions_test.go b/govcd/api_vcd_versions_test.go index 26140ca1a..062a38b41 100644 --- a/govcd/api_vcd_versions_test.go +++ b/govcd/api_vcd_versions_test.go @@ -1,4 +1,4 @@ -// +build api functional ALL +//go:build api || functional || ALL /* * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -184,3 +184,37 @@ func (vcd *TestVCD) Test_GetVcdVersion(check *C) { check.Assert(err, IsNil) check.Assert(result, Equals, false) } + +func (vcd *TestVCD) TestClient_GetSpecificApiVersionOnCondition(check *C) { + clientApiVersion := vcd.client.Client.APIVersion + maxApiSupportVersion, err := vcd.client.Client.MaxSupportedVersion() + check.Assert(err, IsNil) + + fmt.Println("# API minimum required:" + vcd.client.Client.APIVersion) + fmt.Println("# API maximum:" + maxApiSupportVersion) + + type args struct { + versionCondition string + wantedVersion string + } + tests := []struct { + name string + args args + want string + }{ + {name: "ClientHigherThanRequired", args: args{versionCondition: ">=32", wantedVersion: "32"}, want: clientApiVersion}, + {name: "ClientLowerThanRequired", args: args{versionCondition: ">=72.0", wantedVersion: "72.0"}, want: clientApiVersion}, + {name: "ElevateToMaximumSupported", args: args{versionCondition: ">= " + maxApiSupportVersion, wantedVersion: maxApiSupportVersion}, want: maxApiSupportVersion}, + } + + for _, tt := range tests { + fmt.Printf("## " + tt.name + ": ") + + if got := vcd.client.Client.GetSpecificApiVersionOnCondition(tt.args.versionCondition, tt.args.wantedVersion); got != tt.want { + check.Errorf("Client.GetSpecificApiVersionOnCondition() = %v, want %v", got, tt.want) + } else { + fmt.Printf("Got %s from GetSpecificApiVersionOnCondition(\"%s\", \"%s\")\n", + got, tt.args.versionCondition, tt.args.wantedVersion) + } + } +} diff --git a/govcd/catalog.go b/govcd/catalog.go index 405961c0f..0406505f5 100644 --- a/govcd/catalog.go +++ b/govcd/catalog.go @@ -1,5 +1,5 @@ /* - * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + * Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. */ package govcd @@ -10,7 +10,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "math" "net/http" "net/url" @@ -32,6 +31,7 @@ const ( type Catalog struct { Catalog *types.Catalog client *Client + parent organization } func NewCatalog(client *Client) *Catalog { @@ -41,8 +41,8 @@ func NewCatalog(client *Client) *Catalog { } } -// Deletes the Catalog, returning an error if the vCD call fails. -// Link to API call: https://code.vmware.com/apis/220/vcloud#/doc/doc/operations/DELETE-Catalog.html +// Delete deletes the Catalog, returning an error if the vCD call fails. +// Link to API call: https://code.vmware.com/apis/1046/vmware-cloud-director/doc/doc/operations/DELETE-Catalog.html func (catalog *Catalog) Delete(force, recursive bool) error { adminCatalogHREF := catalog.client.VCDHREF @@ -51,21 +51,89 @@ func (catalog *Catalog) Delete(force, recursive bool) error { return err } if catalogID == "" { - return fmt.Errorf("empty ID returned for catalog ID %s", catalog.Catalog.ID) + return fmt.Errorf("empty ID returned for catalog %s", catalog.Catalog.Name) } adminCatalogHREF.Path += "/admin/catalog/" + catalogID + if force && recursive { + // A catalog cannot be removed if it has active tasks, or if any of its items have active tasks + err = catalog.consumeTasks() + if err != nil { + return fmt.Errorf("error while consuming tasks from catalog %s: %s", catalog.Catalog.Name, err) + } + } + req := catalog.client.NewRequest(map[string]string{ "force": strconv.FormatBool(force), "recursive": strconv.FormatBool(recursive), }, http.MethodDelete, adminCatalogHREF, nil) - _, err = checkResp(catalog.client.Http.Do(req)) - + resp, err := checkResp(catalog.client.Http.Do(req)) if err != nil { - return fmt.Errorf("error deleting Catalog %s: %s", catalog.Catalog.ID, err) + return fmt.Errorf("error deleting Catalog %s: %s", catalog.Catalog.Name, err) + } + task := NewTask(catalog.client) + if err = decodeBody(types.BodyTypeXML, resp, task.Task); err != nil { + return fmt.Errorf("error decoding task response: %s", err) } + if task.Task.Status == "error" { + return fmt.Errorf(combinedTaskErrorMessage(task.Task, fmt.Errorf("catalog %s not properly destroyed", catalog.Catalog.Name))) + } + return task.WaitTaskCompletion() +} +// consumeTasks will cancel all catalog tasks and the ones related to its items +// 1. cancel all tasks associated with the catalog and keep them in a list +// 2. find a list of all catalog items +// 3. find a list of all tasks associated with the organization, with name = "syncCatalogItem" or "createCatalogItem" +// 4. loop through the tasks until we find the ones that belong to one of the items - add them to list in 1. +// 5. cancel all the filtered tasks +// 6. wait for the task list until all are finished +func (catalog *Catalog) consumeTasks() error { + allTasks, err := catalog.client.QueryTaskList(map[string]string{ + "status": "running,preRunning,queued", + }) + if err != nil { + return fmt.Errorf("error getting task list from catalog %s: %s", catalog.Catalog.Name, err) + } + var taskList []string + addTask := func(status, href string) { + if status != "success" && status != "error" && status != "aborted" { + quickTask := Task{ + client: catalog.client, + Task: &types.Task{ + HREF: href, + }, + } + err = quickTask.CancelTask() + if err != nil { + util.Logger.Printf("[consumeTasks] error canceling task: %s\n", err) + } + taskList = append(taskList, extractUuid(href)) + } + } + if catalog.Catalog.Tasks != nil && len(catalog.Catalog.Tasks.Task) > 0 { + for _, task := range catalog.Catalog.Tasks.Task { + addTask(task.Status, task.HREF) + } + } + catalogItemRefs, err := catalog.QueryCatalogItemList() + if err != nil { + return fmt.Errorf("error getting catalog %s items list: %s", catalog.Catalog.Name, err) + } + for _, task := range allTasks { + for _, ref := range catalogItemRefs { + catalogItemId := extractUuid(ref.HREF) + if extractUuid(task.Object) == catalogItemId { + addTask(task.Status, task.HREF) + // No break here: the same object can have more than one task + } + } + } + _, err = catalog.client.WaitTaskListCompletion(taskList, true) + if err != nil { + return fmt.Errorf("error while waiting for task list completion for catalog %s: %s", catalog.Catalog.Name, err) + } return nil } @@ -186,7 +254,7 @@ func (cat *Catalog) UploadOvf(ovaFileName, itemName, description string, uploadP return UploadTask{}, err } - vappTemplate, err := queryVappTemplate(cat.client, vappTemplateUrl, itemName) + vappTemplate, err := queryVappTemplateAndVerifyTask(cat.client, vappTemplateUrl, itemName) if err != nil { return UploadTask{}, err } @@ -212,8 +280,17 @@ func (cat *Catalog) UploadOvf(ovaFileName, itemName, description string, uploadP uploadError := *new(error) - //sending upload process to background, this allows no to lock and return task to client - go uploadFiles(cat.client, vappTemplate, &ovfFileDesc, tmpDir, filesAbsPaths, uploadPieceSize, progressCallBack, &uploadError, isOvf) + // sending upload process to background, this allows not to lock and return task to client + // The error should be captured in uploadError, but just in case, we add a logging for the + // main error + go func() { + err = uploadFiles(cat.client, vappTemplate, &ovfFileDesc, tmpDir, filesAbsPaths, uploadPieceSize, progressCallBack, &uploadError, isOvf) + if err != nil { + util.Logger.Println(strings.Repeat("*", 80)) + util.Logger.Printf("*** [DEBUG - UploadOvf] error calling uploadFiles: %s\n", err) + util.Logger.Println(strings.Repeat("*", 80)) + } + }() var task Task for _, item := range vappTemplate.Tasks.Task { @@ -235,6 +312,101 @@ func (cat *Catalog) UploadOvf(ovaFileName, itemName, description string, uploadP return *uploadTask, nil } +// UploadOvfByLink uploads an OVF file to a catalog from remote URL. +// Returns errors if any occur during upload from VCD or upload process. On upload fail client may need to +// remove VCD catalog item which is in failed state. +func (cat *Catalog) UploadOvfByLink(ovfUrl, itemName, description string) (Task, error) { + + if *cat == (Catalog{}) { + return Task{}, errors.New("catalog can not be empty or nil") + } + + for _, catalogItemName := range getExistingCatalogItems(cat) { + if catalogItemName == itemName { + return Task{}, fmt.Errorf("catalog item '%s' already exists. Upload with different name", itemName) + } + } + + catalogItemUploadURL, err := findCatalogItemUploadLink(cat, "application/vnd.vmware.vcloud.uploadVAppTemplateParams+xml") + if err != nil { + return Task{}, err + } + + vappTemplateUrl, err := createItemWithLink(cat.client, catalogItemUploadURL, itemName, description, ovfUrl) + if err != nil { + return Task{}, err + } + + vappTemplate, err := fetchVappTemplate(cat.client, vappTemplateUrl) + if err != nil { + return Task{}, err + } + + var task Task + for _, item := range vappTemplate.Tasks.Task { + task, err = createTaskForVcdImport(cat.client, item.HREF) + if err != nil { + removeCatalogItemOnError(cat.client, vappTemplateUrl, itemName) + return Task{}, err + } + if task.Task.Status == "error" { + removeCatalogItemOnError(cat.client, vappTemplateUrl, itemName) + return Task{}, fmt.Errorf("task did not complete succesfully: %s", task.Task.Description) + } + } + + util.Logger.Printf("[TRACE] task for vcd import created. \n") + + return task, nil +} + +// CaptureVappTemplate captures a vApp template from an existing vApp +func (cat *Catalog) CaptureVappTemplate(captureParams *types.CaptureVAppParams) (*VAppTemplate, error) { + task, err := cat.CaptureVappTemplateAsync(captureParams) + if err != nil { + return nil, err + } + + err = task.WaitTaskCompletion() + if err != nil { + return nil, err + } + + if task.Task == nil || task.Task.Owner == nil || task.Task.Owner.HREF == "" { + return nil, fmt.Errorf("task does not have Owner HREF populated: %#v", task) + } + + // After the task is finished, owner field contains the resulting vApp template + return cat.GetVappTemplateByHref(task.Task.Owner.HREF) +} + +// CaptureVappTemplateAsync triggers vApp template capturing task and returns it +// +// Note. If 'CaptureVAppParams.CopyTpmOnInstantiate' is set, it will be unset for VCD versions +// before 10.4.2 as it would break API call +func (cat *Catalog) CaptureVappTemplateAsync(captureParams *types.CaptureVAppParams) (Task, error) { + util.Logger.Printf("[TRACE] Capturing vApp template to catalog %s", cat.Catalog.Name) + if captureParams == nil { + return Task{}, fmt.Errorf("input CaptureVAppParams cannot be nil") + } + + captureTemplateHref := cat.client.VCDHREF + captureTemplateHref.Path += fmt.Sprintf("/catalog/%s/action/captureVApp", extractUuid(cat.Catalog.ID)) + + captureParams.Xmlns = types.XMLNamespaceVCloud + captureParams.XmlnsNs0 = types.XMLNamespaceOVF + + util.Logger.Printf("[TRACE] Url for capturing vApp template: %s", captureTemplateHref.String()) + + if cat.client.APIVCDMaxVersionIs("< 37.2") { + captureParams.CopyTpmOnInstantiate = nil + util.Logger.Println("[TRACE] Explicitly unsetting 'CopyTpmOnInstantiate' because it was not supported before VCD 10.4.2") + } + + return cat.client.ExecuteTaskRequest(captureTemplateHref.String(), http.MethodPost, + types.MimeCaptureVappTemplateParams, "error capturing vApp Template: %s", captureParams) +} + // Upload files for vCD created upload links. Different approach then vmdk file are // chunked (e.g. test.vmdk.000000000, test.vmdk.000000001 or test.vmdk). vmdk files are chunked if // in description file attribute ChunkSize is not zero. @@ -308,6 +480,7 @@ func uploadFiles(client *Client, vappTemplate *types.VAppTemplate, ovfFileDesc * return err } } + uploadError = nil return nil } @@ -360,7 +533,7 @@ func waitForTempUploadLinks(client *Client, vappTemplateUrl *url.URL, newItemNam for { util.Logger.Printf("[TRACE] Sleep... for 5 seconds.\n") time.Sleep(time.Second * 5) - vAppTemplate, err = queryVappTemplate(client, vappTemplateUrl, newItemName) + vAppTemplate, err = queryVappTemplateAndVerifyTask(client, vappTemplateUrl, newItemName) if err != nil { return nil, err } @@ -372,17 +545,19 @@ func waitForTempUploadLinks(client *Client, vappTemplateUrl *url.URL, newItemNam return vAppTemplate, nil } -func queryVappTemplate(client *Client, vappTemplateUrl *url.URL, newItemName string) (*types.VAppTemplate, error) { - util.Logger.Printf("[TRACE] Querying vapp template: %s\n", vappTemplateUrl) - - vappTemplateParsed := &types.VAppTemplate{} +func queryVappTemplateAndVerifyTask(client *Client, vappTemplateUrl *url.URL, newItemName string) (*types.VAppTemplate, error) { + util.Logger.Printf("[TRACE] Querying vApp template: %s\n", vappTemplateUrl) - _, err := client.ExecuteRequest(vappTemplateUrl.String(), http.MethodGet, - "", "error querying vApp template: %s", nil, vappTemplateParsed) + vappTemplateParsed, err := fetchVappTemplate(client, vappTemplateUrl) if err != nil { return nil, err } + if vappTemplateParsed.Tasks == nil { + util.Logger.Printf("[ERROR] the vApp Template %s does not contain tasks, an error happened during upload: %v", vappTemplateUrl, vappTemplateParsed) + return vappTemplateParsed, fmt.Errorf("the vApp Template %s does not contain tasks, an error happened during upload", vappTemplateUrl) + } + for _, task := range vappTemplateParsed.Tasks.Task { if task.Status == "error" && newItemName == task.Owner.Name { util.Logger.Printf("[Error] %#v", task.Error) @@ -393,12 +568,25 @@ func queryVappTemplate(client *Client, vappTemplateUrl *url.URL, newItemName str return vappTemplateParsed, nil } +func fetchVappTemplate(client *Client, vappTemplateUrl *url.URL) (*types.VAppTemplate, error) { + util.Logger.Printf("[TRACE] Querying vApp template: %s\n", vappTemplateUrl) + + vappTemplateParsed := &types.VAppTemplate{} + + _, err := client.ExecuteRequest(vappTemplateUrl.String(), http.MethodGet, + "", "error fetching vApp template: %s", nil, vappTemplateParsed) + if err != nil { + return nil, err + } + + return vappTemplateParsed, nil +} + // Uploads ovf description file from unarchived provided ova file. As a result vCD will generate temporary upload links which has to be queried later. // Function will return parsed part for upload files from description xml. func uploadOvfDescription(client *Client, ovfFile string, ovfUploadUrl *url.URL) error { util.Logger.Printf("[TRACE] Uploding ovf description with file: %s and url: %s\n", ovfFile, ovfUploadUrl) - // #nosec G304 - linter does not like 'filePath' to be a variable. However this is necessary for file uploads. - openedFile, err := os.Open(ovfFile) + openedFile, err := os.Open(filepath.Clean(ovfFile)) if err != nil { return err } @@ -424,7 +612,7 @@ func uploadOvfDescription(client *Client, ovfFile string, ovfUploadUrl *url.URL) } func parseOvfFileDesc(file *os.File, ovfFileDesc *Envelope) error { - ovfXml, err := ioutil.ReadAll(file) + ovfXml, err := io.ReadAll(file) if err != nil { return err } @@ -474,7 +662,7 @@ func findFilePath(filesAbsPaths []string, fileName string) string { // Initiates creation of item and returns ovf upload url for created item. func createItemForUpload(client *Client, createHREF *url.URL, catalogItemName string, itemDescription string) (*url.URL, error) { - util.Logger.Printf("[TRACE] createItemForUpload: %s, item name: %v, description: %v \n", createHREF, catalogItemName, itemDescription) + util.Logger.Printf("[TRACE] createItemForUpload: %s, item name: %s, description: %s \n", createHREF, catalogItemName, itemDescription) reqBody := bytes.NewBufferString( "" + "" + itemDescription + "" + @@ -487,7 +675,12 @@ func createItemForUpload(client *Client, createHREF *url.URL, catalogItemName st if err != nil { return nil, err } - defer response.Body.Close() + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + util.Logger.Printf("error closing response Body [createItemForUpload]: %s", err) + } + }(response.Body) catalogItemParsed := &types.CatalogItem{} if err = decodeBody(types.BodyTypeXML, response, catalogItemParsed); err != nil { @@ -504,13 +697,50 @@ func createItemForUpload(client *Client, createHREF *url.URL, catalogItemName st return ovfUploadUrl, nil } +// Initiates creation of item in catalog and returns vappTeamplate Url for created item. +func createItemWithLink(client *Client, createHREF *url.URL, catalogItemName, itemDescription, vappTemplateRemoteUrl string) (*url.URL, error) { + util.Logger.Printf("[TRACE] createItemWithLink: %s, item name: %s, description: %s, vappTemplateRemoteUrl: %s \n", + createHREF, catalogItemName, itemDescription, vappTemplateRemoteUrl) + + reqTemplate := `%s` + reqBody := bytes.NewBufferString(fmt.Sprintf(reqTemplate, types.XMLNamespaceVCloud, catalogItemName, vappTemplateRemoteUrl, itemDescription)) + request := client.NewRequest(map[string]string{}, http.MethodPost, *createHREF, reqBody) + request.Header.Add("Content-Type", "application/vnd.vmware.vcloud.uploadVAppTemplateParams+xml") + + response, err := checkResp(client.Http.Do(request)) + if err != nil { + return nil, err + } + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + util.Logger.Printf("error closing response Body [createItemWithLink]: %s", err) + } + }(response.Body) + + catalogItemParsed := &types.CatalogItem{} + if err = decodeBody(types.BodyTypeXML, response, catalogItemParsed); err != nil { + return nil, err + } + + util.Logger.Printf("[TRACE] Catalog item parsed: %#v\n", catalogItemParsed) + + vappTemplateUrl, err := url.ParseRequestURI(catalogItemParsed.Entity.HREF) + if err != nil { + return nil, err + } + + return vappTemplateUrl, nil +} + // Helper method to get path to multi-part files. -//For example a file called test.vmdk with total_file_size = 100 bytes and part_size = 40 bytes, implies the file is made of *3* part files. -// - test.vmdk.000000000 = 40 bytes -// - test.vmdk.000000001 = 40 bytes -// - test.vmdk.000000002 = 20 bytes -//Say base_dir = /dummy_path/, and base_file_name = test.vmdk then -//the output of this function will be [/dummy_path/test.vmdk.000000000, +// For example a file called test.vmdk with total_file_size = 100 bytes and part_size = 40 bytes, implies the file is made of *3* part files. +// - test.vmdk.000000000 = 40 bytes +// - test.vmdk.000000001 = 40 bytes +// - test.vmdk.000000002 = 20 bytes +// +// Say base_dir = /dummy_path/, and base_file_name = test.vmdk then +// the output of this function will be [/dummy_path/test.vmdk.000000000, // /dummy_path/test.vmdk.000000001, /dummy_path/test.vmdk.000000002] func getChunkedFilePaths(baseDir, baseFileName string, totalFileSize, partSize int) []string { var filePaths []string @@ -536,8 +766,7 @@ func getOvfPath(filesAbsPaths []string) (string, error) { } func getOvf(ovfFilePath string) (Envelope, error) { - // #nosec G304 - linter does not like 'filePath' to be a variable. However this is necessary for file uploads. - openedFile, err := os.Open(ovfFilePath) + openedFile, err := os.Open(filepath.Clean(ovfFilePath)) if err != nil { return Envelope{}, err } @@ -619,11 +848,15 @@ func removeCatalogItemOnError(client *Client, vappTemplateLink *url.URL, itemNam for { util.Logger.Printf("[TRACE] Sleep... for 5 seconds.\n") time.Sleep(time.Second * 5) - vAppTemplate, err = queryVappTemplate(client, vappTemplateLink, itemName) + vAppTemplate, err = queryVappTemplateAndVerifyTask(client, vappTemplateLink, itemName) if err != nil { util.Logger.Printf("[Error] Error deleting Catalog item %s: %s", vappTemplateLink, err) } - if len(vAppTemplate.Tasks.Task) > 0 { + if vAppTemplate.Tasks == nil { + util.Logger.Printf("[Error] Error deleting Catalog item %s: it doesn't contain any task", vappTemplateLink) + return + } + if vAppTemplate.Tasks != nil && len(vAppTemplate.Tasks.Task) > 0 { util.Logger.Printf("[TRACE] Task found. Will try to cancel.\n") break } @@ -644,7 +877,14 @@ func removeCatalogItemOnError(client *Client, vappTemplateLink *url.URL, itemNam } } +// UploadMediaImage uploads a media image to the catalog func (cat *Catalog) UploadMediaImage(mediaName, mediaDescription, filePath string, uploadPieceSize int64) (UploadTask, error) { + return cat.UploadMediaFile(mediaName, mediaDescription, filePath, uploadPieceSize, true) +} + +// UploadMediaFile uploads any file to the catalog. +// However, if checkFileIsIso is true, only .ISO are allowed. +func (cat *Catalog) UploadMediaFile(fileName, mediaDescription, filePath string, uploadPieceSize int64, checkFileIsIso bool) (UploadTask, error) { if *cat == (Catalog{}) { return UploadTask{}, errors.New("catalog can not be empty or nil") @@ -655,9 +895,11 @@ func (cat *Catalog) UploadMediaImage(mediaName, mediaDescription, filePath strin return UploadTask{}, err } - isISOGood, err := verifyIso(mediaFilePath) - if err != nil || !isISOGood { - return UploadTask{}, fmt.Errorf("[ERROR] File %s isn't correct iso file: %#v", mediaFilePath, err) + if checkFileIsIso { + isISOGood, err := verifyIso(mediaFilePath) + if err != nil || !isISOGood { + return UploadTask{}, fmt.Errorf("[ERROR] File %s isn't correct iso file: %#v", mediaFilePath, err) + } } file, e := os.Stat(mediaFilePath) @@ -667,8 +909,8 @@ func (cat *Catalog) UploadMediaImage(mediaName, mediaDescription, filePath strin fileSize := file.Size() for _, catalogItemName := range getExistingCatalogItems(cat) { - if catalogItemName == mediaName { - return UploadTask{}, fmt.Errorf("media item '%s' already exists. Upload with different name", mediaName) + if catalogItemName == fileName { + return UploadTask{}, fmt.Errorf("media item '%s' already exists. Upload with different name", fileName) } } @@ -677,17 +919,17 @@ func (cat *Catalog) UploadMediaImage(mediaName, mediaDescription, filePath strin return UploadTask{}, err } - media, err := createMedia(cat.client, catalogItemUploadURL.String(), mediaName, mediaDescription, fileSize) + media, err := createMedia(cat.client, catalogItemUploadURL.String(), fileName, mediaDescription, fileSize) if err != nil { return UploadTask{}, fmt.Errorf("[ERROR] Issue creating media: %#v", err) } - createdMedia, err := queryMedia(cat.client, media.Entity.HREF, mediaName) + createdMedia, err := queryMedia(cat.client, media.Entity.HREF, fileName) if err != nil { return UploadTask{}, err } - return executeUpload(cat.client, createdMedia, mediaFilePath, mediaName, fileSize, uploadPieceSize) + return executeUpload(cat.client, createdMedia, mediaFilePath, fileName, fileSize, uploadPieceSize) } // Refresh gets a fresh copy of the catalog from vCD @@ -727,11 +969,16 @@ func (cat *Catalog) GetCatalogItemByHref(catalogItemHref string) (*CatalogItem, // On success, returns a pointer to the vApp template structure and a nil error // On failure, returns a nil pointer and an error func (cat *Catalog) GetVappTemplateByHref(href string) (*VAppTemplate, error) { + return getVAppTemplateByHref(cat.client, href) +} - vappTemplate := NewVAppTemplate(cat.client) +// getVAppTemplateByHref finds a vApp template by HREF +// On success, returns a pointer to the vApp template structure and a nil error +// On failure, returns a nil pointer and an error +func getVAppTemplateByHref(client *Client, href string) (*VAppTemplate, error) { + vappTemplate := NewVAppTemplate(client) - _, err := cat.client.ExecuteRequest(href, http.MethodGet, - "", "error retrieving catalog item: %s", nil, vappTemplate.VAppTemplate) + _, err := client.ExecuteRequest(href, http.MethodGet, "", "error retrieving vApp Template: %s", nil, vappTemplate.VAppTemplate) if err != nil { return nil, err } @@ -758,6 +1005,17 @@ func (cat *Catalog) GetCatalogItemByName(catalogItemName string, refresh bool) ( return nil, ErrorEntityNotFound } +// GetVAppTemplateByName finds a VAppTemplate by Name +// On success, returns a pointer to the VAppTemplate structure and a nil error +// On failure, returns a nil pointer and an error +func (cat *Catalog) GetVAppTemplateByName(vAppTemplateName string) (*VAppTemplate, error) { + vAppTemplateQueryResult, err := cat.QueryVappTemplateWithName(vAppTemplateName) + if err != nil { + return nil, err + } + return cat.GetVappTemplateByHref(vAppTemplateQueryResult.HREF) +} + // GetCatalogItemById finds a Catalog Item by ID // On success, returns a pointer to the CatalogItem structure and a nil error // On failure, returns a nil pointer and an error @@ -778,7 +1036,28 @@ func (cat *Catalog) GetCatalogItemById(catalogItemId string, refresh bool) (*Cat return nil, ErrorEntityNotFound } -// GetCatalogItemByNameOrId finds a Catalog Item by Name or ID +// GetVAppTemplateById finds a vApp Template by ID. +// On success, returns a pointer to the VAppTemplate structure and a nil error. +// On failure, returns a nil pointer and an error. +func (cat *Catalog) GetVAppTemplateById(vAppTemplateId string) (*VAppTemplate, error) { + return getVAppTemplateById(cat.client, vAppTemplateId) +} + +// getVAppTemplateById finds a vApp Template by ID. +// On success, returns a pointer to the VAppTemplate structure and a nil error. +// On failure, returns a nil pointer and an error. +func getVAppTemplateById(client *Client, vAppTemplateId string) (*VAppTemplate, error) { + vappTemplateHref := client.VCDHREF + vappTemplateHref.Path += "/vAppTemplate/vappTemplate-" + extractUuid(vAppTemplateId) + + vappTemplate, err := getVAppTemplateByHref(client, vappTemplateHref.String()) + if err != nil { + return nil, fmt.Errorf("could not find vApp Template with ID %s: %s", vAppTemplateId, err) + } + return vappTemplate, nil +} + +// GetCatalogItemByNameOrId finds a Catalog Item by Name or ID. // On success, returns a pointer to the CatalogItem structure and a nil error // On failure, returns a nil pointer and an error func (cat *Catalog) GetCatalogItemByNameOrId(identifier string, refresh bool) (*CatalogItem, error) { @@ -791,6 +1070,19 @@ func (cat *Catalog) GetCatalogItemByNameOrId(identifier string, refresh bool) (* return entity.(*CatalogItem), err } +// GetVAppTemplateByNameOrId finds a vApp Template by Name or ID. +// On success, returns a pointer to the VAppTemplate structure and a nil error +// On failure, returns a nil pointer and an error +func (cat *Catalog) GetVAppTemplateByNameOrId(identifier string, refresh bool) (*VAppTemplate, error) { + getByName := func(name string, refresh bool) (interface{}, error) { return cat.GetVAppTemplateByName(name) } + getById := func(id string, refresh bool) (interface{}, error) { return cat.GetVAppTemplateById(id) } + entity, err := getEntityByNameOrIdSkipNonId(getByName, getById, identifier, refresh) + if entity == nil { + return nil, err + } + return entity.(*VAppTemplate), err +} + // QueryMediaList retrieves a list of media items for the catalog func (catalog *Catalog) QueryMediaList() ([]*types.MediaRecordType, error) { typeMedia := "media" @@ -798,10 +1090,10 @@ func (catalog *Catalog) QueryMediaList() ([]*types.MediaRecordType, error) { typeMedia = "adminMedia" } - filter := fmt.Sprintf("catalog==" + url.QueryEscape(catalog.Catalog.HREF)) + filter := fmt.Sprintf("catalog==%s", url.QueryEscape(catalog.Catalog.HREF)) results, err := catalog.client.QueryWithNotEncodedParams(nil, map[string]string{"type": typeMedia, "filter": filter, "filterEncoded": "true"}) if err != nil { - return nil, fmt.Errorf("error querying medias %s", err) + return nil, fmt.Errorf("error querying medias: %s", err) } mediaResults := results.Results.MediaRecord @@ -811,43 +1103,191 @@ func (catalog *Catalog) QueryMediaList() ([]*types.MediaRecordType, error) { return mediaResults, nil } -// getOrgInfo finds the organization to which the entity belongs, and returns its name and ID -func getOrgInfo(client *Client, links types.LinkList, id, name, entityType string) (orgInfoType, error) { - previous, exists := orgInfoCache[id] - if exists { - return previous, nil +// getOrgInfo finds the organization to which the catalog belongs, and returns its name and ID +func (catalog *Catalog) getOrgInfo() (*TenantContext, error) { + org := catalog.parent + if org == nil { + return nil, fmt.Errorf("no parent found for catalog %s", catalog.Catalog.Name) } - var orgId string - var orgHref string - var err error - for _, link := range links { - if link.Rel == "up" && (link.Type == types.MimeOrg || link.Type == types.MimeAdminOrg) { - orgId, err = GetUuidFromHref(link.HREF, true) + + return org.tenantContext() +} + +func publishToExternalOrganizations(client *Client, url string, tenantContext *TenantContext, publishExternalCatalog types.PublishExternalCatalogParams) error { + url = url + "/action/publishToExternalOrganizations" + + publishExternalCatalog.Xmlns = types.XMLNamespaceVCloud + + if tenantContext != nil { + client.SetCustomHeader(getTenantContextHeader(tenantContext)) + } + + err := client.ExecuteRequestWithoutResponse(url, http.MethodPost, + types.PublishExternalCatalog, "error publishing to external organization: %s", publishExternalCatalog) + + if tenantContext != nil { + client.RemoveProvidedCustomHeaders(getTenantContextHeader(tenantContext)) + } + + return err +} + +// PublishToExternalOrganizations publishes a catalog to external organizations. +func (cat *Catalog) PublishToExternalOrganizations(publishExternalCatalog types.PublishExternalCatalogParams) error { + if cat.Catalog == nil { + return fmt.Errorf("cannot publish to external organization, Object is empty") + } + + catalogUrl := cat.Catalog.HREF + if catalogUrl == "nil" || catalogUrl == "" { + return fmt.Errorf("cannot publish to external organization, HREF is empty") + } + + err := publishToExternalOrganizations(cat.client, catalogUrl, nil, publishExternalCatalog) + if err != nil { + return err + } + + err = cat.Refresh() + if err != nil { + return err + } + + return err +} + +// elementSync is a low level function that synchronises a Catalog, AdminCatalog, CatalogItem, or Media item +func elementSync(client *Client, elementHref, label string) error { + task, err := elementLaunchSync(client, elementHref, label) + if err != nil { + return err + } + return task.WaitTaskCompletion() +} + +// queryMediaList retrieves a list of media items for a given catalog or AdminCatalog +func queryMediaList(client *Client, catalogHref string) ([]*types.MediaRecordType, error) { + typeMedia := "media" + if client.IsSysAdmin { + typeMedia = "adminMedia" + } + + filter := fmt.Sprintf("catalog==%s", url.QueryEscape(catalogHref)) + results, err := client.QueryWithNotEncodedParams(nil, map[string]string{"type": typeMedia, "filter": filter, "filterEncoded": "true"}) + if err != nil { + return nil, fmt.Errorf("error querying medias: %s", err) + } + + mediaResults := results.Results.MediaRecord + if client.IsSysAdmin { + mediaResults = results.Results.AdminMediaRecord + } + return mediaResults, nil +} + +// elementLaunchSync is a low level function that starts synchronisation for Catalog, AdminCatalog, CatalogItem, or Media item +func elementLaunchSync(client *Client, elementHref, label string) (*Task, error) { + util.Logger.Printf("[TRACE] elementLaunchSync '%s' \n", label) + href := elementHref + "/action/sync" + syncTask, err := client.ExecuteTaskRequest(href, http.MethodPost, + "", "error synchronizing "+label+": %s", nil) + + if err != nil { + // This process may fail due to a possible race condition: a synchronisation process may start in background + // after we check for existing tasks (in the function that called this one) + // and before we run the request in this function. + // In a Terraform vcd_subscribed_catalog operation, the completeness of the synchronisation + // will be ensured at the next refresh. + if strings.Contains(err.Error(), "LIBRARY_ITEM_SYNC") { + util.Logger.Printf("[SYNC FAILURE] error when launching synchronisation: %s\n", err) + return nil, nil + } + return nil, err + } + return &syncTask, nil +} + +// QueryTaskList retrieves a list of tasks associated to the Catalog +func (catalog *Catalog) QueryTaskList(filter map[string]string) ([]*types.QueryResultTaskRecordType, error) { + var newFilter = map[string]string{ + "object": catalog.Catalog.HREF, + } + for k, v := range filter { + newFilter[k] = v + } + return catalog.client.QueryTaskList(newFilter) +} + +// GetCatalogByHref allows retrieving a catalog from HREF, without a fully qualified Org object +func (client *Client) GetCatalogByHref(catalogHref string) (*Catalog, error) { + catalogHref = strings.Replace(catalogHref, "/api/admin/catalog", "/api/catalog", 1) + + cat := NewCatalog(client) + + _, err := client.ExecuteRequest(catalogHref, http.MethodGet, + "", "error retrieving catalog: %s", nil, cat.Catalog) + + if err != nil { + return nil, err + } + // Setting the catalog parent, necessary to handle the tenant context + org := NewOrg(client) + for _, link := range cat.Catalog.Link { + if link.Rel == "up" && link.Type == types.MimeOrg { + _, err = client.ExecuteRequest(link.HREF, http.MethodGet, + "", "error retrieving parent Org: %s", nil, org.Org) if err != nil { - return orgInfoType{}, err + return nil, fmt.Errorf("error retrieving catalog parent: %s", err) } - orgHref = link.HREF break } } - if orgHref == "" || orgId == "" { - return orgInfoType{}, fmt.Errorf("error retrieving org info for %s %s", entityType, name) - } - var org types.Org - _, err = client.ExecuteRequest(orgHref, http.MethodGet, - "", "error retrieving org: %s", nil, &org) + cat.parent = org + return cat, nil +} + +// GetCatalogById allows retrieving a catalog from ID, without a fully qualified Org object +func (client *Client) GetCatalogById(catalogId string) (*Catalog, error) { + href, err := url.JoinPath(client.VCDHREF.String(), "catalog", extractUuid(catalogId)) if err != nil { - return orgInfoType{}, err + return nil, err } + return client.GetCatalogByHref(href) +} - orgInfoCache[id] = orgInfoType{ - id: orgId, - name: org.Name, +// GetCatalogByName allows retrieving a catalog from name, without a fully qualified Org object +func (client *Client) GetCatalogByName(parentOrg, catalogName string) (*Catalog, error) { + catalogs, err := queryCatalogList(client, nil) + if err != nil { + return nil, err + } + var parentOrgs []string + for _, cat := range catalogs { + if cat.Name == catalogName && cat.OrgName == parentOrg { + return client.GetCatalogByHref(cat.HREF) + } + if cat.Name == catalogName { + parentOrgs = append(parentOrgs, cat.OrgName) + } + } + parents := "" + if len(parentOrgs) > 0 { + parents = fmt.Sprintf(" - Found catalog %s in Orgs %v", catalogName, parentOrgs) } - return orgInfoType{name: org.Name, id: orgId}, nil + return nil, fmt.Errorf("no catalog '%s' found in Org %s%s", catalogName, parentOrg, parents) } -// getOrgInfo finds the organization to which the catalog belongs, and returns its name and ID -func (catalog *Catalog) getOrgInfo() (orgInfoType, error) { - return getOrgInfo(catalog.client, catalog.Catalog.Link, catalog.Catalog.ID, catalog.Catalog.Name, "Catalog") +// WaitForTasks waits for the catalog's tasks to complete +func (cat *Catalog) WaitForTasks() error { + if ResourceInProgress(cat.Catalog.Tasks) { + err := WaitResource(func() (*types.TasksInProgress, error) { + err := cat.Refresh() + if err != nil { + return nil, err + } + return cat.Catalog.Tasks, nil + }) + return err + } + return nil } diff --git a/govcd/catalog_subscription_test.go b/govcd/catalog_subscription_test.go new file mode 100644 index 000000000..1f52c5701 --- /dev/null +++ b/govcd/catalog_subscription_test.go @@ -0,0 +1,378 @@ +//go:build catalog || functional || ALL + +/* + * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "net/url" + "strings" + "time" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +type subscriptionTestData struct { + fromOrg *AdminOrg + toOrg *AdminOrg + ovaPath string + mediaPath string + localCopy bool + storageProfile types.CatalogStorageProfiles + uploadWhen string + preservePublishingCatalog bool + asynchronousSubscription bool +} + +// Test_SubscribedCatalog tests four scenarios of Catalog subscription +// All cases use a publishing catalog in one Org and a subscribing catalog +// in a different Org. +// The scenarios are a combination of these two facts: +// * whether the subscribing catalog was created before or after the publishing catalog was filled +// * whether the subscribing catalog enabled automatic downloads (localCopy) +// +// To see the inner working of the test components, you may run it as follows: +// $ export GOVCD_TASK_MONITOR=simple_show +// $ go test -tags catalog -check.f Test_SubscribedCatalog -vcd-verbose -check.vv -timeout 0 +// When running this way, you will see the tasks originated by the catalogs and the ones started by the catalog items +func (vcd *TestVCD) Test_SubscribedCatalog(check *C) { + vcd.skipIfNotSysAdmin(check) + fromOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + toOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org + "-1") + check.Assert(err, IsNil) + + if toOrg.AdminOrg.Vdcs == nil || len(toOrg.AdminOrg.Vdcs.Vdcs) == 0 { + check.Skip(fmt.Sprintf("receiving org %s does not have any storage", toOrg.AdminOrg.Name)) + } + // TODO: remove this workaround when support for 10.3.3 is dropped + // See Test_PublishToExternalOrganizations for details + fromOrg.AdminOrg.OrgSettings.OrgGeneralSettings.CanPublishCatalogs = true + fromOrg.AdminOrg.OrgSettings.OrgGeneralSettings.CanPublishExternally = true + _, err = fromOrg.Update() + + check.Assert(err, IsNil) + vdc, err := fromOrg.GetVDCByName(vcd.config.VCD.Nsxt.Vdc, false) + check.Assert(err, IsNil) + + storageProfile, err := vdc.FindStorageProfileReference(vcd.config.VCD.StorageProfile.SP1) + check.Assert(err, IsNil) + createStorageProfiles := types.CatalogStorageProfiles{VdcStorageProfile: []*types.Reference{&storageProfile}} + + testSubscribedCatalog(subscriptionTestData{ + fromOrg: fromOrg, + toOrg: toOrg, + ovaPath: vcd.config.OVA.OvaPath, + mediaPath: vcd.config.Media.MediaPath, + localCopy: false, + storageProfile: createStorageProfiles, + uploadWhen: "after_subscription", + asynchronousSubscription: true, + }, check) + + testSubscribedCatalog(subscriptionTestData{ + fromOrg: fromOrg, + toOrg: toOrg, + ovaPath: vcd.config.OVA.OvaPath, + mediaPath: vcd.config.Media.MediaPath, + localCopy: true, + storageProfile: createStorageProfiles, + uploadWhen: "after_subscription", + preservePublishingCatalog: true, + asynchronousSubscription: true, + }, check) + + // For the tests where the items are uploaded before subscription, we can keep the publishing catalog + // from the previous test + testSubscribedCatalog(subscriptionTestData{ + fromOrg: fromOrg, + toOrg: toOrg, + ovaPath: vcd.config.OVA.OvaPath, + mediaPath: vcd.config.Media.MediaPath, + localCopy: false, + storageProfile: createStorageProfiles, + uploadWhen: "before_subscription", + preservePublishingCatalog: true, + }, check) + testSubscribedCatalog(subscriptionTestData{ + fromOrg: fromOrg, + toOrg: toOrg, + ovaPath: vcd.config.OVA.OvaPath, + mediaPath: vcd.config.Media.MediaPath, + localCopy: true, + storageProfile: createStorageProfiles, + uploadWhen: "before_subscription", + preservePublishingCatalog: false, // at the last subtest, we remove the publishing catalog + }, check) + +} + +func uploadTestItems(org *AdminOrg, catalogName, templatePath, mediaPath string, numTemplates, numMedia int) error { + var taskList []*Task + + catalog, err := org.GetCatalogByName(catalogName, true) + if err != nil { + return fmt.Errorf("catalog %s not found: %s", catalogName, err) + } + + for i := 1; i <= numTemplates; i++ { + templateName := fmt.Sprintf("test-vt-%d", i) + uploadTask, err := catalog.UploadOvf(templatePath, templateName, "upload from test", 1024) + if err != nil { + return err + } + taskList = append(taskList, uploadTask.Task) + } + for i := 1; i <= numMedia; i++ { + mediaName := fmt.Sprintf("test_media-%d", i) + uploadTask, err := catalog.UploadMediaImage(mediaName, "upload from test", mediaPath, 1024) + if err != nil { + return err + } + taskList = append(taskList, uploadTask.Task) + } + _, err = WaitTaskListCompletionMonitor(taskList, testMonitor) + fmt.Println() + return err +} + +func testSubscribedCatalog(testData subscriptionTestData, check *C) { + + startSubtest := time.Now() + drawHeader := func(char, msg string) { + fmt.Println(strings.Repeat(char, 80)) + fmt.Printf("%s %s\n", char, msg) + } + drawHeader("*", fmt.Sprintf("START: upload %s - local copy: %v", testData.uploadWhen, testData.localCopy)) + + fromOrg := testData.fromOrg + toOrg := testData.toOrg + + publishingCatalogName := "Publisher" + subscribingCatalogName := "Subscriber" + + var fromCatalog *AdminCatalog + var err error + fromCatalog, err = fromOrg.GetAdminCatalogByName(publishingCatalogName, true) + if err == nil { + drawHeader("-", "publishing catalog retrieved from previous test") + } else { + drawHeader("-", "creating publishing catalog") + fromCatalog, err = fromOrg.CreateCatalogWithStorageProfile(publishingCatalogName, "publisher catalog", &testData.storageProfile) + check.Assert(err, IsNil) + AddToCleanupList(publishingCatalogName, "catalog", fromOrg.AdminOrg.Name, check.TestName()) + } + + subscriptionPassword := "superUnknown" + err = fromCatalog.PublishToExternalOrganizations(types.PublishExternalCatalogParams{ + IsPublishedExternally: addrOf(true), + Password: subscriptionPassword, + IsCachedEnabled: addrOf(true), + PreserveIdentityInfoFlag: addrOf(true), + }) + check.Assert(err, IsNil) + + uploadItemsIf := func(wanted string) { + if wanted != testData.uploadWhen { + return + } + howManyTemplates := 3 + howManyMediaItems := 3 + publishedCatalogItems, err := fromCatalog.QueryCatalogItemList() + if err == nil && len(publishedCatalogItems) == (howManyMediaItems+howManyTemplates) { + return + } + drawHeader("-", fmt.Sprintf("uploading catalog items - %s", wanted)) + err = uploadTestItems(fromOrg, fromCatalog.AdminCatalog.Name, testData.ovaPath, testData.mediaPath, howManyTemplates, howManyMediaItems) + check.Assert(err, IsNil) + } + err = fromCatalog.Refresh() + check.Assert(err, IsNil) + + check.Assert(fromCatalog.AdminCatalog.PublishExternalCatalogParams, NotNil) + check.Assert(fromCatalog.AdminCatalog.PublishExternalCatalogParams.CatalogPublishedUrl, Not(Equals), "") + + uploadItemsIf("before_subscription") + err = fromCatalog.Refresh() + check.Assert(err, IsNil) + + subscriptionUrl, err := fromCatalog.FullSubscriptionUrl() + check.Assert(err, IsNil) + + subscriptionParams := types.ExternalCatalogSubscription{ + SubscribeToExternalFeeds: true, + Location: subscriptionUrl, + Password: subscriptionPassword, + LocalCopy: testData.localCopy, + } + + var toCatalog *AdminCatalog + testSubscribedCatalogWithInvalidParameters(toOrg, subscriptionParams, subscribingCatalogName, subscriptionPassword, testData.localCopy, check) + if testData.asynchronousSubscription { + drawHeader("-", "creating subscribed catalog asynchronously") + // With asynchronous subscription the catalog starts the subscription but does not report its state, which is + // monitored by its internal Task + toCatalog, err = toOrg.CreateCatalogFromSubscriptionAsync( + subscriptionParams, // params + nil, // storage profile + subscribingCatalogName, // catalog name + subscriptionPassword, // password + testData.localCopy) // local copy + } else { + drawHeader("-", "creating subscribed catalog and waiting for completion") + toCatalog, err = toOrg.CreateCatalogFromSubscription( + subscriptionParams, // params + nil, // storage profile + subscribingCatalogName, // catalog name + subscriptionPassword, // password + testData.localCopy, // local copy + 10*time.Minute) // timeout + } + check.Assert(err, IsNil) + AddToCleanupList(subscribingCatalogName, "catalog", toOrg.AdminOrg.Name, check.TestName()) + + if testData.asynchronousSubscription { + err = toCatalog.Refresh() + check.Assert(err, IsNil) + if ResourceInProgress(toCatalog.AdminCatalog.Tasks) { + fmt.Println("catalog subscription tasks still in progress") + for _, task := range toCatalog.AdminCatalog.Tasks.Task { + testMonitor(task) + } + } else { + fmt.Println("catalog subscription tasks complete") + } + } + + uploadItemsIf("after_subscription") + + // If the catalog items were uploaded before the catalog subscription, we don't need to + // synchronise, as the subscription would have got at least the list of items + if testData.uploadWhen != "before_subscription" { + drawHeader("-", "synchronising catalog") + err = toCatalog.Sync() + check.Assert(err, IsNil) + } + + publishedCatalogItems, err := fromCatalog.QueryCatalogItemList() + check.Assert(err, IsNil) + subscribedCatalogItems, err := toCatalog.QueryCatalogItemList() + check.Assert(err, IsNil) + fmt.Printf("Catalog items after catalog sync: %d\n", len(subscribedCatalogItems)) + publishedVappTemplates, err := fromCatalog.QueryVappTemplateList() + check.Assert(err, IsNil) + subscribedVappTemplates, err := toCatalog.QueryVappTemplateList() + check.Assert(err, IsNil) + publishedMediaItems, err := fromCatalog.QueryMediaList() + check.Assert(err, IsNil) + subscribedMediaItems, err := toCatalog.QueryMediaList() + check.Assert(err, IsNil) + + fmt.Printf("vApp template after catalog sync %d\n", len(subscribedVappTemplates)) + fmt.Printf("media item after catalog sync %d\n", len(subscribedMediaItems)) + + check.Assert(len(subscribedCatalogItems), Equals, len(publishedCatalogItems)) + check.Assert(len(subscribedVappTemplates), Equals, len(publishedVappTemplates)) + check.Assert(len(subscribedMediaItems), Equals, len(publishedMediaItems)) + + if testData.localCopy && testData.uploadWhen == "before_subscription" { + // we should have all the contents here if the data was available early + // and the subscribed catalog uses automatic download + retrieveCatalogItems(toCatalog, subscribedCatalogItems, check) + } + + // Synchronising all vApp templates and media items. If the subscription includes local copy, + // the synchronisation has alredy happened, and this extra call is very quick (~5 seconds) + drawHeader("-", "synchronising vApp templates and media items") + tasksVappTemplates, err := toCatalog.LaunchSynchronisationAllVappTemplates() + check.Assert(err, IsNil) + tasksMediaItems, err := toCatalog.LaunchSynchronisationAllMediaItems() + check.Assert(err, IsNil) + + // Wait for all synchronisation tasks to end + var allTasks []*Task + allTasks = append(allTasks, tasksVappTemplates...) + allTasks = append(allTasks, tasksMediaItems...) + _, err = WaitTaskListCompletionMonitor(allTasks, testMonitor) + if !testVerbose { + fmt.Println() + } + check.Assert(err, IsNil) + + // after a full synchronisation, all data should be available under every condition + retrieveCatalogItems(toCatalog, subscribedCatalogItems, check) + + startDelete := time.Now() + err = toCatalog.Delete(true, true) + check.Assert(err, IsNil) + fmt.Printf("subscribed catalog deletion done in %s\n", time.Since(startDelete)) + startDelete = time.Now() + if !testData.preservePublishingCatalog { + err = fromCatalog.Delete(true, true) + check.Assert(err, IsNil) + fmt.Printf("published catalog deletion done in %s\n", time.Since(startDelete)) + } + drawHeader("=", fmt.Sprintf("END: upload %s - local copy: %v - Time taken: %s", testData.uploadWhen, testData.localCopy, time.Since(startSubtest))) +} + +func retrieveCatalogItems(toCatalog *AdminCatalog, subscribed []*types.QueryResultCatalogItemType, check *C) { + for _, item := range subscribed { + catalogItem, err := toCatalog.GetCatalogItemByHref(item.HREF) + check.Assert(err, IsNil) + switch catalogItem.CatalogItem.Entity.Type { + case types.MimeVAppTemplate: + vAppTemplate, err := catalogItem.GetVAppTemplate() + check.Assert(err, IsNil) + check.Assert(vAppTemplate.VAppTemplate.HREF, Equals, catalogItem.CatalogItem.Entity.HREF) + case types.MimeMediaItem: + mediaItem, err := toCatalog.GetMediaByHref(catalogItem.CatalogItem.Entity.HREF) + check.Assert(err, IsNil) + check.Assert(extractUuid(mediaItem.Media.ID), Equals, extractUuid(catalogItem.CatalogItem.Entity.HREF)) + } + } +} + +func testMonitor(task *types.Task) { + if testVerbose { + fmt.Printf("task %s - owner %s - operation %s - status %s - progress %d\n", task.ID, task.Owner.Name, task.Operation, task.Status, task.Progress) + } else { + marker := "." + if task.Status == "success" { + marker = "+" + } + if task.Status == "error" { + marker = "-" + } + fmt.Print(marker) + } +} + +func testSubscribedCatalogWithInvalidParameters(org *AdminOrg, subscription types.ExternalCatalogSubscription, + name, password string, localCopy bool, check *C) { + + uuid := extractUuid(subscription.Location) + params := subscription + params.Location = strings.Replace(params.Location, uuid, "deadbeef-d72f-4a21-a4d2-4dc9e0b36555", 1) + // Use a valid host with invalid UUID + _, err := org.CreateCatalogFromSubscriptionAsync(params, nil, name, password, localCopy) + check.Assert(err, ErrorMatches, ".*RESOURCE_NOT_FOUND.*") + + newUrl, err := url.Parse(subscription.Location) + check.Assert(err, IsNil) + + params = subscription + params.Location = strings.Replace(params.Location, newUrl.Host, "fake.example.com", 1) + // use an invalid host + _, err = org.CreateCatalogFromSubscriptionAsync(params, nil, name, password, localCopy) + check.Assert(err, ErrorMatches, ".*INVALID_URL_OR_PASSWORD.*") + + params = subscription + params.Location = "not-an-URL" + // use an invalid URL + _, err = org.CreateCatalogFromSubscriptionAsync(params, nil, name, password, localCopy) + check.Assert(err, ErrorMatches, ".*UNKNOWN_ERROR.*") +} diff --git a/govcd/catalog_test.go b/govcd/catalog_test.go index 41ea54118..521da0535 100644 --- a/govcd/catalog_test.go +++ b/govcd/catalog_test.go @@ -1,4 +1,4 @@ -// +build catalog functional ALL +//go:build catalog || functional || ALL /* * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -8,7 +8,7 @@ package govcd import ( "fmt" - "io/ioutil" + "io" "log" "os" "strings" @@ -83,6 +83,54 @@ func (vcd *TestVCD) Test_FindCatalogItem(check *C) { check.Assert(catalogItem, IsNil) } +func (vcd *TestVCD) Test_FindVAppTemplate(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + + // Prepare test + cat, err := vcd.org.GetCatalogByName(vcd.config.VCD.Catalog.Name, false) + if err != nil { + check.Skip(fmt.Sprintf("%s: Catalog not found. Test can't proceed", check.TestName())) + return + } + if vcd.config.VCD.Catalog.CatalogItem == "" { + check.Skip(fmt.Sprintf("%s: Catalog Item not given. Test can't proceed", check.TestName())) + } + + // Test cases + vAppTemplate, err := cat.GetVAppTemplateByName(vcd.config.VCD.Catalog.CatalogItem) + check.Assert(err, IsNil) + check.Assert(vAppTemplate.VAppTemplate.Name, Equals, vcd.config.VCD.Catalog.CatalogItem) + if vcd.config.VCD.Catalog.CatalogItemDescription != "" { + check.Assert(vAppTemplate.VAppTemplate.Description, Equals, vcd.config.VCD.Catalog.CatalogItemDescription) + } + + vAppTemplate, err = cat.GetVAppTemplateById(vAppTemplate.VAppTemplate.ID) + check.Assert(err, IsNil) + check.Assert(vAppTemplate.VAppTemplate.Name, Equals, vcd.config.VCD.Catalog.CatalogItem) + if vcd.config.VCD.Catalog.CatalogItemDescription != "" { + check.Assert(vAppTemplate.VAppTemplate.Description, Equals, vcd.config.VCD.Catalog.CatalogItemDescription) + } + + vAppTemplate, err = cat.GetVAppTemplateByNameOrId(vAppTemplate.VAppTemplate.ID, false) + check.Assert(err, IsNil) + check.Assert(vAppTemplate.VAppTemplate.Name, Equals, vcd.config.VCD.Catalog.CatalogItem) + if vcd.config.VCD.Catalog.CatalogItemDescription != "" { + check.Assert(vAppTemplate.VAppTemplate.Description, Equals, vcd.config.VCD.Catalog.CatalogItemDescription) + } + + vAppTemplate, err = cat.GetVAppTemplateByNameOrId(vcd.config.VCD.Catalog.CatalogItem, false) + check.Assert(err, IsNil) + check.Assert(vAppTemplate.VAppTemplate.Name, Equals, vcd.config.VCD.Catalog.CatalogItem) + if vcd.config.VCD.Catalog.CatalogItemDescription != "" { + check.Assert(vAppTemplate.VAppTemplate.Description, Equals, vcd.config.VCD.Catalog.CatalogItemDescription) + } + + // Test non-existent vApp Template + vAppTemplate, err = cat.GetVAppTemplateByName("INVALID") + check.Assert(err, NotNil) + check.Assert(vAppTemplate, IsNil) +} + // Creates a Catalog, updates the description, and checks the changes against the // newly updated catalog. Then deletes the catalog func (vcd *TestVCD) Test_UpdateCatalog(check *C) { @@ -129,8 +177,15 @@ func (vcd *TestVCD) Test_DeleteCatalog(check *C) { check.Assert(err, IsNil) // After a successful creation, the entity is added to the cleanup list. // If something fails after this point, the entity will be removed - AddToCleanupList(TestDeleteCatalog, "catalog", vcd.config.VCD.Org, "Test_DeleteCatalog") + AddToCleanupList(TestDeleteCatalog, "catalog", vcd.config.VCD.Org, check.TestName()) check.Assert(adminCatalog.AdminCatalog.Name, Equals, TestDeleteCatalog) + + checkUploadOvf(vcd, check, vcd.config.OVA.OvaPath, TestDeleteCatalog, TestUploadOvf+"_"+check.TestName(), false) + err = adminCatalog.Delete(false, false) + check.Assert(err, NotNil) + // Catalog is not empty. An attempt to delete without recursion will fail + check.Assert(strings.Contains(err.Error(), "You must remove"), Equals, true) + err = adminCatalog.Delete(true, true) check.Assert(err, IsNil) doesCatalogExist(check, org) @@ -150,13 +205,80 @@ func doesCatalogExist(check *C, org *AdminOrg) { check.Assert(err, NotNil) } +// Creates a Catalog, uploads a vApp template to it, renames it, retrieves it +// using the updated name and checks if it has the same vApp template. +// If it doesn't the assertion fails. +func (vcd *TestVCD) Test_RenameCatalog(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + testRenameCatalog := check.TestName() + testUploadOvf := check.TestName() + "_ovf" + testUploadMedia := check.TestName() + "_media" + + org, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(org, NotNil) + + adminCatalog, err := org.CreateCatalog(testRenameCatalog, testRenameCatalog) + check.Assert(err, IsNil) + check.Assert(adminCatalog, NotNil) + AddToCleanupList(testRenameCatalog, "catalog", vcd.config.VCD.Org, check.TestName()) + + catalog, err := vcd.client.Client.GetCatalogByName(vcd.config.VCD.Org, testRenameCatalog) + check.Assert(err, IsNil) + check.Assert(catalog, NotNil) + + uploadTask, err := adminCatalog.UploadOvf(vcd.config.OVA.OvaPath, testUploadOvf, testUploadOvf, 1024) + check.Assert(err, IsNil) + err = uploadTask.WaitTaskCompletion() + check.Assert(err, IsNil) + + uploadTask, err = catalog.UploadMediaImage(testUploadMedia, testUploadMedia, vcd.config.Media.MediaPath, 1024) + check.Assert(err, IsNil) + err = uploadTask.WaitTaskCompletion() + check.Assert(err, IsNil) + + vAppTemplate1, err := catalog.GetVAppTemplateByName(testUploadOvf) + check.Assert(err, IsNil) + check.Assert(vAppTemplate1, NotNil) + + mediaImage1, err := catalog.GetMediaByName(testUploadMedia, true) + check.Assert(err, IsNil) + check.Assert(mediaImage1, NotNil) + + adminCatalog.AdminCatalog.Name = testRenameCatalog + "_updated" + err = adminCatalog.Update() + check.Assert(err, IsNil) + AddToCleanupList(testRenameCatalog+"_updated", "catalog", vcd.config.VCD.Org, check.TestName()) + + // Get a Catalog using the previously updated name + updatedCatalog, err := vcd.client.Client.GetCatalogByName(vcd.config.VCD.Org, testRenameCatalog+"_updated") + check.Assert(err, IsNil) + check.Assert(updatedCatalog, NotNil) + + vAppTemplate2, err := updatedCatalog.GetVAppTemplateByName(testUploadOvf) + check.Assert(err, IsNil) + check.Assert(vAppTemplate2, NotNil) + + mediaImage2, err := updatedCatalog.GetMediaByName(testUploadMedia, false) + check.Assert(err, IsNil) + check.Assert(mediaImage2, NotNil) + + // Check the HREFs of the vApp templates and media images that were retrieved from + // catalog and updatedCatalog + check.Assert(vAppTemplate1.VAppTemplate.HREF, Equals, vAppTemplate2.VAppTemplate.HREF) + check.Assert(mediaImage1.Media.HREF, Equals, mediaImage2.Media.HREF) + + err = updatedCatalog.Delete(true, true) + check.Assert(err, IsNil) +} + // Tests System function UploadOvf by creating catalog and // checking if provided standard ova file uploaded. func (vcd *TestVCD) Test_UploadOvf(check *C) { fmt.Printf("Running: %s\n", check.TestName()) skipWhenOvaPathMissing(vcd.config.OVA.OvaPath, check) - checkUploadOvf(vcd, check, vcd.config.OVA.OvaPath, vcd.config.VCD.Catalog.Name, TestUploadOvf) + checkUploadOvf(vcd, check, vcd.config.OVA.OvaPath, vcd.config.VCD.Catalog.Name, TestUploadOvf, true) } // Tests System function UploadOvf by creating catalog and @@ -165,7 +287,7 @@ func (vcd *TestVCD) Test_UploadOvf_chunked(check *C) { fmt.Printf("Running: %s\n", check.TestName()) skipWhenOvaPathMissing(vcd.config.OVA.OvaChunkedPath, check) - checkUploadOvf(vcd, check, vcd.config.OVA.OvaChunkedPath, vcd.config.VCD.Catalog.Name, TestUploadOvf+"2") + checkUploadOvf(vcd, check, vcd.config.OVA.OvaChunkedPath, vcd.config.VCD.Catalog.Name, TestUploadOvf+"2", true) } // Tests System function UploadOvf by creating catalog and @@ -195,6 +317,9 @@ func (vcd *TestVCD) Test_UploadOvf_progress_works(check *C) { catalog, err = org.GetCatalogByName(vcd.config.VCD.Catalog.Name, true) check.Assert(err, IsNil) verifyCatalogItemUploaded(check, catalog, itemName) + + // Delete testing catalog item + deleteCatalogItem(check, catalog, itemName) } // Tests System function UploadOvf by creating catalog and @@ -218,9 +343,11 @@ func (vcd *TestVCD) Test_UploadOvf_ShowUploadProgress_works(check *C) { err = uploadTask.ShowUploadProgress() check.Assert(err, IsNil) - w.Close() + err = w.Close() + check.Assert(err, IsNil) + //read stdin - result, _ := ioutil.ReadAll(r) + result, _ := io.ReadAll(r) os.Stdout = oldStdout err = uploadTask.WaitTaskCompletion() @@ -233,6 +360,9 @@ func (vcd *TestVCD) Test_UploadOvf_ShowUploadProgress_works(check *C) { check.Assert(err, IsNil) check.Assert(catalog, NotNil) verifyCatalogItemUploaded(check, catalog, itemName) + + // Delete testing catalog item + deleteCatalogItem(check, catalog, itemName) } // Tests System function UploadOvf by creating catalog, creating catalog item @@ -257,6 +387,9 @@ func (vcd *TestVCD) Test_UploadOvf_error_withSameItem(check *C) { catalog, _ = findCatalog(vcd, check, vcd.config.VCD.Catalog.Name) _, err3 := catalog.UploadOvf(vcd.config.OVA.OvaPath, itemName, "upload from test", 1024) check.Assert(err3.Error(), Matches, ".*already exists. Upload with different name.*") + + // Delete testing catalog item + deleteCatalogItem(check, catalog, itemName) } // Tests System function UploadOvf by creating catalog, uploading file and verifying @@ -282,6 +415,8 @@ func (vcd *TestVCD) Test_UploadOvf_cleaned_extracted_files(check *C) { check.Assert(oldFolderCount, Equals, countFolders()) + // Delete testing catalog item + deleteCatalogItem(check, catalog, itemName) } // Tests System function UploadOvf by creating catalog and @@ -290,7 +425,7 @@ func (vcd *TestVCD) Test_UploadOvfFile(check *C) { fmt.Printf("Running: %s\n", check.TestName()) skipWhenOvaPathMissing(vcd.config.OVA.OvfPath, check) - checkUploadOvf(vcd, check, vcd.config.OVA.OvfPath, vcd.config.VCD.Catalog.Name, TestUploadOvf+"7") + checkUploadOvf(vcd, check, vcd.config.OVA.OvfPath, vcd.config.VCD.Catalog.Name, TestUploadOvf+"7", true) } // Tests System function UploadOvf by creating catalog and @@ -299,11 +434,11 @@ func (vcd *TestVCD) Test_UploadOvf_withoutVMDKSize(check *C) { fmt.Printf("Running: %s\n", check.TestName()) skipWhenOvaPathMissing(vcd.config.OVA.OvaWithoutSizePath, check) - checkUploadOvf(vcd, check, vcd.config.OVA.OvaWithoutSizePath, vcd.config.VCD.Catalog.Name, TestUploadOvf+"8") + checkUploadOvf(vcd, check, vcd.config.OVA.OvaWithoutSizePath, vcd.config.VCD.Catalog.Name, TestUploadOvf+"8", true) } func countFolders() int { - files, err := ioutil.ReadDir(os.TempDir()) + files, err := os.ReadDir(os.TempDir()) if err != nil { log.Fatal(err) } @@ -316,19 +451,24 @@ func countFolders() int { return count } -func checkUploadOvf(vcd *TestVCD, check *C, ovaFileName, catalogName, itemName string) { - catalog, org := findCatalog(vcd, check, vcd.config.VCD.Catalog.Name) +func checkUploadOvf(vcd *TestVCD, check *C, ovaFileName, catalogName, itemName string, deleteItemAtTheEnd bool) { + catalog, org := findCatalog(vcd, check, catalogName) uploadTask, err := catalog.UploadOvf(ovaFileName, itemName, "upload from test", 1024) check.Assert(err, IsNil) err = uploadTask.WaitTaskCompletion() check.Assert(err, IsNil) - AddToCleanupList(itemName, "catalogItem", vcd.org.Org.Name+"|"+vcd.config.VCD.Catalog.Name, "Test_UploadOvf") + AddToCleanupList(itemName, "catalogItem", vcd.org.Org.Name+"|"+catalogName, "checkUploadOvf") catalog, err = org.GetCatalogByName(catalogName, false) check.Assert(err, IsNil) verifyCatalogItemUploaded(check, catalog, itemName) + + // Delete testing catalog item + if deleteItemAtTheEnd { + deleteCatalogItem(check, catalog, itemName) + } } func verifyCatalogItemUploaded(check *C, catalog *Catalog, itemName string) { @@ -364,6 +504,15 @@ func skipWhenOvaPathMissing(ovaPath string, check *C) { } } +func deleteCatalogItem(check *C, catalog *Catalog, itemName string) { + catalogItem, err := catalog.GetCatalogItemByName(itemName, true) + check.Assert(err, IsNil) + check.Assert(catalogItem, NotNil) + + err = catalogItem.Delete() + check.Assert(err, IsNil) +} + // Tests System function UploadMediaImage by checking if provided standard iso file uploaded. func (vcd *TestVCD) Test_CatalogUploadMediaImage(check *C) { fmt.Printf("Running: %s\n", check.TestName()) @@ -384,6 +533,9 @@ func (vcd *TestVCD) Test_CatalogUploadMediaImage(check *C) { check.Assert(err, IsNil) check.Assert(catalog, NotNil) verifyCatalogItemUploaded(check, catalog, TestCatalogUploadMedia) + + // Delete testing catalog item + deleteCatalogItem(check, catalog, TestCatalogUploadMedia) } // Tests System function UploadMediaImage by checking UploadTask.GetUploadProgress returns values of progress. @@ -413,6 +565,9 @@ func (vcd *TestVCD) Test_CatalogUploadMediaImage_progress_works(check *C) { check.Assert(err, IsNil) check.Assert(catalog, NotNil) verifyCatalogItemUploaded(check, catalog, itemName) + + // Delete testing catalog item + deleteCatalogItem(check, catalog, itemName) } // Tests System function UploadMediaImage by checking UploadTask.ShowUploadProgress writes values of progress to stdin. @@ -434,9 +589,10 @@ func (vcd *TestVCD) Test_CatalogUploadMediaImage_ShowUploadProgress_works(check err = uploadTask.ShowUploadProgress() check.Assert(err, IsNil) - w.Close() + err = w.Close() + check.Assert(err, IsNil) //read stdin - result, _ := ioutil.ReadAll(r) + result, _ := io.ReadAll(r) os.Stdout = oldStdout err = uploadTask.WaitTaskCompletion() @@ -449,6 +605,9 @@ func (vcd *TestVCD) Test_CatalogUploadMediaImage_ShowUploadProgress_works(check check.Assert(err, IsNil) check.Assert(catalog, NotNil) verifyCatalogItemUploaded(check, catalog, itemName) + + // Delete testing catalog item + deleteCatalogItem(check, catalog, itemName) } // Tests System function UploadMediaImage by creating media item and expecting specific error @@ -467,6 +626,9 @@ func (vcd *TestVCD) Test_CatalogUploadMediaImage_error_withSameItem(check *C) { check.Assert(err, IsNil) AddToCleanupList(itemName, "mediaCatalogImage", vcd.org.Org.Name+"|"+vcd.config.VCD.Catalog.Name, "Test_CatalogUploadMediaImage_error_withSameItem") + + // Delete testing catalog item + deleteCatalogItem(check, catalog, itemName) } // Tests System function Delete by creating media item and @@ -597,3 +759,767 @@ func (vcd *TestVCD) TestGetVappTemplateByHref(check *C) { check.Assert(vappTemplate.VAppTemplate.Type, Equals, types.MimeVAppTemplate) check.Assert(vappTemplate.VAppTemplate.Name, Equals, catalogItem.CatalogItem.Name) } + +// Test_GetCatalogByNameSharedCatalog creates a separate Org and VDC just to create Catalog and share it with main Org +// One should be able to find shared catalogs from different Organizations +func (vcd *TestVCD) Test_GetCatalogByNameSharedCatalog(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + vcd.skipIfNotSysAdmin(check) + newOrg1, vdc, sharedCatalog := createSharedCatalogInNewOrg(vcd, check, check.TestName()) + + // Try to find the catalog inside Org which owns it - newOrg1 + catalogByName, err := newOrg1.GetCatalogByName(sharedCatalog.Catalog.Name, true) + check.Assert(err, IsNil) + check.Assert(catalogByName.Catalog.Name, Equals, sharedCatalog.Catalog.Name) + + // Try to find the catalog in another Org with which this catalog is shared (vcd.Org) + sharedCatalogByName, err := vcd.org.GetCatalogByName(sharedCatalog.Catalog.Name, false) + check.Assert(err, IsNil) + check.Assert(sharedCatalogByName.Catalog.Name, Equals, sharedCatalog.Catalog.Name) + + cleanupCatalogOrgVdc(check, sharedCatalog, vdc, vcd, newOrg1) +} + +// Test_GetCatalogByIdSharedCatalog creates a separate Org and VDC just to create Catalog and share it with main Org +// One should be able to find shared catalogs from different Organizations +func (vcd *TestVCD) Test_GetCatalogByIdSharedCatalog(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + vcd.skipIfNotSysAdmin(check) + + newOrg1, vdc, sharedCatalog := createSharedCatalogInNewOrg(vcd, check, check.TestName()) + + // Try to find the sharedCatalog inside Org which owns it - newOrg1 + catalogById, err := newOrg1.GetCatalogById(sharedCatalog.Catalog.ID, true) + check.Assert(err, IsNil) + check.Assert(catalogById.Catalog.Name, Equals, sharedCatalog.Catalog.Name) + + // Try to find the sharedCatalog in another Org with which this sharedCatalog is shared (vcd.Org) + sharedCatalogById, err := vcd.org.GetCatalogById(sharedCatalog.Catalog.ID, false) + check.Assert(err, IsNil) + check.Assert(sharedCatalogById.Catalog.Name, Equals, sharedCatalog.Catalog.Name) + + cleanupCatalogOrgVdc(check, sharedCatalog, vdc, vcd, newOrg1) +} + +// Test_GetCatalogByNamePrefersLocal tests that local catalog (in the same Org) is prioritised against shared catalogs +// in other Orgs. It does so by creating another Org with shared Catalog named just like the one in testing catalog +func (vcd *TestVCD) Test_GetCatalogByNamePrefersLocal(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + vcd.skipIfNotSysAdmin(check) + // Create a catalog in new org with exactly the same name as in vcd.Org + newOrg1, vdc, sharedCatalog := createSharedCatalogInNewOrg(vcd, check, vcd.config.VCD.Catalog.Name) + + // Make sure that the Owner Org HREF is the local one for vcd.Org catalog named vcd.config.VCD.Catalog.Name + catalogByNameInTestOrg, err := vcd.org.GetCatalogByName(vcd.config.VCD.Catalog.Name, true) + check.Assert(err, IsNil) + check.Assert(catalogByNameInTestOrg.parent.orgName(), Equals, vcd.org.Org.Name) + + // Make sure that the Owner Org HREF is the local one for vcd.Org catalog named vcd.config.VCD.Catalog.Name + catalogByNameInNewOrg, err := newOrg1.GetCatalogByName(vcd.config.VCD.Catalog.Name, true) + check.Assert(err, IsNil) + check.Assert(catalogByNameInNewOrg.parent.orgName(), Equals, newOrg1.Org.Name) + + cleanupCatalogOrgVdc(check, sharedCatalog, vdc, vcd, newOrg1) +} + +// Test_GetCatalogByNameSharedCatalogOrgUser additionally tests GetOrgByName and GetOrgById using a custom created Org +// Admin user. It tests the following cases: +// * System user must be able to retrieve any catalog - shared or unshared from another Org +// * Org Admin user must be able to retrieve catalog in his own Org +// * Org Admin user must be able to retrieve shared catalog from another Org +// * Org admin user must not be able to retrieve unshared catalog from another Org +func (vcd *TestVCD) Test_GetCatalogByXSharedCatalogOrgUser(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + vcd.skipIfNotSysAdmin(check) + newOrg1, vdc, sharedCatalog := createSharedCatalogInNewOrg(vcd, check, check.TestName()) + + // Create one more additional catalog which is not shared + unsharedCatalog, err := newOrg1.CreateCatalog("unshared-catalog", check.TestName()) + check.Assert(err, IsNil) + AddToCleanupList(unsharedCatalog.Catalog.Name, "catalog", newOrg1.Org.Name, check.TestName()) + + // Try to find the catalog inside Org which owns it - newOrg1 + catalogByName, err := newOrg1.GetCatalogByName(sharedCatalog.Catalog.Name, true) + check.Assert(err, IsNil) + check.Assert(catalogByName.Catalog.Name, Equals, sharedCatalog.Catalog.Name) + + // Try to find the catalog in another Org with which this catalog is shared (vcd.Org) + sharedCatalogByName, err := vcd.org.GetCatalogByName(sharedCatalog.Catalog.Name, false) + check.Assert(err, IsNil) + check.Assert(sharedCatalogByName.Catalog.Name, Equals, sharedCatalog.Catalog.Name) + + // Try to find unshared catalog from another Org with System user + systemUnsharedCatalogByName, err := vcd.org.GetCatalogByName(unsharedCatalog.Catalog.Name, true) + check.Assert(err, IsNil) + check.Assert(systemUnsharedCatalogByName.Catalog.ID, Equals, unsharedCatalog.Catalog.ID) + + // Create an Org Admin user and test that it can find catalog as well + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + orgAdminClient, _, err := newOrgUserConnection(adminOrg, "test-user", "CHANGE-ME", vcd.config.Provider.Url, true) + check.Assert(err, IsNil) + orgAsOrgUser, err := orgAdminClient.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + + // Find a catalog in the same Org using Org Admin user + orgAdminCatalogByNameSameOrg, err := orgAsOrgUser.GetCatalogByName(vcd.config.VCD.Catalog.Name, false) + check.Assert(err, IsNil) + check.Assert(orgAdminCatalogByNameSameOrg.Catalog.Name, Equals, vcd.config.VCD.Catalog.Name) + + orgAdminCatalogByIdSameOrg, err := orgAsOrgUser.GetCatalogById(orgAdminCatalogByNameSameOrg.Catalog.ID, false) + check.Assert(err, IsNil) + check.Assert(orgAdminCatalogByIdSameOrg.Catalog.Name, Equals, orgAdminCatalogByNameSameOrg.Catalog.Name) + check.Assert(orgAdminCatalogByIdSameOrg.Catalog.ID, Equals, orgAdminCatalogByNameSameOrg.Catalog.ID) + + // Find a shared catalog from another Org using Org Admin user + orgAdminCatalogByName, err := orgAsOrgUser.GetCatalogByName(sharedCatalog.Catalog.Name, false) + check.Assert(err, IsNil) + check.Assert(orgAdminCatalogByName.Catalog.Name, Equals, sharedCatalog.Catalog.Name) + check.Assert(orgAdminCatalogByName.Catalog.ID, Equals, sharedCatalog.Catalog.ID) + + orgAdminCatalogById, err := orgAsOrgUser.GetCatalogById(sharedCatalog.Catalog.ID, false) + check.Assert(err, IsNil) + check.Assert(orgAdminCatalogById.Catalog.Name, Equals, sharedCatalog.Catalog.Name) + check.Assert(orgAdminCatalogById.Catalog.ID, Equals, sharedCatalog.Catalog.ID) + + // Try to find unshared catalog from another Org with Org admin user and expect an ErrorEntityNotFound + _, err = orgAsOrgUser.GetCatalogByName(unsharedCatalog.Catalog.Name, true) + check.Assert(ContainsNotFound(err), Equals, true) + + _, err = orgAsOrgUser.GetCatalogById(unsharedCatalog.Catalog.ID, true) + check.Assert(ContainsNotFound(err), Equals, true) + + // Cleanup + err = unsharedCatalog.Delete(true, true) + check.Assert(err, IsNil) + + cleanupCatalogOrgVdc(check, sharedCatalog, vdc, vcd, newOrg1) +} + +func createSharedCatalogInNewOrg(vcd *TestVCD, check *C, newCatalogName string) (*Org, *Vdc, Catalog) { + newOrgName1 := spawnTestOrg(vcd, check, "org") + + newOrg1, err := vcd.client.GetOrgByName(newOrgName1) + check.Assert(err, IsNil) + + // Spawn a VDC inside newly created Org so that there is storage to create new catalog + vdc := spawnTestVdc(vcd, check, newOrgName1) + + catalog, err := newOrg1.CreateCatalog(newCatalogName, "Catalog for testing") + check.Assert(err, IsNil) + AddToCleanupList(newCatalogName, "catalog", newOrgName1, check.TestName()) + + // Share new Catalog in newOrgName1 with default test Org vcd.Org + readOnly := "ReadOnly" + accessControl := &types.ControlAccessParams{ + IsSharedToEveryone: false, + EveryoneAccessLevel: &readOnly, + AccessSettings: &types.AccessSettingList{ + AccessSetting: []*types.AccessSetting{&types.AccessSetting{ + Subject: &types.LocalSubject{ + HREF: vcd.org.Org.HREF, + Name: vcd.org.Org.Name, + Type: types.MimeOrg, + }, + AccessLevel: "ReadOnly", + }}, + }, + } + err = catalog.SetAccessControl(accessControl, false) + check.Assert(err, IsNil) + + return newOrg1, vdc, catalog +} + +func cleanupCatalogOrgVdc(check *C, sharedCatalog Catalog, vdc *Vdc, vcd *TestVCD, newOrg1 *Org) { + // Cleanup catalog, vdc and org + err := sharedCatalog.Delete(true, true) + check.Assert(err, IsNil) + + err = vdc.DeleteWait(true, true) + check.Assert(err, IsNil) + + adminOrg, err := vcd.client.GetAdminOrgByName(newOrg1.Org.Name) + check.Assert(err, IsNil) + err = adminOrg.Delete(true, true) + check.Assert(err, IsNil) +} + +// Creates a Catalog. Publishes catalog to external Org and then deletes the catalog. +func (vcd *TestVCD) Test_PublishToExternalOrganizations(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + + // test with AdminCatalog + catalogName := check.TestName() + catalogDescription := check.TestName() + " description" + + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + + // TODO - remove once VCD is fixed. + // Every Org update causes catalog publishing to be removed and therefore this test fails. + // Turning publishing on right before test to be sure it is tested and passes. + // VCD 10.2.0 <-> 10.3.3 have a bug that even though catalog publishing is enabled adminOrg. + fmt.Println("Overcomming VCD 10.2.0 <-> 10.3.3 bug - explicitly setting catalog sharing") + adminOrg.AdminOrg.OrgSettings.OrgGeneralSettings.CanPublishCatalogs = true + adminOrg.AdminOrg.OrgSettings.OrgGeneralSettings.CanPublishExternally = true + updatedAdminOrg, err := adminOrg.Update() + check.Assert(err, IsNil) + check.Assert(updatedAdminOrg, NotNil) + + adminCatalog, err := adminOrg.CreateCatalog(catalogName, catalogDescription) + check.Assert(err, IsNil) + check.Assert(adminCatalog.AdminCatalog.Name, Equals, catalogName) + check.Assert(adminCatalog.AdminCatalog.Description, Equals, catalogDescription) + + AddToCleanupList(catalogName, "catalog", vcd.config.VCD.Org, check.TestName()) + + err = adminCatalog.PublishToExternalOrganizations(types.PublishExternalCatalogParams{ + IsPublishedExternally: addrOf(true), + IsCachedEnabled: addrOf(true), + Password: "secretOrNot", + PreserveIdentityInfoFlag: addrOf(true), + }) + check.Assert(err, IsNil) + check.Assert(*adminCatalog.AdminCatalog.PublishExternalCatalogParams.IsPublishedExternally, Equals, true) + check.Assert(*adminCatalog.AdminCatalog.PublishExternalCatalogParams.PreserveIdentityInfoFlag, Equals, true) + check.Assert(*adminCatalog.AdminCatalog.PublishExternalCatalogParams.IsCachedEnabled, Equals, true) + check.Assert(adminCatalog.AdminCatalog.PublishExternalCatalogParams.Password, Equals, "******") + + err = adminCatalog.Delete(true, true) + check.Assert(err, IsNil) + + // test with Catalog + org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(org, NotNil) + + catalog, err := org.CreateCatalog(catalogName, catalogDescription) + check.Assert(err, IsNil) + check.Assert(catalog.Catalog.Name, Equals, catalogName) + check.Assert(catalog.Catalog.Description, Equals, catalogDescription) + + AddToCleanupList(catalogName, "catalog", vcd.config.VCD.Org, check.TestName()) + + err = catalog.PublishToExternalOrganizations(types.PublishExternalCatalogParams{ + IsPublishedExternally: addrOf(true), + IsCachedEnabled: addrOf(true), + Password: "secretOrNot", + PreserveIdentityInfoFlag: addrOf(true), + }) + check.Assert(err, IsNil) + check.Assert(*catalog.Catalog.PublishExternalCatalogParams.IsPublishedExternally, Equals, true) + check.Assert(*catalog.Catalog.PublishExternalCatalogParams.PreserveIdentityInfoFlag, Equals, true) + check.Assert(*catalog.Catalog.PublishExternalCatalogParams.IsCachedEnabled, Equals, true) + check.Assert(catalog.Catalog.PublishExternalCatalogParams.Password, Equals, "******") + + err = catalog.PublishToExternalOrganizations(types.PublishExternalCatalogParams{ + IsPublishedExternally: addrOf(true), + IsCachedEnabled: addrOf(false), + Password: "secretOrNot2", + PreserveIdentityInfoFlag: addrOf(false), + }) + check.Assert(err, IsNil) + check.Assert(*catalog.Catalog.PublishExternalCatalogParams.IsPublishedExternally, Equals, true) + check.Assert(*catalog.Catalog.PublishExternalCatalogParams.PreserveIdentityInfoFlag, Equals, false) + check.Assert(*catalog.Catalog.PublishExternalCatalogParams.IsCachedEnabled, Equals, false) + check.Assert(catalog.Catalog.PublishExternalCatalogParams.Password, Equals, "******") + + err = catalog.Delete(true, true) + check.Assert(err, IsNil) +} + +// Tests System function UploadOvfByLink and verifies that +// Task.GetTaskProgress returns values of progress. +func (vcd *TestVCD) Test_UploadOvfByLink_progress_works(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + + if vcd.config.OVA.OvfUrl == "" { + check.Skip("Skipping test because no OVF URL given") + } + + itemName := TestUploadOvf + "URL" + + catalog, org := findCatalog(vcd, check, vcd.config.VCD.Catalog.Name) + + uploadTask, err := catalog.UploadOvfByLink(vcd.config.OVA.OvfUrl, itemName, "upload from test") + check.Assert(err, IsNil) + check.Assert(uploadTask, NotNil) + + for { + if value, err := uploadTask.GetTaskProgress(); value == "100" || err != nil { + check.Assert(err, IsNil) + break + } else { + check.Assert(value, Not(Equals), "") + } + } + err = uploadTask.WaitTaskCompletion() + check.Assert(err, IsNil) + + AddToCleanupList(itemName, "catalogItem", vcd.org.Org.Name+"|"+vcd.config.VCD.Catalog.Name, "Test_UploadOvfByLink_progress_works") + + catalog, err = org.GetCatalogByName(vcd.config.VCD.Catalog.Name, true) + check.Assert(err, IsNil) + verifyCatalogItemUploaded(check, catalog, itemName) + + // Delete testing catalog item + deleteCatalogItem(check, catalog, itemName) +} + +func (vcd *TestVCD) Test_CatalogQueryMediaList(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + + catalogName := vcd.config.VCD.Catalog.Name + if catalogName == "" { + check.Skip("Test_CatalogQueryMediaList: Catalog name not given") + return + } + + cat, err := vcd.org.GetCatalogByName(catalogName, false) + if err != nil { + check.Skip("Test_CatalogQueryMediaList: Catalog not found") + return + } + + medias, err := cat.QueryMediaList() + check.Assert(err, IsNil) + check.Assert(medias, NotNil) + + // Check that number of medias is 1 + // Dump all media structures to easily identify leftover objects if number is not 1 + if len(medias) > 1 { + fmt.Printf("%#v", medias) + } + check.Assert(len(medias), Equals, 1) + + // Check that media name is what it should be + check.Assert(medias[0].Name, Equals, vcd.config.Media.Media) +} + +// Tests System function UploadMediaImage by using provided ISO file of UDF type. +func (vcd *TestVCD) Test_CatalogUploadMediaImageWihUdfTypeIso(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + + if vcd.config.Media.MediaUdfTypePath == "" { + check.Skip("Skipping test because no UDF type ISO path was given") + } + + catalog, org := findCatalog(vcd, check, vcd.config.VCD.Catalog.Name) + + mediaName := check.TestName() + + uploadTask, err := catalog.UploadMediaImage(mediaName, "upload from test", vcd.config.Media.MediaUdfTypePath, 1024) + check.Assert(err, IsNil) + err = uploadTask.WaitTaskCompletion() + check.Assert(err, IsNil) + + AddToCleanupList(mediaName, "mediaCatalogImage", vcd.org.Org.Name+"|"+vcd.config.VCD.Catalog.Name, mediaName) + + catalog, err = org.GetCatalogByName(vcd.config.VCD.Catalog.Name, true) + check.Assert(err, IsNil) + check.Assert(catalog, NotNil) + verifyCatalogItemUploaded(check, catalog, mediaName) + + // Delete testing catalog item + deleteCatalogItem(check, catalog, mediaName) +} + +func (vcd *TestVCD) Test_GetAdminCatalogById(check *C) { + if vcd.config.VCD.Org == "" || vcd.config.VCD.Catalog.Name == "" { + check.Skip("no Org or Catalog found in configuration") + } + + // 1. Get a catalog from an organization + org, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + + adminCatalog, err := org.GetAdminCatalogByName(vcd.config.VCD.Catalog.Name, false) + check.Assert(err, IsNil) + + // 2. retrieve that same catalog from the client alone using HREF + adminCatalogByHref, err := vcd.client.Client.GetAdminCatalogByHref(adminCatalog.AdminCatalog.HREF) + check.Assert(err, IsNil) + check.Assert(adminCatalogByHref.AdminCatalog.HREF, Equals, adminCatalog.AdminCatalog.HREF) + + // 3. retrieve the same catalog again, using ID + adminCatalogById, err := vcd.client.Client.GetAdminCatalogById(adminCatalog.AdminCatalog.ID) + check.Assert(err, IsNil) + check.Assert(adminCatalogById.AdminCatalog.HREF, Equals, adminCatalog.AdminCatalog.HREF) +} + +func (vcd *TestVCD) Test_CatalogAccessAsOrgUsers(check *C) { + if vcd.config.Tenants == nil || len(vcd.config.Tenants) < 2 { + check.Skip("no tenants found in configuration") + } + + if vcd.config.OVA.OvaPath == "" || vcd.config.Media.MediaPath == "" { + check.Skip("no OVA or Media path found in configuration") + } + + org1Name := vcd.config.Tenants[0].SysOrg + user1Name := vcd.config.Tenants[0].User + password1 := vcd.config.Tenants[0].Password + org2Name := vcd.config.Tenants[1].SysOrg + user2Name := vcd.config.Tenants[1].User + password2 := vcd.config.Tenants[1].Password + + org1AsSystem, err := vcd.client.GetAdminOrgByName(org1Name) + check.Assert(err, IsNil) + check.Assert(org1AsSystem, NotNil) + + org2AsSystem, err := vcd.client.GetAdminOrgByName(org2Name) + if err != nil { + if ContainsNotFound(err) { + check.Skip(fmt.Sprintf("organization %s not found", org2Name)) + } + } + check.Assert(err, IsNil) + check.Assert(org2AsSystem, NotNil) + vcdClient1 := NewVCDClient(vcd.client.Client.VCDHREF, true) + err = vcdClient1.Authenticate(user1Name, password1, org1Name) + check.Assert(err, IsNil) + + vcdClient2 := NewVCDClient(vcd.client.Client.VCDHREF, true) + err = vcdClient2.Authenticate(user2Name, password2, org2Name) + check.Assert(err, IsNil) + + org1, err := vcdClient1.GetOrgByName(org1Name) + check.Assert(err, IsNil) + org2, err := vcdClient2.GetOrgByName(org2Name) + check.Assert(err, IsNil) + check.Assert(org2, NotNil) + catalogName := check.TestName() + "-cat" + fmt.Printf("creating catalog %s in org %s\n", catalogName, org1Name) + adminCatalog1AsSystem, err := org1AsSystem.CreateCatalog(catalogName, fmt.Sprintf("catalog %s created in %s", catalogName, org1Name)) + check.Assert(err, IsNil) + AddToCleanupList(catalogName, "catalog", org1Name, check.TestName()) + catalog1AsSystem, err := org1AsSystem.GetCatalogByName(catalogName, true) + check.Assert(err, IsNil) + fmt.Printf("sharing catalog %s from org %s\n", catalogName, org1Name) + err = adminCatalog1AsSystem.SetAccessControl(&types.ControlAccessParams{ + IsSharedToEveryone: false, + AccessSettings: &types.AccessSettingList{ + AccessSetting: []*types.AccessSetting{ + { + Subject: &types.LocalSubject{ + HREF: org2.Org.HREF, + Name: org2Name, + Type: types.MimeOrg, + }, + AccessLevel: types.ControlAccessReadOnly, + }, + }, + }, + }, true) + check.Assert(err, IsNil) + + // populate the catalog + + vappTemplateName := check.TestName() + "-template" + mediaName := check.TestName() + "-media" + fmt.Printf("uploading vApp template into catalog %s\n", catalogName) + task, err := catalog1AsSystem.UploadOvf(vcd.config.OVA.OvaPath, vappTemplateName, vappTemplateName, 1024) + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) + + fmt.Printf("uploading media image into catalog %s\n", catalogName) + uploadTask, err := catalog1AsSystem.UploadMediaImage(mediaName, "upload from test", vcd.config.Media.MediaPath, 1024) + check.Assert(err, IsNil) + err = uploadTask.WaitTaskCompletion() + check.Assert(err, IsNil) + + vAppTemplateAsSystem, err := catalog1AsSystem.GetVAppTemplateByName(vappTemplateName) + check.Assert(err, IsNil) + check.Assert(vAppTemplateAsSystem, NotNil) + mediaRecordAsSystem, err := catalog1AsSystem.GetMediaByName(mediaName, true) + check.Assert(err, IsNil) + check.Assert(mediaRecordAsSystem, NotNil) + + // Retrieve catalog by ID in its own Org + adminCatalog1, err := vcdClient1.Client.GetAdminCatalogById(adminCatalog1AsSystem.AdminCatalog.ID) + check.Assert(err, IsNil) + check.Assert(adminCatalog1.AdminCatalog.HREF, Equals, adminCatalog1AsSystem.AdminCatalog.HREF) + + catalog1, err := vcdClient1.Client.GetCatalogById(adminCatalog1AsSystem.AdminCatalog.ID) + check.Assert(err, IsNil) + check.Assert(catalog1.Catalog.HREF, Equals, catalog1AsSystem.Catalog.HREF) + + startTime := time.Now() + timeout := 100 * time.Second + // Start retrieving catalog in the other org + fmt.Printf("retrieving catalog %s in org %s\n", catalogName, org2Name) + for time.Since(startTime) < timeout { + _, err = vcdClient2.Client.GetAdminCatalogById(adminCatalog1AsSystem.AdminCatalog.ID) + if err == nil { + fmt.Printf("shared catalog available in %s\n", time.Since(startTime)) + break + } + time.Sleep(10 * time.Millisecond) + } + // Retrieve the shared catalog in the other organization + adminCatalog2, err := vcdClient2.Client.GetAdminCatalogById(adminCatalog1AsSystem.AdminCatalog.ID) + check.Assert(err, IsNil) + check.Assert(adminCatalog2, NotNil) + + // Retrieve the catalog from both tenants, using functions that don't rely on organization internals + catalog1FromOrg, err := vcdClient1.Client.GetCatalogByName(org1.Org.Name, catalogName) + check.Assert(err, IsNil) + adminCatalog1FromOrg, err := vcdClient1.Client.GetAdminCatalogByName(org1.Org.Name, catalogName) + check.Assert(err, IsNil) + catalog2FromOrg, err := vcdClient2.Client.GetCatalogByName(org1.Org.Name, catalogName) + check.Assert(err, IsNil) + adminCatalog2FromOrg, err := vcdClient2.Client.GetAdminCatalogByName(org1.Org.Name, catalogName) + check.Assert(err, IsNil) + + // Also retrieve the catalog items from both tenants + vAppTemplate1, err := catalog1FromOrg.GetVAppTemplateByName(vappTemplateName) + check.Assert(err, IsNil) + check.Assert(vAppTemplate1.VAppTemplate.HREF, Equals, vAppTemplateAsSystem.VAppTemplate.HREF) + mediaRecord1, err := catalog1FromOrg.GetMediaByName(mediaName, false) + check.Assert(err, IsNil) + check.Assert(mediaRecord1.Media.HREF, Equals, mediaRecordAsSystem.Media.HREF) + + vAppTemplate2, err := catalog2FromOrg.GetVAppTemplateByName(vappTemplateName) + check.Assert(err, IsNil) + check.Assert(vAppTemplate2.VAppTemplate.HREF, Equals, vAppTemplateAsSystem.VAppTemplate.HREF) + mediaRecord2, err := catalog2FromOrg.GetMediaByName(mediaName, false) + check.Assert(err, IsNil) + check.Assert(mediaRecord2.Media.HREF, Equals, mediaRecordAsSystem.Media.HREF) + + check.Assert(catalog1FromOrg.Catalog.HREF, Equals, catalog1AsSystem.Catalog.HREF) + check.Assert(adminCatalog1FromOrg.AdminCatalog.HREF, Equals, adminCatalog1AsSystem.AdminCatalog.HREF) + check.Assert(adminCatalog2FromOrg.AdminCatalog.HREF, Equals, adminCatalog1AsSystem.AdminCatalog.HREF) + check.Assert(catalog2FromOrg.Catalog.HREF, Equals, catalog1AsSystem.Catalog.HREF) + timeout = 30 * time.Second + startTime = time.Now() + for time.Since(startTime) < timeout { + err = adminCatalog1AsSystem.Delete(true, true) + if err == nil { + fmt.Printf("shared catalog deleted in %s\n", time.Since(startTime)) + break + } + time.Sleep(200 * time.Millisecond) + } + check.Assert(err, IsNil) +} + +func (vcd *TestVCD) Test_CatalogAccessAsOrgUsersReadOnly(check *C) { + if vcd.config.Tenants == nil || len(vcd.config.Tenants) < 2 { + check.Skip("no tenants found in configuration") + } + + if vcd.config.OVA.OvaPath == "" || vcd.config.Media.MediaPath == "" { + check.Skip("no OVA or Media path found in configuration") + } + + org1Name := vcd.config.Tenants[0].SysOrg + user1Name := vcd.config.Tenants[0].User + password1 := vcd.config.Tenants[0].Password + org2Name := vcd.config.Tenants[1].SysOrg + user2Name := vcd.config.Tenants[1].User + password2 := vcd.config.Tenants[1].Password + + vcdClient1 := NewVCDClient(vcd.client.Client.VCDHREF, true) + err := vcdClient1.Authenticate(user1Name, password1, org1Name) + check.Assert(err, IsNil) + + vcdClient2 := NewVCDClient(vcd.client.Client.VCDHREF, true) + err = vcdClient2.Authenticate(user2Name, password2, org2Name) + check.Assert(err, IsNil) + + org1, err := vcdClient1.GetAdminOrgByName(org1Name) + check.Assert(err, IsNil) + org2, err := vcdClient2.GetAdminOrgByName(org2Name) + check.Assert(err, IsNil) + check.Assert(org2, NotNil) + catalogName := check.TestName() + "-cat" + fmt.Printf("creating catalog %s in org %s\n", catalogName, org1Name) + adminCatalog1Created, err := org1.CreateCatalog(catalogName, fmt.Sprintf("catalog %s created in %s", catalogName, org1Name)) + check.Assert(err, IsNil) + AddToCleanupList(catalogName, "catalog", org1Name, check.TestName()) + catalog1AsOrg1, err := org1.GetCatalogByName(catalogName, true) + check.Assert(err, IsNil) + fmt.Printf("sharing catalog %s from org %s\n", catalogName, org1Name) + + err = adminCatalog1Created.SetReadOnlyAccessControl(true) + + check.Assert(err, IsNil) + + // populate the catalog + + vappTemplateName := check.TestName() + "-template" + mediaName := check.TestName() + "-media" + fmt.Printf("uploading vApp template into catalog %s\n", catalogName) + task, err := catalog1AsOrg1.UploadOvf(vcd.config.OVA.OvaPath, vappTemplateName, vappTemplateName, 1024) + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) + + fmt.Printf("uploading media image into catalog %s\n", catalogName) + uploadTask, err := catalog1AsOrg1.UploadMediaImage(mediaName, "upload from test", vcd.config.Media.MediaPath, 1024) + check.Assert(err, IsNil) + err = uploadTask.WaitTaskCompletion() + check.Assert(err, IsNil) + + vAppTemplateAsSystem, err := catalog1AsOrg1.GetVAppTemplateByName(vappTemplateName) + check.Assert(err, IsNil) + check.Assert(vAppTemplateAsSystem, NotNil) + mediaRecordAsSystem, err := catalog1AsOrg1.GetMediaByName(mediaName, true) + check.Assert(err, IsNil) + check.Assert(mediaRecordAsSystem, NotNil) + + // Retrieve catalog by ID in its own Org + adminCatalog1, err := vcdClient1.Client.GetAdminCatalogById(adminCatalog1Created.AdminCatalog.ID) + check.Assert(err, IsNil) + check.Assert(adminCatalog1.AdminCatalog.HREF, Equals, adminCatalog1Created.AdminCatalog.HREF) + + catalog1, err := vcdClient1.Client.GetCatalogById(adminCatalog1Created.AdminCatalog.ID) + check.Assert(err, IsNil) + check.Assert(catalog1.Catalog.HREF, Equals, catalog1AsOrg1.Catalog.HREF) + + startTime := time.Now() + timeout := 100 * time.Second + var timeElapsedToAvailability time.Duration + // Start retrieving catalog in the other org + fmt.Printf("retrieving catalog %s in org %s\n", catalogName, org2Name) + for time.Since(startTime) < timeout { + _, err = vcdClient2.Client.GetAdminCatalogById(adminCatalog1Created.AdminCatalog.ID) + if err == nil { + timeElapsedToAvailability = time.Since(startTime) + fmt.Printf("shared catalog available in %s\n", timeElapsedToAvailability) + break + } + time.Sleep(10 * time.Millisecond) + } + // Retrieve the shared catalog in the other organization + adminCatalog2, err := vcdClient2.Client.GetAdminCatalogById(adminCatalog1Created.AdminCatalog.ID) + check.Assert(err, IsNil) + check.Assert(adminCatalog2, NotNil) + + // Retrieve the catalog from both tenants, using functions that don't rely on organization internals + catalog1FromOrg, err := vcdClient1.Client.GetCatalogByName(org1.AdminOrg.Name, catalogName) + check.Assert(err, IsNil) + adminCatalog1FromOrg, err := vcdClient1.Client.GetAdminCatalogByName(org1.AdminOrg.Name, catalogName) + check.Assert(err, IsNil) + catalog2FromOrg, err := vcdClient2.Client.GetCatalogByName(org1.AdminOrg.Name, catalogName) + check.Assert(err, IsNil) + adminCatalog2FromOrg, err := vcdClient2.Client.GetAdminCatalogByName(org1.AdminOrg.Name, catalogName) + check.Assert(err, IsNil) + + // Also retrieve the catalog items from both tenants + vAppTemplate1, err := catalog1FromOrg.GetVAppTemplateByName(vappTemplateName) + check.Assert(err, IsNil) + check.Assert(vAppTemplate1.VAppTemplate.HREF, Equals, vAppTemplateAsSystem.VAppTemplate.HREF) + mediaRecord1, err := catalog1FromOrg.GetMediaByName(mediaName, false) + check.Assert(err, IsNil) + check.Assert(mediaRecord1.Media.HREF, Equals, mediaRecordAsSystem.Media.HREF) + + vAppTemplate2, err := catalog2FromOrg.GetVAppTemplateByName(vappTemplateName) + check.Assert(err, IsNil) + check.Assert(vAppTemplate2.VAppTemplate.HREF, Equals, vAppTemplateAsSystem.VAppTemplate.HREF) + mediaRecord2, err := catalog2FromOrg.GetMediaByName(mediaName, false) + check.Assert(err, IsNil) + check.Assert(mediaRecord2.Media.HREF, Equals, mediaRecordAsSystem.Media.HREF) + + check.Assert(catalog1FromOrg.Catalog.HREF, Equals, catalog1AsOrg1.Catalog.HREF) + check.Assert(adminCatalog1FromOrg.AdminCatalog.HREF, Equals, adminCatalog1Created.AdminCatalog.HREF) + check.Assert(adminCatalog2FromOrg.AdminCatalog.HREF, Equals, adminCatalog1Created.AdminCatalog.HREF) + check.Assert(catalog2FromOrg.Catalog.HREF, Equals, catalog1AsOrg1.Catalog.HREF) + + isSharedReadOnly, err := adminCatalog1.IsSharedReadOnly() + check.Assert(err, IsNil) + check.Assert(isSharedReadOnly, Equals, true) + + fmt.Println("removing read-only catalog sharing") + err = adminCatalog1Created.SetReadOnlyAccessControl(false) + check.Assert(err, IsNil) + catalog1FromOrg, err = vcdClient1.Client.GetCatalogByName(org1.AdminOrg.Name, catalogName) + check.Assert(err, IsNil) + check.Assert(catalog1FromOrg, NotNil) + fmt.Println("try retrieving read-only catalog from second org") + time.Sleep(timeElapsedToAvailability) + adminCatalog2FromOrg, err = vcdClient2.Client.GetAdminCatalogByName(org1.AdminOrg.Name, catalogName) + check.Assert(err, NotNil) + check.Assert(adminCatalog2FromOrg, IsNil) + + isSharedReadOnly, err = adminCatalog1.IsSharedReadOnly() + check.Assert(err, IsNil) + check.Assert(isSharedReadOnly, Equals, false) + + timeout = 30 * time.Second + startTime = time.Now() + for time.Since(startTime) < timeout { + err = adminCatalog1Created.Delete(true, true) + if err == nil { + fmt.Printf("shared catalog deleted in %s\n", time.Since(startTime)) + break + } + time.Sleep(200 * time.Millisecond) + } + check.Assert(err, IsNil) +} + +func (vcd *TestVCD) Test_CatalogCreateCompleteness(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + catalogName := "TestAdminCatalogCreate" + adminCatalog, err := adminOrg.CreateCatalog(catalogName, catalogName) + check.Assert(err, IsNil) + AddToCleanupList(catalogName, "catalog", vcd.config.VCD.Org, check.TestName()) + metadataLink := adminCatalog.AdminCatalog.Link.ForType(types.MimeMetaData, "add") + check.Assert(metadataLink, NotNil) + err = adminCatalog.Delete(true, true) + check.Assert(err, IsNil) + + catalogName = "TestCatalogCreate" + org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + catalog, err := org.CreateCatalog(catalogName, catalogName) + check.Assert(err, IsNil) + AddToCleanupList(catalogName, "catalog", vcd.config.VCD.Org, check.TestName()) + metadataLink = nil + metadataLink = catalog.Catalog.Link.ForType(types.MimeMetaData, "add") + check.Assert(metadataLink, NotNil) + err = catalog.Delete(true, true) + check.Assert(err, IsNil) +} + +func (vcd *TestVCD) Test_CaptureVapp(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + + vapp, vm := createNsxtVAppAndVm(vcd, check) + check.Assert(vapp, NotNil) + check.Assert(vm, NotNil) + + // retrieve NSX-T Catalog + cat, err := vcd.org.GetCatalogByName(vcd.config.VCD.Catalog.NsxtBackedCatalogName, false) + check.Assert(err, IsNil) + check.Assert(cat, NotNil) + + vAppCaptureParams := &types.CaptureVAppParams{ + Name: check.TestName() + "vm-template", + Source: &types.Reference{ + HREF: vapp.VApp.HREF, + }, + CustomizationSection: types.CaptureVAppParamsCustomizationSection{ + Info: "CustomizeOnInstantiate Settings", + CustomizeOnInstantiate: true, + }, + CopyTpmOnInstantiate: addrOf(false), + } + + templ, err := cat.CaptureVappTemplate(vAppCaptureParams) + check.Assert(err, IsNil) + check.Assert(templ, NotNil) + + err = templ.Delete() + check.Assert(err, IsNil) + + AddToCleanupList(templ.VAppTemplate.Name, "catalogItem", vcd.org.Org.Name+"|"+vcd.config.VCD.Catalog.NsxtBackedCatalogName, check.TestName()) +} diff --git a/govcd/catalogitem.go b/govcd/catalogitem.go index 48e2bcc09..fe321e382 100644 --- a/govcd/catalogitem.go +++ b/govcd/catalogitem.go @@ -37,7 +37,7 @@ func (catalogItem *CatalogItem) GetVAppTemplate() (VAppTemplate, error) { } -// Deletes the Catalog Item, returning an error if the vCD call fails. +// Delete deletes the Catalog Item, returning an error if the vCD call fails. // Link to API call: https://code.vmware.com/apis/220/vcloud#/doc/doc/operations/DELETE-CatalogItem.html func (catalogItem *CatalogItem) Delete() error { util.Logger.Printf("[TRACE] Deleting catalog item: %#v", catalogItem.CatalogItem) @@ -90,16 +90,30 @@ func (vdc *AdminVdc) QueryCatalogItemList() ([]*types.QueryResultCatalogItemType return queryCatalogItemList(vdc.client, "vdc", vdc.AdminVdc.ID) } -// queryVappTemplateList returns a list of vApp templates for the given parent -func queryVappTemplateList(client *Client, parentField, parentValue string) ([]*types.QueryResultVappTemplateType, error) { +// queryVappTemplateListWithParentField returns a list of vApp templates for the given parent +func queryVappTemplateListWithParentField(client *Client, parentField, parentValue string) ([]*types.QueryResultVappTemplateType, error) { + return queryVappTemplateListWithFilter(client, map[string]string{ + parentField: parentValue, + }) +} +// queryVappTemplateListWithFilter returns a list of vApp templates filtered by the given filter map. +// The filter map will build a filter like filterKey==filterValue;filterKey2==filterValue2;... +func queryVappTemplateListWithFilter(client *Client, filter map[string]string) ([]*types.QueryResultVappTemplateType, error) { vappTemplateType := types.QtVappTemplate if client.IsSysAdmin { vappTemplateType = types.QtAdminVappTemplate } + filterEncoded := "" + for k, v := range filter { + filterEncoded += fmt.Sprintf("%s==%s;", url.QueryEscape(k), url.QueryEscape(v)) + } + if len(filterEncoded) > 0 { + filterEncoded = filterEncoded[:len(filterEncoded)-1] // Removes the trailing ';' + } results, err := client.cumulativeQuery(vappTemplateType, nil, map[string]string{ "type": vappTemplateType, - "filter": fmt.Sprintf("%s==%s", parentField, url.QueryEscape(parentValue)), + "filter": filterEncoded, }) if err != nil { return nil, fmt.Errorf("error querying vApp templates %s", err) @@ -114,15 +128,190 @@ func queryVappTemplateList(client *Client, parentField, parentValue string) ([]* // QueryVappTemplateList returns a list of vApp templates for the given VDC func (vdc *Vdc) QueryVappTemplateList() ([]*types.QueryResultVappTemplateType, error) { - return queryVappTemplateList(vdc.client, "vdcName", vdc.Vdc.Name) + return queryVappTemplateListWithParentField(vdc.client, "vdcName", vdc.Vdc.Name) +} + +// QueryVappTemplateWithName returns one vApp template for the given VDC with the given name. +// Returns an error if it finds more than one. +func (vdc *Vdc) QueryVappTemplateWithName(vAppTemplateName string) (*types.QueryResultVappTemplateType, error) { + vAppTemplates, err := queryVappTemplateListWithFilter(vdc.client, map[string]string{ + "vdcName": vdc.Vdc.Name, + "name": vAppTemplateName, + }) + if err != nil { + return nil, err + } + if len(vAppTemplates) != 1 { + if len(vAppTemplates) == 0 { + return nil, ErrorEntityNotFound + } + return nil, fmt.Errorf("found %d vApp Templates with name %s in VDC %s", len(vAppTemplates), vAppTemplateName, vdc.Vdc.Name) + } + return vAppTemplates[0], nil } // QueryVappTemplateList returns a list of vApp templates for the given VDC func (vdc *AdminVdc) QueryVappTemplateList() ([]*types.QueryResultVappTemplateType, error) { - return queryVappTemplateList(vdc.client, "vdcName", vdc.AdminVdc.Name) + return queryVappTemplateListWithParentField(vdc.client, "vdcName", vdc.AdminVdc.Name) +} + +// QueryVappTemplateWithName returns one vApp template for the given VDC with the given name. +// Returns an error if it finds more than one. +func (vdc *AdminVdc) QueryVappTemplateWithName(vAppTemplateName string) (*types.QueryResultVappTemplateType, error) { + vAppTemplates, err := queryVappTemplateListWithFilter(vdc.client, map[string]string{ + "vdcName": vdc.AdminVdc.Name, + "name": vAppTemplateName, + }) + if err != nil { + return nil, err + } + if len(vAppTemplates) != 1 { + if len(vAppTemplates) == 0 { + return nil, ErrorEntityNotFound + } + return nil, fmt.Errorf("found %d vApp Templates with name %s in VDC %s", len(vAppTemplates), vAppTemplateName, vdc.AdminVdc.Name) + } + return vAppTemplates[0], nil } // QueryVappTemplateList returns a list of vApp templates for the given catalog func (catalog *Catalog) QueryVappTemplateList() ([]*types.QueryResultVappTemplateType, error) { - return queryVappTemplateList(catalog.client, "catalogName", catalog.Catalog.Name) + return queryVappTemplateListWithParentField(catalog.client, "catalogName", catalog.Catalog.Name) +} + +// QueryVappTemplateWithName returns one vApp template for the given Catalog with the given name. +// Returns an error if it finds more than one. +func (catalog *Catalog) QueryVappTemplateWithName(vAppTemplateName string) (*types.QueryResultVappTemplateType, error) { + return queryVappTemplateWithName(catalog.client, catalog.Catalog.Name, vAppTemplateName) +} + +// QueryVappTemplateWithName returns one vApp template for the given Catalog with the given name. +// Returns an error if it finds more than one. +func (catalog *AdminCatalog) QueryVappTemplateWithName(vAppTemplateName string) (*types.QueryResultVappTemplateType, error) { + return queryVappTemplateWithName(catalog.client, catalog.AdminCatalog.Name, vAppTemplateName) +} + +// queryVappTemplateWithName returns one vApp template for the given Catalog with the given name. +// Returns an error if it finds more than one. +func queryVappTemplateWithName(client *Client, catalogName, vAppTemplateName string) (*types.QueryResultVappTemplateType, error) { + vAppTemplates, err := queryVappTemplateListWithFilter(client, map[string]string{ + "catalogName": catalogName, + "name": vAppTemplateName, + }) + if err != nil { + return nil, err + } + if len(vAppTemplates) != 1 { + if len(vAppTemplates) == 0 { + return nil, ErrorEntityNotFound + } + return nil, fmt.Errorf("found %d vApp Templates with name %s in Catalog %s", len(vAppTemplates), vAppTemplateName, catalogName) + } + return vAppTemplates[0], nil +} + +// queryCatalogItemFilteredList returns a list of Catalog Items with an optional filter +func queryCatalogItemFilteredList(client *Client, filter map[string]string) ([]*types.QueryResultCatalogItemType, error) { + catalogItemType := types.QtCatalogItem + if client.IsSysAdmin { + catalogItemType = types.QtAdminCatalogItem + } + + filterText := "" + for k, v := range filter { + if filterText != "" { + filterText += ";" + } + filterText += fmt.Sprintf("%s==%s", k, url.QueryEscape(v)) + } + + notEncodedParams := map[string]string{ + "type": catalogItemType, + } + if filterText != "" { + notEncodedParams["filter"] = filterText + } + results, err := client.cumulativeQuery(catalogItemType, nil, notEncodedParams) + if err != nil { + return nil, fmt.Errorf("error querying catalog items %s", err) + } + + if client.IsSysAdmin { + return results.Results.AdminCatalogItemRecord, nil + } else { + return results.Results.CatalogItemRecord, nil + } +} + +// QueryCatalogItemList returns a list of Catalog Item for the given admin catalog +func (catalog *AdminCatalog) QueryCatalogItemList() ([]*types.QueryResultCatalogItemType, error) { + return queryCatalogItemList(catalog.client, "catalog", catalog.AdminCatalog.ID) +} + +// QueryCatalogItem returns a named Catalog Item for the given catalog +func (catalog *AdminCatalog) QueryCatalogItem(name string) (*types.QueryResultCatalogItemType, error) { + return queryCatalogItem(catalog.client, "catalog", catalog.AdminCatalog.ID, name) +} + +// queryCatalogItem returns a named Catalog Item for the given parent +func queryCatalogItem(client *Client, parentField, parentValue, name string) (*types.QueryResultCatalogItemType, error) { + + result, err := queryCatalogItemFilteredList(client, map[string]string{parentField: parentValue, "name": name}) + if err != nil { + return nil, err + } + if len(result) == 0 { + return nil, ErrorEntityNotFound + } + if len(result) > 1 { + return nil, fmt.Errorf("more than one item (%d) found with name %s", len(result), name) + } + return result[0], nil +} + +// queryResultCatalogItemToCatalogItem converts a catalog item as retrieved from a query into a regular one +func queryResultCatalogItemToCatalogItem(client *Client, qr *types.QueryResultCatalogItemType) *CatalogItem { + var catalogItem = NewCatalogItem(client) + catalogItem.CatalogItem = &types.CatalogItem{ + HREF: qr.HREF, + Type: qr.Type, + ID: extractUuid(qr.HREF), + Name: qr.Name, + DateCreated: qr.CreationDate, + Entity: &types.Entity{ + HREF: qr.Entity, + Type: qr.EntityType, + Name: qr.EntityName, + }, + } + return catalogItem +} + +// LaunchSync starts synchronisation of a subscribed Catalog item +func (item *CatalogItem) LaunchSync() (*Task, error) { + util.Logger.Printf("[TRACE] LaunchSync '%s' \n", item.CatalogItem.Name) + err := WaitResource(func() (*types.TasksInProgress, error) { + if item.CatalogItem.Tasks == nil { + return nil, nil + } + err := item.Refresh() + if err != nil { + return nil, err + } + return item.CatalogItem.Tasks, nil + }) + if err != nil { + return nil, err + } + return elementLaunchSync(item.client, item.CatalogItem.HREF, "catalog item") +} + +// Refresh retrieves a fresh copy of the catalog Item +func (item *CatalogItem) Refresh() error { + _, err := item.client.ExecuteRequest(item.CatalogItem.HREF, http.MethodGet, + "", "error retrieving catalog item: %s", nil, item.CatalogItem) + if err != nil { + return err + } + return nil } diff --git a/govcd/catalogitem_test.go b/govcd/catalogitem_test.go index 1bfa8cc82..3d6245566 100644 --- a/govcd/catalogitem_test.go +++ b/govcd/catalogitem_test.go @@ -1,4 +1,4 @@ -// +build catalog functional ALL +//go:build catalog || functional || ALL /* * Copyright 2020 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -191,4 +191,81 @@ func (vcd *TestVCD) TestQueryCatalogItemAndVAppTemplateList(check *C) { check.Assert(vappTemplate, NotNil) } + // Compare vApp templates from query with one retrieved by name + vAppTemplateQueryResult, err := catalog.QueryVappTemplateWithName(queryVappTemplatesByCatalog[0].Name) + check.Assert(err, IsNil) + check.Assert(vAppTemplateQueryResult, NotNil) + check.Assert(vAppTemplateQueryResult, DeepEquals, queryVappTemplatesByCatalog[0]) +} + +func (vcd *TestVCD) Test_DeleteNonEmptyCatalog(check *C) { + skipWhenOvaPathMissing(vcd.config.OVA.OvaPath, check) + + catalogName := check.TestName() + catalogItemName := check.TestName() + "_item" + // Fetching organization + org, err := vcd.client.GetAdminOrgByName(vcd.org.Org.Name) + check.Assert(err, IsNil) + check.Assert(org, NotNil) + catalog, err := org.CreateCatalog(catalogName, catalogName) + check.Assert(err, IsNil) + AddToCleanupList(catalogName, "catalog", vcd.org.Org.Name, check.TestName()) + + check.Assert(catalog, NotNil) + + // add catalogItem + uploadTask, err := catalog.UploadOvf(vcd.config.OVA.OvaPath, catalogItemName, "upload from delete catalog item test", 1024) + check.Assert(err, IsNil) + err = uploadTask.WaitTaskCompletion() + check.Assert(err, IsNil) + AddToCleanupList(catalogItemName, "catalogItem", vcd.org.Org.Name+"|"+catalogName, check.TestName()) + + retrievedCatalog, err := org.GetCatalogByName(catalogName, true) + check.Assert(err, IsNil) + catalogItem, err := retrievedCatalog.GetCatalogItemByName(catalogItemName, true) + check.Assert(err, IsNil) + check.Assert(catalogItem, NotNil) + + err = retrievedCatalog.Delete(true, true) + check.Assert(err, IsNil) + + retrievedCatalog, err = org.GetCatalogByName(catalogName, true) + check.Assert(err, NotNil) + check.Assert(retrievedCatalog, IsNil) +} + +func (vcd *TestVCD) Test_QueryVappTemplateList(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + + catalogName := vcd.config.VCD.Catalog.Name + if catalogName == "" { + check.Skip("Test_QueryVappTemplateList: Catalog name not given") + return + } + + cat, err := vcd.org.GetCatalogByName(catalogName, false) + if err != nil { + check.Skip("Test_QueryVappTemplateList: Catalog not found") + return + } + + vAppTemplates, err := cat.QueryVappTemplateList() + check.Assert(err, IsNil) + check.Assert(vAppTemplates, NotNil) + + // Check the number of vApp templates is one + // Dump all vApp template structures to easily identify leftover objects if number is not 1 + if len(vAppTemplates) > 1 { + fmt.Printf("%#v", vAppTemplates) + } + check.Assert(len(vAppTemplates), Equals, 1) + + // Check the name of the vApp template is what it should be + check.Assert(vAppTemplates[0].Name, Equals, vcd.config.VCD.Catalog.CatalogItem) + + // Check the vApp Template retrieved before is the same as the one retrieved by name + vAppTemplate, err := cat.QueryVappTemplateWithName(vAppTemplates[0].Name) + check.Assert(err, IsNil) + check.Assert(vAppTemplates, NotNil) + check.Assert(vAppTemplate, DeepEquals, vAppTemplates[0]) } diff --git a/govcd/certificate_management.go b/govcd/certificate_management.go new file mode 100644 index 000000000..97fd6c143 --- /dev/null +++ b/govcd/certificate_management.go @@ -0,0 +1,362 @@ +/* + * Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "net/url" + "regexp" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +// Certificate is a structure defining a certificate in VCD +// It is called "Certificate Library" in the UI, and "Certificate Library item" in the API +type Certificate struct { + CertificateLibrary *types.CertificateLibraryItem + Href string + client *Client +} + +// GetCertificateFromLibraryById Returns certificate from library of certificates +func getCertificateFromLibraryById(client *Client, id string, additionalHeader map[string]string) (*Certificate, error) { + endpoint, err := getEndpointByVersion(client) + if err != nil { + return nil, err + } + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + if id == "" { + return nil, fmt.Errorf("empty certificate ID") + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, id) + if err != nil { + return nil, err + } + + certificate := &Certificate{ + CertificateLibrary: &types.CertificateLibraryItem{}, + client: client, + Href: urlRef.String(), + } + + err = client.OpenApiGetItem(minimumApiVersion, urlRef, nil, certificate.CertificateLibrary, additionalHeader) + if err != nil { + return nil, err + } + + return certificate, nil +} + +func getEndpointByVersion(client *Client) (string, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointSSLCertificateLibrary + newerApiVersion, err := client.VersionEqualOrGreater("10.3", 3) + if err != nil { + return "", err + } + if !newerApiVersion { + // in previous version exist only API with mistype in name + endpoint = types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointSSLCertificateLibraryOld + } + return endpoint, err +} + +// GetCertificateFromLibraryById Returns certificate from library of certificates from System Context +func (client *Client) GetCertificateFromLibraryById(id string) (*Certificate, error) { + return getCertificateFromLibraryById(client, id, nil) +} + +// GetCertificateFromLibraryById Returns certificate from library of certificates from Org context +func (adminOrg *AdminOrg) GetCertificateFromLibraryById(id string) (*Certificate, error) { + tenantContext, err := adminOrg.getTenantContext() + if err != nil { + return nil, err + } + return getCertificateFromLibraryById(adminOrg.client, id, getTenantContextHeader(tenantContext)) +} + +// addCertificateToLibrary uploads certificates with configuration details +func addCertificateToLibrary(client *Client, certificateConfig *types.CertificateLibraryItem, + additionalHeader map[string]string) (*Certificate, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointSSLCertificateLibrary + apiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + typeResponse := &Certificate{ + CertificateLibrary: &types.CertificateLibraryItem{}, + client: client, + Href: urlRef.String(), + } + + err = client.OpenApiPostItem(apiVersion, urlRef, nil, + certificateConfig, typeResponse.CertificateLibrary, additionalHeader) + if err != nil { + return nil, err + } + + return typeResponse, nil +} + +// AddCertificateToLibrary uploads certificates with configuration details +func (adminOrg *AdminOrg) AddCertificateToLibrary(certificateConfig *types.CertificateLibraryItem) (*Certificate, error) { + tenantContext, err := adminOrg.getTenantContext() + if err != nil { + return nil, err + } + return addCertificateToLibrary(adminOrg.client, certificateConfig, getTenantContextHeader(tenantContext)) +} + +// AddCertificateToLibrary uploads certificates with configuration details +func (client *Client) AddCertificateToLibrary(certificateConfig *types.CertificateLibraryItem) (*Certificate, error) { + return addCertificateToLibrary(client, certificateConfig, nil) +} + +// getAllCertificateFromLibrary retrieves all certificates. Query parameters can be supplied to perform additional +// filtering +func getAllCertificateFromLibrary(client *Client, queryParameters url.Values, additionalHeader map[string]string) ([]*Certificate, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointSSLCertificateLibrary + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + responses := []*types.CertificateLibraryItem{{}} + err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParameters, &responses, additionalHeader) + if err != nil { + return nil, err + } + + var wrappedCertificates []*Certificate + for _, response := range responses { + urlRef, err := client.OpenApiBuildEndpoint(endpoint, response.Id) + if err != nil { + return nil, err + } + wrappedCertificate := &Certificate{ + CertificateLibrary: response, + client: client, + Href: urlRef.String(), + } + wrappedCertificates = append(wrappedCertificates, wrappedCertificate) + } + + return wrappedCertificates, nil +} + +// GetAllCertificatesFromLibrary retrieves all available certificates from certificate library. +// Query parameters can be supplied to perform additional filtering +func (client *Client) GetAllCertificatesFromLibrary(queryParameters url.Values) ([]*Certificate, error) { + return getAllCertificateFromLibrary(client, queryParameters, nil) +} + +// CountMatchingCertificates searches among all certificates and return the number of certificates +// with the text that matches the given PEM +func (client *Client) CountMatchingCertificates(pem string) (int, error) { + matchingCertificates, err := client.MatchingCertificatesInLibrary(pem) + if err != nil { + return 0, err + } + return len(matchingCertificates), nil +} + +// MatchingCertificatesInLibrary searches among all certificates and return all certificates +// with the text that matches the given PEM +func (client *Client) MatchingCertificatesInLibrary(pem string) ([]*Certificate, error) { + certificates, err := client.GetAllCertificatesFromLibrary(nil) + if err != nil { + return nil, err + } + var matchingCertificates []*Certificate + for _, cert := range certificates { + isSame, err := cert.SameAs(pem) + if err != nil { + return nil, err + } + if isSame { + matchingCertificates = append(matchingCertificates, cert) + } + } + return matchingCertificates, nil +} + +// GetAllCertificatesFromLibrary r retrieves all available certificates from certificate library. +// Query parameters can be supplied to perform additional filtering +func (adminOrg *AdminOrg) GetAllCertificatesFromLibrary(queryParameters url.Values) ([]*Certificate, error) { + tenantContext, err := adminOrg.getTenantContext() + if err != nil { + return nil, err + } + return getAllCertificateFromLibrary(adminOrg.client, queryParameters, getTenantContextHeader(tenantContext)) +} + +// getCertificateFromLibraryByName retrieves certificate from certificate library by given name +// When the alias contains commas, semicolons or asterisks, the encoding is rejected by the API in VCD. +// For this reason, when one or more commas, semicolons or asterisks are present we run the search brute force, +// by fetching all certificates and comparing the alias. +// Also, url.QueryEscape as well as url.Values.Encode() both encode the space as a + character. So we use +// search brute force too. Reference to issue: +// https://github.com/golang/go/issues/4013 +// https://github.com/czos/goamz/pull/11/files +func getCertificateFromLibraryByName(client *Client, name string, additionalHeader map[string]string) (*Certificate, error) { + slowSearch, params := shouldDoSlowSearch("alias", name) + + var foundCertificates []*Certificate + certificates, err := getAllCertificateFromLibrary(client, params, additionalHeader) + if err != nil { + return nil, err + } + if len(certificates) == 0 { + return nil, ErrorEntityNotFound + } + foundCertificates = append(foundCertificates, certificates[0]) + + if slowSearch { + foundCertificates = nil + for _, certificate := range certificates { + if certificate.CertificateLibrary.Alias == name { + foundCertificates = append(foundCertificates, certificate) + } + } + if len(foundCertificates) == 0 { + return nil, ErrorEntityNotFound + } + if len(foundCertificates) > 1 { + return nil, fmt.Errorf("more than one certificate found with name '%s'", name) + } + } + + if len(certificates) > 1 && !slowSearch { + { + return nil, fmt.Errorf("more than one certificate found with name '%s'", name) + } + } + return foundCertificates[0], nil +} + +// GetCertificateFromLibraryByName retrieves certificate from certificate library by given name +func (client *Client) GetCertificateFromLibraryByName(name string) (*Certificate, error) { + return getCertificateFromLibraryByName(client, name, nil) +} + +// GetCertificateFromLibraryByName retrieves certificate from certificate library by given name +func (adminOrg *AdminOrg) GetCertificateFromLibraryByName(name string) (*Certificate, error) { + tenantContext, err := adminOrg.getTenantContext() + if err != nil { + return nil, err + } + return getCertificateFromLibraryByName(adminOrg.client, name, getTenantContextHeader(tenantContext)) +} + +// Update updates existing Certificate. Allows changing only alias and description +func (certificate *Certificate) Update() (*Certificate, error) { + endpoint, err := getEndpointByVersion(certificate.client) + if err != nil { + return nil, err + } + minimumApiVersion, err := certificate.client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + if certificate.CertificateLibrary.Id == "" { + return nil, fmt.Errorf("cannot update certificate without id") + } + + urlRef, err := certificate.client.OpenApiBuildEndpoint(endpoint, certificate.CertificateLibrary.Id) + if err != nil { + return nil, err + } + + returnCertificate := &Certificate{ + CertificateLibrary: &types.CertificateLibraryItem{}, + client: certificate.client, + } + + err = certificate.client.OpenApiPutItem(minimumApiVersion, urlRef, nil, certificate.CertificateLibrary, + returnCertificate.CertificateLibrary, nil) + if err != nil { + return nil, fmt.Errorf("error updating certificate: %s", err) + } + + return returnCertificate, nil +} + +// Delete deletes certificate from Certificate library +func (certificate *Certificate) Delete() error { + endpoint, err := getEndpointByVersion(certificate.client) + if err != nil { + return err + } + minimumApiVersion, err := certificate.client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return err + } + + if certificate.CertificateLibrary.Id == "" { + return fmt.Errorf("cannot delete certificate without id") + } + + urlRef, err := certificate.client.OpenApiBuildEndpoint(endpoint, certificate.CertificateLibrary.Id) + if err != nil { + return err + } + + err = certificate.client.OpenApiDeleteItem(minimumApiVersion, urlRef, nil, nil) + + if err != nil { + return fmt.Errorf("error deleting certificate: %s", err) + } + + return nil +} + +// getCertificateText returns the stripped text of the certificate, without the +// starting and ending markers +func getCertificateText(pem string) (string, error) { + reText, err := regexp.Compile( + `(?s)` + // treats newline as any other character + `-----BEGIN CERTIFICATE-----` + // the 'begin certificate' marker + `(.+)` + // any sequence of characters after the 'begin certificate' marker + `-----END CERTIFICATE-----`) // the 'end certificate' marker + if err != nil { + return "", err + } + + text := reText.FindStringSubmatch(pem) + if len(text) < 2 { + return "", fmt.Errorf("start marker 'BEGIN CERTIFICATE' or end marker 'END CERTIFICATE' not found in input certificate") + } + return text[1], nil +} + +// SameAs returns true if the certificate text matches the text of the provided PEM +// (without the BEGIN CERTIFICATE and END CERTIFICATE markers) +func (certificate *Certificate) SameAs(pem string) (bool, error) { + internalValue, err := getCertificateText(certificate.CertificateLibrary.Certificate) + if err != nil { + return false, err + } + compareValue, err := getCertificateText(pem) + if err != nil { + return false, err + } + return internalValue == compareValue, nil +} diff --git a/govcd/certificate_management_test.go b/govcd/certificate_management_test.go new file mode 100644 index 000000000..9596b4991 --- /dev/null +++ b/govcd/certificate_management_test.go @@ -0,0 +1,210 @@ +//go:build functional || openapi || certificate || ALL + +/* + * Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + _ "embed" + "fmt" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_CertificateInLibrary(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointSSLCertificateLibrary) + + alias := "Test_CertificateInLibrary" + + certificateConfig := &types.CertificateLibraryItem{ + Alias: alias, + Certificate: certificate, + } + createdCertificate, err := vcd.client.Client.AddCertificateToLibrary(certificateConfig) + check.Assert(err, IsNil) + openApiEndpoint, err := getEndpointByVersion(&vcd.client.Client) + check.Assert(err, IsNil) + check.Assert(openApiEndpoint, NotNil) + PrependToCleanupListOpenApi(createdCertificate.CertificateLibrary.Alias, check.TestName(), openApiEndpoint+createdCertificate.CertificateLibrary.Id) + + check.Assert(createdCertificate, NotNil) + check.Assert(createdCertificate.CertificateLibrary.Id, Not(Equals), "") + check.Assert(createdCertificate.CertificateLibrary.Alias, Equals, alias) + check.Assert(createdCertificate.CertificateLibrary.Certificate, Equals, certificate) + + matchesCert, err := createdCertificate.SameAs(certificate) + check.Assert(err, IsNil) + check.Assert(matchesCert, Equals, true) + + fetchedCertificate, err := vcd.client.Client.GetCertificateFromLibraryById(createdCertificate.CertificateLibrary.Id) + check.Assert(err, IsNil) + check.Assert(fetchedCertificate, NotNil) + check.Assert(fetchedCertificate.CertificateLibrary.Alias, Equals, alias) + check.Assert(fetchedCertificate.CertificateLibrary.Certificate, Equals, certificate) + + //test with private key and upload to org context + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.org.Org.Name) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + + aliasForPrivateKey := "Test_CertificateInLibrary_private_key_test" + description := "generated by test" + + privateKeyPassphrase := "test" + certificateWithPrivateKeyConfig := &types.CertificateLibraryItem{ + Alias: aliasForPrivateKey, + Certificate: certificate, + Description: description, + PrivateKey: privateKey, + PrivateKeyPassphrase: privateKeyPassphrase, + } + createdCertificateWithPrivateKeyConfig, err := adminOrg.AddCertificateToLibrary(certificateWithPrivateKeyConfig) + check.Assert(err, IsNil) + openApiEndpoint, err = getEndpointByVersion(&vcd.client.Client) + check.Assert(err, IsNil) + check.Assert(openApiEndpoint, NotNil) + PrependToCleanupListOpenApi(createdCertificateWithPrivateKeyConfig.CertificateLibrary.Alias, check.TestName(), + openApiEndpoint+createdCertificateWithPrivateKeyConfig.CertificateLibrary.Id) + + check.Assert(createdCertificateWithPrivateKeyConfig, NotNil) + check.Assert(createdCertificateWithPrivateKeyConfig.CertificateLibrary.Id, Not(Equals), "") + check.Assert(createdCertificateWithPrivateKeyConfig.CertificateLibrary.Alias, Equals, aliasForPrivateKey) + check.Assert(createdCertificateWithPrivateKeyConfig.CertificateLibrary.Certificate, Equals, certificate) + + fetchedCertificateWithPrivateKey, err := vcd.client.Client.GetCertificateFromLibraryById(createdCertificateWithPrivateKeyConfig.CertificateLibrary.Id) + check.Assert(err, IsNil) + check.Assert(fetchedCertificateWithPrivateKey, NotNil) + check.Assert(fetchedCertificateWithPrivateKey.CertificateLibrary.Alias, Equals, aliasForPrivateKey) + check.Assert(fetchedCertificateWithPrivateKey.CertificateLibrary.Certificate, Equals, certificate) + + // check fetching all certificates + allOrgCertificates, err := adminOrg.GetAllCertificatesFromLibrary(nil) + check.Assert(err, IsNil) + check.Assert(allOrgCertificates, NotNil) + + matchingCertificates, err := vcd.client.Client.MatchingCertificatesInLibrary(certificate) + check.Assert(err, IsNil) + check.Assert(matchingCertificates, NotNil) + + foundCertificates, err := vcd.client.Client.CountMatchingCertificates(certificate) + check.Assert(err, IsNil) + check.Assert(foundCertificates, Equals, len(matchingCertificates)) + check.Assert(foundCertificates, Equals, 1) + + if testVerbose { + fmt.Printf("(org) how many certificates: %d\n", len(allOrgCertificates)) + for i, oneCertificate := range allOrgCertificates { + fmt.Printf("%3d %-20s %-53s %s\n", i, oneCertificate.CertificateLibrary.Alias, + oneCertificate.CertificateLibrary.Id, oneCertificate.CertificateLibrary.Description) + } + } + allExistingCertificates, err := adminOrg.client.GetAllCertificatesFromLibrary(nil) + check.Assert(err, IsNil) + check.Assert(allExistingCertificates, NotNil) + + if testVerbose { + fmt.Printf("(global) how many certificates: %d\n", len(allExistingCertificates)) + for i, oneCertificate := range allExistingCertificates { + fmt.Printf("%3d %-20s %-53s %s\n", i, oneCertificate.CertificateLibrary.Alias, + oneCertificate.CertificateLibrary.Id, oneCertificate.CertificateLibrary.Description) + } + } + + // check fetching certificate by Name + foundCertificate, err := vcd.client.Client.GetCertificateFromLibraryByName(alias) + check.Assert(err, IsNil) + check.Assert(foundCertificate, NotNil) + check.Assert(foundCertificate.CertificateLibrary.Alias, Equals, alias) + + foundCertificateWithPrivateKey, err := adminOrg.GetCertificateFromLibraryByName(aliasForPrivateKey) + check.Assert(err, IsNil) + check.Assert(foundCertificateWithPrivateKey, NotNil) + check.Assert(foundCertificateWithPrivateKey.CertificateLibrary.Alias, Equals, aliasForPrivateKey) + + // check update + newAlias := "newAlias" + newDescription := "newDescription" + foundCertificateWithPrivateKey.CertificateLibrary.Alias = newAlias + foundCertificateWithPrivateKey.CertificateLibrary.Description = newDescription + updateCertificateWithPrivateKey, err := foundCertificateWithPrivateKey.Update() + check.Assert(err, IsNil) + check.Assert(updateCertificateWithPrivateKey, NotNil) + check.Assert(updateCertificateWithPrivateKey.CertificateLibrary.Alias, Equals, newAlias) + check.Assert(updateCertificateWithPrivateKey.CertificateLibrary.Description, Equals, newDescription) + check.Assert(updateCertificateWithPrivateKey.CertificateLibrary.Id, Not(Equals), "") + check.Assert(updateCertificateWithPrivateKey.CertificateLibrary.Certificate, Equals, certificate) + check.Assert(updateCertificateWithPrivateKey.CertificateLibrary.PrivateKey, NotNil) // isn't returned + check.Assert(updateCertificateWithPrivateKey.CertificateLibrary.PrivateKeyPassphrase, NotNil) // isn't returned + + foundCertificate.CertificateLibrary.Alias = newAlias + foundCertificate.CertificateLibrary.Description = newDescription + updateCertificate, err := foundCertificate.Update() + check.Assert(err, IsNil) + check.Assert(updateCertificate, NotNil) + check.Assert(updateCertificate.CertificateLibrary.Alias, Equals, newAlias) + check.Assert(updateCertificate.CertificateLibrary.Description, Equals, newDescription) + check.Assert(updateCertificate.CertificateLibrary.Id, Not(Equals), "") + check.Assert(updateCertificate.CertificateLibrary.Certificate, Equals, certificate) + check.Assert(updateCertificate.CertificateLibrary.PrivateKey, NotNil) // isn't returned + check.Assert(updateCertificate.CertificateLibrary.PrivateKeyPassphrase, NotNil) // isn't returned + + //delete certificate + err = updateCertificateWithPrivateKey.Delete() + check.Assert(err, IsNil) + deletedCertificate, err := vcd.client.Client.GetCertificateFromLibraryById(updateCertificateWithPrivateKey.CertificateLibrary.Id) + check.Assert(ContainsNotFound(err), Equals, true) + check.Assert(deletedCertificate, IsNil) + + err = updateCertificate.Delete() + check.Assert(err, IsNil) + deletedCertificate, err = adminOrg.client.GetCertificateFromLibraryById(updateCertificate.CertificateLibrary.Id) + check.Assert(ContainsNotFound(err), Equals, true) + check.Assert(deletedCertificate, IsNil) + +} + +func (vcd *TestVCD) Test_GetCertificateFromLibraryByName_ValidatesSymbolsInName(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointSSLCertificateLibrary) + + // When alias contains commas, semicolons, stars, or plus signs, the encoding may reject by the API when we try to Query it + // Also, spaces present their own issues + for _, symbol := range []string{";", ",", "+", " ", "*", ":"} { + + alias := fmt.Sprintf("Test%sCertificate%sIn%sLibrary", symbol, symbol, symbol) + + certificateConfig := &types.CertificateLibraryItem{ + Alias: alias, + Certificate: certificate, + } + createdCertificate, err := vcd.client.Client.AddCertificateToLibrary(certificateConfig) + check.Assert(err, IsNil) + openApiEndpoint, err := getEndpointByVersion(&vcd.client.Client) + check.Assert(err, IsNil) + check.Assert(openApiEndpoint, NotNil) + PrependToCleanupListOpenApi(createdCertificate.CertificateLibrary.Alias, check.TestName(), + openApiEndpoint+createdCertificate.CertificateLibrary.Id) + + check.Assert(createdCertificate, NotNil) + check.Assert(createdCertificate.CertificateLibrary.Id, Not(Equals), "") + check.Assert(createdCertificate.CertificateLibrary.Alias, Equals, alias) + check.Assert(createdCertificate.CertificateLibrary.Certificate, Equals, certificate) + + foundCertificate, err := vcd.client.Client.GetCertificateFromLibraryByName(alias) + check.Assert(err, IsNil) + check.Assert(foundCertificate, NotNil) + check.Assert(foundCertificate.CertificateLibrary.Alias, Equals, alias) + + err = foundCertificate.Delete() + check.Assert(err, IsNil) + } +} diff --git a/govcd/certificates_embedded_test.go b/govcd/certificates_embedded_test.go new file mode 100644 index 000000000..aec8da288 --- /dev/null +++ b/govcd/certificates_embedded_test.go @@ -0,0 +1,22 @@ +//go:build functional || openapi || certificate || alb || nsxt || network || ALL + +/* + * Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + _ "embed" +) + +var ( + //go:embed test-resources/cert.pem + certificate string + + //go:embed test-resources/key.pem + privateKey string + + //go:embed test-resources/rootCA.pem + rootCaCertificate string +) diff --git a/govcd/common_test.go b/govcd/common_test.go index 6bdc0f644..8e0701df0 100644 --- a/govcd/common_test.go +++ b/govcd/common_test.go @@ -1,7 +1,7 @@ -// +build api functional catalog vapp gateway network org query extnetwork task vm vdc system disk lb lbAppRule lbAppProfile lbServerPool lbServiceMonitor lbVirtualServer user nsxv affinity ALL +//go:build api || auth || functional || catalog || vapp || gateway || network || org || query || extnetwork || task || vm || vdc || system || disk || lb || lbAppRule || lbAppProfile || lbServerPool || lbServiceMonitor || lbVirtualServer || user || role || nsxv || nsxt || openapi || affinity || search || alb || certificate || vdcGroup || metadata || providervdc || rde || uiPlugin || vsphere || cse || slz || ALL /* - * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + * Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. */ package govcd @@ -9,15 +9,17 @@ package govcd import ( "errors" "fmt" - "io/ioutil" - "net" + "io" "net/http" "net/url" + "os" "regexp" "sort" "strconv" "time" + "github.com/vmware/go-vcloud-director/v2/util" + "github.com/vmware/go-vcloud-director/v2/types/v56" . "gopkg.in/check.v1" @@ -55,10 +57,9 @@ func (vcd *TestVCD) createAndGetResourcesForVmCreation(check *C, vmName string) vappTemplate, err := catalogItem.GetVAppTemplate() check.Assert(err, IsNil) // Compose Raw vApp - err = vdc.ComposeRawVApp(vmName) - check.Assert(err, IsNil) - vapp, err := vdc.GetVAppByName(vmName, true) + vapp, err := vdc.CreateRawVApp(vmName, "") check.Assert(err, IsNil) + check.Assert(vapp, NotNil) // vApp was created - let's add it to cleanup list AddToCleanupList(vmName, "vapp", "", "createTestVapp") // Wait until vApp becomes configurable @@ -104,14 +105,10 @@ func spawnVM(name string, memorySize int, vdc Vdc, vapp VApp, net types.NetworkC fmt.Printf(". Done\n") fmt.Printf("# Applying 2 vCPU and "+strconv.Itoa(memorySize)+"MB configuration for VM '%s'", name) - task, err = vm.ChangeCPUCount(2) - check.Assert(err, IsNil) - err = task.WaitTaskCompletion() + err = vm.ChangeCPU(2, 1) check.Assert(err, IsNil) - task, err = vm.ChangeMemorySize(memorySize) - check.Assert(err, IsNil) - err = task.WaitTaskCompletion() + err = vm.ChangeMemory(int64(memorySize)) check.Assert(err, IsNil) fmt.Printf(". Done\n") @@ -156,29 +153,6 @@ func isItemPhotonOs(item CatalogItem) bool { return true } -// catalogItemIsPhotonOs returns true if test config catalog item is Photon OS image -func catalogItemIsPhotonOs(vcd *TestVCD) bool { - // Get Org, Vdc - org, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) - if err != nil { - return false - } - // Find catalog and catalog item - catalog, err := org.GetCatalogByName(vcd.config.VCD.Catalog.Name, false) - if err != nil { - return false - } - catalogItem, err := catalog.GetCatalogItemByName(vcd.config.VCD.Catalog.CatalogItem, false) - if err != nil { - return false - } - if !isItemPhotonOs(*catalogItem) { - return false - } - - return true -} - // cacheLoadBalancer is meant to store load balancer settings before any operations so that all // configuration can be checked after manipulation func testCacheLoadBalancer(edge EdgeGateway, check *C) (*types.LbGeneralParamsWithXml, string) { @@ -199,9 +173,14 @@ func testGetEdgeEndpointXML(endpoint string, edge EdgeGateway, check *C) string fmt.Sprintf("unable to get XML from endpoint %s: %%s", endpoint), nil, &types.NSXError{}) check.Assert(err, IsNil) - defer resp.Body.Close() + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + util.Logger.Printf("error closing response Body [testGetEdgeEndpointXML]: %s", err) + } + }(resp.Body) - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) check.Assert(err, IsNil) return string(body) @@ -230,50 +209,14 @@ func testCheckLoadBalancerConfig(beforeLb *types.LbGeneralParamsWithXml, beforeL check.Assert(beforeLbXml, DeepEquals, afterLbXml) } -// isTcpPortOpen checks if remote TCP port is open or closed every 8 seconds until timeout is -// reached -func isTcpPortOpen(host, port string, timeout int) bool { - retryTimeout := timeout - // due to the VMs taking long time to boot it needs to be at least 5 minutes - // may be even more in slower environments - if timeout < 5*60 { // 5 minutes - retryTimeout = 5 * 60 // 5 minutes - } - timeOutAfterInterval := time.Duration(retryTimeout) * time.Second - timeoutAfter := time.After(timeOutAfterInterval) - tick := time.NewTicker(time.Duration(8) * time.Second) - - for { - select { - case <-timeoutAfter: - fmt.Printf(" Failed\n") - return false - case <-tick.C: - timeout := time.Second * 3 - conn, err := net.DialTimeout("tcp", net.JoinHostPort(host, port), timeout) - if err != nil { - fmt.Printf(".") - } - // Connection established - the port is open - if conn != nil { - defer conn.Close() - fmt.Printf(" Done\n") - return true - } - } - } - -} - -// moved from vapp_test.go -func createVappForTest(vcd *TestVCD, vappName string) (*VApp, error) { +// deployVappForTest aims to replace createVappForTest +func deployVappForTest(vcd *TestVCD, vappName string) (*VApp, error) { // Populate OrgVDCNetwork - var networks []*types.OrgVDCNetwork net, err := vcd.vdc.GetOrgVdcNetworkByName(vcd.config.VCD.Network.Net1, false) if err != nil { return nil, fmt.Errorf("error finding network : %s", err) } - networks = append(networks, net.OrgVDCNetwork) + // Populate Catalog cat, err := vcd.org.GetCatalogByName(vcd.config.VCD.Catalog.Name, false) if err != nil || cat == nil { @@ -294,22 +237,45 @@ func createVappForTest(vcd *TestVCD, vappName string) (*VApp, error) { if err != nil { return nil, fmt.Errorf("error finding storage profile: %s", err) } - // Compose VApp - task, err := vcd.vdc.ComposeVApp(networks, vAppTemplate, storageProfileRef, vappName, "description", true) + + // Create empty vApp + vapp, err := vcd.vdc.CreateRawVApp(vappName, "description") if err != nil { - return nil, fmt.Errorf("error composing vapp: %s", err) + return nil, fmt.Errorf("error creating vapp: %s", err) } + // After a successful creation, the entity is added to the cleanup list. // If something fails after this point, the entity will be removed AddToCleanupList(vappName, "vapp", "", "createTestVapp") - err = task.WaitTaskCompletion() + + // Create vApp networking + vAppNetworkConfig, err := vapp.AddOrgNetwork(&VappNetworkSettings{}, net.OrgVDCNetwork, false) if err != nil { - return nil, fmt.Errorf("error composing vapp: %s", err) + return nil, fmt.Errorf("error creating vApp network. %s", err) + } + + // Create VM with only one NIC connected to vapp_net + networkConnectionSection := &types.NetworkConnectionSection{ + PrimaryNetworkConnectionIndex: 0, } - // Get VApp - vapp, err := vcd.vdc.GetVAppByName(vappName, true) + + netConn := &types.NetworkConnection{ + Network: vAppNetworkConfig.NetworkConfig[0].NetworkName, + IsConnected: true, + NetworkConnectionIndex: 0, + IPAddressAllocationMode: types.IPAllocationModePool, + } + + networkConnectionSection.NetworkConnection = append(networkConnectionSection.NetworkConnection, netConn) + + task, err := vapp.AddNewVMWithStorageProfile("test_vm", vAppTemplate, networkConnectionSection, &storageProfileRef, true) if err != nil { - return nil, fmt.Errorf("error getting vapp: %s", err) + return nil, fmt.Errorf("error creating the VM: %s", err) + } + + err = task.WaitTaskCompletion() + if err != nil { + return nil, fmt.Errorf("error while waiting for the VM to be created %s", err) } err = vapp.BlockWhileStatus("UNRESOLVED", vapp.client.MaxRetryTimeout) @@ -424,7 +390,7 @@ func (vcd *TestVCD) ensureVMIsSuitableForVMTest(vm *VM) error { if status == types.VAppStatuses[4] { // Prevent affect Test_ChangeMemorySize // because TestVCD.Test_AttachedVMDisk is run before Test_ChangeMemorySize and Test_ChangeMemorySize will fail the test if the VM is powered on, - task, err := vm.PowerOff() + task, err := vm.Undeploy() if err != nil { return err } @@ -669,17 +635,43 @@ func deleteVapp(vcd *TestVCD, name string) error { return nil } -// makeEmptyVapp creates a given vApp without any VM -func makeEmptyVapp(vdc *Vdc, name string) (*VApp, error) { +func deleteNsxtVapp(vcd *TestVCD, name string) error { + vapp, err := vcd.nsxtVdc.GetVAppByName(name, true) + if err != nil { + return fmt.Errorf("error getting vApp: %s", err) + } + task, _ := vapp.Undeploy() + _ = task.WaitTaskCompletion() - err := vdc.ComposeRawVApp(name) + // Detach all Org networks during vApp removal because network removal errors if it happens + // very quickly (as the next task) after vApp removal + task, _ = vapp.RemoveAllNetworks() + err = task.WaitTaskCompletion() if err != nil { - return nil, err + return fmt.Errorf("error removing networks from vApp: %s", err) } - vapp, err := vdc.GetVAppByName(name, true) + + task, err = vapp.Delete() + if err != nil { + return fmt.Errorf("error deleting vApp: %s", err) + } + err = task.WaitTaskCompletion() + if err != nil { + return fmt.Errorf("error waiting for vApp deletion task: %s", err) + } + return nil +} + +// makeEmptyVapp creates a given vApp without any VM +func makeEmptyVapp(vdc *Vdc, name string, description string) (*VApp, error) { + + vapp, err := vdc.CreateRawVApp(name, description) if err != nil { return nil, err } + if vapp == nil { + return nil, fmt.Errorf("[makeEmptyVapp] unexpected nil vApp returned") + } initialVappStatus, err := vapp.GetStatus() if err != nil { return nil, err @@ -700,7 +692,7 @@ func makeEmptyVm(vapp *VApp, name string) (*VM, error) { SizeMb: int64(100), BusNumber: 0, UnitNumber: 0, - ThinProvisioned: takeBoolPointer(true), + ThinProvisioned: addrOf(true), } requestDetails := &types.RecomposeVAppParamsForEmptyVm{ CreateItem: &types.CreateItem{ @@ -709,11 +701,11 @@ func makeEmptyVm(vapp *VApp, name string) (*VM, error) { Description: "created by makeEmptyVm", GuestCustomizationSection: nil, VmSpecSection: &types.VmSpecSection{ - Modified: takeBoolPointer(true), + Modified: addrOf(true), Info: "Virtual Machine specification", OsType: "debian10Guest", - NumCpus: takeIntAddress(1), - NumCoresPerSocket: takeIntAddress(1), + NumCpus: addrOf(1), + NumCoresPerSocket: addrOf(1), CpuResourceMhz: &types.CpuResourceMhz{Configured: 1}, MemoryResourceMb: &types.MemoryResourceMb{Configured: 512}, MediaSection: nil, @@ -735,3 +727,255 @@ func makeEmptyVm(vapp *VApp, name string) (*VM, error) { return vm, nil } + +// spawnTestVdc spawns a VDC in a given adminOrgName to be used in tests +func spawnTestVdc(vcd *TestVCD, check *C, adminOrgName string) *Vdc { + adminOrg, err := vcd.client.GetAdminOrgByName(adminOrgName) + check.Assert(err, IsNil) + + providerVdcHref := getVdcProviderVdcHref(vcd, check) + storageProfile, err := vcd.client.QueryProviderVdcStorageProfileByName(vcd.config.VCD.ProviderVdc.StorageProfile, providerVdcHref) + check.Assert(err, IsNil) + networkPoolHref := getVdcNetworkPoolHref(vcd, check) + + vdcConfiguration := &types.VdcConfiguration{ + Name: check.TestName() + "-VDC", + Xmlns: types.XMLNamespaceVCloud, + AllocationModel: "Flex", + ComputeCapacity: []*types.ComputeCapacity{ + &types.ComputeCapacity{ + CPU: &types.CapacityWithUsage{ + Units: "MHz", + Allocated: 1024, + Limit: 1024, + }, + Memory: &types.CapacityWithUsage{ + Allocated: 1024, + Limit: 1024, + Units: "MB", + }, + }, + }, + VdcStorageProfile: []*types.VdcStorageProfileConfiguration{&types.VdcStorageProfileConfiguration{ + Enabled: addrOf(true), + Units: "MB", + Limit: 1024, + Default: true, + ProviderVdcStorageProfile: &types.Reference{ + HREF: storageProfile.HREF, + }, + }, + }, + NetworkPoolReference: &types.Reference{ + HREF: networkPoolHref, + }, + ProviderVdcReference: &types.Reference{ + HREF: providerVdcHref, + }, + IsEnabled: true, + IsThinProvision: true, + UsesFastProvisioning: true, + IsElastic: addrOf(true), + IncludeMemoryOverhead: addrOf(true), + ResourceGuaranteedMemory: addrOf(1.00), + } + + vdc, err := adminOrg.CreateOrgVdc(vdcConfiguration) + check.Assert(err, IsNil) + check.Assert(vdc, NotNil) + + AddToCleanupList(vdcConfiguration.Name, "vdc", vcd.org.Org.Name, check.TestName()) + + return vdc +} + +// spawnTestOrg spawns an Org to be used in tests +func spawnTestOrg(vcd *TestVCD, check *C, nameSuffix string) string { + newOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + newOrgName := check.TestName() + "-" + nameSuffix + task, err := CreateOrg(vcd.client, newOrgName, newOrgName, newOrgName, newOrg.AdminOrg.OrgSettings, true) + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) + AddToCleanupList(newOrgName, "org", "", check.TestName()) + + return newOrgName +} + +func getVdcProviderVdcHref(vcd *TestVCD, check *C) string { + results, err := vcd.client.QueryWithNotEncodedParams(nil, map[string]string{ + "type": "providerVdc", + "filter": fmt.Sprintf("name==%s", vcd.config.VCD.ProviderVdc.Name), + }) + check.Assert(err, IsNil) + if len(results.Results.VMWProviderVdcRecord) == 0 { + check.Skip(fmt.Sprintf("No Provider VDC found with name '%s'", vcd.config.VCD.ProviderVdc.Name)) + } + providerVdcHref := results.Results.VMWProviderVdcRecord[0].HREF + + return providerVdcHref +} + +func getVdcNetworkPoolHref(vcd *TestVCD, check *C) string { + results, err := vcd.client.QueryWithNotEncodedParams(nil, map[string]string{ + "type": "networkPool", + "filter": fmt.Sprintf("name==%s", vcd.config.VCD.ProviderVdc.NetworkPool), + }) + check.Assert(err, IsNil) + if len(results.Results.NetworkPoolRecord) == 0 { + check.Skip(fmt.Sprintf("No network pool found with name '%s'", vcd.config.VCD.ProviderVdc.NetworkPool)) + } + networkPoolHref := results.Results.NetworkPoolRecord[0].HREF + + return networkPoolHref +} + +// extractIdsFromOpenApiReferences extracts []string with IDs from []types.OpenApiReference which contains ID and Names +func extractIdsFromOpenApiReferences(refs []types.OpenApiReference) []string { + resultStrings := make([]string, len(refs)) + for index := range refs { + resultStrings[index] = refs[index].ID + } + + return resultStrings +} + +// checkSkipWhenApiToken skips the test if the connection was established using an API token +func (vcd *TestVCD) checkSkipWhenApiToken(check *C) { + if vcd.client.Client.UsingAccessToken { + check.Skip("This test can't run on API token") + } +} + +func createNsxtVAppAndVm(vcd *TestVCD, check *C) (*VApp, *VM) { + cat, err := vcd.org.GetCatalogByName(vcd.config.VCD.Catalog.NsxtBackedCatalogName, false) + check.Assert(err, IsNil) + check.Assert(cat, NotNil) + // Populate Catalog Item + catitem, err := cat.GetCatalogItemByName(vcd.config.VCD.Catalog.NsxtCatalogItem, false) + check.Assert(err, IsNil) + check.Assert(catitem, NotNil) + // Get VAppTemplate + vapptemplate, err := catitem.GetVAppTemplate() + check.Assert(err, IsNil) + check.Assert(vapptemplate.VAppTemplate.Children.VM[0].HREF, NotNil) + + return createNsxtVAppAndVmFromCustomTemplate(vcd, check, &vapptemplate) +} + +func createNsxtVAppAndVmFromCustomTemplate(vcd *TestVCD, check *C, vapptemplate *VAppTemplate) (*VApp, *VM) { + vapp, err := vcd.nsxtVdc.CreateRawVApp(check.TestName(), check.TestName()) + check.Assert(err, IsNil) + check.Assert(vapp, NotNil) + // After a successful creation, the entity is added to the cleanup list. + AddToCleanupList(vapp.VApp.Name, "vapp", vcd.nsxtVdc.Vdc.Name, check.TestName()) + + // Check that vApp is powered-off + vappStatus, err := vapp.GetStatus() + check.Assert(err, IsNil) + check.Assert(vappStatus, Equals, "RESOLVED") + + task, err := vapp.PowerOn() + check.Assert(err, IsNil) + check.Assert(task, NotNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) + + vappStatus, err = vapp.GetStatus() + check.Assert(err, IsNil) + check.Assert(vappStatus, Equals, "POWERED_ON") + + // Once the operation is successful, we won't trigger a failure + // until after the vApp deletion + check.Check(vapp.VApp.Name, Equals, check.TestName()) + check.Check(vapp.VApp.Description, Equals, check.TestName()) + + // Construct VM + vmDef := &types.ReComposeVAppParams{ + Ovf: types.XMLNamespaceOVF, + Xsi: types.XMLNamespaceXSI, + Xmlns: types.XMLNamespaceVCloud, + AllEULAsAccepted: true, + // Deploy: false, + Name: vapp.VApp.Name, + // PowerOn: false, // Not touching power state at this phase + SourcedItem: &types.SourcedCompositionItemParam{ + Source: &types.Reference{ + HREF: vapptemplate.VAppTemplate.Children.VM[0].HREF, + Name: check.TestName() + "-vm-tmpl", + }, + VMGeneralParams: &types.VMGeneralParams{ + Description: "test-vm-description", + }, + InstantiationParams: &types.InstantiationParams{ + NetworkConnectionSection: &types.NetworkConnectionSection{}, + }, + }, + } + vm, err := vapp.AddRawVM(vmDef) + check.Assert(err, IsNil) + check.Assert(vm, NotNil) + check.Assert(vm.VM.Name, Equals, vmDef.SourcedItem.Source.Name) + + // Refresh vApp to have latest state + err = vapp.Refresh() + check.Assert(err, IsNil) + + return vapp, vm +} + +// makeVappGroup creates multiple vApps, each with several VMs, +// as defined in `groupDefinition`. +// Returns a list of vApps +func makeVappGroup(label string, vdc *Vdc, groupDefinition map[string][]string) ([]*VApp, error) { + var vappList []*VApp + for vappName, vmNames := range groupDefinition { + existingVapp, err := vdc.GetVAppByName(vappName, false) + if err == nil { + + if existingVapp.VApp.Children == nil || len(existingVapp.VApp.Children.VM) == 0 { + return nil, fmt.Errorf("found vApp %s but without VMs", vappName) + } + foundVms := 0 + for _, vmName := range vmNames { + for _, existingVM := range existingVapp.VApp.Children.VM { + if existingVM.Name == vmName { + foundVms++ + } + } + } + if foundVms < 2 { + return nil, fmt.Errorf("found vApp %s but with %d VMs instead of 2 ", vappName, foundVms) + } + + vappList = append(vappList, existingVapp) + if testVerbose { + fmt.Printf("Using existing vApp %s\n", vappName) + } + continue + } + + if testVerbose { + fmt.Printf("Creating vApp %s\n", vappName) + } + vapp, err := makeEmptyVapp(vdc, vappName, "") + if err != nil { + return nil, err + } + if os.Getenv("GOVCD_KEEP_TEST_OBJECTS") == "" { + AddToCleanupList(vappName, "vapp", vdc.Vdc.Name, label) + } + for _, vmName := range vmNames { + if testVerbose { + fmt.Printf("\tCreating VM %s/%s\n", vappName, vmName) + } + _, err := makeEmptyVm(vapp, vmName) + if err != nil { + return nil, err + } + } + vappList = append(vappList, vapp) + } + return vappList, nil +} diff --git a/govcd/cse.go b/govcd/cse.go new file mode 100644 index 000000000..f4c20b4d7 --- /dev/null +++ b/govcd/cse.go @@ -0,0 +1,394 @@ +package govcd + +import ( + "encoding/json" + "fmt" + semver "github.com/hashicorp/go-version" + "github.com/vmware/go-vcloud-director/v2/types/v56" + "github.com/vmware/go-vcloud-director/v2/util" + "strings" + "time" +) + +// CseCreateKubernetesCluster creates a Kubernetes cluster with the data given as input (CseClusterSettings). If the given +// timeout is 0, it waits forever for the cluster creation. +// +// If the timeout is reached and the cluster is not available (in "provisioned" state), it will return a non-nil CseKubernetesCluster +// with only the cluster ID and an error. This means that the cluster will be left in VCD in any state, and it can be retrieved afterward +// with Org.CseGetKubernetesClusterById and the returned ID. +// +// If the cluster is created correctly, returns all the available data in CseKubernetesCluster or an error if some of the fields +// of the created cluster cannot be calculated or retrieved. +func (org *Org) CseCreateKubernetesCluster(clusterData CseClusterSettings, timeout time.Duration) (*CseKubernetesCluster, error) { + clusterId, err := org.CseCreateKubernetesClusterAsync(clusterData) + if err != nil { + return nil, err + } + + err = waitUntilClusterIsProvisioned(org.client, clusterId, timeout) + if err != nil { + return &CseKubernetesCluster{ + client: org.client, + ID: clusterId, + }, err + } + + return getCseKubernetesClusterById(org.client, clusterId) +} + +// CseCreateKubernetesClusterAsync creates a Kubernetes cluster with the data given as input (CseClusterSettings), but does not +// wait for the creation process to finish, so it doesn't monitor for any errors during the process. It returns just the ID of +// the created cluster. One can manually check the status of the cluster with VCDClient.CseGetKubernetesClusterById and the result of this method. +func (org *Org) CseCreateKubernetesClusterAsync(clusterSettings CseClusterSettings) (string, error) { + if org == nil { + return "", fmt.Errorf("CseCreateKubernetesClusterAsync cannot be called on a nil Organization receiver") + } + + tenantContext, err := org.getTenantContext() + if err != nil { + return "", fmt.Errorf("error creating the CSE Kubernetes cluster: %s", err) + } + + cseSubcomponents, err := getCseComponentsVersions(clusterSettings.CseVersion) + if err != nil { + return "", err + } + + internalSettings, err := clusterSettings.toCseClusterSettingsInternal(*org) + if err != nil { + return "", fmt.Errorf("error creating the CSE Kubernetes cluster: %s", err) + } + + payload, err := internalSettings.getUnmarshalledRdePayload() + if err != nil { + return "", err + } + + rde, err := createRdeAndGetFromTask(org.client, cseKubernetesClusterVendor, cseKubernetesClusterNamespace, cseSubcomponents.CapvcdRdeTypeVersion, + types.DefinedEntity{ + EntityType: internalSettings.RdeType.ID, + Name: internalSettings.Name, + Entity: payload, + }, tenantContext) + if err != nil { + return "", fmt.Errorf("error creating the CSE Kubernetes cluster: %s", err) + } + + return rde.DefinedEntity.ID, nil +} + +// CseGetKubernetesClusterById retrieves a CSE Kubernetes cluster from VCD by its unique ID +func (vcdClient *VCDClient) CseGetKubernetesClusterById(id string) (*CseKubernetesCluster, error) { + return getCseKubernetesClusterById(&vcdClient.Client, id) +} + +// CseGetKubernetesClustersByName retrieves all the CSE Kubernetes clusters from VCD with the given name that belong to the receiver Organization. +// Note: The clusters retrieved won't have a valid ETag to perform operations on them. Use VCDClient.CseGetKubernetesClusterById for that instead. +func (org *Org) CseGetKubernetesClustersByName(cseVersion semver.Version, name string) ([]*CseKubernetesCluster, error) { + cseSubcomponents, err := getCseComponentsVersions(cseVersion) + if err != nil { + return nil, err + } + + rdes, err := getRdesByName(org.client, cseKubernetesClusterVendor, cseKubernetesClusterNamespace, cseSubcomponents.CapvcdRdeTypeVersion, name) + if err != nil { + return nil, err + } + var clusters []*CseKubernetesCluster + for _, rde := range rdes { + if rde.DefinedEntity.Org != nil && rde.DefinedEntity.Org.ID == org.Org.ID { + cluster, err := cseConvertToCseKubernetesClusterType(rde) + if err != nil { + return nil, err + } + clusters = append(clusters, cluster) + } + } + return clusters, nil +} + +// getCseKubernetesClusterById retrieves a CSE Kubernetes cluster from VCD by its unique ID +func getCseKubernetesClusterById(client *Client, clusterId string) (*CseKubernetesCluster, error) { + rde, err := getRdeById(client, clusterId) + if err != nil { + return nil, err + } + return cseConvertToCseKubernetesClusterType(rde) +} + +// Refresh gets the latest information about the receiver CSE Kubernetes cluster and updates its properties. +// All cached fields such as the supported OVAs list (from CseKubernetesCluster.GetSupportedUpgrades) are also cleared. +func (cluster *CseKubernetesCluster) Refresh() error { + refreshed, err := getCseKubernetesClusterById(cluster.client, cluster.ID) + if err != nil { + return fmt.Errorf("failed refreshing the CSE Kubernetes Cluster: %s", err) + } + *cluster = *refreshed + return nil +} + +// GetKubeconfig retrieves the Kubeconfig from an existing CSE Kubernetes cluster that is in provisioned state. +// If refresh=true, it retrieves the latest state of the cluster from VCD before requesting the Kubeconfig. +func (cluster *CseKubernetesCluster) GetKubeconfig(refresh bool) (string, error) { + if refresh { + err := cluster.Refresh() + if err != nil { + return "", err + } + } + + if cluster.State == "" { + return "", fmt.Errorf("cannot get a Kubeconfig of a Kubernetes cluster that does not have a state (expected 'provisioned')") + } + + if cluster.State != "provisioned" { + return "", fmt.Errorf("cannot get a Kubeconfig of a Kubernetes cluster that is not in 'provisioned' state. It is '%s'", cluster.State) + } + + rde, err := getRdeById(cluster.client, cluster.ID) + if err != nil { + return "", err + } + versions, err := getCseComponentsVersions(cluster.CseVersion) + if err != nil { + return "", err + } + + // Auxiliary wrapper of the result, as the invocation returns the RDE and + // what we need is inside of it. + type invocationResult struct { + Capvcd types.Capvcd `json:"entity,omitempty"` + } + result := invocationResult{} + + err = rde.InvokeBehaviorAndMarshal(fmt.Sprintf("urn:vcloud:behavior-interface:getFullEntity:cse:capvcd:%s", versions.CseInterfaceVersion), types.BehaviorInvocation{}, &result) + if err != nil { + return "", fmt.Errorf("could not retrieve the Kubeconfig, the Behavior invocation failed: %s", err) + } + if result.Capvcd.Status.Capvcd.Private == nil { + return "", fmt.Errorf("could not retrieve the Kubeconfig, the Behavior invocation succeeded but the Kubeconfig is nil") + } + if result.Capvcd.Status.Capvcd.Private.KubeConfig == "" { + return "", fmt.Errorf("could not retrieve the Kubeconfig, the Behavior invocation succeeded but the Kubeconfig is empty") + } + return result.Capvcd.Status.Capvcd.Private.KubeConfig, nil +} + +// UpdateWorkerPools executes an update on the receiver cluster to change the existing Worker Pools. +// The input is a map where the key is the Worker pool unique name, and the value is the update payload for that Worker Pool. +// If refresh=true, it retrieves the latest state of the cluster from VCD before updating. +// WARNING: At least one worker pool must have one or more nodes running, otherwise the cluster will be left in an unusable state. +func (cluster *CseKubernetesCluster) UpdateWorkerPools(input map[string]CseWorkerPoolUpdateInput, refresh bool) error { + return cluster.Update(CseClusterUpdateInput{ + WorkerPools: &input, + }, refresh) +} + +// AddWorkerPools executes an update on the receiver cluster to add new Worker Pools. +// If refresh=true, it retrieves the latest state of the cluster from VCD before updating. +func (cluster *CseKubernetesCluster) AddWorkerPools(input []CseWorkerPoolSettings, refresh bool) error { + return cluster.Update(CseClusterUpdateInput{ + NewWorkerPools: &input, + }, refresh) +} + +// UpdateControlPlane executes an update on the receiver cluster to change the existing control plane. +// If refresh=true, it retrieves the latest state of the cluster from VCD before updating. +func (cluster *CseKubernetesCluster) UpdateControlPlane(input CseControlPlaneUpdateInput, refresh bool) error { + return cluster.Update(CseClusterUpdateInput{ + ControlPlane: &input, + }, refresh) +} + +// GetSupportedUpgrades queries all vApp Templates from VCD, one by one, and returns those that can be used for upgrading the cluster. +// As retrieving all OVAs one by one from VCD is expensive, the first time this method is called the returned OVAs are +// cached to avoid querying VCD again multiple times. +// If refreshOvas=true, this cache is cleared out and this method will query VCD for every vApp Template again. +// Therefore, the refreshOvas flag should be set to true only when VCD has new OVAs that need to be considered or after a cluster upgrade. +// NOTE: Any refresh operation from other methods will cause the cache to be cleared. +func (cluster *CseKubernetesCluster) GetSupportedUpgrades(refreshOvas bool) ([]*types.VAppTemplate, error) { + if refreshOvas { + cluster.supportedUpgrades = make([]*types.VAppTemplate, 0) + } + if cluster.State != "provisioned" { + cluster.supportedUpgrades = make([]*types.VAppTemplate, 0) + return cluster.supportedUpgrades, nil + } + if len(cluster.supportedUpgrades) > 0 { + return cluster.supportedUpgrades, nil + } + + vAppTemplates, err := queryVappTemplateListWithFilter(cluster.client, nil) + if err != nil { + return nil, fmt.Errorf("could not get vApp Templates: %s", err) + } + for _, template := range vAppTemplates { + // We can only know if the vApp Template is a TKGm OVA by inspecting its internals, hence we need to retrieve every one + // of them one by one. This is an expensive operation, hence the cache. + vAppTemplate, err := getVAppTemplateById(cluster.client, fmt.Sprintf("urn:vcloud:vapptemplate:%s", extractUuid(template.HREF))) + if err != nil { + continue // This means we cannot retrieve it (maybe due to some rights missing), so we cannot use it. We skip it + } + targetVersions, err := getTkgVersionBundleFromVAppTemplate(vAppTemplate.VAppTemplate) + if err != nil { + continue // This means it's not a TKGm OVA, or it is not supported, so we skip it + } + // The OVA can be used if the TKG version is equal to the actual or higher, and the Kubernetes version is at most 1 minor higher. + if targetVersions.compareTkgVersion(cluster.TkgVersion.String()) >= 0 && targetVersions.kubernetesVersionIsUpgradeableFrom(cluster.KubernetesVersion.String()) { + cluster.supportedUpgrades = append(cluster.supportedUpgrades, vAppTemplate.VAppTemplate) + } + } + return cluster.supportedUpgrades, nil +} + +// UpgradeCluster executes an update on the receiver cluster to upgrade the Kubernetes template of the cluster. +// If the cluster is not upgradeable or the OVA is incorrect, this method will return an error. +// If refresh=true, it retrieves the latest state of the cluster from VCD before updating. +func (cluster *CseKubernetesCluster) UpgradeCluster(kubernetesTemplateOvaId string, refresh bool) error { + return cluster.Update(CseClusterUpdateInput{ + KubernetesTemplateOvaId: &kubernetesTemplateOvaId, + }, refresh) +} + +// SetNodeHealthCheck executes an update on the receiver cluster to enable or disable the machine health check capabilities. +// If refresh=true, it retrieves the latest state of the cluster from VCD before updating. +func (cluster *CseKubernetesCluster) SetNodeHealthCheck(healthCheckEnabled bool, refresh bool) error { + return cluster.Update(CseClusterUpdateInput{ + NodeHealthCheck: &healthCheckEnabled, + }, refresh) +} + +// SetAutoRepairOnErrors executes an update on the receiver cluster to change the flag that controls the auto-repair +// capabilities of CSE. If refresh=true, it retrieves the latest state of the cluster from VCD before updating. +// NOTE: This method can only be used in CSE versions < 4.1.1 +func (cluster *CseKubernetesCluster) SetAutoRepairOnErrors(autoRepairOnErrors bool, refresh bool) error { + return cluster.Update(CseClusterUpdateInput{ + AutoRepairOnErrors: &autoRepairOnErrors, + }, refresh) +} + +// Update executes an update on the receiver CSE Kubernetes Cluster on any of the allowed parameters defined in the input type. +// If refresh=true, it retrieves the latest state of the cluster from VCD before updating. +func (cluster *CseKubernetesCluster) Update(input CseClusterUpdateInput, refresh bool) error { + if refresh { + err := cluster.Refresh() + if err != nil { + return err + } + } + + if cluster.State == "" { + return fmt.Errorf("can't update a Kubernetes cluster that does not have any state") + } + if cluster.State != "provisioned" { + return fmt.Errorf("can't update a Kubernetes cluster that is not in 'provisioned' state, as it is in '%s'", cluster.capvcdType.Status.VcdKe.State) + } + + if input.AutoRepairOnErrors != nil && *input.AutoRepairOnErrors != cluster.AutoRepairOnErrors { + // Since CSE 4.1.1, the AutoRepairOnError toggle can't be modified and is turned off + // automatically by the CSE Server. + + v411, err := semver.NewVersion("4.1.1") + if err != nil { + return err + } + if cluster.CseVersion.GreaterThanOrEqual(v411) { + return fmt.Errorf("the 'Auto Repair on Errors' flag can't be changed after the cluster is created since CSE 4.1.1") + } + cluster.capvcdType.Spec.VcdKe.AutoRepairOnErrors = *input.AutoRepairOnErrors + } + + updatedCapiYaml, err := cluster.updateCapiYaml(input) + if err != nil { + return err + } + cluster.capvcdType.Spec.CapiYaml = updatedCapiYaml + + marshaledPayload, err := json.Marshal(cluster.capvcdType) + if err != nil { + return err + } + entityContent := map[string]interface{}{} + err = json.Unmarshal(marshaledPayload, &entityContent) + if err != nil { + return err + } + + // We do this loop to increase the chances that the Kubernetes cluster is successfully updated, as the update operation + // can clash with the CSE Server updates on the same RDE. If the CSE Server does an update just before we do, the ETag + // verification will fail, so we must retry. + retries := 0 + maxRetries := 5 + updated := false + for retries <= maxRetries { + rde, err := getRdeById(cluster.client, cluster.ID) + if err != nil { + return err + } + + rde.DefinedEntity.Entity = entityContent + err = rde.Update(*rde.DefinedEntity) + if err == nil { + updated = true + break + } + if err != nil { + // If it's an ETag error, we just retry without waiting + if !strings.Contains(strings.ToLower(err.Error()), "etag") { + return err + } + } + retries++ + util.Logger.Printf("[DEBUG] The request to update the Kubernetes cluster '%s' failed due to a ETag lock. Trying again", cluster.ID) + } + + if !updated { + return fmt.Errorf("could not update the Kubernetes cluster '%s' after %d retries, due to an ETag lock blocking the operations", cluster.ID, maxRetries) + } + + return cluster.Refresh() +} + +// Delete deletes a CSE Kubernetes cluster, waiting the specified amount of time. If the timeout is reached, this method +// returns an error, even if the cluster is already marked for deletion. +func (cluster *CseKubernetesCluster) Delete(timeout time.Duration) error { + var elapsed time.Duration + start := time.Now() + markForDelete := false + forceDelete := false + for elapsed <= timeout || timeout == 0 { // If the user specifies timeout=0, we wait forever + rde, err := getRdeById(cluster.client, cluster.ID) + if err != nil { + if ContainsNotFound(err) { + return nil // The RDE is gone, so the process is completed and there's nothing more to do + } + return fmt.Errorf("could not retrieve the Kubernetes cluster with ID '%s': %s", cluster.ID, err) + } + + markForDelete = traverseMapAndGet[bool](rde.DefinedEntity.Entity, "spec.vcdKe.markForDelete", ".") + forceDelete = traverseMapAndGet[bool](rde.DefinedEntity.Entity, "spec.vcdKe.forceDelete", ".") + + if !markForDelete || !forceDelete { + // Mark the cluster for deletion + rde.DefinedEntity.Entity["spec"].(map[string]interface{})["vcdKe"].(map[string]interface{})["markForDelete"] = true + rde.DefinedEntity.Entity["spec"].(map[string]interface{})["vcdKe"].(map[string]interface{})["forceDelete"] = true + err = rde.Update(*rde.DefinedEntity) + if err != nil { + // We ignore any ETag error. This just means a clash with the CSE Server, we just try again + if !strings.Contains(strings.ToLower(err.Error()), "etag") { + return fmt.Errorf("could not mark the Kubernetes cluster with ID '%s' to be deleted: %s", cluster.ID, err) + } + } + } + + util.Logger.Printf("[DEBUG] Cluster '%s' is still not deleted, will check again in 10 seconds", cluster.ID) + time.Sleep(10 * time.Second) + elapsed = time.Since(start) + } + + // We give a hint to the user about the deletion process result + if markForDelete && forceDelete { + return fmt.Errorf("timeout of %s reached, the cluster was successfully marked for deletion but was not removed in time", timeout) + } + return fmt.Errorf("timeout of %s reached, the cluster was not marked for deletion, please try again", timeout) +} diff --git a/govcd/cse/4.1/autoscaler.tmpl b/govcd/cse/4.1/autoscaler.tmpl new file mode 100644 index 000000000..0f1171a9b --- /dev/null +++ b/govcd/cse/4.1/autoscaler.tmpl @@ -0,0 +1,197 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: cluster-autoscaler + namespace: kube-system + labels: + app: cluster-autoscaler +spec: + selector: + matchLabels: + app: cluster-autoscaler + replicas: {{.AutoscalerReplicas}} + template: + metadata: + labels: + app: cluster-autoscaler + annotations: + cluster-autoscaler.kubernetes.io/safe-to-evict: "false" + spec: + serviceAccountName: cluster-autoscaler + containers: + - image: k8s.gcr.io/autoscaling/cluster-autoscaler:{{.AutoscalerVersion}} + name: cluster-autoscaler + resources: + limits: + cpu: 100m + memory: 500Mi + requests: + cpu: 100m + memory: 500Mi + command: + - /cluster-autoscaler + - --v=4 + - --stderrthreshold=info + - --cloud-provider=clusterapi + - --expendable-pods-priority-cutoff=-10 + - --scale-down-delay-after-delete=10s + - --scale-down-delay-after-add=10s + - --scale-down-delay-after-failure=10s + - --expander=least-waste + - --node-group-auto-discovery=clusterapi:namespace={{.TargetNamespace}} + - --balance-similar-node-groups + - --skip-nodes-with-system-pods=false +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: cluster-autoscaler + namespace: kube-system +--- +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: cluster-autoscaler +rules: + - apiGroups: + - "" + resources: + - namespaces + - persistentvolumeclaims + - persistentvolumes + - pods + - replicationcontrollers + - services + verbs: + - get + - list + - watch + - apiGroups: + - "" + resources: + - nodes + verbs: + - get + - list + - update + - watch + - apiGroups: + - "" + resources: + - pods/eviction + verbs: + - create + - apiGroups: + - policy + resources: + - poddisruptionbudgets + verbs: + - list + - watch + - apiGroups: + - storage.k8s.io + resources: + - csinodes + - storageclasses + - csidrivers + - csistoragecapacities + verbs: + - get + - list + - watch + - apiGroups: + - batch + resources: + - jobs + verbs: + - list + - watch + - apiGroups: + - apps + resources: + - daemonsets + - replicasets + - statefulsets + verbs: + - list + - watch + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch + - apiGroups: + - "" + resources: + - configmaps + verbs: + - create + - delete + - get + - update + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - create + - get + - update + - apiGroups: + - cluster.x-k8s.io + resources: + - machinedeployments + - machinedeployments/scale + - machines + - machinesets + - machinepools + verbs: + - get + - list + - update + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: cluster-autoscaler + namespace: kube-system + labels: + k8s-addon: cluster-autoscaler.addons.k8s.io + k8s-app: cluster-autoscaler +rules: + - apiGroups: + - "" + resources: + - configmaps + verbs: + - create + - list + - watch + - apiGroups: + - "" + resources: + - configmaps + resourceNames: + - cluster-autoscaler-status + - cluster-autoscaler-priority-expander + verbs: + - delete + - get + - update + - watch +--- +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: cluster-autoscaler +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-autoscaler +subjects: +- kind: ServiceAccount + name: cluster-autoscaler + namespace: kube-system diff --git a/govcd/cse/4.1/capiyaml_cluster.tmpl b/govcd/cse/4.1/capiyaml_cluster.tmpl new file mode 100644 index 000000000..82aef730c --- /dev/null +++ b/govcd/cse/4.1/capiyaml_cluster.tmpl @@ -0,0 +1,163 @@ +apiVersion: cluster.x-k8s.io/v1beta1 +kind: Cluster +metadata: + name: "{{.ClusterName}}" + namespace: "{{.TargetNamespace}}" + labels: + cluster-role.tkg.tanzu.vmware.com/management: "" + tanzuKubernetesRelease: "{{.TkrVersion}}" + tkg.tanzu.vmware.com/cluster-name: "{{.ClusterName}}" + annotations: + osInfo: "ubuntu,20.04,amd64" + TKGVERSION: "{{.TkgVersion}}" +spec: + clusterNetwork: + pods: + cidrBlocks: + - "{{.PodCidr}}" + serviceDomain: cluster.local + services: + cidrBlocks: + - "{{.ServiceCidr}}" + controlPlaneRef: + apiVersion: controlplane.cluster.x-k8s.io/v1beta1 + kind: KubeadmControlPlane + name: "{{.ClusterName}}-control-plane-node-pool" + namespace: "{{.TargetNamespace}}" + infrastructureRef: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 + kind: VCDCluster + name: "{{.ClusterName}}" + namespace: "{{.TargetNamespace}}" +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 +kind: VCDCluster +metadata: + name: "{{.ClusterName}}" + namespace: "{{.TargetNamespace}}" +spec: + site: "{{.VcdSite}}" + org: "{{.Org}}" + ovdc: "{{.OrgVdc}}" + ovdcNetwork: "{{.OrgVdcNetwork}}" + {{- if .ControlPlaneEndpoint}} + controlPlaneEndpoint: + host: "{{.ControlPlaneEndpoint}}" + port: 6443 + {{- end}} + {{- if .VirtualIpSubnet}} + loadBalancerConfigSpec: + vipSubnet: "{{.VirtualIpSubnet}}" + {{- end}} + useAsManagementCluster: false + userContext: + secretRef: + name: capi-user-credentials + namespace: "{{.TargetNamespace}}" +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 +kind: VCDMachineTemplate +metadata: + name: "{{.ClusterName}}-control-plane-node-pool" + namespace: "{{.TargetNamespace}}" +spec: + template: + spec: + catalog: "{{.Catalog}}" + template: "{{.VAppTemplate}}" + sizingPolicy: "{{.ControlPlaneSizingPolicy}}" + placementPolicy: "{{.ControlPlanePlacementPolicy}}" + storageProfile: "{{.ControlPlaneStorageProfile}}" + diskSize: {{.ControlPlaneDiskSize}} +--- +apiVersion: controlplane.cluster.x-k8s.io/v1beta1 +kind: KubeadmControlPlane +metadata: + name: "{{.ClusterName}}-control-plane-node-pool" + namespace: "{{.TargetNamespace}}" +spec: + kubeadmConfigSpec: + preKubeadmCommands: + - mv /etc/ssl/certs/custom_certificate_*.crt /usr/local/share/ca-certificates && update-ca-certificates + {{- if .Base64Certificates}} + files: + {{- range $i, $cert := .Base64Certificates}} + - encoding: base64 + content: {{$cert}} + owner: root + permissions: "0644" + path: /etc/ssl/certs/custom_certificate_{{$i}}.crt + {{- end}} + {{- end}} + clusterConfiguration: + apiServer: + certSANs: + - localhost + - 127.0.0.1 + controllerManager: + extraArgs: + enable-hostpath-provisioner: "true" + dns: + imageRepository: "{{.ContainerRegistryUrl}}" + imageTag: "{{.DnsVersion}}" + etcd: + local: + imageRepository: "{{.ContainerRegistryUrl}}" + imageTag: "{{.EtcdVersion}}" + imageRepository: "{{.ContainerRegistryUrl}}" + users: + - name: root + sshAuthorizedKeys: + - "{{.SshPublicKey}}" + initConfiguration: + nodeRegistration: + criSocket: /run/containerd/containerd.sock + kubeletExtraArgs: + eviction-hard: nodefs.available<0%,nodefs.inodesFree<0%,imagefs.available<0% + cloud-provider: external + joinConfiguration: + nodeRegistration: + criSocket: /run/containerd/containerd.sock + kubeletExtraArgs: + eviction-hard: nodefs.available<0%,nodefs.inodesFree<0%,imagefs.available<0% + cloud-provider: external + machineTemplate: + infrastructureRef: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 + kind: VCDMachineTemplate + name: "{{.ClusterName}}-control-plane-node-pool" + namespace: "{{.TargetNamespace}}" + replicas: {{.ControlPlaneMachineCount}} + version: "{{.KubernetesVersion}}" +--- +apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 +kind: KubeadmConfigTemplate +metadata: + name: "{{.ClusterName}}-kct" + namespace: "{{.TargetNamespace}}" +spec: + template: + spec: + users: + - name: root + sshAuthorizedKeys: + - "{{.SshPublicKey}}" + useExperimentalRetryJoin: true + preKubeadmCommands: + - mv /etc/ssl/certs/custom_certificate_*.crt /usr/local/share/ca-certificates && update-ca-certificates + {{- if .Base64Certificates}} + files: + {{- range $i, $cert := .Base64Certificates}} + - encoding: base64 + content: {{$cert}} + owner: root + permissions: "0644" + path: /etc/ssl/certs/custom_certificate_{{$i}}.crt + {{- end}} + {{- end}} + joinConfiguration: + nodeRegistration: + criSocket: /run/containerd/containerd.sock + kubeletExtraArgs: + eviction-hard: nodefs.available<0%,nodefs.inodesFree<0%,imagefs.available<0% + cloud-provider: external diff --git a/govcd/cse/4.1/capiyaml_mhc.tmpl b/govcd/cse/4.1/capiyaml_mhc.tmpl new file mode 100644 index 000000000..5d6d912ba --- /dev/null +++ b/govcd/cse/4.1/capiyaml_mhc.tmpl @@ -0,0 +1,22 @@ +apiVersion: cluster.x-k8s.io/v1beta1 +kind: MachineHealthCheck +metadata: + name: "{{.ClusterName}}" + namespace: "{{.TargetNamespace}}" + labels: + clusterctl.cluster.x-k8s.io: "" + clusterctl.cluster.x-k8s.io/move: "" +spec: + clusterName: "{{.ClusterName}}" + maxUnhealthy: "{{.MaxUnhealthyNodePercentage}}" + nodeStartupTimeout: "{{.NodeStartupTimeout}}" + selector: + matchLabels: + cluster.x-k8s.io/cluster-name: "{{.ClusterName}}" + unhealthyConditions: + - type: Ready + status: Unknown + timeout: "{{.NodeUnknownTimeout}}" + - type: Ready + status: "False" + timeout: "{{.NodeNotReadyTimeout}}" diff --git a/govcd/cse/4.1/capiyaml_workerpool.tmpl b/govcd/cse/4.1/capiyaml_workerpool.tmpl new file mode 100644 index 000000000..b1ebc1c2b --- /dev/null +++ b/govcd/cse/4.1/capiyaml_workerpool.tmpl @@ -0,0 +1,48 @@ +apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 +kind: VCDMachineTemplate +metadata: + name: "{{.NodePoolName}}" + namespace: "{{.TargetNamespace}}" +spec: + template: + spec: + catalog: "{{.Catalog}}" + template: "{{.VAppTemplate}}" + sizingPolicy: "{{.NodePoolSizingPolicy}}" + placementPolicy: "{{.NodePoolPlacementPolicy}}" + storageProfile: "{{.NodePoolStorageProfile}}" + diskSize: "{{.NodePoolDiskSize}}" + enableNvidiaGPU: {{.NodePoolEnableGpu}} +--- +apiVersion: cluster.x-k8s.io/v1beta1 +kind: MachineDeployment +metadata: + name: "{{.NodePoolName}}" + namespace: "{{.TargetNamespace}}" + {{- if and .AutoscalerMaxSize .AutoscalerMinSize}} + annotations: + cluster.x-k8s.io/cluster-api-autoscaler-node-group-max-size: "{{.AutoscalerMaxSize}}" + cluster.x-k8s.io/cluster-api-autoscaler-node-group-min-size: "{{.AutoscalerMinSize}}" + {{- end}} +spec: + clusterName: "{{.ClusterName}}" + {{- if .NodePoolMachineCount}} + replicas: {{.NodePoolMachineCount}} + {{- end}} + selector: + matchLabels: null + template: + spec: + bootstrap: + configRef: + apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 + kind: KubeadmConfigTemplate + name: "{{.ClusterName}}-kct" + namespace: "{{.TargetNamespace}}" + clusterName: "{{.ClusterName}}" + infrastructureRef: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 + kind: VCDMachineTemplate + name: "{{.NodePoolName}}" + namespace: "{{.TargetNamespace}}" + version: "{{.KubernetesVersion}}" diff --git a/govcd/cse/4.1/rde.tmpl b/govcd/cse/4.1/rde.tmpl new file mode 100644 index 000000000..e5ea3e2b8 --- /dev/null +++ b/govcd/cse/4.1/rde.tmpl @@ -0,0 +1,31 @@ +{ + "apiVersion": "capvcd.vmware.com/v1.1", + "kind": "CAPVCDCluster", + "name": "{{.Name}}", + "metadata": { + "name": "{{.Name}}", + "orgName": "{{.Org}}", + "site": "{{.VcdUrl}}", + "virtualDataCenterName": "{{.Vdc}}" + }, + "spec": { + "vcdKe": { + "isVCDKECluster": true, + "markForDelete": {{.Delete}}, + "forceDelete": {{.ForceDelete}}, + "autoRepairOnErrors": {{.AutoRepairOnErrors}}, + {{- if .DefaultStorageClassName }} + "defaultStorageClassOptions": { + "filesystem": "{{.DefaultStorageClassFileSystem}}", + "k8sStorageClassName": "{{.DefaultStorageClassName}}", + "vcdStorageProfileName": "{{.DefaultStorageClassStorageProfile}}", + "useDeleteReclaimPolicy": {{.DefaultStorageClassUseDeleteReclaimPolicy}} + }, + {{- end }} + "secure": { + "apiToken": "{{.ApiToken}}" + } + }, + "capiYaml": "{{.CapiYaml}}" + } +} diff --git a/govcd/cse/4.2/autoscaler.tmpl b/govcd/cse/4.2/autoscaler.tmpl new file mode 100644 index 000000000..0f1171a9b --- /dev/null +++ b/govcd/cse/4.2/autoscaler.tmpl @@ -0,0 +1,197 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: cluster-autoscaler + namespace: kube-system + labels: + app: cluster-autoscaler +spec: + selector: + matchLabels: + app: cluster-autoscaler + replicas: {{.AutoscalerReplicas}} + template: + metadata: + labels: + app: cluster-autoscaler + annotations: + cluster-autoscaler.kubernetes.io/safe-to-evict: "false" + spec: + serviceAccountName: cluster-autoscaler + containers: + - image: k8s.gcr.io/autoscaling/cluster-autoscaler:{{.AutoscalerVersion}} + name: cluster-autoscaler + resources: + limits: + cpu: 100m + memory: 500Mi + requests: + cpu: 100m + memory: 500Mi + command: + - /cluster-autoscaler + - --v=4 + - --stderrthreshold=info + - --cloud-provider=clusterapi + - --expendable-pods-priority-cutoff=-10 + - --scale-down-delay-after-delete=10s + - --scale-down-delay-after-add=10s + - --scale-down-delay-after-failure=10s + - --expander=least-waste + - --node-group-auto-discovery=clusterapi:namespace={{.TargetNamespace}} + - --balance-similar-node-groups + - --skip-nodes-with-system-pods=false +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: cluster-autoscaler + namespace: kube-system +--- +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: cluster-autoscaler +rules: + - apiGroups: + - "" + resources: + - namespaces + - persistentvolumeclaims + - persistentvolumes + - pods + - replicationcontrollers + - services + verbs: + - get + - list + - watch + - apiGroups: + - "" + resources: + - nodes + verbs: + - get + - list + - update + - watch + - apiGroups: + - "" + resources: + - pods/eviction + verbs: + - create + - apiGroups: + - policy + resources: + - poddisruptionbudgets + verbs: + - list + - watch + - apiGroups: + - storage.k8s.io + resources: + - csinodes + - storageclasses + - csidrivers + - csistoragecapacities + verbs: + - get + - list + - watch + - apiGroups: + - batch + resources: + - jobs + verbs: + - list + - watch + - apiGroups: + - apps + resources: + - daemonsets + - replicasets + - statefulsets + verbs: + - list + - watch + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch + - apiGroups: + - "" + resources: + - configmaps + verbs: + - create + - delete + - get + - update + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - create + - get + - update + - apiGroups: + - cluster.x-k8s.io + resources: + - machinedeployments + - machinedeployments/scale + - machines + - machinesets + - machinepools + verbs: + - get + - list + - update + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: cluster-autoscaler + namespace: kube-system + labels: + k8s-addon: cluster-autoscaler.addons.k8s.io + k8s-app: cluster-autoscaler +rules: + - apiGroups: + - "" + resources: + - configmaps + verbs: + - create + - list + - watch + - apiGroups: + - "" + resources: + - configmaps + resourceNames: + - cluster-autoscaler-status + - cluster-autoscaler-priority-expander + verbs: + - delete + - get + - update + - watch +--- +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: cluster-autoscaler +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-autoscaler +subjects: +- kind: ServiceAccount + name: cluster-autoscaler + namespace: kube-system diff --git a/govcd/cse/4.2/capiyaml_cluster.tmpl b/govcd/cse/4.2/capiyaml_cluster.tmpl new file mode 100644 index 000000000..82aef730c --- /dev/null +++ b/govcd/cse/4.2/capiyaml_cluster.tmpl @@ -0,0 +1,163 @@ +apiVersion: cluster.x-k8s.io/v1beta1 +kind: Cluster +metadata: + name: "{{.ClusterName}}" + namespace: "{{.TargetNamespace}}" + labels: + cluster-role.tkg.tanzu.vmware.com/management: "" + tanzuKubernetesRelease: "{{.TkrVersion}}" + tkg.tanzu.vmware.com/cluster-name: "{{.ClusterName}}" + annotations: + osInfo: "ubuntu,20.04,amd64" + TKGVERSION: "{{.TkgVersion}}" +spec: + clusterNetwork: + pods: + cidrBlocks: + - "{{.PodCidr}}" + serviceDomain: cluster.local + services: + cidrBlocks: + - "{{.ServiceCidr}}" + controlPlaneRef: + apiVersion: controlplane.cluster.x-k8s.io/v1beta1 + kind: KubeadmControlPlane + name: "{{.ClusterName}}-control-plane-node-pool" + namespace: "{{.TargetNamespace}}" + infrastructureRef: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 + kind: VCDCluster + name: "{{.ClusterName}}" + namespace: "{{.TargetNamespace}}" +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 +kind: VCDCluster +metadata: + name: "{{.ClusterName}}" + namespace: "{{.TargetNamespace}}" +spec: + site: "{{.VcdSite}}" + org: "{{.Org}}" + ovdc: "{{.OrgVdc}}" + ovdcNetwork: "{{.OrgVdcNetwork}}" + {{- if .ControlPlaneEndpoint}} + controlPlaneEndpoint: + host: "{{.ControlPlaneEndpoint}}" + port: 6443 + {{- end}} + {{- if .VirtualIpSubnet}} + loadBalancerConfigSpec: + vipSubnet: "{{.VirtualIpSubnet}}" + {{- end}} + useAsManagementCluster: false + userContext: + secretRef: + name: capi-user-credentials + namespace: "{{.TargetNamespace}}" +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 +kind: VCDMachineTemplate +metadata: + name: "{{.ClusterName}}-control-plane-node-pool" + namespace: "{{.TargetNamespace}}" +spec: + template: + spec: + catalog: "{{.Catalog}}" + template: "{{.VAppTemplate}}" + sizingPolicy: "{{.ControlPlaneSizingPolicy}}" + placementPolicy: "{{.ControlPlanePlacementPolicy}}" + storageProfile: "{{.ControlPlaneStorageProfile}}" + diskSize: {{.ControlPlaneDiskSize}} +--- +apiVersion: controlplane.cluster.x-k8s.io/v1beta1 +kind: KubeadmControlPlane +metadata: + name: "{{.ClusterName}}-control-plane-node-pool" + namespace: "{{.TargetNamespace}}" +spec: + kubeadmConfigSpec: + preKubeadmCommands: + - mv /etc/ssl/certs/custom_certificate_*.crt /usr/local/share/ca-certificates && update-ca-certificates + {{- if .Base64Certificates}} + files: + {{- range $i, $cert := .Base64Certificates}} + - encoding: base64 + content: {{$cert}} + owner: root + permissions: "0644" + path: /etc/ssl/certs/custom_certificate_{{$i}}.crt + {{- end}} + {{- end}} + clusterConfiguration: + apiServer: + certSANs: + - localhost + - 127.0.0.1 + controllerManager: + extraArgs: + enable-hostpath-provisioner: "true" + dns: + imageRepository: "{{.ContainerRegistryUrl}}" + imageTag: "{{.DnsVersion}}" + etcd: + local: + imageRepository: "{{.ContainerRegistryUrl}}" + imageTag: "{{.EtcdVersion}}" + imageRepository: "{{.ContainerRegistryUrl}}" + users: + - name: root + sshAuthorizedKeys: + - "{{.SshPublicKey}}" + initConfiguration: + nodeRegistration: + criSocket: /run/containerd/containerd.sock + kubeletExtraArgs: + eviction-hard: nodefs.available<0%,nodefs.inodesFree<0%,imagefs.available<0% + cloud-provider: external + joinConfiguration: + nodeRegistration: + criSocket: /run/containerd/containerd.sock + kubeletExtraArgs: + eviction-hard: nodefs.available<0%,nodefs.inodesFree<0%,imagefs.available<0% + cloud-provider: external + machineTemplate: + infrastructureRef: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 + kind: VCDMachineTemplate + name: "{{.ClusterName}}-control-plane-node-pool" + namespace: "{{.TargetNamespace}}" + replicas: {{.ControlPlaneMachineCount}} + version: "{{.KubernetesVersion}}" +--- +apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 +kind: KubeadmConfigTemplate +metadata: + name: "{{.ClusterName}}-kct" + namespace: "{{.TargetNamespace}}" +spec: + template: + spec: + users: + - name: root + sshAuthorizedKeys: + - "{{.SshPublicKey}}" + useExperimentalRetryJoin: true + preKubeadmCommands: + - mv /etc/ssl/certs/custom_certificate_*.crt /usr/local/share/ca-certificates && update-ca-certificates + {{- if .Base64Certificates}} + files: + {{- range $i, $cert := .Base64Certificates}} + - encoding: base64 + content: {{$cert}} + owner: root + permissions: "0644" + path: /etc/ssl/certs/custom_certificate_{{$i}}.crt + {{- end}} + {{- end}} + joinConfiguration: + nodeRegistration: + criSocket: /run/containerd/containerd.sock + kubeletExtraArgs: + eviction-hard: nodefs.available<0%,nodefs.inodesFree<0%,imagefs.available<0% + cloud-provider: external diff --git a/govcd/cse/4.2/capiyaml_mhc.tmpl b/govcd/cse/4.2/capiyaml_mhc.tmpl new file mode 100644 index 000000000..5d6d912ba --- /dev/null +++ b/govcd/cse/4.2/capiyaml_mhc.tmpl @@ -0,0 +1,22 @@ +apiVersion: cluster.x-k8s.io/v1beta1 +kind: MachineHealthCheck +metadata: + name: "{{.ClusterName}}" + namespace: "{{.TargetNamespace}}" + labels: + clusterctl.cluster.x-k8s.io: "" + clusterctl.cluster.x-k8s.io/move: "" +spec: + clusterName: "{{.ClusterName}}" + maxUnhealthy: "{{.MaxUnhealthyNodePercentage}}" + nodeStartupTimeout: "{{.NodeStartupTimeout}}" + selector: + matchLabels: + cluster.x-k8s.io/cluster-name: "{{.ClusterName}}" + unhealthyConditions: + - type: Ready + status: Unknown + timeout: "{{.NodeUnknownTimeout}}" + - type: Ready + status: "False" + timeout: "{{.NodeNotReadyTimeout}}" diff --git a/govcd/cse/4.2/capiyaml_workerpool.tmpl b/govcd/cse/4.2/capiyaml_workerpool.tmpl new file mode 100644 index 000000000..b1ebc1c2b --- /dev/null +++ b/govcd/cse/4.2/capiyaml_workerpool.tmpl @@ -0,0 +1,48 @@ +apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 +kind: VCDMachineTemplate +metadata: + name: "{{.NodePoolName}}" + namespace: "{{.TargetNamespace}}" +spec: + template: + spec: + catalog: "{{.Catalog}}" + template: "{{.VAppTemplate}}" + sizingPolicy: "{{.NodePoolSizingPolicy}}" + placementPolicy: "{{.NodePoolPlacementPolicy}}" + storageProfile: "{{.NodePoolStorageProfile}}" + diskSize: "{{.NodePoolDiskSize}}" + enableNvidiaGPU: {{.NodePoolEnableGpu}} +--- +apiVersion: cluster.x-k8s.io/v1beta1 +kind: MachineDeployment +metadata: + name: "{{.NodePoolName}}" + namespace: "{{.TargetNamespace}}" + {{- if and .AutoscalerMaxSize .AutoscalerMinSize}} + annotations: + cluster.x-k8s.io/cluster-api-autoscaler-node-group-max-size: "{{.AutoscalerMaxSize}}" + cluster.x-k8s.io/cluster-api-autoscaler-node-group-min-size: "{{.AutoscalerMinSize}}" + {{- end}} +spec: + clusterName: "{{.ClusterName}}" + {{- if .NodePoolMachineCount}} + replicas: {{.NodePoolMachineCount}} + {{- end}} + selector: + matchLabels: null + template: + spec: + bootstrap: + configRef: + apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 + kind: KubeadmConfigTemplate + name: "{{.ClusterName}}-kct" + namespace: "{{.TargetNamespace}}" + clusterName: "{{.ClusterName}}" + infrastructureRef: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 + kind: VCDMachineTemplate + name: "{{.NodePoolName}}" + namespace: "{{.TargetNamespace}}" + version: "{{.KubernetesVersion}}" diff --git a/govcd/cse/4.2/rde.tmpl b/govcd/cse/4.2/rde.tmpl new file mode 100644 index 000000000..7eb0011b3 --- /dev/null +++ b/govcd/cse/4.2/rde.tmpl @@ -0,0 +1,32 @@ +{ + "apiVersion": "capvcd.vmware.com/v1.1", + "kind": "CAPVCDCluster", + "name": "{{.Name}}", + "metadata": { + "name": "{{.Name}}", + "orgName": "{{.Org}}", + "site": "{{.VcdUrl}}", + "virtualDataCenterName": "{{.Vdc}}" + }, + "spec": { + "vcdKe": { + "isVCDKECluster": true, + "markForDelete": {{.Delete}}, + "forceDelete": {{.ForceDelete}}, + "autoRepairOnErrors": {{.AutoRepairOnErrors}}, + {{- if .DefaultStorageClassName }} + "defaultStorageClassOptions": { + "filesystem": "{{.DefaultStorageClassFileSystem}}", + "k8sStorageClassName": "{{.DefaultStorageClassName}}", + "vcdStorageProfileName": "{{.DefaultStorageClassStorageProfile}}", + "useDeleteReclaimPolicy": {{.DefaultStorageClassUseDeleteReclaimPolicy}} + }, + {{- end }} + "secure": { + "apiToken": "{{.ApiToken}}" + } + }, + "capiYaml": "{{.CapiYaml}}", + "projector": { "operations": [] } + } +} diff --git a/govcd/cse/tkg_versions.json b/govcd/cse/tkg_versions.json new file mode 100644 index 000000000..ea8ca2fa2 --- /dev/null +++ b/govcd/cse/tkg_versions.json @@ -0,0 +1,128 @@ +{ + "v1.28.4+vmware.1-tkg.1-1e7baa840b8869c8bdce0cafff0da59d": { + "tkg": "v2.5.0", + "tkr": "v1.28.4---vmware.1-tkg.1-rc.5", + "etcd": "v3.5.10_vmware.1", + "coreDns": "v1.10.1_vmware.13" + }, + "v1.27.8+vmware.1-tkg.1-e77cdad8d69e4f76f2ded5e1356235b3": { + "tkg": "v2.5.0", + "tkr": "v1.27.8---vmware.1-tkg.1-rc.5", + "etcd": "v3.5.10_vmware.1", + "coreDns": "v1.10.1_vmware.12" + }, + "v1.27.5+vmware.1-tkg.1-0eb96d2f9f4f705ac87c40633d4b69st": { + "tkg": "v2.4.0", + "tkr": "v1.27.5---vmware.1-tkg.1", + "etcd": "v3.5.7_vmware.6", + "coreDns": "v1.10.1_vmware.7" + }, + "v1.26.11+vmware.1-tkg.1-6d29b7d826cdaa3535e156392e8d18cc": { + "tkg": "v2.5.0", + "tkr": "v1.26.11---vmware.1-tkg.1-rc.5", + "etcd": "v3.5.10_vmware.1", + "coreDns": "v1.9.3_vmware.19" + }, + "v1.26.8+vmware.1-tkg.1-b8c57a6c8c98d227f74e7b1a9eef27st": { + "tkg": "v2.4.0", + "tkr": "v1.26.8---vmware.1-tkg.1", + "etcd": "v3.5.6_vmware.20", + "coreDns": "v1.9.3_vmware.16" + }, + "v1.26.8+vmware.1-tkg.1-0edd4dafbefbdb503f64d5472e500cf8": { + "tkg": "v2.3.1", + "tkr": "v1.26.8---vmware.1-tkg.2", + "etcd": "v3.5.6_vmware.20", + "coreDns": "v1.9.3_vmware.16" + }, + "v1.25.13+vmware.1-tkg.1-0031669997707d1c644156b8fc31ebst": { + "tkg": "v2.4.0", + "tkr": "v1.25.13---vmware.1-tkg.1", + "etcd": "v3.5.6_vmware.20", + "coreDns": "v1.9.3_vmware.16" + }, + "v1.25.13+vmware.1-tkg.1-6f7650434fd3787d751e8fb3c9e2153d": { + "tkg": "v2.3.1", + "tkr": "v1.25.13---vmware.1-tkg.2", + "etcd": "v3.5.6_vmware.20", + "coreDns": "v1.9.3_vmware.11" + }, + "v1.25.7+vmware.2-tkg.1-8a74b9f12e488c54605b3537acb683bc": { + "tkg": "v2.2.0", + "tkr": "v1.25.7---vmware.2-tkg.1", + "etcd": "v3.5.6_vmware.9", + "coreDns": "v1.9.3_vmware.8" + }, + "v1.24.17+vmware.1-tkg.1-9f70d901a7d851fb115411e6790fdeae": { + "tkg": "v2.3.1", + "tkr": "v1.24.17---vmware.1-tkg.1", + "etcd": "v3.5.6_vmware.19", + "coreDns": "v1.8.6_vmware.26" + }, + "v1.24.11+vmware.1-tkg.1-2ccb2a001f8bd8f15f1bfbc811071830": { + "tkg": "v2.2.0", + "tkr": "v1.24.11---vmware.1-tkg.1", + "etcd": "v3.5.6_vmware.10", + "coreDns": "v1.8.6_vmware.18" + }, + "v1.24.10+vmware.1-tkg.1-765d418b72c247c2310384e640ee075e": { + "tkg": "v2.1.1", + "tkr": "v1.24.10---vmware.1-tkg.2", + "etcd": "v3.5.6_vmware.6", + "coreDns": "v1.8.6_vmware.17" + }, + "v1.23.17+vmware.1-tkg.1-ee4d95d5d08cd7f31da47d1480571754": { + "tkg": "v2.2.0", + "tkr": "v1.23.17---vmware.1-tkg.1", + "etcd": "v3.5.6_vmware.11", + "coreDns": "v1.8.6_vmware.19" + }, + "v1.23.16+vmware.1-tkg.1-eb0de9755338b944ea9652e6f758b3ce": { + "tkg": "v2.1.1", + "tkr": "v1.23.16---vmware.1-tkg.1", + "etcd": "v3.5.6_vmware.5", + "coreDns": "v1.8.6_vmware.16" + }, + "v1.22.17+vmware.1-tkg.1-df08b304658a6cf17f5e74dc0ab7543c": { + "tkg": "v2.1.1", + "tkr": "v1.22.17---vmware.1-tkg.1", + "etcd": "v3.5.6_vmware.1", + "coreDns": "v1.8.4_vmware.10" + }, + "v1.22.9+vmware.1-tkg.1-2182cbabee08edf480ee9bc5866d6933": { + "tkg": "v1.5.4", + "tkr": "v1.22.9---vmware.1-tkg.1", + "etcd": "v3.5.4_vmware.2", + "coreDns": "v1.8.4_vmware.9" + }, + "v1.21.11+vmware.1-tkg.2-d788dbbb335710c0a0d1a28670057896": { + "tkg": "v1.5.4", + "tkr": "v1.21.11---vmware.1-tkg.2", + "etcd": "v3.4.13_vmware.27", + "coreDns": "v1.8.0_vmware.13" + }, + "v1.21.8+vmware.1-tkg.2-ed3c93616a02968be452fe1934a1d37c": { + "tkg": "v1.4.3", + "tkr": "v1.21.8---vmware.1-tkg.2", + "etcd": "v3.4.13_vmware.25", + "coreDns": "v1.8.0_vmware.11" + }, + "v1.20.15+vmware.1-tkg.2-839faf7d1fa7fa356be22b72170ce1a8": { + "tkg": "v1.5.4", + "tkr": "v1.20.15---vmware.1-tkg.2", + "etcd": "v3.4.13_vmware.23", + "coreDns": "v1.7.0_vmware.15" + }, + "v1.20.14+vmware.1-tkg.2-5a5027ce2528a6229acb35b38ff8084e": { + "tkg": "v1.4.3", + "tkr": "v1.20.14---vmware.1-tkg.2", + "etcd": "v3.4.13_vmware.23", + "coreDns": "v1.7.0_vmware.15" + }, + "v1.19.16+vmware.1-tkg.2-fba68db15591c15fcd5f26b512663a42": { + "tkg": "v1.4.3", + "tkr": "v1.19.16---vmware.1-tkg.2", + "etcd": "v3.4.13_vmware.19", + "coreDns": "v1.7.0_vmware.15" + } +} diff --git a/govcd/cse_internal.go b/govcd/cse_internal.go new file mode 100644 index 000000000..ad88fc4bc --- /dev/null +++ b/govcd/cse_internal.go @@ -0,0 +1,301 @@ +package govcd + +import ( + "bytes" + "embed" + "encoding/json" + "fmt" + semver "github.com/hashicorp/go-version" + "strconv" + "strings" + "text/template" +) + +// This collection of files contains all the Go Templates and resources required for the Container Service Extension (CSE) methods +// to work. +// +//go:embed cse +var cseFiles embed.FS + +// getUnmarshalledRdePayload gets the unmarshalled JSON payload to create the Runtime Defined Entity that represents +// a CSE Kubernetes cluster, by using the receiver information. This method uses all the Go Templates stored in cseFiles +func (clusterSettings *cseClusterSettingsInternal) getUnmarshalledRdePayload() (map[string]interface{}, error) { + if clusterSettings == nil { + return nil, fmt.Errorf("the receiver CSE Kubernetes cluster settings object is nil") + } + capiYaml, err := clusterSettings.generateCapiYamlAsJsonString() + if err != nil { + return nil, err + } + + templateArgs := map[string]string{ + "Name": clusterSettings.Name, + "Org": clusterSettings.OrganizationName, + "VcdUrl": clusterSettings.VcdUrl, + "Vdc": clusterSettings.VdcName, + "Delete": "false", + "ForceDelete": "false", + "AutoRepairOnErrors": strconv.FormatBool(clusterSettings.AutoRepairOnErrors), + "ApiToken": clusterSettings.ApiToken, + "CapiYaml": capiYaml, + } + + if clusterSettings.DefaultStorageClass.StorageProfileName != "" { + templateArgs["DefaultStorageClassStorageProfile"] = clusterSettings.DefaultStorageClass.StorageProfileName + templateArgs["DefaultStorageClassName"] = clusterSettings.DefaultStorageClass.Name + templateArgs["DefaultStorageClassUseDeleteReclaimPolicy"] = strconv.FormatBool(clusterSettings.DefaultStorageClass.UseDeleteReclaimPolicy) + templateArgs["DefaultStorageClassFileSystem"] = clusterSettings.DefaultStorageClass.Filesystem + } + + rdeTemplate, err := getCseTemplate(clusterSettings.CseVersion, "rde") + if err != nil { + return nil, err + } + + rdePayload := template.Must(template.New(clusterSettings.Name).Parse(rdeTemplate)) + buf := &bytes.Buffer{} + if err := rdePayload.Execute(buf, templateArgs); err != nil { + return nil, fmt.Errorf("could not render the Go template with the RDE JSON: %s", err) + } + + var result interface{} + err = json.Unmarshal(buf.Bytes(), &result) + if err != nil { + return nil, fmt.Errorf("could not generate a correct RDE payload: %s", err) + } + + return result.(map[string]interface{}), nil +} + +// generateCapiYamlAsJsonString generates the "capiYaml" property of the RDE that represents a Kubernetes cluster. This +// "capiYaml" property is a YAML encoded as a JSON string. This method uses the Go Templates stored in cseFiles. +func (clusterSettings *cseClusterSettingsInternal) generateCapiYamlAsJsonString() (string, error) { + if clusterSettings == nil { + return "", fmt.Errorf("the receiver cluster settings is nil") + } + + capiYamlTemplate, err := getCseTemplate(clusterSettings.CseVersion, "capiyaml_cluster") + if err != nil { + return "", err + } + + // This YAML snippet contains special strings, such as "%,", that render wrong using the Go template engine + sanitizedCapiYamlTemplate := strings.NewReplacer("%", "%%").Replace(capiYamlTemplate) + capiYaml := template.Must(template.New(clusterSettings.Name + "-cluster").Parse(sanitizedCapiYamlTemplate)) + + nodePoolYaml, err := clusterSettings.generateWorkerPoolsYaml() + if err != nil { + return "", err + } + + memoryHealthCheckYaml, err := clusterSettings.generateMachineHealthCheckYaml() + if err != nil { + return "", err + } + + autoscalerNeeded := false + // We'll need the Autoscaler YAML document if at least one Worker Pool uses it + for _, wp := range clusterSettings.WorkerPools { + if wp.Autoscaler != nil { + autoscalerNeeded = true + break + } + } + autoscalerYaml := "" + if autoscalerNeeded { + autoscalerYaml, err = clusterSettings.generateAutoscalerYaml() + if err != nil { + return "", err + } + } + + templateArgs := map[string]interface{}{ + "ClusterName": clusterSettings.Name, + "TargetNamespace": clusterSettings.Name + "-ns", + "TkrVersion": clusterSettings.TkgVersionBundle.TkrVersion, + "TkgVersion": clusterSettings.TkgVersionBundle.TkgVersion, + "PodCidr": clusterSettings.PodCidr, + "ServiceCidr": clusterSettings.ServiceCidr, + "VcdSite": clusterSettings.VcdUrl, + "Org": clusterSettings.OrganizationName, + "OrgVdc": clusterSettings.VdcName, + "OrgVdcNetwork": clusterSettings.NetworkName, + "Catalog": clusterSettings.CatalogName, + "VAppTemplate": clusterSettings.KubernetesTemplateOvaName, + "ControlPlaneSizingPolicy": clusterSettings.ControlPlane.SizingPolicyName, + "ControlPlanePlacementPolicy": clusterSettings.ControlPlane.PlacementPolicyName, + "ControlPlaneStorageProfile": clusterSettings.ControlPlane.StorageProfileName, + "ControlPlaneDiskSize": fmt.Sprintf("%dGi", clusterSettings.ControlPlane.DiskSizeGi), + "ControlPlaneMachineCount": strconv.Itoa(clusterSettings.ControlPlane.MachineCount), + "ControlPlaneEndpoint": clusterSettings.ControlPlane.Ip, + "DnsVersion": clusterSettings.TkgVersionBundle.CoreDnsVersion, + "EtcdVersion": clusterSettings.TkgVersionBundle.EtcdVersion, + "ContainerRegistryUrl": clusterSettings.VcdKeConfig.ContainerRegistryUrl, + "KubernetesVersion": clusterSettings.TkgVersionBundle.KubernetesVersion, + "SshPublicKey": clusterSettings.SshPublicKey, + "VirtualIpSubnet": clusterSettings.VirtualIpSubnet, + "Base64Certificates": clusterSettings.VcdKeConfig.Base64Certificates, + } + + buf := &bytes.Buffer{} + if err := capiYaml.Execute(buf, templateArgs); err != nil { + return "", fmt.Errorf("could not generate a correct CAPI YAML: %s", err) + } + + // The final "pretty" YAML. To embed it in the final payload it must be marshaled into a one-line JSON string + prettyYaml := "" + if memoryHealthCheckYaml != "" { + prettyYaml += fmt.Sprintf("%s\n---\n", memoryHealthCheckYaml) + } + if autoscalerYaml != "" { + prettyYaml += fmt.Sprintf("%s\n---\n", autoscalerYaml) + } + prettyYaml += fmt.Sprintf("%s\n---\n%s", nodePoolYaml, buf.String()) + + // We don't use a standard json.Marshal() as the YAML contains special characters that are not encoded properly, such as '<'. + buf.Reset() + enc := json.NewEncoder(buf) + enc.SetEscapeHTML(false) + err = enc.Encode(prettyYaml) + if err != nil { + return "", fmt.Errorf("could not encode the CAPI YAML into a JSON string: %s", err) + } + + // Removes trailing quotes from the final JSON string + return strings.Trim(strings.TrimSpace(buf.String()), "\""), nil +} + +// generateWorkerPoolsYaml generates YAML blocks corresponding to the cluster Worker Pools. The blocks are separated by +// the standard YAML separator (---), but does not add one at the end. +func (clusterSettings *cseClusterSettingsInternal) generateWorkerPoolsYaml() (string, error) { + if clusterSettings == nil { + return "", fmt.Errorf("the receiver CSE Kubernetes cluster settings object is nil") + } + + workerPoolsTemplate, err := getCseTemplate(clusterSettings.CseVersion, "capiyaml_workerpool") + if err != nil { + return "", err + } + + workerPools := template.Must(template.New(clusterSettings.Name + "-worker-pool").Parse(workerPoolsTemplate)) + resultYaml := "" + buf := &bytes.Buffer{} + + // We can have many Worker Pools, we build a YAML object for each one of them. + for i, wp := range clusterSettings.WorkerPools { + + // Check the correctness of the Compute Policies in the node pool block + if wp.PlacementPolicyName != "" && wp.VGpuPolicyName != "" { + return "", fmt.Errorf("the Worker Pool '%s' should have either a Placement Policy or a vGPU Policy, not both", wp.Name) + } + placementPolicy := wp.PlacementPolicyName + if wp.VGpuPolicyName != "" { + // For convenience, we just use one of the variables as both cannot be set at same time + placementPolicy = wp.VGpuPolicyName + } + + args := map[string]string{ + "ClusterName": clusterSettings.Name, + "NodePoolName": wp.Name, + "TargetNamespace": clusterSettings.Name + "-ns", + "Catalog": clusterSettings.CatalogName, + "VAppTemplate": clusterSettings.KubernetesTemplateOvaName, + "NodePoolSizingPolicy": wp.SizingPolicyName, + "NodePoolPlacementPolicy": placementPolicy, // Can be either Placement or vGPU policy + "NodePoolStorageProfile": wp.StorageProfileName, + "NodePoolDiskSize": fmt.Sprintf("%dGi", wp.DiskSizeGi), + "NodePoolEnableGpu": strconv.FormatBool(wp.VGpuPolicyName != ""), + "KubernetesVersion": clusterSettings.TkgVersionBundle.KubernetesVersion, + } + + if wp.Autoscaler != nil { + args["AutoscalerMaxSize"] = strconv.Itoa(wp.Autoscaler.MaxSize) + args["AutoscalerMinSize"] = strconv.Itoa(wp.Autoscaler.MinSize) + } else { + args["NodePoolMachineCount"] = strconv.Itoa(wp.MachineCount) + } + + if err := workerPools.Execute(buf, args); err != nil { + return "", fmt.Errorf("could not generate a correct Worker Pool '%s' YAML block: %s", wp.Name, err) + } + resultYaml += fmt.Sprintf("%s\n", buf.String()) + if i < len(clusterSettings.WorkerPools)-1 { + resultYaml += "---\n" + } + buf.Reset() + } + return resultYaml, nil +} + +// generateMachineHealthCheckYaml generates a YAML block corresponding to the cluster Machine Health Check. +// The generated YAML does not contain a separator (---) at the end. +func (clusterSettings *cseClusterSettingsInternal) generateMachineHealthCheckYaml() (string, error) { + if clusterSettings == nil { + return "", fmt.Errorf("the receiver CSE Kubernetes cluster settings object is nil") + } + + if clusterSettings.VcdKeConfig.NodeStartupTimeout == "" && + clusterSettings.VcdKeConfig.NodeUnknownTimeout == "" && + clusterSettings.VcdKeConfig.NodeNotReadyTimeout == "" && + clusterSettings.VcdKeConfig.MaxUnhealthyNodesPercentage == 0 { + return "", nil + } + + mhcTemplate, err := getCseTemplate(clusterSettings.CseVersion, "capiyaml_mhc") + if err != nil { + return "", err + } + + machineHealthCheck := template.Must(template.New(clusterSettings.Name + "-mhc").Parse(mhcTemplate)) + buf := &bytes.Buffer{} + + if err := machineHealthCheck.Execute(buf, map[string]string{ + "ClusterName": clusterSettings.Name, + "TargetNamespace": clusterSettings.Name + "-ns", + // With the 'percentage' suffix + "MaxUnhealthyNodePercentage": fmt.Sprintf("%.0f%%", clusterSettings.VcdKeConfig.MaxUnhealthyNodesPercentage), + // These values coming from VCDKEConfig (CSE Server settings) may have an "s" suffix. We make sure we don't duplicate it + "NodeStartupTimeout": fmt.Sprintf("%ss", strings.ReplaceAll(clusterSettings.VcdKeConfig.NodeStartupTimeout, "s", "")), + "NodeUnknownTimeout": fmt.Sprintf("%ss", strings.ReplaceAll(clusterSettings.VcdKeConfig.NodeUnknownTimeout, "s", "")), + "NodeNotReadyTimeout": fmt.Sprintf("%ss", strings.ReplaceAll(clusterSettings.VcdKeConfig.NodeNotReadyTimeout, "s", "")), + }); err != nil { + return "", fmt.Errorf("could not generate a correct Machine Health Check YAML: %s", err) + } + return fmt.Sprintf("%s\n", buf.String()), nil + +} + +// generateAutoscalerYaml generates YAML documents corresponding to the cluster Autoscaler +func (clusterSettings *cseClusterSettingsInternal) generateAutoscalerYaml() (string, error) { + if clusterSettings == nil { + return "", fmt.Errorf("the receiver CSE Kubernetes cluster settings object is nil") + } + + k8sVersion, err := semver.NewVersion(clusterSettings.TkgVersionBundle.KubernetesVersion) + if err != nil { + return "", err + } + k8sVersionSegments := k8sVersion.Segments() + + autoscalerTemplate, err := getCseTemplate(clusterSettings.CseVersion, "autoscaler") + if err != nil { + return "", err + } + + autoscaler := template.Must(template.New(clusterSettings.Name + "-autoscaler").Parse(autoscalerTemplate)) + resultYaml := "" + buf := &bytes.Buffer{} + + if err := autoscaler.Execute(buf, map[string]string{ + "TargetNamespace": clusterSettings.Name + "-ns", + "AutoscalerReplicas": "1", + "AutoscalerVersion": fmt.Sprintf("v%d.%d.0", k8sVersionSegments[0], k8sVersionSegments[1]), // Autoscaler version matches the Kubernetes minor + }); err != nil { + return "", fmt.Errorf("could not generate a correct Autoscaler YAML block: %s", err) + } + resultYaml += fmt.Sprintf("%s\n", buf.String()) + + buf.Reset() + + return resultYaml, nil +} diff --git a/govcd/cse_internal_unit_test.go b/govcd/cse_internal_unit_test.go new file mode 100644 index 000000000..bd3f9efc0 --- /dev/null +++ b/govcd/cse_internal_unit_test.go @@ -0,0 +1,279 @@ +//go:build unit || ALL + +package govcd + +import ( + semver "github.com/hashicorp/go-version" + "os" + "reflect" + "strings" + "testing" +) + +// Test_cseClusterSettingsInternal_generateCapiYamlAsJsonString tests the generateCapiYamlAsJsonString method with a +// cseClusterSettingsInternal receiver. Given some valid or invalid CSE Settings, the tests runs the generateCapiYamlAsJsonString +// method and checks that the returned JSON string corresponds to the expected settings that were specified. +func Test_cseClusterSettingsInternal_generateCapiYamlAsJsonString(t *testing.T) { + cseVersion, err := semver.NewVersion("4.2.1") + if err != nil { + t.Fatal(err) + } + capiYaml, err := os.ReadFile("test-resources/capiYaml.yaml") + if err != nil { + t.Fatalf("could not read YAML test file: %s", err) + } + baseUnmarshaledYaml, err := unmarshalMultipleYamlDocuments(string(capiYaml)) + if err != nil { + t.Fatalf("could not unmarshal YAML test file: %s", err) + } + + tests := []struct { + name string + input cseClusterSettingsInternal + expectedFunc func() []map[string]interface{} + wantErr string + }{ + { + name: "correct YAML without optionals", + input: cseClusterSettingsInternal{ + CseVersion: *cseVersion, + Name: "test1", + OrganizationName: "tenant_org", + VdcName: "tenant_vdc", + NetworkName: "tenant_net_routed", + KubernetesTemplateOvaName: "ubuntu-2004-kube-v1.25.7+vmware.2-tkg.1-8a74b9f12e488c54605b3537acb683bc", + TkgVersionBundle: tkgVersionBundle{ + EtcdVersion: "v3.5.6_vmware.9", + CoreDnsVersion: "v1.9.3_vmware.8", + TkgVersion: "v2.2.0", + TkrVersion: "v1.25.7---vmware.2-tkg.1", + KubernetesVersion: "v1.25.7+vmware.2", + }, + CatalogName: "tkgm_catalog", + ControlPlane: cseControlPlaneSettingsInternal{ + MachineCount: 1, + DiskSizeGi: 20, + SizingPolicyName: "TKG small", + StorageProfileName: "*", + }, + WorkerPools: []cseWorkerPoolSettingsInternal{ + { + Name: "node-pool-1", + MachineCount: 1, + DiskSizeGi: 20, + SizingPolicyName: "TKG small", + StorageProfileName: "*", + }, + }, + VcdKeConfig: vcdKeConfig{ + MaxUnhealthyNodesPercentage: 100, + NodeStartupTimeout: "900", + NodeNotReadyTimeout: "300", + NodeUnknownTimeout: "200", + ContainerRegistryUrl: "projects.registry.vmware.com/tkg", + }, + Owner: "dummy", + ApiToken: "dummy", + VcdUrl: "https://www.my-vcd-instance.com", + PodCidr: "100.96.0.0/11", + ServiceCidr: "100.64.0.0/13", + }, + expectedFunc: func() []map[string]interface{} { + return baseUnmarshaledYaml + }, + }, + { + name: "correct YAML without MachineHealthCheck", + input: cseClusterSettingsInternal{ + CseVersion: *cseVersion, + Name: "test1", + OrganizationName: "tenant_org", + VdcName: "tenant_vdc", + NetworkName: "tenant_net_routed", + KubernetesTemplateOvaName: "ubuntu-2004-kube-v1.25.7+vmware.2-tkg.1-8a74b9f12e488c54605b3537acb683bc", + TkgVersionBundle: tkgVersionBundle{ + EtcdVersion: "v3.5.6_vmware.9", + CoreDnsVersion: "v1.9.3_vmware.8", + TkgVersion: "v2.2.0", + TkrVersion: "v1.25.7---vmware.2-tkg.1", + KubernetesVersion: "v1.25.7+vmware.2", + }, + CatalogName: "tkgm_catalog", + ControlPlane: cseControlPlaneSettingsInternal{ + MachineCount: 1, + DiskSizeGi: 20, + SizingPolicyName: "TKG small", + StorageProfileName: "*", + }, + WorkerPools: []cseWorkerPoolSettingsInternal{ + { + Name: "node-pool-1", + MachineCount: 1, + DiskSizeGi: 20, + SizingPolicyName: "TKG small", + StorageProfileName: "*", + }, + }, + VcdKeConfig: vcdKeConfig{ + ContainerRegistryUrl: "projects.registry.vmware.com/tkg", + }, + Owner: "dummy", + ApiToken: "dummy", + VcdUrl: "https://www.my-vcd-instance.com", + PodCidr: "100.96.0.0/11", + ServiceCidr: "100.64.0.0/13", + }, + // The expected result is the base YAML without the MachineHealthCheck + expectedFunc: func() []map[string]interface{} { + var result []map[string]interface{} + for _, doc := range baseUnmarshaledYaml { + if doc["kind"] == "MachineHealthCheck" { + continue // Remove the MachineHealthCheck document from the expected result + } + result = append(result, doc) + } + return result + }, + }, + { + name: "correct YAML with every possible option", + input: cseClusterSettingsInternal{ + CseVersion: *cseVersion, + Name: "test1", + OrganizationName: "tenant_org", + VdcName: "tenant_vdc", + NetworkName: "tenant_net_routed", + KubernetesTemplateOvaName: "ubuntu-2004-kube-v1.25.7+vmware.2-tkg.1-8a74b9f12e488c54605b3537acb683bc", + TkgVersionBundle: tkgVersionBundle{ + EtcdVersion: "v3.5.6_vmware.9", + CoreDnsVersion: "v1.9.3_vmware.8", + TkgVersion: "v2.2.0", + TkrVersion: "v1.25.7---vmware.2-tkg.1", + KubernetesVersion: "v1.25.7+vmware.2", + }, + CatalogName: "tkgm_catalog", + ControlPlane: cseControlPlaneSettingsInternal{ + MachineCount: 1, + DiskSizeGi: 20, + SizingPolicyName: "TKG small", + StorageProfileName: "*", + Ip: "1.2.3.4", + }, + WorkerPools: []cseWorkerPoolSettingsInternal{ + { + Name: "node-pool-1", + MachineCount: 1, + DiskSizeGi: 20, + SizingPolicyName: "TKG small", + StorageProfileName: "*", + }, + }, + VcdKeConfig: vcdKeConfig{ + MaxUnhealthyNodesPercentage: 100, + NodeStartupTimeout: "900", + NodeNotReadyTimeout: "300", + NodeUnknownTimeout: "200", + ContainerRegistryUrl: "projects.registry.vmware.com/tkg", + Base64Certificates: []string{ + "Zm9vCg==", + "Zm9vMgo=", + }, + }, + VirtualIpSubnet: "6.7.8.9/24", + Owner: "dummy", + ApiToken: "dummy", + VcdUrl: "https://www.my-vcd-instance.com", + PodCidr: "100.96.0.0/11", + ServiceCidr: "100.64.0.0/13", + }, + // The expected result is the base YAML with the Control Plane extra IPs + expectedFunc: func() []map[string]interface{} { + var result []map[string]interface{} + for _, doc := range baseUnmarshaledYaml { + if doc["kind"] == "VCDCluster" { + // Add the extra items to the document of the expected result + doc["spec"].(map[string]interface{})["controlPlaneEndpoint"] = map[string]interface{}{"host": "1.2.3.4"} + doc["spec"].(map[string]interface{})["controlPlaneEndpoint"].(map[string]interface{})["port"] = float64(6443) + doc["spec"].(map[string]interface{})["loadBalancerConfigSpec"] = map[string]interface{}{"vipSubnet": "6.7.8.9/24"} + } + if doc["kind"] == "KubeadmControlPlane" { + doc["spec"].(map[string]interface{})["kubeadmConfigSpec"].(map[string]interface{})["files"] = []interface{}{ + map[string]interface{}{ + "encoding": "base64", + "content": "Zm9vCg==", + "owner": "root", + "permissions": "0644", + "path": "/etc/ssl/certs/custom_certificate_0.crt", + }, + map[string]interface{}{ + "encoding": "base64", + "content": "Zm9vMgo=", + "owner": "root", + "permissions": "0644", + "path": "/etc/ssl/certs/custom_certificate_1.crt", + }, + } + } + if doc["kind"] == "KubeadmConfigTemplate" { + doc["spec"].(map[string]interface{})["template"].(map[string]interface{})["spec"].(map[string]interface{})["files"] = []interface{}{ + map[string]interface{}{ + "encoding": "base64", + "content": "Zm9vCg==", + "owner": "root", + "permissions": "0644", + "path": "/etc/ssl/certs/custom_certificate_0.crt", + }, + map[string]interface{}{ + "encoding": "base64", + "content": "Zm9vMgo=", + "owner": "root", + "permissions": "0644", + "path": "/etc/ssl/certs/custom_certificate_1.crt", + }, + } + } + result = append(result, doc) + } + return result + }, + }, + { + name: "wrong YAML with both Placement and vGPU policies in a Worker Pool", + input: cseClusterSettingsInternal{ + CseVersion: *cseVersion, + WorkerPools: []cseWorkerPoolSettingsInternal{ + { + Name: "node-pool-1", + MachineCount: 1, + DiskSizeGi: 20, + SizingPolicyName: "TKG small", + PlacementPolicyName: "policy", + VGpuPolicyName: "policy", + StorageProfileName: "*", + }, + }, + }, + wantErr: "the Worker Pool 'node-pool-1' should have either a Placement Policy or a vGPU Policy, not both", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.input.generateCapiYamlAsJsonString() + if err != nil { + if err.Error() != tt.wantErr { + t.Errorf("generateCapiYamlAsJsonString() error = %v, wantErr %v", err, tt.wantErr) + } + return + } + gotUnmarshaled, err := unmarshalMultipleYamlDocuments(strings.NewReplacer("\\n", "\n", "\\\"", "\"").Replace(got)) + if err != nil { + t.Fatalf("could not unmarshal obtained YAML: %s", err) + } + + expected := tt.expectedFunc() + if !reflect.DeepEqual(expected, gotUnmarshaled) { + t.Errorf("generateCapiYamlAsJsonString() got =\n%#v\nwant =\n%#v\n", gotUnmarshaled, expected) + } + }) + } +} diff --git a/govcd/cse_test.go b/govcd/cse_test.go new file mode 100644 index 000000000..69e64eecb --- /dev/null +++ b/govcd/cse_test.go @@ -0,0 +1,900 @@ +//go:build functional || openapi || cse || ALL + +/* + * Copyright 2024 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + semver "github.com/hashicorp/go-version" + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" + "net/url" + "os" + "reflect" + "strings" + "time" +) + +func requireCseConfig(check *C, testConfig TestConfig) { + skippedPrefix := fmt.Sprintf("skipped %s because:", check.TestName()) + if cse := os.Getenv("TEST_VCD_CSE"); cse == "" { + check.Skip(fmt.Sprintf("%s the environment variable TEST_VCD_CSE is not set", skippedPrefix)) + } + cseConfigValues := reflect.ValueOf(testConfig.Cse) + cseConfigType := cseConfigValues.Type() + for i := 0; i < cseConfigValues.NumField(); i++ { + if cseConfigValues.Field(i).String() == "" { + check.Skip(fmt.Sprintf("%s the config value '%s' inside 'cse' block of govcd_test_config.yaml is not set", skippedPrefix, cseConfigType.Field(i).Name)) + } + } +} + +// Test_Cse tests all possible combinations of the CSE CRUD operations. +func (vcd *TestVCD) Test_Cse(check *C) { + requireCseConfig(check, vcd.config) + + // Prerequisites: We need to read several items before creating the cluster. + org, err := vcd.client.GetOrgByName(vcd.config.Cse.TenantOrg) + check.Assert(err, IsNil) + + catalog, err := org.GetCatalogByName(vcd.config.Cse.OvaCatalog, false) + check.Assert(err, IsNil) + + ova, err := catalog.GetVAppTemplateByName(vcd.config.Cse.OvaName) + check.Assert(err, IsNil) + + tkgBundle, err := getTkgVersionBundleFromVAppTemplate(ova.VAppTemplate) + check.Assert(err, IsNil) + + vdc, err := org.GetVDCByName(vcd.config.Cse.TenantVdc, false) + check.Assert(err, IsNil) + + net, err := vdc.GetOrgVdcNetworkByName(vcd.config.Cse.RoutedNetwork, false) + check.Assert(err, IsNil) + + sp, err := vdc.FindStorageProfileReference(vcd.config.Cse.StorageProfile) + check.Assert(err, IsNil) + + policies, err := vcd.client.GetAllVdcComputePoliciesV2(url.Values{ + "filter": []string{"name==TKG small"}, + }) + check.Assert(err, IsNil) + check.Assert(len(policies), Equals, 1) + + token, err := vcd.client.CreateToken(vcd.config.Provider.SysOrg, check.TestName()) + check.Assert(err, IsNil) + defer func() { + err = token.Delete() + check.Assert(err, IsNil) + }() + AddToCleanupListOpenApi(token.Token.Name, check.TestName(), types.OpenApiPathVersion1_0_0+types.OpenApiEndpointTokens+token.Token.ID) + + apiToken, err := token.GetInitialApiToken() + check.Assert(err, IsNil) + + cseVersion, err := semver.NewVersion(vcd.config.Cse.Version) + check.Assert(err, IsNil) + check.Assert(cseVersion, NotNil) + + sshPublicKey := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCrCI+QkLjgQVqR7c7dJfawJqCslVomo5I25JdolqlteX7RCUq0yncWyS+8MTYWCS03sm1jOroLOeuji8CDKCDCcKwQerJiOFoJS+VOK5xCjJ2u8RBGlIpXNcmIh2VriRJrV7TCKrFMSKLNF4/n83q4gWI/YPf6/dRhpPB72HYrdI4omvRlU4GG09jMmgiz+5Yb8wJEXYMsJni+MwPzFKe6TbMcqjBusDyeFGAhgyN7QJGpdNhAn1sqvqZrW2QjaE8P+4t8RzBo8B2ucyQazd6+lbYmOHq9366LjG160snzXrFzlARc4hhpjMzu9Bcm6i3ZZI70qhIbmi5IonbbVh8t" + // Create the cluster + clusterSettings := CseClusterSettings{ + Name: "test-cse", + OrganizationId: org.Org.ID, + VdcId: vdc.Vdc.ID, + NetworkId: net.OrgVDCNetwork.ID, + KubernetesTemplateOvaId: ova.VAppTemplate.ID, + CseVersion: *cseVersion, + ControlPlane: CseControlPlaneSettings{ + MachineCount: 1, + DiskSizeGi: 20, + SizingPolicyId: policies[0].VdcComputePolicyV2.ID, + StorageProfileId: sp.ID, + Ip: "", + }, + WorkerPools: []CseWorkerPoolSettings{{ + Name: "worker-pool-1", + MachineCount: 1, + DiskSizeGi: 20, + SizingPolicyId: policies[0].VdcComputePolicyV2.ID, + StorageProfileId: sp.ID, + }}, + DefaultStorageClass: &CseDefaultStorageClassSettings{ + StorageProfileId: sp.ID, + Name: "storage-class-1", + ReclaimPolicy: "delete", + Filesystem: "ext4", + }, + Owner: vcd.config.Provider.User, + ApiToken: apiToken.RefreshToken, + NodeHealthCheck: true, + PodCidr: "100.96.0.0/11", + ServiceCidr: "100.64.0.0/13", + SshPublicKey: sshPublicKey, + AutoRepairOnErrors: true, + } + cluster, err := org.CseCreateKubernetesCluster(clusterSettings, 150*time.Minute) + + // We assure that the cluster gets always deleted, even if the creation failed. + // Deletion process only needs the cluster ID + defer func() { + check.Assert(cluster, NotNil) + check.Assert(cluster.client, NotNil) + check.Assert(cluster.ID, Not(Equals), "") + err = cluster.Delete(0) + check.Assert(err, IsNil) + }() + + check.Assert(err, IsNil) + assertCseClusterCreation(check, cluster, clusterSettings, tkgBundle) + + kubeconfig, err := cluster.GetKubeconfig(false) + check.Assert(err, IsNil) + check.Assert(true, Equals, strings.Contains(kubeconfig, cluster.Name)) + check.Assert(true, Equals, strings.Contains(kubeconfig, "client-certificate-data")) + check.Assert(true, Equals, strings.Contains(kubeconfig, "certificate-authority-data")) + check.Assert(true, Equals, strings.Contains(kubeconfig, "client-key-data")) + + err = cluster.Refresh() + check.Assert(err, IsNil) + + clusterGet, err := vcd.client.CseGetKubernetesClusterById(cluster.ID) + check.Assert(err, IsNil) + assertCseClusterEquals(check, clusterGet, cluster) + check.Assert(clusterGet.Etag, Not(Equals), "") + + allClusters, err := org.CseGetKubernetesClustersByName(clusterGet.CseVersion, clusterGet.Name) + check.Assert(err, IsNil) + check.Assert(len(allClusters), Equals, 1) + assertCseClusterEquals(check, allClusters[0], clusterGet) + check.Assert(allClusters[0].Etag, Equals, "") // Can't recover ETag by name + + // Update worker pool with autoscaler + err = cluster.UpdateWorkerPools(map[string]CseWorkerPoolUpdateInput{clusterSettings.WorkerPools[0].Name: { + Autoscaler: &CseWorkerPoolAutoscaler{ + MaxSize: 2, + MinSize: 1, + }}}, true) + check.Assert(err, IsNil) + foundWorkerPool := false + for _, nodePool := range cluster.WorkerPools { + if nodePool.Name == clusterSettings.WorkerPools[0].Name { + foundWorkerPool = true + check.Assert(nodePool.MachineCount, Equals, 0) // The field is not used + check.Assert(nodePool.Autoscaler, NotNil) + check.Assert(nodePool.Autoscaler.MaxSize, Equals, 2) + check.Assert(nodePool.Autoscaler.MinSize, Equals, 1) + } + } + check.Assert(foundWorkerPool, Equals, true) + + // Update worker pool from Autoscaling to static 2 nodes + err = cluster.UpdateWorkerPools(map[string]CseWorkerPoolUpdateInput{clusterSettings.WorkerPools[0].Name: {MachineCount: 2}}, true) + check.Assert(err, IsNil) + foundWorkerPool = false + for _, nodePool := range cluster.WorkerPools { + if nodePool.Name == clusterSettings.WorkerPools[0].Name { + foundWorkerPool = true + check.Assert(nodePool.MachineCount, Equals, 2) + check.Assert(nodePool.Autoscaler, IsNil) // Autoscaler should be deactivated + } + } + check.Assert(foundWorkerPool, Equals, true) + + // Add two new worker pools, one with autoscaler + err = cluster.AddWorkerPools([]CseWorkerPoolSettings{{ + Name: "new-pool-1", + MachineCount: 1, + DiskSizeGi: 20, + }, { + Name: "new-pool-2", + DiskSizeGi: 20, + Autoscaler: &CseWorkerPoolAutoscaler{ + MaxSize: 2, + MinSize: 1, + }, + }}, true) + check.Assert(err, IsNil) + foundWorkerPool1, foundWorkerPool2 := false, false + for _, nodePool := range cluster.WorkerPools { + if nodePool.Name == "new-pool-1" { + foundWorkerPool1 = true + check.Assert(nodePool.MachineCount, Equals, 1) + check.Assert(nodePool.DiskSizeGi, Equals, 20) + check.Assert(nodePool.SizingPolicyId, Equals, "") + check.Assert(nodePool.StorageProfileId, Equals, "") + check.Assert(nodePool.Autoscaler, IsNil) + } + if nodePool.Name == "new-pool-2" { + foundWorkerPool2 = true + check.Assert(nodePool.MachineCount, Equals, 0) // Not used + check.Assert(nodePool.DiskSizeGi, Equals, 20) + check.Assert(nodePool.SizingPolicyId, Equals, "") + check.Assert(nodePool.StorageProfileId, Equals, "") + check.Assert(nodePool.Autoscaler, NotNil) + check.Assert(nodePool.Autoscaler.MinSize, Equals, 1) + check.Assert(nodePool.Autoscaler.MaxSize, Equals, 2) + } + } + check.Assert(foundWorkerPool1, Equals, true) + check.Assert(foundWorkerPool2, Equals, true) + + // Update control plane from 1 node to 3 (needs to be an odd number) + err = cluster.UpdateControlPlane(CseControlPlaneUpdateInput{MachineCount: 3}, true) + check.Assert(err, IsNil) + check.Assert(cluster.ControlPlane.MachineCount, Equals, 3) + + // Turn off the node health check + err = cluster.SetNodeHealthCheck(false, true) + check.Assert(err, IsNil) + check.Assert(cluster.NodeHealthCheck, Equals, false) + + // Update the auto repair flag + check.Assert(err, IsNil) + err = cluster.SetAutoRepairOnErrors(false, true) + check.Assert(err, IsNil) // It won't fail in CSE >4.1.0 as the flag is already false, so we update nothing. + check.Assert(cluster.AutoRepairOnErrors, Equals, false) + + // Upgrade the cluster if possible + upgradeOvas, err := cluster.GetSupportedUpgrades(true) + check.Assert(err, IsNil) + if len(upgradeOvas) > 0 { + err = cluster.UpgradeCluster(upgradeOvas[0].ID, true) + check.Assert(err, IsNil) + check.Assert(cluster.KubernetesVersion, Not(Equals), clusterGet.KubernetesVersion) + check.Assert(cluster.TkgVersion, Not(Equals), clusterGet.TkgVersion) + check.Assert(cluster.KubernetesTemplateOvaId, Not(Equals), clusterGet.KubernetesTemplateOvaId) + upgradeOvas, err = cluster.GetSupportedUpgrades(true) + check.Assert(err, IsNil) + check.Assert(len(upgradeOvas), Equals, 0) + } else { + fmt.Println("WARNING: CseKubernetesCluster.UpgradeCluster method not tested. It was skipped as there's no OVA to upgrade the cluster") + } + + // Helps to delete the cluster faster, also tests generic update method + err = cluster.Update(CseClusterUpdateInput{ + ControlPlane: &CseControlPlaneUpdateInput{MachineCount: 1}, + WorkerPools: &map[string]CseWorkerPoolUpdateInput{ + clusterSettings.WorkerPools[0].Name: { + MachineCount: 1, + }, + "new-pool-1": { + MachineCount: 0, + }, + "new-pool-2": { + MachineCount: 0, // Should remove autoscaler + }, + }, + }, true) + check.Assert(err, IsNil) + check.Assert(cluster.ControlPlane.MachineCount, Equals, 1) + for _, pool := range cluster.WorkerPools { + if pool.Name == "new-pool-1" { + check.Assert(pool.MachineCount, Equals, 0) + check.Assert(pool.Autoscaler, IsNil) + } else if pool.Name == "new-pool-2" { + check.Assert(pool.MachineCount, Equals, 0) + check.Assert(pool.Autoscaler, IsNil) + } else { + check.Assert(pool.MachineCount, Equals, 1) + } + } +} + +// Test_CseWithAutoscaler tests the autoscaling capabilities in CSE clusters +func (vcd *TestVCD) Test_CseWithAutoscaler(check *C) { + requireCseConfig(check, vcd.config) + + // Prerequisites: We need to read several items before creating the cluster. + org, err := vcd.client.GetOrgByName(vcd.config.Cse.TenantOrg) + check.Assert(err, IsNil) + + catalog, err := org.GetCatalogByName(vcd.config.Cse.OvaCatalog, false) + check.Assert(err, IsNil) + + ova, err := catalog.GetVAppTemplateByName(vcd.config.Cse.OvaName) + check.Assert(err, IsNil) + + tkgBundle, err := getTkgVersionBundleFromVAppTemplate(ova.VAppTemplate) + check.Assert(err, IsNil) + + vdc, err := org.GetVDCByName(vcd.config.Cse.TenantVdc, false) + check.Assert(err, IsNil) + + net, err := vdc.GetOrgVdcNetworkByName(vcd.config.Cse.RoutedNetwork, false) + check.Assert(err, IsNil) + + sp, err := vdc.FindStorageProfileReference(vcd.config.Cse.StorageProfile) + check.Assert(err, IsNil) + + policies, err := vcd.client.GetAllVdcComputePoliciesV2(url.Values{ + "filter": []string{"name==TKG small"}, + }) + check.Assert(err, IsNil) + check.Assert(len(policies), Equals, 1) + + token, err := vcd.client.CreateToken(vcd.config.Provider.SysOrg, check.TestName()) + check.Assert(err, IsNil) + defer func() { + err = token.Delete() + check.Assert(err, IsNil) + }() + AddToCleanupListOpenApi(token.Token.Name, check.TestName(), types.OpenApiPathVersion1_0_0+types.OpenApiEndpointTokens+token.Token.ID) + + apiToken, err := token.GetInitialApiToken() + check.Assert(err, IsNil) + + cseVersion, err := semver.NewVersion(vcd.config.Cse.Version) + check.Assert(err, IsNil) + check.Assert(cseVersion, NotNil) + + // Create the cluster + clusterSettings := CseClusterSettings{ + Name: "test-cse-autoscaler", + OrganizationId: org.Org.ID, + VdcId: vdc.Vdc.ID, + NetworkId: net.OrgVDCNetwork.ID, + KubernetesTemplateOvaId: ova.VAppTemplate.ID, + CseVersion: *cseVersion, + ControlPlane: CseControlPlaneSettings{ + MachineCount: 1, + DiskSizeGi: 20, + SizingPolicyId: policies[0].VdcComputePolicyV2.ID, + StorageProfileId: sp.ID, + Ip: "", + }, + WorkerPools: []CseWorkerPoolSettings{{ + Name: "worker-pool-1", + Autoscaler: &CseWorkerPoolAutoscaler{ + MaxSize: 2, + MinSize: 1, + }, + DiskSizeGi: 20, + SizingPolicyId: policies[0].VdcComputePolicyV2.ID, + StorageProfileId: sp.ID, + }}, + DefaultStorageClass: &CseDefaultStorageClassSettings{ + StorageProfileId: sp.ID, + Name: "storage-class-1", + ReclaimPolicy: "delete", + Filesystem: "ext4", + }, + Owner: vcd.config.Provider.User, + ApiToken: apiToken.RefreshToken, + NodeHealthCheck: true, + PodCidr: "100.96.0.0/11", + ServiceCidr: "100.64.0.0/13", + AutoRepairOnErrors: true, + } + cluster, err := org.CseCreateKubernetesCluster(clusterSettings, 150*time.Minute) + + // We assure that the cluster gets always deleted, even if the creation failed. + // Deletion process only needs the cluster ID + defer func() { + check.Assert(cluster, NotNil) + check.Assert(cluster.client, NotNil) + check.Assert(cluster.ID, Not(Equals), "") + err = cluster.Delete(0) + check.Assert(err, IsNil) + }() + + check.Assert(err, IsNil) + assertCseClusterCreation(check, cluster, clusterSettings, tkgBundle) + + kubeconfig, err := cluster.GetKubeconfig(false) + check.Assert(err, IsNil) + check.Assert(true, Equals, strings.Contains(kubeconfig, cluster.Name)) + check.Assert(true, Equals, strings.Contains(kubeconfig, "client-certificate-data")) + check.Assert(true, Equals, strings.Contains(kubeconfig, "certificate-authority-data")) + check.Assert(true, Equals, strings.Contains(kubeconfig, "client-key-data")) + + err = cluster.Refresh() + check.Assert(err, IsNil) + + clusterGet, err := vcd.client.CseGetKubernetesClusterById(cluster.ID) + check.Assert(err, IsNil) + assertCseClusterEquals(check, clusterGet, cluster) + check.Assert(clusterGet.Etag, Not(Equals), "") + + allClusters, err := org.CseGetKubernetesClustersByName(clusterGet.CseVersion, clusterGet.Name) + check.Assert(err, IsNil) + check.Assert(len(allClusters), Equals, 1) + assertCseClusterEquals(check, allClusters[0], clusterGet) + check.Assert(allClusters[0].Etag, Equals, "") // Can't recover ETag by name + + // Update worker pool and deactivate autoscaler + err = cluster.UpdateWorkerPools(map[string]CseWorkerPoolUpdateInput{clusterSettings.WorkerPools[0].Name: { + Autoscaler: &CseWorkerPoolAutoscaler{ + MaxSize: 10, + MinSize: 1, + }}}, true) + check.Assert(err, IsNil) + foundWorkerPool := false + for _, nodePool := range cluster.WorkerPools { + if nodePool.Name == clusterSettings.WorkerPools[0].Name { + foundWorkerPool = true + check.Assert(nodePool.MachineCount, Equals, 0) // The field is not used + check.Assert(nodePool.Autoscaler, NotNil) + check.Assert(nodePool.Autoscaler.MinSize, Equals, 1) + check.Assert(nodePool.Autoscaler.MaxSize, Equals, 10) + } + } + check.Assert(foundWorkerPool, Equals, true) +} + +// Test_CseFailure tests cluster creation errors and their consequences +func (vcd *TestVCD) Test_CseFailure(check *C) { + requireCseConfig(check, vcd.config) + + // Prerequisites: We need to read several items before creating the cluster. + org, err := vcd.client.GetOrgByName(vcd.config.Cse.TenantOrg) + check.Assert(err, IsNil) + + catalog, err := org.GetCatalogByName(vcd.config.Cse.OvaCatalog, false) + check.Assert(err, IsNil) + + ova, err := catalog.GetVAppTemplateByName(vcd.config.Cse.OvaName) + check.Assert(err, IsNil) + + vdc, err := org.GetVDCByName(vcd.config.Cse.TenantVdc, false) + check.Assert(err, IsNil) + + net, err := vdc.GetOrgVdcNetworkByName(vcd.config.Cse.RoutedNetwork, false) + check.Assert(err, IsNil) + + sp, err := vdc.FindStorageProfileReference(vcd.config.Cse.StorageProfile) + check.Assert(err, IsNil) + + policies, err := vcd.client.GetAllVdcComputePoliciesV2(url.Values{ + "filter": []string{"name==TKG small"}, + }) + check.Assert(err, IsNil) + check.Assert(len(policies), Equals, 1) + + token, err := vcd.client.CreateToken(vcd.config.Provider.SysOrg, check.TestName()) + check.Assert(err, IsNil) + defer func() { + err = token.Delete() + check.Assert(err, IsNil) + }() + AddToCleanupListOpenApi(token.Token.Name, check.TestName(), types.OpenApiPathVersion1_0_0+types.OpenApiEndpointTokens+token.Token.ID) + + apiToken, err := token.GetInitialApiToken() + check.Assert(err, IsNil) + + cseVersion, err := semver.NewVersion(vcd.config.Cse.Version) + check.Assert(err, IsNil) + check.Assert(cseVersion, NotNil) + + componentsVersions, err := getCseComponentsVersions(*cseVersion) + check.Assert(err, IsNil) + check.Assert(componentsVersions, NotNil) + + // Create the cluster + clusterSettings := CseClusterSettings{ + Name: "test-cse-fail", + OrganizationId: org.Org.ID, + VdcId: vdc.Vdc.ID, + NetworkId: net.OrgVDCNetwork.ID, + KubernetesTemplateOvaId: ova.VAppTemplate.ID, + CseVersion: *cseVersion, + ControlPlane: CseControlPlaneSettings{ + MachineCount: 1, + DiskSizeGi: 20, + SizingPolicyId: policies[0].VdcComputePolicyV2.ID, + StorageProfileId: sp.ID, + Ip: "", + }, + WorkerPools: []CseWorkerPoolSettings{{ + Name: "worker-pool-1", + MachineCount: 1, + DiskSizeGi: 20, + SizingPolicyId: policies[0].VdcComputePolicyV2.ID, + StorageProfileId: sp.ID, + }}, + Owner: vcd.config.Provider.User, + ApiToken: apiToken.RefreshToken, + NodeHealthCheck: true, + PodCidr: "1.1.1.1/24", // This should make the cluster fail + ServiceCidr: "1.1.1.1/24", // This should make the cluster fail + AutoRepairOnErrors: false, // Must be false to avoid never-ending loops + } + cluster, err := org.CseCreateKubernetesCluster(clusterSettings, 150*time.Minute) + + // We assure that the cluster gets always deleted. + // Deletion process only needs the cluster ID + defer func() { + check.Assert(cluster, NotNil) + check.Assert(cluster.client, NotNil) + check.Assert(cluster.ID, Not(Equals), "") + err = cluster.Delete(0) + check.Assert(err, IsNil) + }() + + check.Assert(err, NotNil) + check.Assert(cluster.client, NotNil) + check.Assert(cluster.ID, Not(Equals), "") + + clusterGet, err := vcd.client.CseGetKubernetesClusterById(cluster.ID) + check.Assert(err, IsNil) + // We don't get an error when we retrieve a failed cluster, but some fields are missing + check.Assert(clusterGet.ID, Equals, cluster.ID) + check.Assert(clusterGet.Etag, Not(Equals), "") + check.Assert(clusterGet.State, Equals, "error") + check.Assert(len(clusterGet.Events), Not(Equals), 0) + + err = cluster.Refresh() + check.Assert(err, IsNil) + assertCseClusterEquals(check, cluster, clusterGet) + + allClusters, err := org.CseGetKubernetesClustersByName(clusterGet.CseVersion, clusterGet.Name) + check.Assert(err, IsNil) + check.Assert(len(allClusters), Equals, 1) + assertCseClusterEquals(check, allClusters[0], clusterGet) + check.Assert(allClusters[0].Etag, Equals, "") // Can't recover ETag by name + + _, err = cluster.GetKubeconfig(false) + check.Assert(err, NotNil) + + // All updates should fail + err = cluster.UpdateWorkerPools(map[string]CseWorkerPoolUpdateInput{clusterSettings.WorkerPools[0].Name: {MachineCount: 1}}, true) + check.Assert(err, NotNil) + err = cluster.AddWorkerPools([]CseWorkerPoolSettings{{ + Name: "i-dont-care-i-will-fail", + MachineCount: 1, + DiskSizeGi: 20, + }}, true) + check.Assert(err, NotNil) + err = cluster.UpdateControlPlane(CseControlPlaneUpdateInput{MachineCount: 1}, true) + check.Assert(err, NotNil) + err = cluster.SetNodeHealthCheck(false, true) + check.Assert(err, NotNil) + err = cluster.SetAutoRepairOnErrors(false, true) + check.Assert(err, NotNil) + + upgradeOvas, err := cluster.GetSupportedUpgrades(true) + check.Assert(err, IsNil) + check.Assert(len(upgradeOvas), Equals, 0) + + err = cluster.UpgradeCluster(clusterSettings.KubernetesTemplateOvaId, true) + check.Assert(err, NotNil) +} + +// Test_CseValidationErrors tests validation errors during cluster creation request +func (vcd *TestVCD) Test_CseValidationErrors(check *C) { + requireCseConfig(check, vcd.config) + + org, err := vcd.client.GetOrgByName(vcd.config.Cse.TenantOrg) + check.Assert(err, IsNil) + + settings := CseClusterSettings{} + + // Wrong CSE version + cseVersion, err := semver.NewVersion("9.0.0") + check.Assert(err, IsNil) + check.Assert(cseVersion, NotNil) + settings.CseVersion = *cseVersion + _, err = org.CseCreateKubernetesCluster(settings, 0) + check.Assert(err, NotNil) + check.Assert(err.Error() == fmt.Sprintf("the Container Service Extension version '%s' is not supported", settings.CseVersion.String()), Equals, true) + cseVersion, err = semver.NewVersion(vcd.config.Cse.Version) + check.Assert(err, IsNil) + check.Assert(cseVersion, NotNil) + settings.CseVersion = *cseVersion + + // Wrong name + settings.Name = "NotAValidName%%%1" + _, err = org.CseCreateKubernetesCluster(settings, 0) + check.Assert(err, NotNil) + check.Assert(err.Error() == fmt.Sprintf("error creating the CSE Kubernetes cluster: the name '%s' must contain only lowercase alphanumeric characters or '-', start with an alphabetic character, end with an alphanumeric, and contain at most 31 characters", settings.Name), Equals, true) + + settings.Name = "valid" + + // Missing Organization ID + _, err = org.CseCreateKubernetesCluster(settings, 0) + check.Assert(err, NotNil) + check.Assert(err.Error() == "error creating the CSE Kubernetes cluster: the Organization ID is required", Equals, true) + + settings.OrganizationId = org.Org.ID + + // Missing VDC ID + _, err = org.CseCreateKubernetesCluster(settings, 0) + check.Assert(err, NotNil) + check.Assert(err.Error() == "error creating the CSE Kubernetes cluster: the VDC ID is required", Equals, true) + + vdc, err := org.GetVDCByName(vcd.config.Cse.TenantVdc, false) + check.Assert(err, IsNil) + settings.VdcId = vdc.Vdc.ID + + // Missing Network ID + _, err = org.CseCreateKubernetesCluster(settings, 0) + check.Assert(err, NotNil) + check.Assert(err.Error() == "error creating the CSE Kubernetes cluster: the Network ID is required", Equals, true) + + net, err := vdc.GetOrgVdcNetworkByName(vcd.config.Cse.RoutedNetwork, false) + check.Assert(err, IsNil) + settings.NetworkId = net.OrgVDCNetwork.ID + + // Missing Kubernetes OVA + _, err = org.CseCreateKubernetesCluster(settings, 0) + check.Assert(err, NotNil) + check.Assert(err.Error() == "error creating the CSE Kubernetes cluster: the Kubernetes Template OVA ID is required", Equals, true) + + catalog, err := org.GetCatalogByName(vcd.config.Cse.OvaCatalog, false) + check.Assert(err, IsNil) + ova, err := catalog.GetVAppTemplateByName(vcd.config.Cse.OvaName) + check.Assert(err, IsNil) + settings.KubernetesTemplateOvaId = ova.VAppTemplate.ID + + // No control plane nodes + _, err = org.CseCreateKubernetesCluster(settings, 0) + check.Assert(err, NotNil) + check.Assert(err.Error() == "error creating the CSE Kubernetes cluster: number of Control Plane nodes must be odd and higher than 0, but it was '0'", Equals, true) + + settings.ControlPlane.MachineCount = 2 + + // Wrong control plane nodes, it should not be even + _, err = org.CseCreateKubernetesCluster(settings, 0) + check.Assert(err, NotNil) + check.Assert(err.Error() == "error creating the CSE Kubernetes cluster: number of Control Plane nodes must be odd and higher than 0, but it was '2'", Equals, true) + + settings.ControlPlane.MachineCount = 1 + + // Wrong disk size for the control plane + _, err = org.CseCreateKubernetesCluster(settings, 0) + check.Assert(err, NotNil) + check.Assert(err.Error() == "error creating the CSE Kubernetes cluster: disk size for the Control Plane in Gibibytes (Gi) must be at least 20, but it was '0'", Equals, true) + + settings.ControlPlane.DiskSizeGi = 20 + + // No worker pool + _, err = org.CseCreateKubernetesCluster(settings, 0) + check.Assert(err, NotNil) + check.Assert(err.Error() == "error creating the CSE Kubernetes cluster: there must be at least one Worker Pool", Equals, true) + + settings.WorkerPools = []CseWorkerPoolSettings{ + {}, + } + + // Wrong worker pool name + settings.WorkerPools[0].Name = "NotAValidName%%%1" + _, err = org.CseCreateKubernetesCluster(settings, 0) + check.Assert(err, NotNil) + check.Assert(err.Error() == fmt.Sprintf("error creating the CSE Kubernetes cluster: the Worker Pool name '%s' must contain only lowercase alphanumeric characters or '-', start with an alphabetic character, end with an alphanumeric, and contain at most 31 characters", settings.WorkerPools[0].Name), Equals, true) + + settings.WorkerPools[0].Name = "wp-1" + + // No worker pool replicas + _, err = org.CseCreateKubernetesCluster(settings, 0) + check.Assert(err, NotNil) + check.Assert(err.Error() == fmt.Sprintf("error creating the CSE Kubernetes cluster: number of Worker Pool '%s' nodes must higher than 0, but it was '0'", settings.WorkerPools[0].Name), Equals, true) + + settings.WorkerPools[0].MachineCount = 1 + + // Try to set the autoscaler and the static machine count at same time + settings.WorkerPools[0].Autoscaler = &CseWorkerPoolAutoscaler{MaxSize: 1, MinSize: 5} + _, err = org.CseCreateKubernetesCluster(settings, 0) + check.Assert(err, NotNil) + check.Assert(err.Error() == fmt.Sprintf("error creating the CSE Kubernetes cluster: the Worker Pool '%s' is using Autoscaler (min=5,max=1), so can't set MachineCount to '1'", settings.WorkerPools[0].Name), Equals, true) + + // The autoscaler is configured wrong (min > max) + settings.WorkerPools[0].MachineCount = 0 + _, err = org.CseCreateKubernetesCluster(settings, 0) + check.Assert(err, NotNil) + check.Assert(err.Error() == fmt.Sprintf("error creating the CSE Kubernetes cluster: the Autoscaler maximum size for Worker Pool '%s' cannot be less than the minimum", settings.WorkerPools[0].Name), Equals, true) + + // The autoscaler is configured wrong (max < 0) + settings.WorkerPools[0].Autoscaler.MaxSize = -5 + settings.WorkerPools[0].Autoscaler.MinSize = -10 + _, err = org.CseCreateKubernetesCluster(settings, 0) + check.Assert(err, NotNil) + check.Assert(err.Error() == fmt.Sprintf("error creating the CSE Kubernetes cluster: the Autoscaler maximum size for Worker Pool '%s' must be a positive number", settings.WorkerPools[0].Name), Equals, true) + + // The autoscaler is configured wrong (min < 0) + settings.WorkerPools[0].Autoscaler.MaxSize = 5 + _, err = org.CseCreateKubernetesCluster(settings, 0) + check.Assert(err, NotNil) + check.Assert(err.Error() == fmt.Sprintf("error creating the CSE Kubernetes cluster: the Autoscaler minimum size for Worker Pool '%s' must be a positive number", settings.WorkerPools[0].Name), Equals, true) + + // Wrong disk size for the worker pool + settings.WorkerPools[0].Autoscaler.MinSize = 1 + _, err = org.CseCreateKubernetesCluster(settings, 0) + check.Assert(err, NotNil) + check.Assert(err.Error() == fmt.Sprintf("error creating the CSE Kubernetes cluster: disk size for the Worker Pool '%s' in Gibibytes (Gi) must be at least 20, but it was '0'", settings.WorkerPools[0].Name), Equals, true) + + settings.WorkerPools[0].DiskSizeGi = 20 + settings.WorkerPools = append(settings.WorkerPools, CseWorkerPoolSettings{Name: "wp-1"}) + + // Repeated worker pool name + _, err = org.CseCreateKubernetesCluster(settings, 0) + check.Assert(err, NotNil) + check.Assert(err.Error() == fmt.Sprintf("error creating the CSE Kubernetes cluster: the names of the Worker Pools must be unique, but '%s' is repeated", settings.WorkerPools[0].Name), Equals, true) + + settings.WorkerPools[1] = CseWorkerPoolSettings{Name: "wp-2", MachineCount: 1, DiskSizeGi: 20} + + // Missing API token + _, err = org.CseCreateKubernetesCluster(settings, 0) + check.Assert(err, NotNil) + check.Assert(err.Error() == "error creating the CSE Kubernetes cluster: the API token is required", Equals, true) + + token, err := vcd.client.CreateToken(vcd.config.Provider.SysOrg, check.TestName()) + check.Assert(err, IsNil) + defer func() { + err = token.Delete() + check.Assert(err, IsNil) + }() + AddToCleanupListOpenApi(token.Token.Name, check.TestName(), types.OpenApiPathVersion1_0_0+types.OpenApiEndpointTokens+token.Token.ID) + + apiToken, err := token.GetInitialApiToken() + check.Assert(err, IsNil) + settings.ApiToken = apiToken.RefreshToken + + // Missing Pod CIDR + _, err = org.CseCreateKubernetesCluster(settings, 0) + check.Assert(err, NotNil) + check.Assert(err.Error() == "error creating the CSE Kubernetes cluster: the Pod CIDR is required", Equals, true) + + // Wrong Pod CIDR + settings.PodCidr = "256.700.1.278/800" + _, err = org.CseCreateKubernetesCluster(settings, 0) + check.Assert(err, NotNil) + check.Assert(strings.Contains(err.Error(), "error creating the CSE Kubernetes cluster: the Pod CIDR is malformed"), Equals, true) + + // Missing Service CIDR + settings.PodCidr = "192.168.1.0/20" + _, err = org.CseCreateKubernetesCluster(settings, 0) + check.Assert(err, NotNil) + check.Assert(err.Error() == "error creating the CSE Kubernetes cluster: the Service CIDR is required", Equals, true) + + // Wrong Service CIDR + settings.ServiceCidr = "256.700.1.278/800" + _, err = org.CseCreateKubernetesCluster(settings, 0) + check.Assert(err, NotNil) + check.Assert(strings.Contains(err.Error(), "error creating the CSE Kubernetes cluster: the Service CIDR is malformed"), Equals, true) + + // Wrong Virtual IP subnet + settings.ServiceCidr = "192.168.1.0/20" + settings.VirtualIpSubnet = "256.700.1.278/800" + _, err = org.CseCreateKubernetesCluster(settings, 0) + check.Assert(err, NotNil) + check.Assert(strings.Contains(err.Error(), "error creating the CSE Kubernetes cluster: the Virtual IP Subnet is malformed"), Equals, true) + + // Wrong Control Plane IP + settings.VirtualIpSubnet = "192.154.1.0/20" + settings.ControlPlane.Ip = "256.700.1.278" + _, err = org.CseCreateKubernetesCluster(settings, 0) + check.Assert(err, NotNil) + check.Assert(strings.Contains(err.Error(), "error creating the CSE Kubernetes cluster: the Control Plane IP is malformed"), Equals, true) + + // Wrong default storage class name + settings.ControlPlane.Ip = "1.1.1.1" + settings.DefaultStorageClass = &CseDefaultStorageClassSettings{Name: "NotAValidName%%%1"} + _, err = org.CseCreateKubernetesCluster(settings, 0) + check.Assert(err, NotNil) + check.Assert(err.Error() == fmt.Sprintf("error creating the CSE Kubernetes cluster: the Default Storage Class name '%s' must contain only lowercase alphanumeric characters or '-', start with an alphabetic character, end with an alphanumeric, and contain at most 31 characters", settings.DefaultStorageClass.Name), Equals, true) + + // Missing Storage profile ID + settings.DefaultStorageClass.Name = "sp-1" + _, err = org.CseCreateKubernetesCluster(settings, 0) + check.Assert(err, NotNil) + check.Assert(err.Error() == "error creating the CSE Kubernetes cluster: the Storage Profile ID for the Default Storage Class is required", Equals, true) + + sp, err := vdc.FindStorageProfileReference(vcd.config.Cse.StorageProfile) + check.Assert(err, IsNil) + + policies, err := vcd.client.GetAllVdcComputePoliciesV2(url.Values{ + "filter": []string{"name==TKG small"}, + }) + check.Assert(err, IsNil) + check.Assert(len(policies), Equals, 1) + + // Wrong retaining policy in the default storage class + settings.DefaultStorageClass.StorageProfileId = sp.ID + settings.DefaultStorageClass.ReclaimPolicy = "whatever" + _, err = org.CseCreateKubernetesCluster(settings, 0) + check.Assert(err, NotNil) + check.Assert(err.Error() == "error creating the CSE Kubernetes cluster: the Reclaim Policy for the Default Storage Class must be either 'delete' or 'retain', but it was 'whatever'", Equals, true) + + // Wrong filesystem in the default storage class + settings.DefaultStorageClass.ReclaimPolicy = "delete" + settings.DefaultStorageClass.Filesystem = "oops" + _, err = org.CseCreateKubernetesCluster(settings, 0) + check.Assert(err, NotNil) + check.Assert(err.Error() == "error creating the CSE Kubernetes cluster: the filesystem for the Default Storage Class must be either 'ext4' or 'xfs', but it was 'oops'", Equals, true) +} + +func assertCseClusterCreation(check *C, createdCluster *CseKubernetesCluster, settings CseClusterSettings, expectedKubernetesData tkgVersionBundle) { + check.Assert(createdCluster, NotNil) + check.Assert(createdCluster.CseVersion.Original(), Equals, settings.CseVersion.Original()) + check.Assert(createdCluster.Name, Equals, settings.Name) + check.Assert(createdCluster.OrganizationId, Equals, settings.OrganizationId) + check.Assert(createdCluster.VdcId, Equals, settings.VdcId) + check.Assert(createdCluster.NetworkId, Equals, settings.NetworkId) + check.Assert(createdCluster.KubernetesTemplateOvaId, Equals, settings.KubernetesTemplateOvaId) + check.Assert(createdCluster.ControlPlane.MachineCount, Equals, settings.ControlPlane.MachineCount) + check.Assert(createdCluster.ControlPlane.SizingPolicyId, Equals, settings.ControlPlane.SizingPolicyId) + check.Assert(createdCluster.ControlPlane.PlacementPolicyId, Equals, settings.ControlPlane.PlacementPolicyId) + check.Assert(createdCluster.ControlPlane.StorageProfileId, Equals, settings.ControlPlane.StorageProfileId) + check.Assert(createdCluster.ControlPlane.DiskSizeGi, Equals, settings.ControlPlane.DiskSizeGi) + if settings.ControlPlane.Ip != "" { + check.Assert(createdCluster.ControlPlane.Ip, Equals, settings.ControlPlane.Ip) + } else { + check.Assert(createdCluster.ControlPlane.Ip, Not(Equals), "") + } + check.Assert(createdCluster.WorkerPools, DeepEquals, settings.WorkerPools) + if settings.DefaultStorageClass != nil { + check.Assert(createdCluster.DefaultStorageClass, NotNil) + check.Assert(*createdCluster.DefaultStorageClass, DeepEquals, *settings.DefaultStorageClass) + } + if settings.Owner != "" { + check.Assert(createdCluster.Owner, Equals, settings.Owner) + } else { + check.Assert(createdCluster.Owner, Not(Equals), "") + } + check.Assert(createdCluster.ApiToken, Not(Equals), settings.ApiToken) + check.Assert(createdCluster.ApiToken, Equals, "******") // This one can't be recovered + check.Assert(createdCluster.NodeHealthCheck, Equals, settings.NodeHealthCheck) + check.Assert(createdCluster.PodCidr, Equals, settings.PodCidr) + check.Assert(createdCluster.ServiceCidr, Equals, settings.ServiceCidr) + check.Assert(createdCluster.SshPublicKey, Equals, settings.SshPublicKey) + check.Assert(createdCluster.VirtualIpSubnet, Equals, settings.VirtualIpSubnet) + check.Assert(createdCluster.SshPublicKey, Equals, settings.SshPublicKey) + + v411, err := semver.NewVersion("4.1.1") + check.Assert(err, IsNil) + if settings.CseVersion.GreaterThanOrEqual(v411) { + // Since CSE 4.1.1, the flag is automatically switched off when the cluster is created + check.Assert(createdCluster.AutoRepairOnErrors, Equals, false) + } else { + check.Assert(createdCluster.AutoRepairOnErrors, Equals, settings.AutoRepairOnErrors) + } + check.Assert(createdCluster.VirtualIpSubnet, Equals, settings.VirtualIpSubnet) + check.Assert(true, Equals, strings.Contains(createdCluster.ID, "urn:vcloud:entity:vmware:capvcdCluster:")) + check.Assert(createdCluster.Etag, Not(Equals), "") + check.Assert(createdCluster.KubernetesVersion.Original(), Equals, expectedKubernetesData.KubernetesVersion) + check.Assert(createdCluster.TkgVersion.Original(), Equals, expectedKubernetesData.TkgVersion) + check.Assert(createdCluster.CapvcdVersion.Original(), Not(Equals), "") + check.Assert(createdCluster.CpiVersion.Original(), Not(Equals), "") + check.Assert(createdCluster.CsiVersion.Original(), Not(Equals), "") + check.Assert(len(createdCluster.ClusterResourceSetBindings), Not(Equals), 0) + check.Assert(createdCluster.State, Equals, "provisioned") + check.Assert(len(createdCluster.Events), Not(Equals), 0) +} + +func assertCseClusterEquals(check *C, obtainedCluster, expectedCluster *CseKubernetesCluster) { + check.Assert(expectedCluster, NotNil) + check.Assert(obtainedCluster, NotNil) + check.Assert(obtainedCluster.CseVersion.Original(), Equals, expectedCluster.CseVersion.Original()) + check.Assert(obtainedCluster.Name, Equals, expectedCluster.Name) + check.Assert(obtainedCluster.OrganizationId, Equals, expectedCluster.OrganizationId) + check.Assert(obtainedCluster.VdcId, Equals, expectedCluster.VdcId) + check.Assert(obtainedCluster.NetworkId, Equals, expectedCluster.NetworkId) + check.Assert(obtainedCluster.KubernetesTemplateOvaId, Equals, expectedCluster.KubernetesTemplateOvaId) + check.Assert(obtainedCluster.ControlPlane, DeepEquals, expectedCluster.ControlPlane) + check.Assert(obtainedCluster.WorkerPools, DeepEquals, expectedCluster.WorkerPools) + if expectedCluster.DefaultStorageClass != nil { + check.Assert(obtainedCluster.DefaultStorageClass, NotNil) + check.Assert(*obtainedCluster.DefaultStorageClass, DeepEquals, *expectedCluster.DefaultStorageClass) + } + check.Assert(obtainedCluster.Owner, Equals, expectedCluster.Owner) + check.Assert(obtainedCluster.ApiToken, Equals, "******") // This one can't be recovered + check.Assert(obtainedCluster.NodeHealthCheck, Equals, expectedCluster.NodeHealthCheck) + check.Assert(obtainedCluster.PodCidr, Equals, expectedCluster.PodCidr) + check.Assert(obtainedCluster.ServiceCidr, Equals, expectedCluster.ServiceCidr) + check.Assert(obtainedCluster.SshPublicKey, Equals, expectedCluster.SshPublicKey) + check.Assert(obtainedCluster.VirtualIpSubnet, Equals, expectedCluster.VirtualIpSubnet) + check.Assert(obtainedCluster.AutoRepairOnErrors, Equals, expectedCluster.AutoRepairOnErrors) + check.Assert(obtainedCluster.VirtualIpSubnet, Equals, expectedCluster.VirtualIpSubnet) + check.Assert(obtainedCluster.ID, Equals, expectedCluster.ID) + check.Assert(obtainedCluster.KubernetesVersion.Original(), Equals, expectedCluster.KubernetesVersion.Original()) + check.Assert(obtainedCluster.TkgVersion.Original(), Equals, expectedCluster.TkgVersion.Original()) + check.Assert(obtainedCluster.CapvcdVersion.Original(), Equals, expectedCluster.CapvcdVersion.Original()) + check.Assert(obtainedCluster.CpiVersion.Original(), Equals, expectedCluster.CpiVersion.Original()) + check.Assert(obtainedCluster.CsiVersion.Original(), Equals, expectedCluster.CsiVersion.Original()) + check.Assert(obtainedCluster.ClusterResourceSetBindings, DeepEquals, expectedCluster.ClusterResourceSetBindings) + check.Assert(obtainedCluster.State, Equals, expectedCluster.State) + check.Assert(len(obtainedCluster.Events) >= len(expectedCluster.Events), Equals, true) +} diff --git a/govcd/cse_type.go b/govcd/cse_type.go new file mode 100644 index 000000000..802303d3f --- /dev/null +++ b/govcd/cse_type.go @@ -0,0 +1,212 @@ +package govcd + +import ( + semver "github.com/hashicorp/go-version" + "github.com/vmware/go-vcloud-director/v2/types/v56" + "time" +) + +// CseKubernetesCluster is a type for managing an existing Kubernetes cluster created by the Container Service Extension (CSE) +type CseKubernetesCluster struct { + CseClusterSettings + ID string + Etag string + KubernetesVersion semver.Version + TkgVersion semver.Version + CapvcdVersion semver.Version + ClusterResourceSetBindings []string + CpiVersion semver.Version + CsiVersion semver.Version + State string + Events []CseClusterEvent + + client *Client + capvcdType *types.Capvcd + supportedUpgrades []*types.VAppTemplate // Caches the vApp templates that can be used to upgrade a cluster. +} + +// CseClusterSettings defines the required configuration of a Container Service Extension (CSE) Kubernetes cluster. +type CseClusterSettings struct { + CseVersion semver.Version + Name string + OrganizationId string + VdcId string + NetworkId string + KubernetesTemplateOvaId string + ControlPlane CseControlPlaneSettings + WorkerPools []CseWorkerPoolSettings + DefaultStorageClass *CseDefaultStorageClassSettings // Optional + Owner string // Optional, if not set will pick the current session user from the VCDClient + ApiToken string + NodeHealthCheck bool + PodCidr string + ServiceCidr string + SshPublicKey string + VirtualIpSubnet string + AutoRepairOnErrors bool +} + +// CseControlPlaneSettings defines the required configuration of a Control Plane of a Container Service Extension (CSE) Kubernetes cluster. +type CseControlPlaneSettings struct { + MachineCount int + DiskSizeGi int + SizingPolicyId string // Optional + PlacementPolicyId string // Optional + StorageProfileId string // Optional + Ip string // Optional +} + +// CseWorkerPoolSettings defines the required configuration of a Worker Pool of a Container Service Extension (CSE) Kubernetes cluster. +type CseWorkerPoolSettings struct { + Name string + MachineCount int // If the Autoscaler is enabled, this field is ignored + DiskSizeGi int + SizingPolicyId string // Optional + PlacementPolicyId string // Optional + VGpuPolicyId string // Optional + StorageProfileId string // Optional + Autoscaler *CseWorkerPoolAutoscaler // Optional, enables the Autoscaler if not nil. If it is nil for all worker pools, the Autoscaler will be disabled +} + +// CseWorkerPoolAutoscaler defines the required configuration of the Autoscaling capabilities of a CSE Kubernetes cluster Worker Pool. +type CseWorkerPoolAutoscaler struct { + MaxSize int + MinSize int +} + +// CseDefaultStorageClassSettings defines the required configuration of a Default Storage Class of a Container Service Extension (CSE) Kubernetes cluster. +type CseDefaultStorageClassSettings struct { + StorageProfileId string + Name string + ReclaimPolicy string // Must be either "delete" or "retain" + Filesystem string // Must be either "ext4" or "xfs" +} + +// CseClusterEvent is an event that has occurred during the lifetime of a Container Service Extension (CSE) Kubernetes cluster. +type CseClusterEvent struct { + Name string + Type string + ResourceId string + ResourceName string + OccurredAt time.Time + Details string +} + +// CseClusterUpdateInput defines the required configuration that a Container Service Extension (CSE) Kubernetes cluster needs in order to be updated. +type CseClusterUpdateInput struct { + KubernetesTemplateOvaId *string + ControlPlane *CseControlPlaneUpdateInput + WorkerPools *map[string]CseWorkerPoolUpdateInput // Maps a node pool name with its contents + NewWorkerPools *[]CseWorkerPoolSettings + NodeHealthCheck *bool + AutoRepairOnErrors *bool +} + +// CseControlPlaneUpdateInput defines the required configuration that the Control Plane of the Container Service Extension (CSE) Kubernetes cluster +// needs in order to be updated. +type CseControlPlaneUpdateInput struct { + MachineCount int +} + +// CseWorkerPoolUpdateInput defines the required configuration that a Worker Pool of the Container Service Extension (CSE) Kubernetes cluster +// needs in order to be updated. +type CseWorkerPoolUpdateInput struct { + MachineCount int // If the Autoscaler is enabled, this field is ignored + Autoscaler *CseWorkerPoolAutoscaler // Optional, enables the Autoscaler if not nil. If it is nil for all worker pools, the Autoscaler will be disabled +} + +// cseClusterSettingsInternal defines the required arguments that are required by the CSE Server used internally to specify +// a Kubernetes cluster. These are not set by the user, but instead they are computed from a valid +// CseClusterSettings object in the CseClusterSettings.toCseClusterSettingsInternal method. These fields are then +// inserted in Go templates to render a final JSON that is valid to be used as the cluster Runtime Defined Entity (RDE) payload. +// +// The main difference between CseClusterSettings and this structure is that the first one uses IDs and this one uses names, among +// other differences like the computed tkgVersionBundle. +type cseClusterSettingsInternal struct { + CseVersion semver.Version + Name string + OrganizationName string + VdcName string + NetworkName string + KubernetesTemplateOvaName string + TkgVersionBundle tkgVersionBundle + CatalogName string + RdeType *types.DefinedEntityType + ControlPlane cseControlPlaneSettingsInternal + WorkerPools []cseWorkerPoolSettingsInternal + DefaultStorageClass cseDefaultStorageClassInternal + VcdKeConfig vcdKeConfig + Owner string + ApiToken string + VcdUrl string + VirtualIpSubnet string + SshPublicKey string + PodCidr string + ServiceCidr string + AutoRepairOnErrors bool +} + +// tkgVersionBundle is a type that contains all the versions of the components of +// a Kubernetes cluster that can be obtained with the internal properties of the Kubernetes Template OVAs downloaded from +// https://customerconnect.vmware.com +type tkgVersionBundle struct { + EtcdVersion string + CoreDnsVersion string + TkgVersion string + TkrVersion string + KubernetesVersion string +} + +// cseControlPlaneSettingsInternal defines the Control Plane inside cseClusterSettingsInternal +type cseControlPlaneSettingsInternal struct { + MachineCount int + DiskSizeGi int + SizingPolicyName string + PlacementPolicyName string + StorageProfileName string + Ip string +} + +// cseWorkerPoolSettingsInternal defines a Worker Pool inside cseClusterSettingsInternal +type cseWorkerPoolSettingsInternal struct { + Name string + MachineCount int + DiskSizeGi int + SizingPolicyName string + PlacementPolicyName string + VGpuPolicyName string + StorageProfileName string + Autoscaler *CseWorkerPoolAutoscaler +} + +// cseDefaultStorageClassInternal defines a Default Storage Class inside cseClusterSettingsInternal +type cseDefaultStorageClassInternal struct { + StorageProfileName string + Name string + UseDeleteReclaimPolicy bool + Filesystem string +} + +// vcdKeConfig is a type that contains only the required and relevant fields from the VCDKEConfig (CSE Server) configuration, +// such as the Machine Health Check settings. +type vcdKeConfig struct { + MaxUnhealthyNodesPercentage float64 + NodeStartupTimeout string + NodeNotReadyTimeout string + NodeUnknownTimeout string + ContainerRegistryUrl string + Base64Certificates []string +} + +// cseComponentsVersions is a type that registers the versions of the subcomponents of a specific CSE Version +type cseComponentsVersions struct { + VcdKeConfigRdeTypeVersion string + CapvcdRdeTypeVersion string + CseInterfaceVersion string +} + +// Constants used internally to manage CSE Kubernetes clusters +const ( + cseKubernetesClusterVendor = "vmware" + cseKubernetesClusterNamespace = "capvcdCluster" +) diff --git a/govcd/cse_util.go b/govcd/cse_util.go new file mode 100644 index 000000000..4155c1ee4 --- /dev/null +++ b/govcd/cse_util.go @@ -0,0 +1,1023 @@ +package govcd + +import ( + _ "embed" + "encoding/base64" + "encoding/json" + "fmt" + semver "github.com/hashicorp/go-version" + "github.com/vmware/go-vcloud-director/v2/types/v56" + "github.com/vmware/go-vcloud-director/v2/util" + "net" + "net/url" + "regexp" + "sort" + "strconv" + "strings" + "time" +) + +// getCseComponentsVersions gets the versions of the subcomponents that are part of Container Service Extension. +// NOTE: This function should be updated on every CSE release to update the supported versions. +func getCseComponentsVersions(cseVersion semver.Version) (*cseComponentsVersions, error) { + v43, _ := semver.NewVersion("4.3.0") + v42, _ := semver.NewVersion("4.2.0") + v41, _ := semver.NewVersion("4.1.0") + err := fmt.Errorf("the Container Service Extension version '%s' is not supported", cseVersion.String()) + + if cseVersion.GreaterThanOrEqual(v43) { + return nil, err + } + if cseVersion.GreaterThanOrEqual(v42) { + return &cseComponentsVersions{ + VcdKeConfigRdeTypeVersion: "1.1.0", + CapvcdRdeTypeVersion: "1.3.0", + CseInterfaceVersion: "1.0.0", + }, nil + } + if cseVersion.GreaterThanOrEqual(v41) { + return &cseComponentsVersions{ + VcdKeConfigRdeTypeVersion: "1.1.0", + CapvcdRdeTypeVersion: "1.2.0", + CseInterfaceVersion: "1.0.0", + }, nil + } + return nil, err +} + +// cseConvertToCseKubernetesClusterType takes a generic RDE that must represent an existing CSE Kubernetes cluster, +// and transforms it to an equivalent CseKubernetesCluster object that represents the same cluster, but +// it is easy to explore and consume. If the input RDE is not a CSE Kubernetes cluster, this method +// will obviously return an error. +// +// The transformation from a generic RDE to a CseKubernetesCluster is done by querying VCD for every needed item, +// such as Network IDs, Compute Policies IDs, vApp Template IDs, etc. It deeply explores the RDE contents +// (even the CAPI YAML) to retrieve information and getting the missing pieces from VCD. +// +// WARNING: Don't use this method inside loops or avoid calling it multiple times in a row, as it performs many queries +// to VCD. +func cseConvertToCseKubernetesClusterType(rde *DefinedEntity) (*CseKubernetesCluster, error) { + requiredType := fmt.Sprintf("%s:%s", cseKubernetesClusterVendor, cseKubernetesClusterNamespace) + + if !strings.Contains(rde.DefinedEntity.ID, requiredType) || !strings.Contains(rde.DefinedEntity.EntityType, requiredType) { + return nil, fmt.Errorf("the receiver RDE is not a '%s' entity, it is '%s'", requiredType, rde.DefinedEntity.EntityType) + } + + entityBytes, err := json.Marshal(rde.DefinedEntity.Entity) + if err != nil { + return nil, fmt.Errorf("could not marshal the RDE contents to create a capvcdType instance: %s", err) + } + + capvcd := &types.Capvcd{} + err = json.Unmarshal(entityBytes, &capvcd) + if err != nil { + return nil, fmt.Errorf("could not unmarshal the RDE contents to create a Capvcd instance: %s", err) + } + + result := &CseKubernetesCluster{ + CseClusterSettings: CseClusterSettings{ + Name: rde.DefinedEntity.Name, + ApiToken: "******", // We must not return this one, we return the "standard" 6-asterisk value + AutoRepairOnErrors: capvcd.Spec.VcdKe.AutoRepairOnErrors, + ControlPlane: CseControlPlaneSettings{}, + }, + ID: rde.DefinedEntity.ID, + Etag: rde.Etag, + ClusterResourceSetBindings: make([]string, len(capvcd.Status.Capvcd.ClusterResourceSetBindings)), + State: capvcd.Status.VcdKe.State, + Events: make([]CseClusterEvent, 0), + client: rde.client, + capvcdType: capvcd, + supportedUpgrades: make([]*types.VAppTemplate, 0), + } + + // Add all events to the resulting cluster + for _, s := range capvcd.Status.VcdKe.EventSet { + result.Events = append(result.Events, CseClusterEvent{ + Name: s.Name, + Type: "event", + ResourceId: s.VcdResourceId, + ResourceName: s.VcdResourceName, + OccurredAt: s.OccurredAt, + Details: s.AdditionalDetails.DetailedEvent, + }) + } + for _, s := range capvcd.Status.VcdKe.ErrorSet { + result.Events = append(result.Events, CseClusterEvent{ + Name: s.Name, + Type: "error", + ResourceId: s.VcdResourceId, + ResourceName: s.VcdResourceName, + OccurredAt: s.OccurredAt, + Details: s.AdditionalDetails.DetailedError, + }) + } + for _, s := range capvcd.Status.Capvcd.EventSet { + result.Events = append(result.Events, CseClusterEvent{ + Name: s.Name, + Type: "event", + ResourceId: s.VcdResourceId, + ResourceName: s.VcdResourceName, + OccurredAt: s.OccurredAt, + Details: s.Name, + }) + } + for _, s := range capvcd.Status.Capvcd.ErrorSet { + result.Events = append(result.Events, CseClusterEvent{ + Name: s.Name, + Type: "error", + ResourceId: s.VcdResourceId, + ResourceName: s.VcdResourceName, + OccurredAt: s.OccurredAt, + Details: s.AdditionalDetails.DetailedError, + }) + } + for _, s := range capvcd.Status.Cpi.EventSet { + result.Events = append(result.Events, CseClusterEvent{ + Name: s.Name, + Type: "event", + ResourceId: s.VcdResourceId, + ResourceName: s.VcdResourceName, + OccurredAt: s.OccurredAt, + Details: s.Name, + }) + } + for _, s := range capvcd.Status.Cpi.ErrorSet { + result.Events = append(result.Events, CseClusterEvent{ + Name: s.Name, + Type: "error", + ResourceId: s.VcdResourceId, + ResourceName: s.VcdResourceName, + OccurredAt: s.OccurredAt, + Details: s.AdditionalDetails.DetailedError, + }) + } + for _, s := range capvcd.Status.Csi.EventSet { + result.Events = append(result.Events, CseClusterEvent{ + Name: s.Name, + Type: "event", + ResourceId: s.VcdResourceId, + ResourceName: s.VcdResourceName, + OccurredAt: s.OccurredAt, + Details: s.Name, + }) + } + for _, s := range capvcd.Status.Csi.ErrorSet { + result.Events = append(result.Events, CseClusterEvent{ + Name: s.Name, + Type: "error", + ResourceId: s.VcdResourceId, + ResourceName: s.VcdResourceName, + OccurredAt: s.OccurredAt, + Details: s.AdditionalDetails.DetailedError, + }) + } + for _, s := range capvcd.Status.Projector.EventSet { + result.Events = append(result.Events, CseClusterEvent{ + Name: s.Name, + Type: "event", + ResourceId: s.VcdResourceId, + ResourceName: s.VcdResourceName, + OccurredAt: s.OccurredAt, + Details: s.Name, + }) + } + for _, s := range capvcd.Status.Projector.ErrorSet { + result.Events = append(result.Events, CseClusterEvent{ + Name: s.Name, + Type: "error", + ResourceId: s.VcdResourceId, + ResourceName: s.VcdResourceName, + OccurredAt: s.OccurredAt, + Details: s.AdditionalDetails.DetailedError, + }) + } + sort.SliceStable(result.Events, func(i, j int) bool { + return result.Events[i].OccurredAt.After(result.Events[j].OccurredAt) + }) + + if capvcd.Status.Capvcd.CapvcdVersion != "" { + version, err := semver.NewVersion(capvcd.Status.Capvcd.CapvcdVersion) + if err != nil { + return nil, fmt.Errorf("could not read Capvcd version: %s", err) + } + result.CapvcdVersion = *version + } + + if capvcd.Status.Cpi.Version != "" { + version, err := semver.NewVersion(strings.TrimSpace(capvcd.Status.Cpi.Version)) // Note: We use trim as the version comes with spacing characters + if err != nil { + return nil, fmt.Errorf("could not read CPI version: %s", err) + } + result.CpiVersion = *version + } + + if capvcd.Status.Csi.Version != "" { + version, err := semver.NewVersion(capvcd.Status.Csi.Version) + if err != nil { + return nil, fmt.Errorf("could not read CSI version: %s", err) + } + result.CsiVersion = *version + } + + if capvcd.Status.VcdKe.VcdKeVersion != "" { + cseVersion, err := semver.NewVersion(capvcd.Status.VcdKe.VcdKeVersion) + if err != nil { + return nil, fmt.Errorf("could not read the CSE Version that the cluster uses: %s", err) + } + // Remove the possible version suffixes as we just want MAJOR.MINOR.PATCH + // TODO: This can be replaced with (*cseVersion).Core() in newer versions of the library + cseVersionSegs := (*cseVersion).Segments() + cseVersion, err = semver.NewVersion(fmt.Sprintf("%d.%d.%d", cseVersionSegs[0], cseVersionSegs[1], cseVersionSegs[2])) + if err != nil { + return nil, fmt.Errorf("could not read the CSE Version that the cluster uses: %s", err) + } + result.CseVersion = *cseVersion + } + + // Retrieve the Owner + if rde.DefinedEntity.Owner != nil { + result.Owner = rde.DefinedEntity.Owner.Name + } + + // Retrieve the Organization ID + for i, binding := range capvcd.Status.Capvcd.ClusterResourceSetBindings { + result.ClusterResourceSetBindings[i] = binding.Name + } + + if len(capvcd.Status.Capvcd.ClusterApiStatus.ApiEndpoints) > 0 { + result.ControlPlane.Ip = capvcd.Status.Capvcd.ClusterApiStatus.ApiEndpoints[0].Host + } + + if len(result.capvcdType.Status.Capvcd.VcdProperties.Organizations) > 0 { + result.OrganizationId = result.capvcdType.Status.Capvcd.VcdProperties.Organizations[0].Id + } + + // If the Org/VDC information is not set, we can't continue retrieving information for the cluster. + // This scenario is when the cluster is not correctly provisioned (Error state) + if len(result.capvcdType.Status.Capvcd.VcdProperties.OrgVdcs) == 0 { + return result, nil + } + + // NOTE: The code below, until the end of this function, requires the Org/VDC information + + // Retrieve the VDC ID + result.VdcId = result.capvcdType.Status.Capvcd.VcdProperties.OrgVdcs[0].Id + // FIXME: This is a workaround, because for some reason the OrgVdcs[*].Id property contains the VDC name instead of the VDC ID. + // Once this is fixed, this conditional should not be needed anymore. + if result.VdcId == result.capvcdType.Status.Capvcd.VcdProperties.OrgVdcs[0].Name { + vdcs, err := queryOrgVdcList(rde.client, map[string]string{}) + if err != nil { + return nil, fmt.Errorf("could not get VDC IDs as no VDC was found: %s", err) + } + found := false + for _, vdc := range vdcs { + if vdc.Name == result.capvcdType.Status.Capvcd.VcdProperties.OrgVdcs[0].Name { + result.VdcId = fmt.Sprintf("urn:vcloud:vdc:%s", extractUuid(vdc.HREF)) + found = true + break + } + } + if !found { + return nil, fmt.Errorf("could not get VDC IDs as no VDC with name '%s' was found", result.capvcdType.Status.Capvcd.VcdProperties.OrgVdcs[0].Name) + } + } + + // Retrieve the Network ID + params := url.Values{} + params.Add("filter", fmt.Sprintf("name==%s", result.capvcdType.Status.Capvcd.VcdProperties.OrgVdcs[0].OvdcNetworkName)) + params = queryParameterFilterAnd("orgVdc.id=="+result.VdcId, params) + params = queryParameterFilterAnd("_context==includeAccessible", params) + networks, err := getAllOpenApiOrgVdcNetworks(rde.client, params, nil) + if err != nil { + return nil, fmt.Errorf("could not read Org VDC Network from Capvcd type: %s", err) + } + if len(networks) != 1 { + return nil, fmt.Errorf("expected one Org VDC Network from Capvcd type, but got %d", len(networks)) + } + result.NetworkId = networks[0].OpenApiOrgVdcNetwork.ID + + // Here we retrieve several items that we need from now onwards, like Storage Profiles and Compute Policies + storageProfiles := map[string]string{} + if rde.client.IsSysAdmin { + allSp, err := queryAdminOrgVdcStorageProfilesByVdcId(rde.client, result.VdcId) + if err != nil { + return nil, fmt.Errorf("could not get all the Storage Profiles: %s", err) + } + for _, recordType := range allSp { + storageProfiles[recordType.Name] = fmt.Sprintf("urn:vcloud:vdcstorageProfile:%s", extractUuid(recordType.HREF)) + } + } else { + allSp, err := queryOrgVdcStorageProfilesByVdcId(rde.client, result.VdcId) + if err != nil { + return nil, fmt.Errorf("could not get all the Storage Profiles: %s", err) + } + for _, recordType := range allSp { + storageProfiles[recordType.Name] = fmt.Sprintf("urn:vcloud:vdcstorageProfile:%s", extractUuid(recordType.HREF)) + } + } + + computePolicies, err := getAllVdcComputePoliciesV2(rde.client, nil) + if err != nil { + return nil, fmt.Errorf("could not get all the Compute Policies: %s", err) + } + + if result.capvcdType.Spec.VcdKe.DefaultStorageClassOptions.K8SStorageClassName != "" { // This would mean there is a Default Storage Class defined + result.DefaultStorageClass = &CseDefaultStorageClassSettings{ + Name: result.capvcdType.Spec.VcdKe.DefaultStorageClassOptions.K8SStorageClassName, + ReclaimPolicy: "retain", + Filesystem: result.capvcdType.Spec.VcdKe.DefaultStorageClassOptions.Filesystem, + } + for spName, spId := range storageProfiles { + if spName == result.capvcdType.Spec.VcdKe.DefaultStorageClassOptions.VcdStorageProfileName { + result.DefaultStorageClass.StorageProfileId = spId + } + } + if result.capvcdType.Spec.VcdKe.DefaultStorageClassOptions.UseDeleteReclaimPolicy { + result.DefaultStorageClass.ReclaimPolicy = "delete" + } + } + + // NOTE: We get the remaining elements from the CAPI YAML, despite they are also inside capvcdType.Status. + // The reason is that any change on the cluster is immediately reflected in the CAPI YAML, but not in the capvcdType.Status + // elements, which may take more than 10 minutes to be refreshed. + yamlDocuments, err := unmarshalMultipleYamlDocuments(result.capvcdType.Spec.CapiYaml) + if err != nil { + return nil, err + } + + // We need a map of worker pools and not a slice, because there are two types of YAML documents + // that contain data about a specific worker pool (VCDMachineTemplate and MachineDeployment), and we can get them in no + // particular order, so we store the worker pools with their name as key. This way we can easily (O(1)) fetch and update them. + workerPools := map[string]CseWorkerPoolSettings{} + for _, yamlDocument := range yamlDocuments { + switch yamlDocument["kind"] { + case "KubeadmControlPlane": + result.ControlPlane.MachineCount = int(traverseMapAndGet[float64](yamlDocument, "spec.replicas", ".")) + users := traverseMapAndGet[[]interface{}](yamlDocument, "spec.kubeadmConfigSpec.users", ".") + if len(users) == 0 { + return nil, fmt.Errorf("expected 'spec.kubeadmConfigSpec.users' slice to not to be empty") + } + keys := traverseMapAndGet[[]interface{}](users[0], "sshAuthorizedKeys", ".") + if len(keys) > 0 { + result.SshPublicKey = keys[0].(string) // Optional field + } + + version, err := semver.NewVersion(traverseMapAndGet[string](yamlDocument, "spec.version", ".")) + if err != nil { + return nil, fmt.Errorf("could not read Kubernetes version: %s", err) + } + result.KubernetesVersion = *version + + case "VCDMachineTemplate": + name := traverseMapAndGet[string](yamlDocument, "metadata.name", ".") + sizingPolicyName := traverseMapAndGet[string](yamlDocument, "spec.template.spec.sizingPolicy", ".") + placementPolicyName := traverseMapAndGet[string](yamlDocument, "spec.template.spec.placementPolicy", ".") + storageProfileName := traverseMapAndGet[string](yamlDocument, "spec.template.spec.storageProfile", ".") + diskSizeGi, err := strconv.Atoi(strings.ReplaceAll(traverseMapAndGet[string](yamlDocument, "spec.template.spec.diskSize", "."), "Gi", "")) + if err != nil { + return nil, err + } + + if strings.Contains(name, "control-plane-node-pool") { + // This is the single Control Plane + for _, policy := range computePolicies { + if sizingPolicyName == policy.VdcComputePolicyV2.Name && policy.VdcComputePolicyV2.IsSizingOnly { + result.ControlPlane.SizingPolicyId = policy.VdcComputePolicyV2.ID + } else if placementPolicyName == policy.VdcComputePolicyV2.Name && !policy.VdcComputePolicyV2.IsSizingOnly { + result.ControlPlane.PlacementPolicyId = policy.VdcComputePolicyV2.ID + } + } + for spName, spId := range storageProfiles { + if storageProfileName == spName { + result.ControlPlane.StorageProfileId = spId + } + } + + result.ControlPlane.DiskSizeGi = diskSizeGi + + // We retrieve the Kubernetes Template OVA just once for the Control Plane because all YAML blocks share the same + vAppTemplateName := traverseMapAndGet[string](yamlDocument, "spec.template.spec.template", ".") + catalogName := traverseMapAndGet[string](yamlDocument, "spec.template.spec.catalog", ".") + vAppTemplates, err := queryVappTemplateListWithFilter(rde.client, map[string]string{ + "catalogName": catalogName, + "name": vAppTemplateName, + }) + if err != nil { + return nil, fmt.Errorf("could not find any vApp Template with name '%s' in Catalog '%s': %s", vAppTemplateName, catalogName, err) + } + if len(vAppTemplates) == 0 { + return nil, fmt.Errorf("could not find any vApp Template with name '%s' in Catalog '%s'", vAppTemplateName, catalogName) + } + // The records don't have ID set, so we calculate it + result.KubernetesTemplateOvaId = fmt.Sprintf("urn:vcloud:vapptemplate:%s", extractUuid(vAppTemplates[0].HREF)) + } else { + // This is one Worker Pool. We need to check the map of worker pools, just in case we already saved the + // machine count from MachineDeployment. + if _, ok := workerPools[name]; !ok { + workerPools[name] = CseWorkerPoolSettings{} + } + workerPool := workerPools[name] + workerPool.Name = name + for _, policy := range computePolicies { + if sizingPolicyName == policy.VdcComputePolicyV2.Name && policy.VdcComputePolicyV2.IsSizingOnly { + workerPool.SizingPolicyId = policy.VdcComputePolicyV2.ID + } else if placementPolicyName == policy.VdcComputePolicyV2.Name && !policy.VdcComputePolicyV2.IsSizingOnly && !policy.VdcComputePolicyV2.IsVgpuPolicy { + workerPool.PlacementPolicyId = policy.VdcComputePolicyV2.ID + } else if placementPolicyName == policy.VdcComputePolicyV2.Name && !policy.VdcComputePolicyV2.IsSizingOnly && policy.VdcComputePolicyV2.IsVgpuPolicy { + workerPool.VGpuPolicyId = policy.VdcComputePolicyV2.ID + } + } + for spName, spId := range storageProfiles { + if storageProfileName == spName { + workerPool.StorageProfileId = spId + } + } + workerPool.DiskSizeGi = diskSizeGi + workerPools[name] = workerPool // Override the worker pool with the updated data + } + case "MachineDeployment": + name := traverseMapAndGet[string](yamlDocument, "metadata.name", ".") + // This is one Worker Pool. We need to check the map of worker pools, just in case we already saved the + // other information from VCDMachineTemplate. + if _, ok := workerPools[name]; !ok { + workerPools[name] = CseWorkerPoolSettings{} + } + workerPool := workerPools[name] + + // This will be 0 if Autoscaler is enabled + workerPool.MachineCount = int(traverseMapAndGet[float64](yamlDocument, "spec.replicas", ".")) + + // Get Autoscaler values + autoscalerMax := traverseMapAndGet[string](yamlDocument, "metadata#annotations#cluster.x-k8s.io/cluster-api-autoscaler-node-group-max-size", "#") + autoscalerMin := traverseMapAndGet[string](yamlDocument, "metadata#annotations#cluster.x-k8s.io/cluster-api-autoscaler-node-group-min-size", "#") + if autoscalerMax != "" && autoscalerMin != "" { + maxSize, err := strconv.Atoi(autoscalerMax) + if err != nil { + return nil, fmt.Errorf("error reading Autoscaler max size for pool '%s': %s", name, err) + } + minSize, err := strconv.Atoi(autoscalerMin) + if err != nil { + return nil, fmt.Errorf("error reading Autoscaler min size for pool '%s': %s", name, err) + } + workerPool.Autoscaler = &CseWorkerPoolAutoscaler{ + MaxSize: maxSize, + MinSize: minSize, + } + } + workerPools[name] = workerPool // Override the worker pool with the updated data + case "VCDCluster": + result.VirtualIpSubnet = traverseMapAndGet[string](yamlDocument, "spec.loadBalancerConfigSpec.vipSubnet", ".") + case "Cluster": + version, err := semver.NewVersion(traverseMapAndGet[string](yamlDocument, "metadata.annotations.TKGVERSION", ".")) + if err != nil { + return nil, fmt.Errorf("could not read TKG version: %s", err) + } + result.TkgVersion = *version + + cidrBlocks := traverseMapAndGet[[]interface{}](yamlDocument, "spec.clusterNetwork.pods.cidrBlocks", ".") + if len(cidrBlocks) == 0 { + return nil, fmt.Errorf("expected at least one 'spec.clusterNetwork.pods.cidrBlocks' item") + } + result.PodCidr = cidrBlocks[0].(string) + + cidrBlocks = traverseMapAndGet[[]interface{}](yamlDocument, "spec.clusterNetwork.services.cidrBlocks", ".") + if len(cidrBlocks) == 0 { + return nil, fmt.Errorf("expected at least one 'spec.clusterNetwork.services.cidrBlocks' item") + } + result.ServiceCidr = cidrBlocks[0].(string) + case "MachineHealthCheck": + // This is quite simple, if we find this document, means that Machine Health Check is enabled + result.NodeHealthCheck = true + } + } + result.WorkerPools = make([]CseWorkerPoolSettings, len(workerPools)) + i := 0 + for _, workerPool := range workerPools { + result.WorkerPools[i] = workerPool + i++ + } + + return result, nil +} + +// waitUntilClusterIsProvisioned waits for the Kubernetes cluster to be in "provisioned" state, either indefinitely (if timeout = 0) +// or until the timeout is reached. +// If one of the states of the cluster at a given point is "error", this function also checks whether the cluster has the "AutoRepairOnErrors" flag enabled, +// so it keeps waiting if it's true. +// If timeout is reached before the cluster is in "provisioned" state, it returns an error. +func waitUntilClusterIsProvisioned(client *Client, clusterId string, timeout time.Duration) error { + var elapsed time.Duration + sleepTime := 10 + + start := time.Now() + capvcd := &types.Capvcd{} + for elapsed <= timeout || timeout == 0 { // If the user specifies timeout=0, we wait forever + rde, err := getRdeById(client, clusterId) + if err != nil { + return err + } + + // Here we don't use cseConvertToCseKubernetesClusterType to avoid calling VCD. We only need the state. + entityBytes, err := json.Marshal(rde.DefinedEntity.Entity) + if err != nil { + return fmt.Errorf("could not check the Kubernetes cluster state: %s", err) + } + err = json.Unmarshal(entityBytes, &capvcd) + if err != nil { + return fmt.Errorf("could not check the Kubernetes cluster state: %s", err) + } + + switch capvcd.Status.VcdKe.State { + case "provisioned": + return nil + case "error": + // We just finish if auto-recovery is disabled, otherwise we just let CSE fixing things in background + if !capvcd.Spec.VcdKe.AutoRepairOnErrors { + // Give feedback about what went wrong + errors := "" + for _, event := range capvcd.Status.Capvcd.ErrorSet { + errors += fmt.Sprintf("%s,\n", event.AdditionalDetails.DetailedError) + } + return fmt.Errorf("got an error and 'AutoRepairOnErrors' is disabled, aborting. Error events:\n%s", errors) + } + } + + util.Logger.Printf("[DEBUG] Cluster '%s' is in '%s' state, will check again in %d seconds", rde.DefinedEntity.ID, capvcd.Status.VcdKe.State, sleepTime) + elapsed = time.Since(start) + time.Sleep(time.Duration(sleepTime) * time.Second) + } + return fmt.Errorf("timeout of %s reached, latest cluster state obtained was '%s'", timeout, capvcd.Status.VcdKe.State) +} + +// validate validates the receiver CseClusterSettings. Returns an error if any of the fields is empty or wrong. +func (input *CseClusterSettings) validate() error { + if input == nil { + return fmt.Errorf("the receiver CseClusterSettings cannot be nil") + } + // This regular expression is used to validate the constraints placed by Container Service Extension on the names + // of the components of the Kubernetes clusters: + // Names must contain only lowercase alphanumeric characters or '-', start with an alphabetic character, end with an alphanumeric, and contain at most 31 characters. + cseNamesRegex, err := regexp.Compile(`^[a-z](?:[a-z0-9-]{0,29}[a-z0-9])?$`) + if err != nil { + return fmt.Errorf("could not compile regular expression '%s'", err) + } + + _, err = getCseComponentsVersions(input.CseVersion) + if err != nil { + return err + } + if !cseNamesRegex.MatchString(input.Name) { + return fmt.Errorf("the name '%s' must contain only lowercase alphanumeric characters or '-', start with an alphabetic character, end with an alphanumeric, and contain at most 31 characters", input.Name) + } + if input.OrganizationId == "" { + return fmt.Errorf("the Organization ID is required") + } + if input.VdcId == "" { + return fmt.Errorf("the VDC ID is required") + } + if input.NetworkId == "" { + return fmt.Errorf("the Network ID is required") + } + if input.KubernetesTemplateOvaId == "" { + return fmt.Errorf("the Kubernetes Template OVA ID is required") + } + if input.ControlPlane.MachineCount < 1 || input.ControlPlane.MachineCount%2 == 0 { + return fmt.Errorf("number of Control Plane nodes must be odd and higher than 0, but it was '%d'", input.ControlPlane.MachineCount) + } + if input.ControlPlane.DiskSizeGi < 20 { + return fmt.Errorf("disk size for the Control Plane in Gibibytes (Gi) must be at least 20, but it was '%d'", input.ControlPlane.DiskSizeGi) + } + if len(input.WorkerPools) == 0 { + return fmt.Errorf("there must be at least one Worker Pool") + } + existingWorkerPools := map[string]bool{} + for _, workerPool := range input.WorkerPools { + if !cseNamesRegex.MatchString(workerPool.Name) { + return fmt.Errorf("the Worker Pool name '%s' must contain only lowercase alphanumeric characters or '-', start with an alphabetic character, end with an alphanumeric, and contain at most 31 characters", workerPool.Name) + } + if _, alreadyExists := existingWorkerPools[workerPool.Name]; alreadyExists { + return fmt.Errorf("the names of the Worker Pools must be unique, but '%s' is repeated", workerPool.Name) + } + if workerPool.Autoscaler == nil && workerPool.MachineCount < 1 { + return fmt.Errorf("number of Worker Pool '%s' nodes must higher than 0, but it was '%d'", workerPool.Name, workerPool.MachineCount) + } + if workerPool.Autoscaler != nil { + if workerPool.MachineCount > 0 { + return fmt.Errorf("the Worker Pool '%s' is using Autoscaler (min=%d,max=%d), so can't set MachineCount to '%d'", workerPool.Name, workerPool.Autoscaler.MinSize, workerPool.Autoscaler.MaxSize, workerPool.MachineCount) + } + if workerPool.Autoscaler.MaxSize < workerPool.Autoscaler.MinSize { + return fmt.Errorf("the Autoscaler maximum size for Worker Pool '%s' cannot be less than the minimum", workerPool.Name) + } + if workerPool.Autoscaler.MaxSize < 0 { + return fmt.Errorf("the Autoscaler maximum size for Worker Pool '%s' must be a positive number", workerPool.Name) + } + if workerPool.Autoscaler.MinSize < 0 { + return fmt.Errorf("the Autoscaler minimum size for Worker Pool '%s' must be a positive number", workerPool.Name) + } + } + if workerPool.DiskSizeGi < 20 { + return fmt.Errorf("disk size for the Worker Pool '%s' in Gibibytes (Gi) must be at least 20, but it was '%d'", workerPool.Name, workerPool.DiskSizeGi) + } + existingWorkerPools[workerPool.Name] = true + } + if input.DefaultStorageClass != nil { // This field is optional + if !cseNamesRegex.MatchString(input.DefaultStorageClass.Name) { + return fmt.Errorf("the Default Storage Class name '%s' must contain only lowercase alphanumeric characters or '-', start with an alphabetic character, end with an alphanumeric, and contain at most 31 characters", input.DefaultStorageClass.Name) + } + if input.DefaultStorageClass.StorageProfileId == "" { + return fmt.Errorf("the Storage Profile ID for the Default Storage Class is required") + } + if input.DefaultStorageClass.ReclaimPolicy != "delete" && input.DefaultStorageClass.ReclaimPolicy != "retain" { + return fmt.Errorf("the Reclaim Policy for the Default Storage Class must be either 'delete' or 'retain', but it was '%s'", input.DefaultStorageClass.ReclaimPolicy) + } + if input.DefaultStorageClass.Filesystem != "ext4" && input.DefaultStorageClass.Filesystem != "xfs" { + return fmt.Errorf("the filesystem for the Default Storage Class must be either 'ext4' or 'xfs', but it was '%s'", input.DefaultStorageClass.Filesystem) + } + } + if input.ApiToken == "" { + return fmt.Errorf("the API token is required") + } + if input.PodCidr == "" { + return fmt.Errorf("the Pod CIDR is required") + } + if _, _, err := net.ParseCIDR(input.PodCidr); err != nil { + return fmt.Errorf("the Pod CIDR is malformed: %s", err) + } + if input.ServiceCidr == "" { + return fmt.Errorf("the Service CIDR is required") + } + if _, _, err := net.ParseCIDR(input.ServiceCidr); err != nil { + return fmt.Errorf("the Service CIDR is malformed: %s", err) + } + if input.VirtualIpSubnet != "" { + if _, _, err := net.ParseCIDR(input.VirtualIpSubnet); err != nil { + return fmt.Errorf("the Virtual IP Subnet is malformed: %s", err) + } + } + if input.ControlPlane.Ip != "" { + if r := net.ParseIP(input.ControlPlane.Ip); r == nil { + return fmt.Errorf("the Control Plane IP is malformed: %s", input.ControlPlane.Ip) + } + } + return nil +} + +// toCseClusterSettingsInternal transforms user input data (CseClusterSettings) into the final payload that +// will be used to define a Container Service Extension Kubernetes cluster (cseClusterSettingsInternal). +// +// For example, the most relevant transformation is the change of the item IDs that are present in CseClusterSettings +// (such as CseClusterSettings.KubernetesTemplateOvaId) to their corresponding Names (e.g. cseClusterSettingsInternal.KubernetesTemplateOvaName), +// which are the identifiers that Container Service Extension uses internally. +func (input *CseClusterSettings) toCseClusterSettingsInternal(org Org) (*cseClusterSettingsInternal, error) { + err := input.validate() + if err != nil { + return nil, err + } + + output := &cseClusterSettingsInternal{} + if org.Org == nil { + return nil, fmt.Errorf("could not retrieve the Organization, it is nil") + } + output.OrganizationName = org.Org.Name + + vdc, err := org.GetVDCById(input.VdcId, true) + if err != nil { + return nil, fmt.Errorf("could not retrieve the VDC with ID '%s': %s", input.VdcId, err) + } + output.VdcName = vdc.Vdc.Name + + vAppTemplate, err := getVAppTemplateById(org.client, input.KubernetesTemplateOvaId) + if err != nil { + return nil, fmt.Errorf("could not retrieve the Kubernetes Template OVA with ID '%s': %s", input.KubernetesTemplateOvaId, err) + } + output.KubernetesTemplateOvaName = vAppTemplate.VAppTemplate.Name + + tkgVersions, err := getTkgVersionBundleFromVAppTemplate(vAppTemplate.VAppTemplate) + if err != nil { + return nil, fmt.Errorf("could not retrieve the required information from the Kubernetes Template OVA: %s", err) + } + output.TkgVersionBundle = tkgVersions + + catalogName, err := vAppTemplate.GetCatalogName() + if err != nil { + return nil, fmt.Errorf("could not retrieve the Catalog name where the the Kubernetes Template OVA '%s' (%s) is hosted: %s", input.KubernetesTemplateOvaId, vAppTemplate.VAppTemplate.Name, err) + } + output.CatalogName = catalogName + + network, err := vdc.GetOrgVdcNetworkById(input.NetworkId, true) + if err != nil { + return nil, fmt.Errorf("could not retrieve the Org VDC Network with ID '%s': %s", input.NetworkId, err) + } + output.NetworkName = network.OrgVDCNetwork.Name + + cseComponentsVersions, err := getCseComponentsVersions(input.CseVersion) + if err != nil { + return nil, err + } + rdeType, err := getRdeType(org.client, cseKubernetesClusterVendor, cseKubernetesClusterNamespace, cseComponentsVersions.CapvcdRdeTypeVersion) + if err != nil { + return nil, err + } + output.RdeType = rdeType.DefinedEntityType + + // Gather all the IDs of the Compute Policies and Storage Profiles, so we can transform them to Names in bulk. + var computePolicyIds []string + var storageProfileIds []string + for _, w := range input.WorkerPools { + computePolicyIds = append(computePolicyIds, w.SizingPolicyId, w.PlacementPolicyId, w.VGpuPolicyId) + storageProfileIds = append(storageProfileIds, w.StorageProfileId) + } + computePolicyIds = append(computePolicyIds, input.ControlPlane.SizingPolicyId, input.ControlPlane.PlacementPolicyId) + storageProfileIds = append(storageProfileIds, input.ControlPlane.StorageProfileId) + if input.DefaultStorageClass != nil { + storageProfileIds = append(storageProfileIds, input.DefaultStorageClass.StorageProfileId) + } + + idToNameCache, err := idToNames(org.client, computePolicyIds, storageProfileIds) + if err != nil { + return nil, err + } + + // Now that everything is cached in memory, we can build the Node pools and Storage Class payloads in a trivial way. + output.WorkerPools = make([]cseWorkerPoolSettingsInternal, len(input.WorkerPools)) + for i, w := range input.WorkerPools { + output.WorkerPools[i] = cseWorkerPoolSettingsInternal{ + Name: w.Name, + MachineCount: w.MachineCount, + DiskSizeGi: w.DiskSizeGi, + } + output.WorkerPools[i].SizingPolicyName = idToNameCache[w.SizingPolicyId] + output.WorkerPools[i].PlacementPolicyName = idToNameCache[w.PlacementPolicyId] + output.WorkerPools[i].VGpuPolicyName = idToNameCache[w.VGpuPolicyId] + output.WorkerPools[i].StorageProfileName = idToNameCache[w.StorageProfileId] + + if w.Autoscaler != nil { + output.WorkerPools[i].Autoscaler = &CseWorkerPoolAutoscaler{ + MaxSize: w.Autoscaler.MaxSize, + MinSize: w.Autoscaler.MinSize, + } + } + } + output.ControlPlane = cseControlPlaneSettingsInternal{ + MachineCount: input.ControlPlane.MachineCount, + DiskSizeGi: input.ControlPlane.DiskSizeGi, + SizingPolicyName: idToNameCache[input.ControlPlane.SizingPolicyId], + PlacementPolicyName: idToNameCache[input.ControlPlane.PlacementPolicyId], + StorageProfileName: idToNameCache[input.ControlPlane.StorageProfileId], + Ip: input.ControlPlane.Ip, + } + + if input.DefaultStorageClass != nil { + output.DefaultStorageClass = cseDefaultStorageClassInternal{ + StorageProfileName: idToNameCache[input.DefaultStorageClass.StorageProfileId], + Name: input.DefaultStorageClass.Name, + Filesystem: input.DefaultStorageClass.Filesystem, + } + output.DefaultStorageClass.UseDeleteReclaimPolicy = false + if input.DefaultStorageClass.ReclaimPolicy == "delete" { + output.DefaultStorageClass.UseDeleteReclaimPolicy = true + } + } + + vcdKeConfig, err := getVcdKeConfig(org.client, cseComponentsVersions.VcdKeConfigRdeTypeVersion, input.NodeHealthCheck) + if err != nil { + return nil, err + } + output.VcdKeConfig = vcdKeConfig + + output.Owner = input.Owner + if input.Owner == "" { + sessionInfo, err := org.client.GetSessionInfo() + if err != nil { + return nil, fmt.Errorf("error getting the Owner: %s", err) + } + output.Owner = sessionInfo.User.Name + } + + output.VcdUrl = strings.Replace(org.client.VCDHREF.String(), "/api", "", 1) + + // These don't change, don't need mapping + output.ApiToken = input.ApiToken + output.AutoRepairOnErrors = input.AutoRepairOnErrors + output.CseVersion = input.CseVersion + output.Name = input.Name + output.PodCidr = input.PodCidr + output.ServiceCidr = input.ServiceCidr + output.SshPublicKey = input.SshPublicKey + output.VirtualIpSubnet = input.VirtualIpSubnet + + return output, nil +} + +// getTkgVersionBundleFromVAppTemplate returns a tkgVersionBundle with the details of +// all the Kubernetes cluster components versions given a valid Kubernetes Template OVA. +// If it is not a valid Kubernetes Template OVA, returns an error. +func getTkgVersionBundleFromVAppTemplate(template *types.VAppTemplate) (tkgVersionBundle, error) { + result := tkgVersionBundle{} + if template == nil { + return result, fmt.Errorf("the Kubernetes Template OVA is nil") + } + if template.Children == nil || len(template.Children.VM) == 0 { + return result, fmt.Errorf("the Kubernetes Template OVA '%s' doesn't have any child VM", template.Name) + } + if template.Children.VM[0].ProductSection == nil { + return result, fmt.Errorf("the Product section of the Kubernetes Template OVA '%s' is empty, can't proceed", template.Name) + } + id := "" + for _, prop := range template.Children.VM[0].ProductSection.Property { + if prop != nil && prop.Key == "VERSION" { + id = prop.DefaultValue // Use DefaultValue and not Value as the value we want is in the "value" attr + } + } + if id == "" { + return result, fmt.Errorf("could not find any VERSION property inside the Kubernetes Template OVA '%s' Product section", template.Name) + } + + tkgVersionsMap := "cse/tkg_versions.json" + cseTkgVersionsJson, err := cseFiles.ReadFile(tkgVersionsMap) + if err != nil { + return result, fmt.Errorf("failed reading %s: %s", tkgVersionsMap, err) + } + + versionsMap := map[string]interface{}{} + err = json.Unmarshal(cseTkgVersionsJson, &versionsMap) + if err != nil { + return result, fmt.Errorf("failed unmarshalling %s: %s", tkgVersionsMap, err) + } + versionMap, ok := versionsMap[id] + if !ok { + return result, fmt.Errorf("the Kubernetes Template OVA '%s' is not supported", template.Name) + } + + // We don't need to check the Split result because the map checking above guarantees that the ID is well-formed. + idParts := strings.Split(id, "-") + result.KubernetesVersion = idParts[0] + result.TkrVersion = versionMap.(map[string]interface{})["tkr"].(string) + result.TkgVersion = versionMap.(map[string]interface{})["tkg"].(string) + result.EtcdVersion = versionMap.(map[string]interface{})["etcd"].(string) + result.CoreDnsVersion = versionMap.(map[string]interface{})["coreDns"].(string) + return result, nil +} + +// compareTkgVersion returns -1, 0 or 1 if the receiver TKG version is less than, equal or higher to the input TKG version. +// If they cannot be compared it returns -2. +func (tkgVersions tkgVersionBundle) compareTkgVersion(tkgVersion string) int { + receiverVersion, err := semver.NewVersion(tkgVersions.TkgVersion) + if err != nil { + return -2 + } + inputVersion, err := semver.NewVersion(tkgVersion) + if err != nil { + return -2 + } + return receiverVersion.Compare(inputVersion) +} + +// kubernetesVersionIsUpgradeableFrom returns true either if the receiver Kubernetes version is exactly one minor version higher +// than the given input version (being the minor digit the 'Y' in 'X.Y.Z') or if the minor is the same, but the patch is higher +// (being the minor digit the 'Z' in 'X.Y.Z'). +// Any malformed version returns false. +// Examples: +// * "1.19.2".kubernetesVersionIsUpgradeableFrom("1.18.7") = true +// * "1.19.2".kubernetesVersionIsUpgradeableFrom("1.19.2") = false +// * "1.19.2".kubernetesVersionIsUpgradeableFrom("1.19.0") = true +// * "1.19.10".kubernetesVersionIsUpgradeableFrom("1.18.0") = true +// * "1.20.2".kubernetesVersionIsUpgradeableFrom("1.18.7") = false +// * "1.21.2".kubernetesVersionIsUpgradeableFrom("1.18.7") = false +// * "1.18.0".kubernetesVersionIsUpgradeableFrom("1.18.7") = false +func (tkgVersions tkgVersionBundle) kubernetesVersionIsUpgradeableFrom(kubernetesVersion string) bool { + upgradeToVersion, err := semver.NewVersion(tkgVersions.KubernetesVersion) + if err != nil { + return false + } + fromVersion, err := semver.NewVersion(kubernetesVersion) + if err != nil { + return false + } + + if upgradeToVersion.Equal(fromVersion) { + return false + } + + upgradeToVersionSegments := upgradeToVersion.Segments() + if len(upgradeToVersionSegments) < 2 { + return false + } + fromVersionSegments := fromVersion.Segments() + if len(fromVersionSegments) < 2 { + return false + } + + majorIsEqual := upgradeToVersionSegments[0] == fromVersionSegments[0] + minorIsJustOneHigher := upgradeToVersionSegments[1]-1 == fromVersionSegments[1] + minorIsEqual := upgradeToVersionSegments[1] == fromVersionSegments[1] + patchIsHigher := upgradeToVersionSegments[2] > fromVersionSegments[2] + + return majorIsEqual && (minorIsJustOneHigher || (minorIsEqual && patchIsHigher)) +} + +// getVcdKeConfig gets the required information from the CSE Server configuration RDE (VCDKEConfig), such as the +// Machine Health Check settings and the Container Registry URL. +func getVcdKeConfig(client *Client, vcdKeConfigVersion string, retrieveMachineHealtchCheckInfo bool) (vcdKeConfig, error) { + result := vcdKeConfig{} + rdes, err := getRdesByName(client, "vmware", "VCDKEConfig", vcdKeConfigVersion, "vcdKeConfig") + if err != nil { + return result, err + } + if len(rdes) != 1 { + return result, fmt.Errorf("expected exactly one VCDKEConfig RDE with version '%s', but got %d", vcdKeConfigVersion, len(rdes)) + } + + profiles, ok := rdes[0].DefinedEntity.Entity["profiles"].([]interface{}) + if !ok { + return result, fmt.Errorf("wrong format of VCDKEConfig RDE contents, expected a 'profiles' array") + } + if len(profiles) == 0 { + return result, fmt.Errorf("wrong format of VCDKEConfig RDE contents, expected a non-empty 'profiles' element") + } + + // We append /tkg as required, even in air-gapped environments: + // https://docs.vmware.com/en/VMware-Cloud-Director-Container-Service-Extension/4.2/VMware-Cloud-Director-Container-Service-Extension-Install-provider-4.2/GUID-B5C19221-2ECA-4DCD-8EA1-8E391F6217C1.html + result.ContainerRegistryUrl = fmt.Sprintf("%s/tkg", profiles[0].(map[string]interface{})["containerRegistryUrl"]) + + k8sConfig, ok := profiles[0].(map[string]interface{})["K8Config"].(map[string]interface{}) + if !ok { + return result, fmt.Errorf("wrong format of VCDKEConfig RDE contents, expected a 'K8Config' object") + } + certificates, ok := k8sConfig["certificateAuthorities"] + if ok { + result.Base64Certificates = make([]string, len(certificates.([]interface{}))) + for i, certificate := range certificates.([]interface{}) { + result.Base64Certificates[i] = base64.StdEncoding.EncodeToString([]byte(certificate.(string))) + } + } + + if retrieveMachineHealtchCheckInfo { + mhc, ok := profiles[0].(map[string]interface{})["K8Config"].(map[string]interface{})["mhc"] + if !ok { + // If there is no "mhc" entry in the VCDKEConfig JSON, we skip setting this part of the Kubernetes cluster configuration + return result, nil + } + result.MaxUnhealthyNodesPercentage = mhc.(map[string]interface{})["maxUnhealthyNodes"].(float64) + result.NodeStartupTimeout = mhc.(map[string]interface{})["nodeStartupTimeout"].(string) + result.NodeNotReadyTimeout = mhc.(map[string]interface{})["nodeUnknownTimeout"].(string) + result.NodeUnknownTimeout = mhc.(map[string]interface{})["nodeNotReadyTimeout"].(string) + } + + return result, nil +} + +// idToNames returns a map that associates Compute Policies/Storage Profiles IDs with their respective names. +// This is useful as the input to create/update a cluster uses different entities IDs, but CSE cluster creation/update process uses Names. +// For that reason, we need to transform IDs to Names by querying VCD +func idToNames(client *Client, computePolicyIds, storageProfileIds []string) (map[string]string, error) { + result := map[string]string{ + "": "", // Default empty value to map optional values that were not set, to avoid extra checks. For example, an empty vGPU Policy. + } + // Retrieve the Compute Policies and Storage Profiles names and put them in the resulting map. This map also can + // be used to reduce the calls to VCD. The URN format used by VCD guarantees that IDs are unique, so there is no possibility of clashes here. + for _, id := range storageProfileIds { + if _, alreadyPresent := result[id]; !alreadyPresent { + storageProfile, err := getStorageProfileById(client, id) + if err != nil { + return nil, fmt.Errorf("could not retrieve Storage Profile with ID '%s': %s", id, err) + } + result[id] = storageProfile.Name + } + } + for _, id := range computePolicyIds { + if _, alreadyPresent := result[id]; !alreadyPresent { + computePolicy, err := getVdcComputePolicyV2ById(client, id) + if err != nil { + return nil, fmt.Errorf("could not retrieve Compute Policy with ID '%s': %s", id, err) + } + result[id] = computePolicy.VdcComputePolicyV2.Name + } + } + return result, nil +} + +// getCseTemplate reads the Go template present in the embedded cseFiles filesystem. +func getCseTemplate(cseVersion semver.Version, templateName string) (string, error) { + minimumVersion, err := semver.NewVersion("4.1") + if err != nil { + return "", err + } + if cseVersion.LessThan(minimumVersion) { + return "", fmt.Errorf("the Container Service minimum version is '%s'", minimumVersion.String()) + } + versionSegments := cseVersion.Segments() + // We try with major.minor.patch + fullTemplatePath := fmt.Sprintf("cse/%d.%d.%d/%s.tmpl", versionSegments[0], versionSegments[1], versionSegments[2], templateName) + result, err := cseFiles.ReadFile(fullTemplatePath) + if err != nil { + // We try now just with major.minor + fullTemplatePath = fmt.Sprintf("cse/%d.%d/%s.tmpl", versionSegments[0], versionSegments[1], templateName) + result, err = cseFiles.ReadFile(fullTemplatePath) + if err != nil { + return "", fmt.Errorf("could not read Go template '%s.tmpl' for CSE version %s", templateName, cseVersion.String()) + } + } + return string(result), nil +} diff --git a/govcd/cse_util_unit_test.go b/govcd/cse_util_unit_test.go new file mode 100644 index 000000000..abbeed63b --- /dev/null +++ b/govcd/cse_util_unit_test.go @@ -0,0 +1,362 @@ +//go:build unit || ALL + +/* + * Copyright 2024 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + semver "github.com/hashicorp/go-version" + "github.com/vmware/go-vcloud-director/v2/types/v56" + "reflect" + "testing" +) + +func Test_getCseComponentsVersions(t *testing.T) { + tests := []struct { + name string + cseVersion string + want *cseComponentsVersions + wantErr bool + }{ + { + name: "CSE 4.0 is not supported", + cseVersion: "4.0", + wantErr: true, + }, + { + name: "CSE 4.1 is supported", + cseVersion: "4.1", + want: &cseComponentsVersions{ + VcdKeConfigRdeTypeVersion: "1.1.0", + CapvcdRdeTypeVersion: "1.2.0", + CseInterfaceVersion: "1.0.0", + }, + wantErr: false, + }, + { + name: "CSE 4.1.1 is supported", + cseVersion: "4.1.1", + want: &cseComponentsVersions{ + VcdKeConfigRdeTypeVersion: "1.1.0", + CapvcdRdeTypeVersion: "1.2.0", + CseInterfaceVersion: "1.0.0", + }, + wantErr: false, + }, + { + name: "CSE 4.1.1a is equivalent to 4.1.1", + cseVersion: "4.1.1a", + want: &cseComponentsVersions{ + VcdKeConfigRdeTypeVersion: "1.1.0", + CapvcdRdeTypeVersion: "1.2.0", + CseInterfaceVersion: "1.0.0", + }, + wantErr: false, + }, + { + name: "CSE 4.2 is supported", + cseVersion: "4.2", + want: &cseComponentsVersions{ + VcdKeConfigRdeTypeVersion: "1.1.0", + CapvcdRdeTypeVersion: "1.3.0", + CseInterfaceVersion: "1.0.0", + }, + wantErr: false, + }, + { + name: "CSE 4.3 is not supported", + cseVersion: "4.3", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + version, err := semver.NewVersion(tt.cseVersion) + if err != nil { + t.Fatalf("could not parse test version: %s", err) + } + got, err := getCseComponentsVersions(*version) + if (err != nil) != tt.wantErr { + t.Errorf("getCseComponentsVersions() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("getCseComponentsVersions() got = %v, want %v", got, tt.want) + } + }) + } +} + +// Test_getTkgVersionBundleFromVAppTemplate tests getTkgVersionBundleFromVAppTemplate function +func Test_getTkgVersionBundleFromVAppTemplate(t *testing.T) { + tests := []struct { + name string + kubernetesTemplateOva *types.VAppTemplate + want tkgVersionBundle + wantErr string + }{ + { + name: "ova is nil", + kubernetesTemplateOva: nil, + wantErr: "the Kubernetes Template OVA is nil", + }, + { + name: "ova without children", + kubernetesTemplateOva: &types.VAppTemplate{ + Name: "dummy", + Children: nil, + }, + wantErr: "the Kubernetes Template OVA 'dummy' doesn't have any child VM", + }, + { + name: "ova with nil children", + kubernetesTemplateOva: &types.VAppTemplate{ + Name: "dummy", + Children: &types.VAppTemplateChildren{VM: nil}, + }, + wantErr: "the Kubernetes Template OVA 'dummy' doesn't have any child VM", + }, + { + name: "ova with nil product section", + kubernetesTemplateOva: &types.VAppTemplate{ + Name: "dummy", + Children: &types.VAppTemplateChildren{VM: []*types.VAppTemplate{ + { + ProductSection: nil, + }, + }}, + }, + wantErr: "the Product section of the Kubernetes Template OVA 'dummy' is empty, can't proceed", + }, + { + name: "ova with no version in the product section", + kubernetesTemplateOva: &types.VAppTemplate{ + Name: "dummy", + Children: &types.VAppTemplateChildren{VM: []*types.VAppTemplate{ + { + ProductSection: &types.ProductSection{ + Property: []*types.Property{ + { + Key: "foo", + DefaultValue: "bar", + }, + }, + }, + }, + }}, + }, + wantErr: "could not find any VERSION property inside the Kubernetes Template OVA 'dummy' Product section", + }, + { + name: "correct ova", + kubernetesTemplateOva: &types.VAppTemplate{ + Name: "dummy", + Children: &types.VAppTemplateChildren{VM: []*types.VAppTemplate{ + { + ProductSection: &types.ProductSection{ + Property: []*types.Property{ + { + Key: "VERSION", + DefaultValue: "v1.25.7+vmware.2-tkg.1-8a74b9f12e488c54605b3537acb683bc", + }, + }, + }, + }, + }}, + }, + want: tkgVersionBundle{ + EtcdVersion: "v3.5.6_vmware.9", + CoreDnsVersion: "v1.9.3_vmware.8", + TkgVersion: "v2.2.0", + TkrVersion: "v1.25.7---vmware.2-tkg.1", + KubernetesVersion: "v1.25.7+vmware.2", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := getTkgVersionBundleFromVAppTemplate(tt.kubernetesTemplateOva) + if err != nil { + if tt.wantErr != err.Error() { + t.Errorf("getTkgVersionBundleFromVAppTemplate() error = %v, wantErr = %v", err, tt.wantErr) + } + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("getTkgVersionBundleFromVAppTemplate() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_tkgVersionBundle_compareTkgVersion(t *testing.T) { + tests := []struct { + name string + receiverTkgVersion string + comparedTkgVersion string + want int + }{ + { + name: "same TKG version", + receiverTkgVersion: "v1.4.3", + comparedTkgVersion: "v1.4.3", + want: 0, + }, + { + name: "receiver TKG version is higher", + receiverTkgVersion: "v1.4.4", + comparedTkgVersion: "v1.4.3", + want: 1, + }, + { + name: "receiver TKG version is lower", + receiverTkgVersion: "v1.4.2", + comparedTkgVersion: "v1.4.3", + want: -1, + }, + { + name: "receiver TKG version is wrong", + receiverTkgVersion: "foo", + comparedTkgVersion: "v1.4.3", + want: -2, + }, + { + name: "compared TKG version is wrong", + receiverTkgVersion: "v1.4.3", + comparedTkgVersion: "foo", + want: -2, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tkgVersions := tkgVersionBundle{ + TkgVersion: tt.receiverTkgVersion, + } + if got := tkgVersions.compareTkgVersion(tt.comparedTkgVersion); got != tt.want { + t.Errorf("compareTkgVersion() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_tkgVersionBundle_kubernetesVersionIsUpgradeableFrom(t *testing.T) { + tests := []struct { + name string + upgradeToVersion string + fromVersion string + want bool + }{ + { + name: "same Kubernetes versions", + upgradeToVersion: "1.20.2+vmware.1", + fromVersion: "1.20.2+vmware.1", + want: false, + }, + { + name: "the Kubernetes patch is higher", + upgradeToVersion: "1.21.9+vmware.1", + fromVersion: "1.21.7+vmware.1", + want: true, + }, + { + name: "one Kubernetes minor higher", + upgradeToVersion: "1.21.9+vmware.1", + fromVersion: "1.20.2+vmware.1", + want: true, + }, + { + name: "the Kubernetes patch is lower", + upgradeToVersion: "1.20.0+vmware.1", + fromVersion: "1.20.7+vmware.1", + want: false, + }, + { + name: "one Kubernetes minor lower", + upgradeToVersion: "1.19.9+vmware.1", + fromVersion: "1.20.2+vmware.1", + want: false, + }, + { + name: "several Kubernetes minors higher", + upgradeToVersion: "1.22.9+vmware.1", + fromVersion: "1.20.2+vmware.1", + want: false, + }, + { + name: "wrong receiver Kubernetes version", + upgradeToVersion: "foo", + fromVersion: "1.20.2+vmware.1", + want: false, + }, + { + name: "wrong compared Kubernetes version", + upgradeToVersion: "1.20.2+vmware.1", + fromVersion: "foo", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tkgVersions := tkgVersionBundle{ + KubernetesVersion: tt.upgradeToVersion, + } + if got := tkgVersions.kubernetesVersionIsUpgradeableFrom(tt.fromVersion); got != tt.want { + t.Errorf("kubernetesVersionIsUpgradeableFrom() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_getCseTemplate(t *testing.T) { + v40, err := semver.NewVersion("4.0") + if err != nil { + t.Fatalf("could not create 4.0 version object") + } + v41, err := semver.NewVersion("4.1") + if err != nil { + t.Fatalf("could not create 4.1 version object") + } + v410, err := semver.NewVersion("4.1.0") + if err != nil { + t.Fatalf("could not create 4.1.0 version object") + } + v411, err := semver.NewVersion("4.1.1") + if err != nil { + t.Fatalf("could not create 4.1.1 version object") + } + v421, err := semver.NewVersion("4.2.1") + if err != nil { + t.Fatalf("could not create 4.2.1 version object") + } + + tmpl41, err := getCseTemplate(*v41, "rde") + if err != nil { + t.Fatal(err) + } + tmpl410, err := getCseTemplate(*v410, "rde") + if err != nil { + t.Fatal(err) + } + tmpl411, err := getCseTemplate(*v411, "rde") + if err != nil { + t.Fatal(err) + } + if tmpl41 == "" || tmpl41 != tmpl410 || tmpl41 != tmpl411 || tmpl410 != tmpl411 { + t.Fatalf("templates should be the same:\n4.1: %s\n4.1.0: %s\n4.1.1: %s", tmpl41, tmpl410, tmpl411) + } + + tmpl420, err := getCseTemplate(*v421, "rde") + if err != nil { + t.Fatal(err) + } + if tmpl420 == "" { + t.Fatalf("the obtained template for %s is empty", v421.String()) + } + + _, err = getCseTemplate(*v40, "rde") + if err == nil && err.Error() != "the Container Service minimum version is '4.1.0'" { + t.Fatalf("expected an error but got %s", err) + } +} diff --git a/govcd/cse_yaml.go b/govcd/cse_yaml.go new file mode 100644 index 000000000..a924c6f69 --- /dev/null +++ b/govcd/cse_yaml.go @@ -0,0 +1,521 @@ +package govcd + +import ( + "fmt" + semver "github.com/hashicorp/go-version" + "github.com/vmware/go-vcloud-director/v2/types/v56" + "sigs.k8s.io/yaml" + "strconv" + "strings" +) + +// updateCapiYaml takes a YAML and modifies its Kubernetes Template OVA, its Control plane, its Worker pools +// and its Node Health Check capabilities, by using the new values provided as input. +// If some of the values of the input is not provided, it doesn't change them. +// If none of the values is provided, it just returns the same untouched YAML. +func (cluster *CseKubernetesCluster) updateCapiYaml(input CseClusterUpdateInput) (string, error) { + if cluster == nil || cluster.capvcdType == nil { + return "", fmt.Errorf("receiver cluster is nil") + } + + if input.ControlPlane == nil && input.WorkerPools == nil && input.NodeHealthCheck == nil && input.KubernetesTemplateOvaId == nil && input.NewWorkerPools == nil { + return cluster.capvcdType.Spec.CapiYaml, nil + } + + // The YAML contains multiple documents, so we cannot use a simple yaml.Unmarshal() as this one just gets the first + // document it finds. + yamlDocs, err := unmarshalMultipleYamlDocuments(cluster.capvcdType.Spec.CapiYaml) + if err != nil { + return cluster.capvcdType.Spec.CapiYaml, fmt.Errorf("error unmarshalling YAML: %s", err) + } + + if input.ControlPlane != nil { + err := cseUpdateControlPlaneInYaml(yamlDocs, *input.ControlPlane) + if err != nil { + return cluster.capvcdType.Spec.CapiYaml, err + } + } + + // Modify or add the autoscaler capabilities + yamlDocs, err = cseUpdateAutoscalerInYaml(yamlDocs, cluster.Name, cluster.CseVersion, cluster.KubernetesVersion, input.WorkerPools, input.NewWorkerPools) + if err != nil { + return cluster.capvcdType.Spec.CapiYaml, err + } + + if input.WorkerPools != nil { + err := cseUpdateWorkerPoolsInYaml(yamlDocs, *input.WorkerPools) + if err != nil { + return cluster.capvcdType.Spec.CapiYaml, err + } + } + + // Order matters. We need to add the new pools before updating the Kubernetes template. + if input.NewWorkerPools != nil { + // Worker pool names must be unique + for _, existingPool := range cluster.WorkerPools { + for _, newPool := range *input.NewWorkerPools { + if newPool.Name == existingPool.Name { + return cluster.capvcdType.Spec.CapiYaml, fmt.Errorf("there is an existing Worker Pool with name '%s'", existingPool.Name) + } + } + } + + yamlDocs, err = cseAddWorkerPoolsInYaml(yamlDocs, *cluster, *input.NewWorkerPools) + if err != nil { + return cluster.capvcdType.Spec.CapiYaml, err + } + } + + // As a side note, we can't optimize this one with "if equals do nothing" because + // in order to retrieve the current value we would need to explore the YAML anyway, which is what we also need to do to update it. + // Also, even if we did it, the current value obtained from YAML would be a Name, but the new value is an ID, so we would need to query VCD anyway + // as well. + // So in this special case this "optimization" would optimize nothing. The same happens with other YAML values. + if input.KubernetesTemplateOvaId != nil { + vAppTemplate, err := getVAppTemplateById(cluster.client, *input.KubernetesTemplateOvaId) + if err != nil { + return cluster.capvcdType.Spec.CapiYaml, fmt.Errorf("could not retrieve the Kubernetes Template OVA with ID '%s': %s", *input.KubernetesTemplateOvaId, err) + } + // Check the versions of the selected OVA before upgrading + versions, err := getTkgVersionBundleFromVAppTemplate(vAppTemplate.VAppTemplate) + if err != nil { + return cluster.capvcdType.Spec.CapiYaml, fmt.Errorf("could not retrieve the TKG versions of OVA '%s': %s", *input.KubernetesTemplateOvaId, err) + } + if versions.compareTkgVersion(cluster.capvcdType.Status.Capvcd.Upgrade.Current.TkgVersion) < 0 || !versions.kubernetesVersionIsUpgradeableFrom(cluster.capvcdType.Status.Capvcd.Upgrade.Current.KubernetesVersion) { + return cluster.capvcdType.Spec.CapiYaml, fmt.Errorf("cannot perform an OVA change as the new one '%s' has an older TKG/Kubernetes version (%s/%s)", vAppTemplate.VAppTemplate.Name, versions.TkgVersion, versions.KubernetesVersion) + } + err = cseUpdateKubernetesTemplateInYaml(yamlDocs, vAppTemplate.VAppTemplate) + if err != nil { + return cluster.capvcdType.Spec.CapiYaml, err + } + } + + if input.NodeHealthCheck != nil { + cseComponentsVersions, err := getCseComponentsVersions(cluster.CseVersion) + if err != nil { + return "", err + } + vcdKeConfig, err := getVcdKeConfig(cluster.client, cseComponentsVersions.VcdKeConfigRdeTypeVersion, *input.NodeHealthCheck) + if err != nil { + return "", err + } + yamlDocs, err = cseUpdateNodeHealthCheckInYaml(yamlDocs, cluster.Name, cluster.CseVersion, vcdKeConfig) + if err != nil { + return "", err + } + } + + return marshalMultipleYamlDocuments(yamlDocs) +} + +// cseUpdateKubernetesTemplateInYaml modifies the given Kubernetes cluster YAML by modifying the Kubernetes Template OVA +// used by all the cluster elements. +// The caveat here is that not only VCDMachineTemplate needs to be changed with the new OVA name, but also +// other fields that reference the related Kubernetes version, TKG version and other derived information. +func cseUpdateKubernetesTemplateInYaml(yamlDocuments []map[string]interface{}, kubernetesTemplateOva *types.VAppTemplate) error { + tkgBundle, err := getTkgVersionBundleFromVAppTemplate(kubernetesTemplateOva) + if err != nil { + return err + } + for _, d := range yamlDocuments { + switch d["kind"] { + case "VCDMachineTemplate": + ok := traverseMapAndGet[string](d, "spec.template.spec.template", ".") != "" + if !ok { + return fmt.Errorf("the VCDMachineTemplate 'spec.template.spec.template' field is missing") + } + d["spec"].(map[string]interface{})["template"].(map[string]interface{})["spec"].(map[string]interface{})["template"] = kubernetesTemplateOva.Name + case "MachineDeployment": + ok := traverseMapAndGet[string](d, "spec.template.spec.version", ".") != "" + if !ok { + return fmt.Errorf("the MachineDeployment 'spec.template.spec.version' field is missing") + } + d["spec"].(map[string]interface{})["template"].(map[string]interface{})["spec"].(map[string]interface{})["version"] = tkgBundle.KubernetesVersion + case "Cluster": + ok := traverseMapAndGet[string](d, "metadata.annotations.TKGVERSION", ".") != "" + if !ok { + return fmt.Errorf("the Cluster 'metadata.annotations.TKGVERSION' field is missing") + } + d["metadata"].(map[string]interface{})["annotations"].(map[string]interface{})["TKGVERSION"] = tkgBundle.TkgVersion + ok = traverseMapAndGet[string](d, "metadata.labels.tanzuKubernetesRelease", ".") != "" + if !ok { + return fmt.Errorf("the Cluster 'metadata.labels.tanzuKubernetesRelease' field is missing") + } + d["metadata"].(map[string]interface{})["labels"].(map[string]interface{})["tanzuKubernetesRelease"] = tkgBundle.TkrVersion + case "KubeadmControlPlane": + ok := traverseMapAndGet[string](d, "spec.version", ".") != "" + if !ok { + return fmt.Errorf("the KubeadmControlPlane 'spec.version' field is missing") + } + d["spec"].(map[string]interface{})["version"] = tkgBundle.KubernetesVersion + ok = traverseMapAndGet[string](d, "spec.kubeadmConfigSpec.clusterConfiguration.dns.imageTag", ".") != "" + if !ok { + return fmt.Errorf("the KubeadmControlPlane 'spec.kubeadmConfigSpec.clusterConfiguration.dns.imageTag' field is missing") + } + d["spec"].(map[string]interface{})["kubeadmConfigSpec"].(map[string]interface{})["clusterConfiguration"].(map[string]interface{})["dns"].(map[string]interface{})["imageTag"] = tkgBundle.CoreDnsVersion + ok = traverseMapAndGet[string](d, "spec.kubeadmConfigSpec.clusterConfiguration.etcd.local.imageTag", ".") != "" + if !ok { + return fmt.Errorf("the KubeadmControlPlane 'spec.kubeadmConfigSpec.clusterConfiguration.etcd.local.imageTag' field is missing") + } + d["spec"].(map[string]interface{})["kubeadmConfigSpec"].(map[string]interface{})["clusterConfiguration"].(map[string]interface{})["etcd"].(map[string]interface{})["local"].(map[string]interface{})["imageTag"] = tkgBundle.EtcdVersion + case "Deployment": + // Update also the autoscaler version + deploymentName := traverseMapAndGet[string](d, "metadata.name", ".") + if deploymentName == "cluster-autoscaler" { + k8sVersion, err := semver.NewVersion(tkgBundle.KubernetesVersion) + if err != nil { + return err + } + k8sVersionSegments := k8sVersion.Segments() + d["spec"].(map[string]interface{})["template"].(map[string]interface{})["spec"].(map[string]interface{})["containers"].([]interface{})[0].(map[string]interface{})["image"] = fmt.Sprintf("k8s.gcr.io/autoscaling/cluster-autoscaler:v%d.%d.0", k8sVersionSegments[0], k8sVersionSegments[1]) + } + } + } + return nil +} + +// cseUpdateControlPlaneInYaml modifies the given Kubernetes cluster YAML contents by changing the Control Plane with the input parameters. +func cseUpdateControlPlaneInYaml(yamlDocuments []map[string]interface{}, input CseControlPlaneUpdateInput) error { + if input.MachineCount < 1 || input.MachineCount%2 == 0 { + return fmt.Errorf("incorrect machine count for Control Plane: %d. Should be at least 1 and an odd number", input.MachineCount) + } + + updated := false + for _, d := range yamlDocuments { + if d["kind"] != "KubeadmControlPlane" { + continue + } + d["spec"].(map[string]interface{})["replicas"] = float64(input.MachineCount) // As it was originally unmarshalled as a float64 + updated = true + } + if !updated { + return fmt.Errorf("could not find the KubeadmControlPlane object in the YAML") + } + return nil +} + +// cseUpdateControlPlaneInYaml modifies the given Kubernetes cluster YAML contents by changing +// the existing Worker Pools with the input parameters. +func cseUpdateWorkerPoolsInYaml(yamlDocuments []map[string]interface{}, workerPools map[string]CseWorkerPoolUpdateInput) error { + updated := 0 + + for _, d := range yamlDocuments { + if d["kind"] != "MachineDeployment" { + continue + } + + workerPoolName := traverseMapAndGet[string](d, "metadata.name", ".") + if workerPoolName == "" { + return fmt.Errorf("the MachineDeployment 'metadata.name' field is empty") + } + + workerPoolToUpdate := "" + for wpName := range workerPools { + if wpName == workerPoolName { + workerPoolToUpdate = wpName + } + } + // This worker pool must not be updated as it is not present in the input, continue searching for the ones we want + if workerPoolToUpdate == "" { + continue + } + + if workerPools[workerPoolToUpdate].Autoscaler != nil { + if workerPools[workerPoolToUpdate].Autoscaler.MinSize > workerPools[workerPoolToUpdate].Autoscaler.MaxSize { + return fmt.Errorf("incorrect MinSize for worker pool %s: %d should be less than the maximum %d", workerPoolToUpdate, workerPools[workerPoolToUpdate].Autoscaler.MinSize, workerPools[workerPoolToUpdate].Autoscaler.MaxSize) + } + if d["metadata"].(map[string]interface{})["annotations"] == nil { + d["metadata"].(map[string]interface{})["annotations"] = map[string]interface{}{} + } + d["metadata"].(map[string]interface{})["annotations"].(map[string]interface{})["cluster.x-k8s.io/cluster-api-autoscaler-node-group-max-size"] = strconv.Itoa(workerPools[workerPoolToUpdate].Autoscaler.MaxSize) + d["metadata"].(map[string]interface{})["annotations"].(map[string]interface{})["cluster.x-k8s.io/cluster-api-autoscaler-node-group-min-size"] = strconv.Itoa(workerPools[workerPoolToUpdate].Autoscaler.MinSize) + delete(d["spec"].(map[string]interface{}), "replicas") // This is required to avoid conflicts with Autoscaler + } else { + if workerPools[workerPoolToUpdate].MachineCount < 0 { + return fmt.Errorf("incorrect machine count for worker pool %s: %d. Should be at least 0", workerPoolToUpdate, workerPools[workerPoolToUpdate].MachineCount) + } + d["spec"].(map[string]interface{})["replicas"] = float64(workerPools[workerPoolToUpdate].MachineCount) // As it was originally unmarshalled as a float64 + + // Removes the autoscaler information, as we used static replicas + if d["metadata"].(map[string]interface{})["annotations"] != nil { + delete(d["metadata"].(map[string]interface{})["annotations"].(map[string]interface{}), "cluster.x-k8s.io/cluster-api-autoscaler-node-group-max-size") + delete(d["metadata"].(map[string]interface{})["annotations"].(map[string]interface{}), "cluster.x-k8s.io/cluster-api-autoscaler-node-group-min-size") + } + } + updated++ + } + if updated != len(workerPools) { + return fmt.Errorf("could not update all the Node pools. Updated %d, expected %d", updated, len(workerPools)) + } + return nil +} + +// cseAddWorkerPoolsInYaml modifies the given Kubernetes cluster YAML contents by adding new Worker Pools +// described by the input parameters. +// NOTE: This function doesn't modify the input, but returns a copy of the YAML with the added unmarshalled documents. +func cseAddWorkerPoolsInYaml(docs []map[string]interface{}, cluster CseKubernetesCluster, newWorkerPools []CseWorkerPoolSettings) ([]map[string]interface{}, error) { + if len(newWorkerPools) == 0 { + return docs, nil + } + + var computePolicyIds []string + var storageProfileIds []string + for _, w := range newWorkerPools { + computePolicyIds = append(computePolicyIds, w.SizingPolicyId, w.PlacementPolicyId, w.VGpuPolicyId) + storageProfileIds = append(storageProfileIds, w.StorageProfileId) + } + + idToNameCache, err := idToNames(cluster.client, computePolicyIds, storageProfileIds) + if err != nil { + return nil, err + } + + internalSettings := cseClusterSettingsInternal{WorkerPools: make([]cseWorkerPoolSettingsInternal, len(newWorkerPools))} + for i, workerPool := range newWorkerPools { + internalSettings.WorkerPools[i] = cseWorkerPoolSettingsInternal{ + Name: workerPool.Name, + MachineCount: workerPool.MachineCount, + DiskSizeGi: workerPool.DiskSizeGi, + StorageProfileName: idToNameCache[workerPool.StorageProfileId], + SizingPolicyName: idToNameCache[workerPool.SizingPolicyId], + VGpuPolicyName: idToNameCache[workerPool.VGpuPolicyId], + PlacementPolicyName: idToNameCache[workerPool.PlacementPolicyId], + } + if workerPool.Autoscaler != nil { + internalSettings.WorkerPools[i].Autoscaler = &CseWorkerPoolAutoscaler{ + MaxSize: workerPool.Autoscaler.MaxSize, + MinSize: workerPool.Autoscaler.MinSize, + } + } + } + + // Extra information needed to render the YAML. As all the worker pools share the same + // Kubernetes OVA name, version and Catalog, we pick this info from any of the available ones. + for _, doc := range docs { + if internalSettings.CatalogName == "" && doc["kind"] == "VCDMachineTemplate" { + internalSettings.CatalogName = traverseMapAndGet[string](doc, "spec.template.spec.catalog", ".") + } + if internalSettings.KubernetesTemplateOvaName == "" && doc["kind"] == "VCDMachineTemplate" { + internalSettings.KubernetesTemplateOvaName = traverseMapAndGet[string](doc, "spec.template.spec.template", ".") + } + if internalSettings.TkgVersionBundle.KubernetesVersion == "" && doc["kind"] == "MachineDeployment" { + internalSettings.TkgVersionBundle.KubernetesVersion = traverseMapAndGet[string](doc, "spec.template.spec.version", ".") + } + if internalSettings.CatalogName != "" && internalSettings.KubernetesTemplateOvaName != "" && internalSettings.TkgVersionBundle.KubernetesVersion != "" { + break + } + } + internalSettings.Name = cluster.Name + internalSettings.CseVersion = cluster.CseVersion + nodePoolsYaml, err := internalSettings.generateWorkerPoolsYaml() + if err != nil { + return nil, err + } + + newWorkerPoolsYamlDocs, err := unmarshalMultipleYamlDocuments(nodePoolsYaml) + if err != nil { + return nil, err + } + + result := make([]map[string]interface{}, len(docs)) + copy(result, docs) + return append(result, newWorkerPoolsYamlDocs...), nil +} + +// cseUpdateNodeHealthCheckInYaml updates the Kubernetes cluster described in the given YAML documents by adding or removing +// the MachineHealthCheck object. +// NOTE: This function doesn't modify the input, but returns a copy of the YAML with the modifications. +func cseUpdateNodeHealthCheckInYaml(yamlDocuments []map[string]interface{}, clusterName string, cseVersion semver.Version, vcdKeConfig vcdKeConfig) ([]map[string]interface{}, error) { + mhcPosition := -1 + result := make([]map[string]interface{}, len(yamlDocuments)) + for i, d := range yamlDocuments { + if d["kind"] == "MachineHealthCheck" { + mhcPosition = i + } + result[i] = d + } + + machineHealthCheckEnabled := vcdKeConfig.NodeUnknownTimeout != "" && vcdKeConfig.NodeStartupTimeout != "" && vcdKeConfig.NodeNotReadyTimeout != "" && + vcdKeConfig.MaxUnhealthyNodesPercentage != 0 + + if mhcPosition < 0 { + // There is no MachineHealthCheck block + if !machineHealthCheckEnabled { + // We don't want it neither, so nothing to do + return result, nil + } + + // We need to add the block to the slice of YAML documents + settings := &cseClusterSettingsInternal{CseVersion: cseVersion, Name: clusterName, VcdKeConfig: vcdKeConfig} + mhcYaml, err := settings.generateMachineHealthCheckYaml() + if err != nil { + return nil, err + } + var mhc map[string]interface{} + err = yaml.Unmarshal([]byte(mhcYaml), &mhc) + if err != nil { + return nil, err + } + result = append(result, mhc) + } else { + // There is a MachineHealthCheck block + if machineHealthCheckEnabled { + // We want it, but it is already there, so nothing to do + return result, nil + } + + // We don't want Machine Health Checks, we delete the YAML document + result[mhcPosition] = result[len(result)-1] // We override the MachineHealthCheck block with the last document + result = result[:len(result)-1] // We remove the last document (now duplicated) + } + return result, nil +} + +// cseUpdateAutoscalerInYaml adds a new YAML document (Autoscaler) to the output if the input worker pools require it and it's not present. +// If it's present, modifies the YAML documents by scaling the Autoscaler replicas to 1. +// If none of the input worker pools requires autoscaling, the YAML documents are modified to reduce the Autoscaler replicas to 0. +func cseUpdateAutoscalerInYaml(yamlDocuments []map[string]interface{}, clusterName string, cseVersion, kubernetesVersion semver.Version, + existingWorkerPools *map[string]CseWorkerPoolUpdateInput, newWorkerPools *[]CseWorkerPoolSettings) ([]map[string]interface{}, error) { + autoscalerNeeded := false + // We'll need the Autoscaler YAML document if at least one Worker Pool uses it + if existingWorkerPools != nil { + for _, wp := range *existingWorkerPools { + if wp.Autoscaler != nil { + autoscalerNeeded = true + break + } + } + } + + // We'll need the Autoscaler YAML document if at least one of the new Worker Pools uses it + if !autoscalerNeeded && newWorkerPools != nil { + for _, wp := range *newWorkerPools { + if wp.Autoscaler != nil { + autoscalerNeeded = true + break + } + } + } + + // Search for Autoscaler YAML document + for _, d := range yamlDocuments { + if d["kind"] != "Deployment" { + continue + } + if traverseMapAndGet[string](d, "metadata.name", ".") != "cluster-autoscaler" { + continue + } + if traverseMapAndGet[string](d, "metadata.namespace", ".") != "kube-system" { + continue + } + // Reaching here means that an Autoscaler was found. We need to modify its configuration. + if autoscalerNeeded { + d["spec"].(map[string]interface{})["replicas"] = float64(1) // As it was originally unmarshalled as a float64 + } else { + d["spec"].(map[string]interface{})["replicas"] = float64(0) // As it was originally unmarshalled as a float64 + } + // We also keep the image up-to-date with the Kubernetes version + k8sVersionSegments := kubernetesVersion.Segments() + d["spec"].(map[string]interface{})["template"].(map[string]interface{})["spec"].(map[string]interface{})["containers"].([]interface{})[0].(map[string]interface{})["image"] = fmt.Sprintf("k8s.gcr.io/autoscaling/cluster-autoscaler:v%d.%d.0", k8sVersionSegments[0], k8sVersionSegments[1]) + return yamlDocuments, nil + } + + // This part is only reached if we didn't find any Autoscaler document, so we add it new if it's needed. + if autoscalerNeeded { + settings := &cseClusterSettingsInternal{Name: clusterName, CseVersion: cseVersion, TkgVersionBundle: tkgVersionBundle{KubernetesVersion: kubernetesVersion.String()}} + autoscalerYaml, err := settings.generateAutoscalerYaml() + if err != nil { + return nil, err + } + autoscaler, err := unmarshalMultipleYamlDocuments(autoscalerYaml) + if err != nil { + return nil, err + } + return append(yamlDocuments, autoscaler...), nil + } + + // Otherwise the documents are returned without change + return yamlDocuments, nil +} + +// marshalMultipleYamlDocuments takes a slice of maps representing multiple YAML documents (one per item in the slice) and +// marshals all of them into a single string with the corresponding separators "---". +func marshalMultipleYamlDocuments(yamlDocuments []map[string]interface{}) (string, error) { + result := "" + for i, yamlDoc := range yamlDocuments { + updatedSingleDoc, err := yaml.Marshal(yamlDoc) + if err != nil { + return "", fmt.Errorf("error marshaling the updated CAPVCD YAML '%v': %s", yamlDoc, err) + } + result += fmt.Sprintf("%s\n", updatedSingleDoc) + if i < len(yamlDocuments)-1 { // The last document doesn't need the YAML separator + result += "---\n" + } + } + return result, nil +} + +// unmarshalMultipleYamlDocuments takes a multi-document YAML (multiple YAML documents are separated by "---") and +// unmarshalls all of them into a slice of generic maps with the corresponding content. +func unmarshalMultipleYamlDocuments(yamlDocuments string) ([]map[string]interface{}, error) { + if len(strings.TrimSpace(yamlDocuments)) == 0 { + return []map[string]interface{}{}, nil + } + + splitYamlDocs := strings.Split(yamlDocuments, "---\n") + result := make([]map[string]interface{}, len(splitYamlDocs)) + for i, yamlDoc := range splitYamlDocs { + err := yaml.Unmarshal([]byte(yamlDoc), &result[i]) + if err != nil { + return nil, fmt.Errorf("could not unmarshal document %s: %s", yamlDoc, err) + } + } + + return result, nil +} + +// traverseMapAndGet traverses the input interface{}, which should be a map of maps, by following the path specified as +// "keyA%keyB%keyC%keyD" (if keySeparator="%"), or "keyA.keyB.keyC.keyD" (if keySeparator="."), etc. doing something similar to, +// visually speaking, map["keyA"]["keyB"]["keyC"]["keyD"], or in other words, it goes inside every inner map iteratively, +// until the given path is finished. +// If the path doesn't lead to any value, or if the value is nil, or there is any other issue, returns the "zero" value of T. +func traverseMapAndGet[T any](input interface{}, path string, keySeparator string) T { + var nothing T + if input == nil { + return nothing + } + inputMap, ok := input.(map[string]interface{}) + if !ok { + return nothing + } + if len(inputMap) == 0 { + return nothing + } + pathUnits := strings.Split(path, keySeparator) + completed := false + i := 0 + var result interface{} + for !completed { + subPath := pathUnits[i] + traversed, ok := inputMap[subPath] + if !ok { + return nothing + } + if i < len(pathUnits)-1 { + traversedMap, ok := traversed.(map[string]interface{}) + if !ok { + return nothing + } + inputMap = traversedMap + } else { + completed = true + result = traversed + } + i++ + } + resultTyped, ok := result.(T) + if !ok { + return nothing + } + return resultTyped +} diff --git a/govcd/cse_yaml_unit_test.go b/govcd/cse_yaml_unit_test.go new file mode 100644 index 000000000..9e4703b61 --- /dev/null +++ b/govcd/cse_yaml_unit_test.go @@ -0,0 +1,627 @@ +//go:build unit || ALL + +package govcd + +import ( + semver "github.com/hashicorp/go-version" + "github.com/vmware/go-vcloud-director/v2/types/v56" + "os" + "reflect" + "strings" + "testing" +) + +// Test_cseUpdateKubernetesTemplateInYaml tests the update process of the Kubernetes template OVA in a CAPI YAML. +func Test_cseUpdateKubernetesTemplateInYaml(t *testing.T) { + capiYaml, err := os.ReadFile("test-resources/capiYaml.yaml") + if err != nil { + t.Fatalf("could not read CAPI YAML test file: %s", err) + } + + yamlDocs, err := unmarshalMultipleYamlDocuments(string(capiYaml)) + if err != nil { + t.Fatalf("could not unmarshal CAPI YAML test file: %s", err) + } + + oldOvaVersion := "v1.25.7+vmware.2-tkg.1-8a74b9f12e488c54605b3537acb683bc" // Matches the version in capiYaml.yaml + if strings.Count(string(capiYaml), oldOvaVersion) < 2 { + t.Fatalf("the testing YAML doesn't contain the OVA to change") + } + + oldTkgBundle, err := getTkgVersionBundleFromVAppTemplate(&types.VAppTemplate{ + Name: "dummy", + Children: &types.VAppTemplateChildren{VM: []*types.VAppTemplate{ + { + ProductSection: &types.ProductSection{ + Property: []*types.Property{ + { + Key: "VERSION", + DefaultValue: oldOvaVersion, + }, + }, + }, + }, + }}, + }) + if err != nil { + t.Fatal(err) + } + + // We call the function to update the old OVA with the new one + newOva := &types.VAppTemplate{ + ID: "urn:vcloud:vapptemplate:e23b8a5c-e676-4d82-9050-c906a3ac2fea", + Name: "dummy", + Children: &types.VAppTemplateChildren{VM: []*types.VAppTemplate{ + { + ProductSection: &types.ProductSection{ + Property: []*types.Property{ + { + Key: "VERSION", + DefaultValue: "v1.19.16+vmware.1-tkg.2-fba68db15591c15fcd5f26b512663a42", + }, + }, + }, + }, + }}, + } + newTkgBundle, err := getTkgVersionBundleFromVAppTemplate(newOva) + if err != nil { + t.Fatal(err) + } + + err = cseUpdateKubernetesTemplateInYaml(yamlDocs, newOva) + if err != nil { + t.Fatal(err) + } + + updatedYaml, err := marshalMultipleYamlDocuments(yamlDocs) + if err != nil { + t.Fatalf("error marshaling %v: %s", yamlDocs, err) + } + + // No document should have the old OVA + if strings.Count(updatedYaml, newOva.Name) < 2 || strings.Contains(updatedYaml, oldOvaVersion) { + t.Fatalf("failed updating the Kubernetes OVA template:\n%s", updatedYaml) + } + if !strings.Contains(updatedYaml, newTkgBundle.KubernetesVersion) || strings.Contains(updatedYaml, oldTkgBundle.KubernetesVersion) { + t.Fatalf("failed updating the Kubernetes version:\n%s", updatedYaml) + } + if !strings.Contains(updatedYaml, newTkgBundle.TkrVersion) || strings.Contains(updatedYaml, oldTkgBundle.TkrVersion) { + t.Fatalf("failed updating the Tanzu release version:\n%s", updatedYaml) + } + if !strings.Contains(updatedYaml, newTkgBundle.TkgVersion) || strings.Contains(updatedYaml, oldTkgBundle.TkgVersion) { + t.Fatalf("failed updating the Tanzu grid version:\n%s", updatedYaml) + } + if !strings.Contains(updatedYaml, newTkgBundle.CoreDnsVersion) || strings.Contains(updatedYaml, oldTkgBundle.CoreDnsVersion) { + t.Fatalf("failed updating the CoreDNS version:\n%s", updatedYaml) + } + if !strings.Contains(updatedYaml, newTkgBundle.EtcdVersion) || strings.Contains(updatedYaml, oldTkgBundle.EtcdVersion) { + t.Fatalf("failed updating the Etcd version:\n%s", updatedYaml) + } +} + +// Test_cseUpdateWorkerPoolsInYaml tests the update process of the Worker pools in a CAPI YAML. +func Test_cseUpdateWorkerPoolsInYaml(t *testing.T) { + capiYaml, err := os.ReadFile("test-resources/capiYaml.yaml") + if err != nil { + t.Fatalf("could not read CAPI YAML test file: %s", err) + } + + yamlDocs, err := unmarshalMultipleYamlDocuments(string(capiYaml)) + if err != nil { + t.Fatalf("could not unmarshal CAPI YAML test file: %s", err) + } + // We explore the YAML documents to get the current Worker pool + oldNodePools := map[string]CseWorkerPoolUpdateInput{} + for _, document := range yamlDocs { + if document["kind"] != "MachineDeployment" { + continue + } + + workerPoolName := traverseMapAndGet[string](document, "metadata.name", ".") + if workerPoolName == "" { + t.Fatalf("incorrect CAPI YAML: %s", err) + } + + oldNodePools[workerPoolName] = CseWorkerPoolUpdateInput{ + MachineCount: int(traverseMapAndGet[float64](document, "spec.replicas", ".")), + } + } + if len(oldNodePools) == 0 { + t.Fatalf("didn't get any valid worker node pool") + } + + // We call the function to update the old pools with the new ones + newReplicas := 66 + newNodePools := map[string]CseWorkerPoolUpdateInput{} + for name := range oldNodePools { + newNodePools[name] = CseWorkerPoolUpdateInput{ + MachineCount: newReplicas, + } + } + err = cseUpdateWorkerPoolsInYaml(yamlDocs, newNodePools) + if err != nil { + t.Fatal(err) + } + + // The worker pools should have now the new details updated + for _, document := range yamlDocs { + if document["kind"] != "MachineDeployment" { + continue + } + + retrievedReplicas := traverseMapAndGet[float64](document, "spec.replicas", ".") + if traverseMapAndGet[float64](document, "spec.replicas", ".") != float64(newReplicas) { + t.Fatalf("expected %d replicas but got %0.f", newReplicas, retrievedReplicas) + } + autoscalerMinSize := traverseMapAndGet[string](document, "metadata|annotations|cluster.x-k8s.io/cluster-api-autoscaler-node-group-min-size", "|") + if autoscalerMinSize != "" { + t.Fatalf("didn't expect autoscaler min size but got '%s'", autoscalerMinSize) + } + autoscalerMaxSize := traverseMapAndGet[string](document, "metadata|annotations|cluster.x-k8s.io/cluster-api-autoscaler-node-group-max-size", "|") + if autoscalerMaxSize != "" { + t.Fatalf("didn't expect autoscaler max size but got '%s'", autoscalerMaxSize) + } + } + + // We call the function to update the pools with autoscaling + newNodePools = map[string]CseWorkerPoolUpdateInput{} + for name := range oldNodePools { + newNodePools[name] = CseWorkerPoolUpdateInput{ + MachineCount: -1, // We don't care about replicas as we use autoscaling + Autoscaler: &CseWorkerPoolAutoscaler{ + MaxSize: 50, + MinSize: 10, + }, + } + } + err = cseUpdateWorkerPoolsInYaml(yamlDocs, newNodePools) + if err != nil { + t.Fatal(err) + } + + // The worker pools should have now the new details updated + for _, document := range yamlDocs { + if document["kind"] != "MachineDeployment" { + continue + } + + retrievedReplicas := traverseMapAndGet[float64](document, "spec.replicas", ".") + if retrievedReplicas != 0 { + t.Fatalf("didn't expect replicas but got '%0.f'", retrievedReplicas) + } + autoscalerMinSize := traverseMapAndGet[string](document, "metadata|annotations|cluster.x-k8s.io/cluster-api-autoscaler-node-group-min-size", "|") + if autoscalerMinSize != "10" { + t.Fatalf("expected autoscaler min size '10' but got '%s'", autoscalerMinSize) + } + autoscalerMaxSize := traverseMapAndGet[string](document, "metadata|annotations|cluster.x-k8s.io/cluster-api-autoscaler-node-group-max-size", "|") + if autoscalerMaxSize != "50" { + t.Fatalf("expected autoscaler min size '50' but got '%s'", autoscalerMaxSize) + } + } + + // Corner case: Wrong replicas + newReplicas = -1 + newNodePools = map[string]CseWorkerPoolUpdateInput{} + for name := range oldNodePools { + newNodePools[name] = CseWorkerPoolUpdateInput{ + MachineCount: newReplicas, + Autoscaler: nil, + } + } + err = cseUpdateWorkerPoolsInYaml(yamlDocs, newNodePools) + if err == nil { + t.Fatal("Expected an error, but got none") + } + + // Corner case: No worker pool with that name exists + newNodePools = map[string]CseWorkerPoolUpdateInput{ + "not-exist": {}, + } + err = cseUpdateWorkerPoolsInYaml(yamlDocs, newNodePools) + if err == nil { + t.Fatal("Expected an error, but got none") + } +} + +// Test_cseUpdateControlPlaneInYaml tests the update process of the Control Plane in a CAPI YAML. +func Test_cseUpdateControlPlaneInYaml(t *testing.T) { + capiYaml, err := os.ReadFile("test-resources/capiYaml.yaml") + if err != nil { + t.Fatalf("could not read CAPI YAML test file: %s", err) + } + + yamlDocs, err := unmarshalMultipleYamlDocuments(string(capiYaml)) + if err != nil { + t.Fatalf("could not unmarshal CAPI YAML test file: %s", err) + } + // We explore the YAML documents to get the current Control plane + oldControlPlane := CseControlPlaneUpdateInput{} + for _, document := range yamlDocs { + if document["kind"] != "KubeadmControlPlane" { + continue + } + + oldControlPlane = CseControlPlaneUpdateInput{ + MachineCount: int(traverseMapAndGet[float64](document, "spec.replicas", ".")), + } + } + if reflect.DeepEqual(oldControlPlane, CseWorkerPoolUpdateInput{}) { + t.Fatalf("didn't get any valid Control Plane") + } + + // We call the function to update the control plane + newReplicas := 67 + newControlPlane := CseControlPlaneUpdateInput{ + MachineCount: newReplicas, + } + err = cseUpdateControlPlaneInYaml(yamlDocs, newControlPlane) + if err != nil { + t.Fatal(err) + } + + // The control plane should have now the new details updated + for _, document := range yamlDocs { + if document["kind"] != "KubeadmControlPlane" { + continue + } + + retrievedReplicas := traverseMapAndGet[float64](document, "spec.replicas", ".") + if retrievedReplicas != float64(newReplicas) { + t.Fatalf("expected %d replicas but got %0.f", newReplicas, retrievedReplicas) + } + } + + // Corner case: Wrong replicas + newReplicas = -1 + newControlPlane = CseControlPlaneUpdateInput{ + MachineCount: newReplicas, + } + err = cseUpdateControlPlaneInYaml(yamlDocs, newControlPlane) + if err == nil { + t.Fatal("Expected an error, but got none") + } + + newReplicas = 2 // Should be odd, hence fails + newControlPlane = CseControlPlaneUpdateInput{ + MachineCount: newReplicas, + } + err = cseUpdateControlPlaneInYaml(yamlDocs, newControlPlane) + if err == nil { + t.Fatal("Expected an error, but got none") + } +} + +// Test_cseUpdateNodeHealthCheckInYaml tests the update process of the Machine Health Check capabilities in a CAPI YAML. +func Test_cseUpdateNodeHealthCheckInYaml(t *testing.T) { + capiYaml, err := os.ReadFile("test-resources/capiYaml.yaml") + if err != nil { + t.Fatalf("could not read CAPI YAML test file: %s", err) + } + + yamlDocs, err := unmarshalMultipleYamlDocuments(string(capiYaml)) + if err != nil { + t.Fatalf("could not unmarshal CAPI YAML test file: %s", err) + } + + clusterName := "" + for _, doc := range yamlDocs { + if doc["kind"] != "Cluster" { + continue + } + clusterName = traverseMapAndGet[string](doc, "metadata.name", ".") + } + if clusterName == "" { + t.Fatal("could not find the cluster name in the CAPI YAML test file") + } + + v, err := semver.NewVersion("4.1") + if err != nil { + t.Fatalf("incorrect version: %s", err) + } + + // Deactivates Machine Health Check + yamlDocs, err = cseUpdateNodeHealthCheckInYaml(yamlDocs, clusterName, *v, vcdKeConfig{}) + if err != nil { + t.Fatal(err) + } + + // The resulting documents should not have that document + for _, document := range yamlDocs { + if document["kind"] == "MachineHealthCheck" { + t.Fatal("Expected the MachineHealthCheck to be deleted, but it is there") + } + } + + // Enables Machine Health Check + yamlDocs, err = cseUpdateNodeHealthCheckInYaml(yamlDocs, clusterName, *v, vcdKeConfig{ + MaxUnhealthyNodesPercentage: 12, + NodeStartupTimeout: "34", + NodeNotReadyTimeout: "56", + NodeUnknownTimeout: "78", + }) + if err != nil { + t.Fatal(err) + } + + // The resulting documents should have a MachineHealthCheck + found := false + for _, document := range yamlDocs { + if document["kind"] != "MachineHealthCheck" { + continue + } + maxUnhealthy := traverseMapAndGet[string](document, "spec.maxUnhealthy", ".") + if maxUnhealthy != "12%" { + t.Fatalf("expected a 'spec.maxUnhealthy' = 12%%, but got %s", maxUnhealthy) + } + nodeStartupTimeout := traverseMapAndGet[string](document, "spec.nodeStartupTimeout", ".") + if nodeStartupTimeout != "34s" { + t.Fatalf("expected a 'spec.nodeStartupTimeout' = 34s, but got %s", nodeStartupTimeout) + } + found = true + } + if !found { + t.Fatalf("expected a MachineHealthCheck block but got nothing") + } +} + +// Test_unmarshalMultplieYamlDocuments tests the unmarshalling of multiple YAML documents with unmarshalMultplieYamlDocuments +func Test_unmarshalMultplieYamlDocuments(t *testing.T) { + capiYaml, err := os.ReadFile("test-resources/capiYaml.yaml") + if err != nil { + t.Fatalf("could not read YAML test file: %s", err) + } + + tests := []struct { + name string + yamlDocuments string + want int + wantErr bool + }{ + { + name: "unmarshal correct amount of documents", + yamlDocuments: string(capiYaml), + want: 8, + wantErr: false, + }, + { + name: "unmarshal single yaml document", + yamlDocuments: "test: foo", + want: 1, + wantErr: false, + }, + { + name: "unmarshal empty yaml document", + yamlDocuments: "", + want: 0, + }, + { + name: "unmarshal wrong yaml document", + yamlDocuments: "thisIsNotAYaml", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := unmarshalMultipleYamlDocuments(tt.yamlDocuments) + if (err != nil) != tt.wantErr { + t.Errorf("unmarshalMultplieYamlDocuments() error = %v, wantErr %v", err, tt.wantErr) + return + } + if len(got) != tt.want { + t.Errorf("unmarshalMultplieYamlDocuments() got %d documents, want %d", len(got), tt.want) + } + }) + } +} + +// Test_marshalMultplieYamlDocuments tests the marshalling of multiple YAML documents with marshalMultplieYamlDocuments +func Test_marshalMultplieYamlDocuments(t *testing.T) { + capiYaml, err := os.ReadFile("test-resources/capiYaml.yaml") + if err != nil { + t.Fatalf("could not read YAML test file: %s", err) + } + + unmarshaledCapiYaml, err := unmarshalMultipleYamlDocuments(string(capiYaml)) + if err != nil { + t.Fatalf("could not unmarshal the YAML test file: %s", err) + } + + tests := []struct { + name string + yamlDocuments []map[string]interface{} + want []map[string]interface{} + wantErr bool + }{ + { + name: "marshal correct amount of documents", + yamlDocuments: unmarshaledCapiYaml, + want: unmarshaledCapiYaml, + wantErr: false, + }, + { + name: "marshal empty slice", + yamlDocuments: []map[string]interface{}{}, + want: []map[string]interface{}{}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := marshalMultipleYamlDocuments(tt.yamlDocuments) + if (err != nil) != tt.wantErr { + t.Errorf("marshalMultipleYamlDocuments() error = %v, wantErr %v", err, tt.wantErr) + return + } + gotUnmarshaled, err := unmarshalMultipleYamlDocuments(got) // We unmarshal the result to compare it exactly with DeepEqual + if err != nil { + t.Errorf("unmarshalMultipleYamlDocuments() failed %s", err) + return + } + if !reflect.DeepEqual(gotUnmarshaled, tt.want) { + t.Errorf("marshalMultipleYamlDocuments() got =\n%v, want =\n%v", gotUnmarshaled, tt.want) + } + }) + } +} + +// Test_traverseMapAndGet tests traverseMapAndGet function +func Test_traverseMapAndGet(t *testing.T) { + type args struct { + input interface{} + path string + separator string + } + tests := []struct { + name string + args args + wantType string + want interface{} + }{ + { + name: "input is nil", + args: args{ + input: nil, + separator: ".", + }, + wantType: "string", + want: "", + }, + { + name: "input is not a map", + args: args{ + input: "error", + separator: ".", + }, + wantType: "string", + want: "", + }, + { + name: "map is empty", + args: args{ + input: map[string]interface{}{}, + separator: ".", + }, + wantType: "float64", + want: float64(0), + }, + { + name: "map does not have key", + args: args{ + input: map[string]interface{}{ + "keyA": "value", + }, + path: "keyB", + separator: ".", + }, + wantType: "string", + want: "", + }, + { + name: "map has a single simple key", + args: args{ + input: map[string]interface{}{ + "keyA": "value", + }, + path: "keyA", + separator: ".", + }, + wantType: "string", + want: "value", + }, + { + name: "map has a single complex key", + args: args{ + input: map[string]interface{}{ + "keyA": map[string]interface{}{ + "keyB": "value", + }, + }, + path: "keyA", + separator: ".", + }, + wantType: "map", + want: map[string]interface{}{ + "keyB": "value", + }, + }, + { + name: "map has a complex structure", + args: args{ + input: map[string]interface{}{ + "keyA": map[string]interface{}{ + "keyB": map[string]interface{}{ + "keyC": "value", + }, + }, + }, + path: "keyA.keyB.keyC", + separator: ".", + }, + wantType: "string", + want: "value", + }, + { + name: "requested path is deeper than the map structure", + args: args{ + input: map[string]interface{}{ + "keyA": map[string]interface{}{ + "keyB": map[string]interface{}{ + "keyC": "value", + }, + }, + }, + path: "keyA.keyB.keyC.keyD", + separator: ".", + }, + wantType: "string", + want: "", + }, + { + name: "obtained value does not correspond to the desired type", + args: args{ + input: map[string]interface{}{ + "keyA": map[string]interface{}{ + "keyB": map[string]interface{}{ + "keyC": map[string]interface{}{}, + }, + }, + }, + path: "keyA.keyB.keyC", + separator: ".", + }, + wantType: "string", + want: "", + }, + { + name: "requested path has special characters but separator is different", + args: args{ + input: map[string]interface{}{ + "keyA.foo.bar": "result", + }, + path: "keyA.foo.bar", + separator: "#", + }, + wantType: "string", + want: "result", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got interface{} + if tt.wantType == "string" { + got = traverseMapAndGet[string](tt.args.input, tt.args.path, tt.args.separator) + } else if tt.wantType == "map" { + got = traverseMapAndGet[map[string]interface{}](tt.args.input, tt.args.path, tt.args.separator) + } else if tt.wantType == "float64" { + got = traverseMapAndGet[float64](tt.args.input, tt.args.path, tt.args.separator) + } else { + t.Fatalf("wantType type not used in this test") + } + + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("traverseMapAndGet() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/govcd/defined_entity.go b/govcd/defined_entity.go new file mode 100644 index 000000000..87746c86f --- /dev/null +++ b/govcd/defined_entity.go @@ -0,0 +1,660 @@ +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "encoding/json" + "fmt" + "net/url" + "strings" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +const ( + labelDefinedEntity = "Defined Entity" + labelDefinedEntityAccessControl = "Defined Entity Access Control" + labelDefinedEntityType = "Defined Entity Type" + labelRdeBehavior = "RDE Behavior" + labelRdeBehaviorOverride = "RDE Behavior Override" + labelRdeBehaviorAccessControl = "RDE Behavior Access Control" +) + +// DefinedEntityType is a type for handling Runtime Defined Entity (RDE) Type definitions. +// Note. Running a few of these operations in parallel may corrupt database in VCD (at least <= 10.4.2) +type DefinedEntityType struct { + DefinedEntityType *types.DefinedEntityType + client *Client +} + +// 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 (d DefinedEntityType) wrap(inner *types.DefinedEntityType) *DefinedEntityType { + d.DefinedEntityType = inner + return &d +} + +// DefinedEntity represents an instance of a Runtime Defined Entity (RDE) +type DefinedEntity struct { + DefinedEntity *types.DefinedEntity + Etag string // Populated by VCDClient.GetRdeById, DefinedEntityType.GetRdeById, DefinedEntity.Update + client *Client +} + +// 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 (d DefinedEntity) wrap(inner *types.DefinedEntity) *DefinedEntity { + d.DefinedEntity = inner + return &d +} + +// CreateRdeType creates a Runtime Defined Entity Type. +// Only a System administrator can create RDE Types. +func (vcdClient *VCDClient) CreateRdeType(rde *types.DefinedEntityType) (*DefinedEntityType, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntityTypes, + entityLabel: labelDefinedEntityType, + } + outerType := DefinedEntityType{client: &vcdClient.Client} + return createOuterEntity(&vcdClient.Client, outerType, c, rde) +} + +// GetAllRdeTypes retrieves all Runtime Defined Entity Types. Query parameters can be supplied to perform additional filtering. +func (vcdClient *VCDClient) GetAllRdeTypes(queryParameters url.Values) ([]*DefinedEntityType, error) { + return getAllRdeTypes(&vcdClient.Client, queryParameters) +} + +// getAllRdeTypes retrieves all Runtime Defined Entity Types. Query parameters can be supplied to perform additional filtering. +func getAllRdeTypes(client *Client, queryParameters url.Values) ([]*DefinedEntityType, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntityTypes, + entityLabel: labelDefinedEntityType, + queryParameters: queryParameters, + } + + outerType := DefinedEntityType{client: client} + return getAllOuterEntities[DefinedEntityType, types.DefinedEntityType](client, outerType, c) +} + +// GetRdeType gets a Runtime Defined Entity Type by its unique combination of vendor, nss and version. +func (vcdClient *VCDClient) GetRdeType(vendor, nss, version string) (*DefinedEntityType, error) { + return getRdeType(&vcdClient.Client, vendor, nss, version) +} + +// getRdeType gets a Runtime Defined Entity Type by its unique combination of vendor, nss and version. +func getRdeType(client *Client, vendor, nss, version string) (*DefinedEntityType, error) { + queryParameters := url.Values{} + queryParameters.Add("filter", fmt.Sprintf("vendor==%s;nss==%s;version==%s", vendor, nss, version)) + rdeTypes, err := getAllRdeTypes(client, queryParameters) + if err != nil { + return nil, err + } + + if len(rdeTypes) == 0 { + return nil, fmt.Errorf("%s could not find the Runtime Defined Entity Type with vendor %s, nss %s and version %s", ErrorEntityNotFound, vendor, nss, version) + } + + if len(rdeTypes) > 1 { + return nil, fmt.Errorf("found more than 1 Runtime Defined Entity Type with vendor %s, nss %s and version %s", vendor, nss, version) + } + + return rdeTypes[0], nil +} + +// GetRdeTypeById gets a Runtime Defined Entity Type by its ID. +func (vcdClient *VCDClient) GetRdeTypeById(id string) (*DefinedEntityType, error) { + c := crudConfig{ + entityLabel: labelDefinedEntityType, + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntityTypes, + endpointParams: []string{id}, + } + + outerType := DefinedEntityType{client: &vcdClient.Client} + return getOuterEntity[DefinedEntityType, types.DefinedEntityType](&vcdClient.Client, outerType, c) +} + +// Update updates the receiver Runtime Defined Entity Type with the values given by the input. +// Only a System administrator can create RDE Types. +func (rdeType *DefinedEntityType) Update(rdeTypeToUpdate types.DefinedEntityType) error { + if rdeType.DefinedEntityType.ID == "" { + return fmt.Errorf("ID of the receiver Runtime Defined Entity Type is empty") + } + + if rdeTypeToUpdate.ID != "" && rdeTypeToUpdate.ID != rdeType.DefinedEntityType.ID { + return fmt.Errorf("ID of the receiver Runtime Defined Entity and the input ID don't match") + } + + // Name and schema are mandatory, even when we don't want to update them, so we populate them in this situation to avoid errors + // and make this method more user friendly. + if rdeTypeToUpdate.Name == "" { + rdeTypeToUpdate.Name = rdeType.DefinedEntityType.Name + } + if rdeTypeToUpdate.Schema == nil || len(rdeTypeToUpdate.Schema) == 0 { + rdeTypeToUpdate.Schema = rdeType.DefinedEntityType.Schema + } + rdeTypeToUpdate.Version = rdeType.DefinedEntityType.Version + rdeTypeToUpdate.Nss = rdeType.DefinedEntityType.Nss + rdeTypeToUpdate.Vendor = rdeType.DefinedEntityType.Vendor + + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntityTypes, + endpointParams: []string{rdeType.DefinedEntityType.ID}, + entityLabel: labelDefinedEntityType, + } + + resultDefinedEntityType, err := updateInnerEntity(rdeType.client, c, &rdeTypeToUpdate) + if err != nil { + return err + } + // Only if there was no error in request we overwrite pointer receiver as otherwise it would + // wipe out existing data + rdeType.DefinedEntityType = resultDefinedEntityType + + return nil +} + +// Delete deletes the receiver Runtime Defined Entity Type. +// Only a System administrator can delete RDE Types. +func (rdeType *DefinedEntityType) Delete() error { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntityTypes, + endpointParams: []string{rdeType.DefinedEntityType.ID}, + entityLabel: labelDefinedEntityType, + } + + if err := deleteEntityById(rdeType.client, c); err != nil { + return err + } + + rdeType.DefinedEntityType = &types.DefinedEntityType{} + return nil +} + +// GetAllBehaviors retrieves all the Behaviors of the receiver RDE Type. +func (rdeType *DefinedEntityType) GetAllBehaviors(queryParameters url.Values) ([]*types.Behavior, error) { + if rdeType.DefinedEntityType.ID == "" { + return nil, fmt.Errorf("ID of the receiver Defined Entity Type is empty") + } + return getAllBehaviors(rdeType.client, rdeType.DefinedEntityType.ID, types.OpenApiEndpointRdeTypeBehaviors, queryParameters) +} + +// GetBehaviorById retrieves a unique Behavior that belongs to the receiver RDE Type and is determined by the +// input ID. The ID can be a RDE Interface Behavior ID or a RDE Type overridden Behavior ID. +func (rdeType *DefinedEntityType) GetBehaviorById(id string) (*types.Behavior, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeTypeBehaviors, + endpointParams: []string{rdeType.DefinedEntityType.ID, id}, + entityLabel: labelRdeBehavior, + } + return getInnerEntity[types.Behavior](rdeType.client, c) +} + +// GetBehaviorByName retrieves a unique Behavior that belongs to the receiver RDE Type and is named after +// the input. +func (rdeType *DefinedEntityType) GetBehaviorByName(name string) (*types.Behavior, error) { + behaviors, err := rdeType.GetAllBehaviors(nil) + if err != nil { + return nil, fmt.Errorf("could not get the Behaviors of the Defined Entity Type with ID '%s': %s", rdeType.DefinedEntityType.ID, err) + } + label := fmt.Sprintf("Defined Entity Behavior with name '%s' in Defined Entity Type with ID '%s': %s", name, rdeType.DefinedEntityType.ID, ErrorEntityNotFound) + return localFilterOneOrError(label, behaviors, "Name", name) +} + +// UpdateBehaviorOverride overrides an Interface Behavior. Only Behavior description and execution can be overridden. +// It returns the new Behavior, result of the override (with a new ID). +func (rdeType *DefinedEntityType) UpdateBehaviorOverride(behavior types.Behavior) (*types.Behavior, error) { + if rdeType.DefinedEntityType.ID == "" { + return nil, fmt.Errorf("ID of the receiver Defined Entity Type is empty") + } + if behavior.ID == "" { + return nil, fmt.Errorf("ID of the Behavior to override is empty") + } + + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeTypeBehaviors, + endpointParams: []string{rdeType.DefinedEntityType.ID, behavior.ID}, + entityLabel: labelRdeBehaviorOverride, + } + return updateInnerEntity(rdeType.client, c, &behavior) +} + +// DeleteBehaviorOverride removes a Behavior specified by its ID from the receiver Defined Entity Type. +// The ID can be the Interface Behavior ID or the Type Behavior ID (the overridden one). +func (rdeType *DefinedEntityType) DeleteBehaviorOverride(behaviorId string) error { + if rdeType.DefinedEntityType.ID == "" { + return fmt.Errorf("ID of the receiver Defined Entity Type is empty") + } + + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeTypeBehaviors, + endpointParams: []string{rdeType.DefinedEntityType.ID, behaviorId}, + entityLabel: labelRdeBehaviorOverride, + } + return deleteEntityById(rdeType.client, c) +} + +// SetBehaviorAccessControls sets the given slice of BehaviorAccess to the receiver Defined Entity Type. +// If the input is nil, it removes all access controls from the receiver Defined Entity Type. +func (det *DefinedEntityType) SetBehaviorAccessControls(acls []*types.BehaviorAccess) error { + if det.DefinedEntityType.ID == "" { + return fmt.Errorf("ID of the receiver Defined Entity Type is empty") + } + + sanitizedAcls := acls + if acls == nil { + sanitizedAcls = []*types.BehaviorAccess{} + } + + // Wrap it in OpenAPI pages, this endpoint requires it + rawMessage, err := json.Marshal(sanitizedAcls) + if err != nil { + return fmt.Errorf("error setting Access controls in payload: %s", err) + } + payload := types.OpenApiPages{ + Values: rawMessage, + } + + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeTypeBehaviorAccessControls, + endpointParams: []string{det.DefinedEntityType.ID}, + entityLabel: labelRdeBehaviorAccessControl, + } + _, err = updateInnerEntity(det.client, c, &payload) + if err != nil { + return err + } + + return nil +} + +// GetAllBehaviorsAccessControls gets all the Behaviors Access Controls from the receiver DefinedEntityType. +// Query parameters can be supplied to modify pagination. +func (det *DefinedEntityType) GetAllBehaviorsAccessControls(queryParameters url.Values) ([]*types.BehaviorAccess, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeTypeBehaviorAccessControls, + queryParameters: queryParameters, + endpointParams: []string{det.DefinedEntityType.ID}, + entityLabel: labelRdeBehaviorAccessControl, + } + return getAllInnerEntities[types.BehaviorAccess](det.client, c) +} + +// GetAllRdes gets all the RDE instances of the given vendor, nss and version. +func (vcdClient *VCDClient) GetAllRdes(vendor, nss, version string, queryParameters url.Values) ([]*DefinedEntity, error) { + return getAllRdes(&vcdClient.Client, vendor, nss, version, queryParameters) +} + +// GetAllRdes gets all the RDE instances of the receiver type. +func (rdeType *DefinedEntityType) GetAllRdes(queryParameters url.Values) ([]*DefinedEntity, error) { + return getAllRdes(rdeType.client, rdeType.DefinedEntityType.Vendor, rdeType.DefinedEntityType.Nss, rdeType.DefinedEntityType.Version, queryParameters) +} + +// getAllRdes gets all the RDE instances of the given vendor, nss and version. +// Supports filtering with the given queryParameters. +func getAllRdes(client *Client, vendor, nss, version string, queryParameters url.Values) ([]*DefinedEntity, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntitiesTypes, + entityLabel: labelDefinedEntityType, + queryParameters: queryParameters, + endpointParams: []string{vendor, "/", nss, "/", version}, + } + + outerType := DefinedEntity{client: client} + return getAllOuterEntities[DefinedEntity, types.DefinedEntity](client, outerType, c) +} + +// GetRdesByName gets RDE instances with the given name that belongs to the receiver type. +// VCD allows to have many RDEs with the same name, hence this function returns a slice. +func (rdeType *DefinedEntityType) GetRdesByName(name string) ([]*DefinedEntity, error) { + return getRdesByName(rdeType.client, rdeType.DefinedEntityType.Vendor, rdeType.DefinedEntityType.Nss, rdeType.DefinedEntityType.Version, name) +} + +// GetRdesByName gets RDE instances with the given name and the given vendor, nss and version. +// VCD allows to have many RDEs with the same name, hence this function returns a slice. +func (vcdClient *VCDClient) GetRdesByName(vendor, nss, version, name string) ([]*DefinedEntity, error) { + return getRdesByName(&vcdClient.Client, vendor, nss, version, name) +} + +// getRdesByName gets RDE instances with the given name and the given vendor, nss and version. +// VCD allows to have many RDEs with the same name, hence this function returns a slice. +func getRdesByName(client *Client, vendor, nss, version, name string) ([]*DefinedEntity, error) { + queryParameters := url.Values{} + queryParameters.Add("filter", fmt.Sprintf("name==%s", name)) + rdeTypes, err := getAllRdes(client, vendor, nss, version, queryParameters) + if err != nil { + return nil, err + } + + if len(rdeTypes) == 0 { + return nil, fmt.Errorf("%s could not find the Runtime Defined Entity with name '%s'", ErrorEntityNotFound, name) + } + + return rdeTypes, nil +} + +// GetRdeById gets a Runtime Defined Entity by its ID. +// Getting a RDE by ID populates the ETag field in the returned object. +func (rdeType *DefinedEntityType) GetRdeById(id string) (*DefinedEntity, error) { + return getRdeById(rdeType.client, id) +} + +// GetRdeById gets a Runtime Defined Entity by its ID. +// Getting a RDE by ID populates the ETag field in the returned object. +func (vcdClient *VCDClient) GetRdeById(id string) (*DefinedEntity, error) { + return getRdeById(&vcdClient.Client, id) +} + +// getRdeById gets a Runtime Defined Entity by its ID. +// Getting a RDE by ID populates the ETag field in the returned object. +func getRdeById(client *Client, id string) (*DefinedEntity, error) { + c := crudConfig{ + entityLabel: labelDefinedEntityType, + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntities, + endpointParams: []string{id}, + } + + outerType := DefinedEntity{client: client} + result, headers, err := getOuterEntityWithHeaders(client, outerType, c) + if err != nil { + return nil, err + } + result.Etag = headers.Get("Etag") + return result, nil +} + +// CreateRde creates an entity of the type of the receiver Runtime Defined Entity (RDE) type. +// The input doesn't need to specify the type ID, as it gets it from the receiver RDE type. +// The input tenant context allows to create the RDE in a given org if the creator is a System admin. +// NOTE: After RDE creation, some actor should Resolve it, otherwise the RDE state will be "PRE_CREATED" +// and the generated VCD task will remain at 1% until resolved. +func (rdeType *DefinedEntityType) CreateRde(entity types.DefinedEntity, tenantContext *TenantContext) (*DefinedEntity, error) { + entity.EntityType = rdeType.DefinedEntityType.ID + task, err := createRde(rdeType.client, entity, tenantContext) + if err != nil { + return nil, err + } + return getRdeFromTask(rdeType.client, task) +} + +// CreateRde creates an entity of the type of the given vendor, nss and version. +// NOTE: After RDE creation, some actor should Resolve it, otherwise the RDE state will be "PRE_CREATED" +// and the generated VCD task will remain at 1% until resolved. +func (vcdClient *VCDClient) CreateRde(vendor, nss, version string, entity types.DefinedEntity, tenantContext *TenantContext) (*DefinedEntity, error) { + return createRdeAndGetFromTask(&vcdClient.Client, vendor, nss, version, entity, tenantContext) +} + +// createRdeAndGetFromTask creates an entity of the type of the given vendor, nss and version. +// NOTE: After RDE creation, some actor should Resolve it, otherwise the RDE state will be "PRE_CREATED" +// and the generated VCD task will remain at 1% until resolved. +func createRdeAndGetFromTask(client *Client, vendor, nss, version string, entity types.DefinedEntity, tenantContext *TenantContext) (*DefinedEntity, error) { + entity.EntityType = fmt.Sprintf("urn:vcloud:type:%s:%s:%s", vendor, nss, version) + task, err := createRde(client, entity, tenantContext) + if err != nil { + return nil, err + } + return getRdeFromTask(client, task) +} + +// CreateRde creates an entity of the type of the receiver Runtime Defined Entity (RDE) type. +// The input doesn't need to specify the type ID, as it gets it from the receiver RDE type. If it is specified anyway, +// it must match the type ID of the receiver RDE type. +// NOTE: After RDE creation, some actor should Resolve it, otherwise the RDE state will be "PRE_CREATED" +// and the generated VCD task will remain at 1% until resolved. +func createRde(client *Client, entity types.DefinedEntity, tenantContext *TenantContext) (*Task, error) { + if entity.EntityType == "" { + return nil, fmt.Errorf("ID of the Runtime Defined Entity type is empty") + } + + if entity.Entity == nil || len(entity.Entity) == 0 { + return nil, fmt.Errorf("the entity JSON is empty") + } + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntityTypes + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, entity.EntityType) + if err != nil { + return nil, err + } + + task, err := client.OpenApiPostItemAsyncWithHeaders(apiVersion, urlRef, nil, entity, getTenantContextHeader(tenantContext)) + if err != nil { + return nil, err + } + // The refresh is needed as the task only has the HREF at the moment + err = task.Refresh() + if err != nil { + return nil, err + } + return &task, nil +} + +// getRdeFromTask gets the Runtime Defined Entity from a given Task. This method is useful after RDE creation, as +// the API just returns a Task with the RDE details inside. +func getRdeFromTask(client *Client, task *Task) (*DefinedEntity, error) { + if task.Task == nil { + return nil, fmt.Errorf("could not retrieve the RDE from task, as it is nil") + } + rdeId := "" + if task.Task.Owner == nil { + // Try to retrieve the ID from the "Operation" field + beginning := strings.LastIndex(task.Task.Operation, "(") + end := strings.LastIndex(task.Task.Operation, ")") + if beginning < 0 || end < 0 || beginning >= end { + return nil, fmt.Errorf("could not retrieve the RDE from the task with ID '%s'", task.Task.ID) + } + rdeId = task.Task.Operation[beginning+1 : end] + } else { + rdeId = task.Task.Owner.ID + } + + return getRdeById(client, rdeId) +} + +// State is a function to check if any of the elements in the path to 'rde.DefinedEntity.State' are +// nil and return 'string' value instead of '*string' +func (rde *DefinedEntity) State() string { + if rde == nil || rde.DefinedEntity == nil || rde.DefinedEntity.State == nil { + return "" + } + + return *rde.DefinedEntity.State +} + +// Resolve needs to be called after an RDE is successfully created. It makes the receiver RDE usable if the JSON entity +// is valid, reaching a state of RESOLVED. If it fails, the state will be RESOLUTION_ERROR, +// and it will need to Update the JSON entity. +// Resolving a RDE populates the ETag field in the receiver object. +func (rde *DefinedEntity) Resolve() error { + client := rde.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntitiesResolve + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, rde.DefinedEntity.ID)) + if err != nil { + return err + } + + headers, err := client.OpenApiPostItemAndGetHeaders(apiVersion, urlRef, nil, nil, rde.DefinedEntity, nil) + if err != nil { + return amendRdeApiError(client, err) + } + rde.Etag = headers.Get("Etag") + + return nil +} + +// Refresh reloads RDE +func (rde *DefinedEntity) Refresh() error { + client := rde.client + + refreshedRde, err := getRdeById(client, rde.DefinedEntity.ID) + if err != nil { + return fmt.Errorf("error refreshing RDE: %s", err) + } + rde.DefinedEntity = refreshedRde.DefinedEntity + + return nil +} + +// Update updates the receiver Runtime Defined Entity with the values given by the input. This method is useful +// if rde.Resolve() failed and a JSON entity change is needed. +// Updating a RDE populates the ETag field in the receiver object. +func (rde *DefinedEntity) Update(rdeToUpdate types.DefinedEntity) error { + if rde.DefinedEntity.ID == "" { + return fmt.Errorf("ID of the receiver Runtime Defined Entity is empty") + } + + // Name is mandatory, despite we don't want to update it, so we populate it in this situation to avoid errors + // and make this method more user friendly. + if rdeToUpdate.Name == "" { + rdeToUpdate.Name = rde.DefinedEntity.Name + } + + if rde.Etag == "" { + // We need to get an Etag to perform the update + retrievedRde, err := getRdeById(rde.client, rde.DefinedEntity.ID) + if err != nil { + return err + } + if retrievedRde.Etag == "" { + return fmt.Errorf("could not retrieve a valid Etag to perform an update to RDE %s", retrievedRde.DefinedEntity.ID) + } + rde.Etag = retrievedRde.Etag + } + + c := crudConfig{ + entityLabel: labelDefinedEntity, + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntities, + endpointParams: []string{rde.DefinedEntity.ID}, + additionalHeader: map[string]string{"If-Match": rde.Etag}, + } + + resultDefinedEntity, headers, err := updateInnerEntityWithHeaders(rde.client, c, &rdeToUpdate) + if err != nil { + return err + } + // Only if there was no error in request we overwrite pointer receiver as otherwise it would + // wipe out existing data + rde.DefinedEntity = resultDefinedEntity + rde.Etag = headers.Get("Etag") + + return nil +} + +// Delete deletes the receiver Runtime Defined Entity. +func (rde *DefinedEntity) Delete() error { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntities, + endpointParams: []string{rde.DefinedEntity.ID}, + entityLabel: labelDefinedEntity, + } + + if err := deleteEntityById(rde.client, c); err != nil { + return amendRdeApiError(rde.client, err) + } + + rde.DefinedEntity = &types.DefinedEntity{} + rde.Etag = "" + return nil +} + +// InvokeBehavior calls a Behavior identified by the given ID with the given execution parameters. +// Returns the invocation result as a raw string. +func (rde *DefinedEntity) InvokeBehavior(behaviorId string, invocation types.BehaviorInvocation) (string, error) { + client := rde.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntitiesBehaviorsInvocations + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return "", err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, rde.DefinedEntity.ID, behaviorId)) + if err != nil { + return "", err + } + + task, err := client.OpenApiPostItemAsync(apiVersion, urlRef, nil, invocation) + if err != nil { + return "", err + } + + err = task.WaitTaskCompletion() + if err != nil { + return "", err + } + + if task.Task.Result == nil { + return "", fmt.Errorf("the Task '%s' returned an empty Result content", task.Task.ID) + } + + return task.Task.Result.ResultContent.Text, nil +} + +// InvokeBehaviorAndMarshal calls a Behavior identified by the given ID with the given execution parameters. +// Returns the invocation result marshaled with the input object. +func (rde *DefinedEntity) InvokeBehaviorAndMarshal(behaviorId string, invocation types.BehaviorInvocation, output interface{}) error { + result, err := rde.InvokeBehavior(behaviorId, invocation) + if err != nil { + return err + } + + err = json.Unmarshal([]byte(result), &output) + if err != nil { + return fmt.Errorf("error marshaling the invocation result '%s': %s", result, err) + } + + return nil +} + +// SetAccessControl sets Defined Entity Access Control +func (de *DefinedEntity) SetAccessControl(acl *types.DefinedEntityAccess) (*types.DefinedEntityAccess, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntityAccessControls, + endpointParams: []string{de.DefinedEntity.ID}, + entityLabel: labelDefinedEntityAccessControl, + } + return createInnerEntity(de.client, c, acl) +} + +// GetAllAccessControls gets all Defined Entity Access Controls from the receiver DefinedEntity. +// Query parameters can be supplied to modify search criteria. +func (de *DefinedEntity) GetAllAccessControls(queryParameters url.Values) ([]*types.DefinedEntityAccess, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntityAccessControls, + queryParameters: queryParameters, + endpointParams: []string{de.DefinedEntity.ID}, + entityLabel: labelDefinedEntityAccessControl, + } + return getAllInnerEntities[types.DefinedEntityAccess](de.client, c) +} + +// GetAccessControlById gets all Defined Entity Access Controls from the receiver DefinedEntity. +func (de *DefinedEntity) GetAccessControlById(id string) (*types.DefinedEntityAccess, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntityAccessControls, + endpointParams: []string{de.DefinedEntity.ID, id}, + entityLabel: labelDefinedEntityAccessControl, + } + return getInnerEntity[types.DefinedEntityAccess](de.client, c) +} + +// DeleteAccessControl removes a given Access Control +func (de *DefinedEntity) DeleteAccessControl(acl *types.DefinedEntityAccess) error { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntityAccessControls, + endpointParams: []string{de.DefinedEntity.ID, acl.Id}, + entityLabel: labelDefinedEntityAccessControl, + } + return deleteEntityById(de.client, c) +} diff --git a/govcd/defined_entity_test.go b/govcd/defined_entity_test.go new file mode 100644 index 000000000..df21a8306 --- /dev/null +++ b/govcd/defined_entity_test.go @@ -0,0 +1,580 @@ +//go:build functional || openapi || rde || ALL + +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +// Test_RdeAndRdeType tests the CRUD operations for the RDE Type with both System administrator and a tenant user. +func (vcd *TestVCD) Test_RdeAndRdeType(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + + for _, endpoint := range []string{ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntityTypes, + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntitiesResolve, + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntities, + } { + skipOpenApiEndpointTest(vcd, check, endpoint) + } + + if len(vcd.config.Tenants) == 0 { + check.Skip("skipping as there is no configured tenant users") + } + + // Creates the clients for the System admin and the Tenant user + systemAdministratorClient := vcd.client + tenantUserClient := NewVCDClient(vcd.client.Client.VCDHREF, true) + err := tenantUserClient.Authenticate(vcd.config.Tenants[0].User, vcd.config.Tenants[0].Password, vcd.config.Tenants[0].SysOrg) + check.Assert(err, IsNil) + + unmarshaledRdeTypeSchema, err := loadRdeTypeSchemaFromTestResources() + check.Assert(err, IsNil) + check.Assert(true, Equals, len(unmarshaledRdeTypeSchema) > 0) + + // First, it checks how many exist already, as VCD contains some pre-defined ones. + allRdeTypesBySystemAdmin, err := systemAdministratorClient.GetAllRdeTypes(nil) + check.Assert(err, IsNil) + alreadyPresentRdes := len(allRdeTypesBySystemAdmin) + + // For the tenant, in VCD versions lower than 39.0 it returns 0 RDE Types, but no error. + // In newer VCD versions, it returns some pre-defined RDE Types. + allRdeTypesByTenant, err := tenantUserClient.GetAllRdeTypes(nil) + check.Assert(err, IsNil) + if vcd.client.Client.APIVCDMaxVersionIs("< 39.0") { + check.Assert(len(allRdeTypesByTenant), Equals, 0) + } else { + check.Assert(true, Equals, len(allRdeTypesByTenant) > 0) + } + + // Then we create a new RDE Type with System administrator. + // Can't put check.TestName() in nss due to a bug in VCD 10.4.1 that causes RDEs to fail on GET once created with special characters like "." + vendor := "vmware" + nss := strings.ReplaceAll(check.TestName()+"name", ".", "") + version := "1.2.3" + rdeTypeToCreate := &types.DefinedEntityType{ + Name: check.TestName(), + Nss: nss, + Version: version, + Description: "Description of " + check.TestName(), + Schema: unmarshaledRdeTypeSchema, + Vendor: vendor, + Interfaces: []string{"urn:vcloud:interface:vmware:k8s:1.0.0"}, + } + createdRdeType, err := systemAdministratorClient.CreateRdeType(rdeTypeToCreate) + check.Assert(err, IsNil) + check.Assert(createdRdeType, NotNil) + check.Assert(createdRdeType.DefinedEntityType.Name, Equals, rdeTypeToCreate.Name) + check.Assert(createdRdeType.DefinedEntityType.Nss, Equals, rdeTypeToCreate.Nss) + check.Assert(createdRdeType.DefinedEntityType.Version, Equals, rdeTypeToCreate.Version) + check.Assert(createdRdeType.DefinedEntityType.Schema, NotNil) + check.Assert(createdRdeType.DefinedEntityType.Schema["type"], Equals, "object") + check.Assert(createdRdeType.DefinedEntityType.Schema["definitions"], NotNil) + check.Assert(createdRdeType.DefinedEntityType.Schema["required"], NotNil) + check.Assert(createdRdeType.DefinedEntityType.Schema["properties"], NotNil) + AddToCleanupListOpenApi(createdRdeType.DefinedEntityType.ID, check.TestName(), types.OpenApiPathVersion1_0_0+types.OpenApiEndpointRdeEntityTypes+createdRdeType.DefinedEntityType.ID) + + // Tenants can't create RDE Types + nilRdeType, err := tenantUserClient.CreateRdeType(&types.DefinedEntityType{ + Name: check.TestName(), + Nss: "notworking", + Version: "4.5.6", + Schema: unmarshaledRdeTypeSchema, + Vendor: "willfail", + }) + check.Assert(err, NotNil) + check.Assert(nilRdeType, IsNil) + check.Assert(strings.Contains(err.Error(), "ACCESS_TO_RESOURCE_IS_FORBIDDEN"), Equals, true) + + // Assign rights to the tenant user, so it can perform following operations. + // We don't need to clean the rights afterwards as deleting the RDE Type deletes the associated bundle + // with its rights. + role, err := systemAdministratorClient.Client.GetGlobalRoleByName("Organization Administrator") + check.Assert(err, IsNil) + check.Assert(role, NotNil) + + rightsBundleName := fmt.Sprintf("%s:%s Entitlement", vendor, nss) + rightsBundle, err := systemAdministratorClient.Client.GetRightsBundleByName(rightsBundleName) + check.Assert(err, IsNil) + check.Assert(rightsBundle, NotNil) + + err = rightsBundle.PublishAllTenants() + check.Assert(err, IsNil) + + rights, err := rightsBundle.GetRights(nil) + check.Assert(err, IsNil) + check.Assert(len(rights), Not(Equals), 0) + + var rightsToAdd []types.OpenApiReference + for _, right := range rights { + if strings.Contains(right.Name, fmt.Sprintf("%s:%s", vendor, nss)) { + rightsToAdd = append(rightsToAdd, types.OpenApiReference{ + Name: right.Name, + ID: right.ID, + }) + } + } + check.Assert(rightsToAdd, NotNil) + check.Assert(len(rightsToAdd), Not(Equals), 0) + + err = role.AddRights(rightsToAdd) + check.Assert(err, IsNil) + + // As we created a new RDE Type, we check the new count is correct in both System admin and Tenant user + allRdeTypesBySystemAdmin, err = systemAdministratorClient.GetAllRdeTypes(nil) + check.Assert(err, IsNil) + check.Assert(len(allRdeTypesBySystemAdmin), Equals, alreadyPresentRdes+1) + + // Count is existing RDE Types + 1 for tenant user + alreadyPresentRdes = len(allRdeTypesByTenant) + allRdeTypesByTenant, err = tenantUserClient.GetAllRdeTypes(nil) + check.Assert(err, IsNil) + check.Assert(len(allRdeTypesByTenant), Equals, alreadyPresentRdes+1) + + // Test the multiple ways of getting a RDE Types in both users. + obtainedRdeTypeBySysAdmin, err := systemAdministratorClient.GetRdeTypeById(createdRdeType.DefinedEntityType.ID) + check.Assert(err, IsNil) + check.Assert(*obtainedRdeTypeBySysAdmin.DefinedEntityType, DeepEquals, *createdRdeType.DefinedEntityType) + + // The RDE Type retrieved by the tenant should be the same as the retrieved by Sysadmin + obtainedRdeTypeByTenant, err := tenantUserClient.GetRdeTypeById(createdRdeType.DefinedEntityType.ID) + check.Assert(err, IsNil) + check.Assert(*obtainedRdeTypeByTenant.DefinedEntityType, DeepEquals, *obtainedRdeTypeBySysAdmin.DefinedEntityType) + + obtainedRdeTypeBySysAdmin, err = systemAdministratorClient.GetRdeType(createdRdeType.DefinedEntityType.Vendor, createdRdeType.DefinedEntityType.Nss, createdRdeType.DefinedEntityType.Version) + check.Assert(err, IsNil) + check.Assert(*obtainedRdeTypeBySysAdmin.DefinedEntityType, DeepEquals, *obtainedRdeTypeBySysAdmin.DefinedEntityType) + + // The RDE Type retrieved by the tenant should be the same as the retrieved by Sysadmin + obtainedRdeTypeByTenant, err = tenantUserClient.GetRdeType(createdRdeType.DefinedEntityType.Vendor, createdRdeType.DefinedEntityType.Nss, createdRdeType.DefinedEntityType.Version) + check.Assert(err, IsNil) + check.Assert(*obtainedRdeTypeByTenant.DefinedEntityType, DeepEquals, *obtainedRdeTypeBySysAdmin.DefinedEntityType) + + // We don't want to update the name nor the schema. It should populate them from the receiver object automatically + err = obtainedRdeTypeBySysAdmin.Update(types.DefinedEntityType{ + Description: rdeTypeToCreate.Description + "UpdatedByAdmin", + }) + check.Assert(err, IsNil) + check.Assert(obtainedRdeTypeBySysAdmin.DefinedEntityType.Description, Equals, rdeTypeToCreate.Description+"UpdatedByAdmin") + + testRdeCrudWithGivenType(check, obtainedRdeTypeBySysAdmin) + testRdeCrudAsTenant(check, obtainedRdeTypeByTenant.DefinedEntityType.Vendor, obtainedRdeTypeByTenant.DefinedEntityType.Nss, obtainedRdeTypeByTenant.DefinedEntityType.Version, vcd.client) + + // We delete it with Sysadmin + deletedId := createdRdeType.DefinedEntityType.ID + err = createdRdeType.Delete() + check.Assert(err, IsNil) + check.Assert(*createdRdeType.DefinedEntityType, DeepEquals, types.DefinedEntityType{}) + + _, err = systemAdministratorClient.GetRdeTypeById(deletedId) + check.Assert(err, NotNil) + check.Assert(strings.Contains(err.Error(), ErrorEntityNotFound.Error()), Equals, true) +} + +// testRdeCrudWithGivenType is a sub-section of Test_Rde that is focused on testing all RDE instances casuistics. +// This would be the viewpoint of a System admin as they can retrieve and manipulate RDE types. +func testRdeCrudWithGivenType(check *C, rdeType *DefinedEntityType) { + + // We are missing the mandatory field "foo" on purpose + rdeEntityJson := []byte(` + { + "bar": "stringValue1", + "prop2": { + "subprop1": "stringValue2", + "subprop2": [ + "stringValue3", + "stringValue4" + ] + } + }`) + + var unmarshaledRdeEntityJson map[string]interface{} + err := json.Unmarshal(rdeEntityJson, &unmarshaledRdeEntityJson) + check.Assert(err, IsNil) + + rde, err := rdeType.CreateRde(types.DefinedEntity{ + Name: check.TestName(), + ExternalId: "123", + Entity: unmarshaledRdeEntityJson, + }, nil) + check.Assert(err, IsNil) + check.Assert(rde.DefinedEntity.Name, Equals, check.TestName()) + check.Assert(*rde.DefinedEntity.State, Equals, "PRE_CREATED") + + // If we don't resolve the RDE, we cannot delete it + err = rde.Delete() + check.Assert(err, NotNil) + check.Assert(true, Equals, strings.Contains(err.Error(), "RDE_ENTITY_NOT_RESOLVED")) + + // Resolution should fail as we missed to add a mandatory field + err = rde.Resolve() + eTag := rde.Etag + check.Assert(err, IsNil) + // The RDE can be automatically deleted now as rde.Resolve() was called successfully + AddToCleanupListOpenApi(rde.DefinedEntity.ID, check.TestName(), types.OpenApiPathVersion1_0_0+types.OpenApiEndpointRdeEntities+rde.DefinedEntity.ID) + + check.Assert(*rde.DefinedEntity.State, Equals, "RESOLUTION_ERROR") + check.Assert(eTag, Not(Equals), "") + + // We amend it + unmarshaledRdeEntityJson["foo"] = map[string]interface{}{"key": "stringValue5"} + err = rde.Update(types.DefinedEntity{ + Entity: unmarshaledRdeEntityJson, + }) + check.Assert(err, IsNil) + check.Assert(*rde.DefinedEntity.State, Equals, "RESOLUTION_ERROR") + check.Assert(rde.Etag, Not(Equals), "") + check.Assert(rde.Etag, Not(Equals), eTag) + eTag = rde.Etag + + // This time it should resolve + err = rde.Resolve() + check.Assert(err, IsNil) + check.Assert(*rde.DefinedEntity.State, Equals, "RESOLVED") + check.Assert(rde.Etag, Not(Equals), "") + check.Assert(rde.Etag, Not(Equals), eTag) + + // Delete the RDE instance now that it's resolved + deletedId := rde.DefinedEntity.ID + err = rde.Delete() + check.Assert(err, IsNil) + check.Assert(*rde.DefinedEntity, DeepEquals, types.DefinedEntity{}) + + // RDE should not exist anymore + _, err = rdeType.GetRdeById(deletedId) + check.Assert(err, NotNil) + check.Assert(strings.Contains(err.Error(), ErrorEntityNotFound.Error()), Equals, true) +} + +// testRdeCrudAsTenant is a sub-section of Test_Rde that is focused on testing all RDE instances casuistics without specifying the +// RDE type. This would be the viewpoint of a tenant as they can't get RDE types. +func testRdeCrudAsTenant(check *C, vendor string, namespace string, version string, vcdClient *VCDClient) { + // We are missing the mandatory field "foo" on purpose + rdeEntityJson := []byte(` + { + "bar": "stringValue1", + "prop2": { + "subprop1": "stringValue2", + "subprop2": [ + "stringValue3", + "stringValue4" + ] + } + }`) + + var unmarshaledRdeEntityJson map[string]interface{} + err := json.Unmarshal(rdeEntityJson, &unmarshaledRdeEntityJson) + check.Assert(err, IsNil) + + rde, err := vcdClient.CreateRde(vendor, namespace, version, types.DefinedEntity{ + Name: check.TestName(), + ExternalId: "123", + Entity: unmarshaledRdeEntityJson, + }, nil) + check.Assert(err, IsNil) + check.Assert(rde.DefinedEntity.Name, Equals, check.TestName()) + check.Assert(*rde.DefinedEntity.State, Equals, "PRE_CREATED") + + // If we don't resolve the RDE, we cannot delete it + err = rde.Delete() + check.Assert(err, NotNil) + check.Assert(true, Equals, strings.Contains(err.Error(), "RDE_ENTITY_NOT_RESOLVED")) + + // Resolution should fail as we missed to add a mandatory field + err = rde.Resolve() + eTag := rde.Etag + check.Assert(err, IsNil) + check.Assert(*rde.DefinedEntity.State, Equals, "RESOLUTION_ERROR") + check.Assert(eTag, Not(Equals), "") + + // We amend it + unmarshaledRdeEntityJson["foo"] = map[string]interface{}{"key": "stringValue5"} + err = rde.Update(types.DefinedEntity{ + Entity: unmarshaledRdeEntityJson, + }) + check.Assert(err, IsNil) + check.Assert(*rde.DefinedEntity.State, Equals, "RESOLUTION_ERROR") + check.Assert(rde.Etag, Not(Equals), "") + check.Assert(rde.Etag, Not(Equals), eTag) + eTag = rde.Etag + + // This time it should resolve + err = rde.Resolve() + check.Assert(err, IsNil) + check.Assert(*rde.DefinedEntity.State, Equals, "RESOLVED") + check.Assert(rde.Etag, Not(Equals), "") + check.Assert(rde.Etag, Not(Equals), eTag) + + // The RDE can't be deleted until rde.Resolve() is called + AddToCleanupListOpenApi(rde.DefinedEntity.ID, check.TestName(), types.OpenApiPathVersion1_0_0+types.OpenApiEndpointRdeEntities+rde.DefinedEntity.ID) + + // Delete the RDE instance now that it's resolved + deletedId := rde.DefinedEntity.ID + err = rde.Delete() + check.Assert(err, IsNil) + check.Assert(*rde.DefinedEntity, DeepEquals, types.DefinedEntity{}) + check.Assert(rde.Etag, Equals, "") + + // RDE should not exist anymore + _, err = vcdClient.GetRdeById(deletedId) + check.Assert(err, NotNil) + check.Assert(strings.Contains(err.Error(), ErrorEntityNotFound.Error()), Equals, true) +} + +// loadRdeTypeSchemaFromTestResources loads the RDE schema present in the test-resources folder and unmarshals it +// into a map. Returns an error if something fails along the way. +func loadRdeTypeSchemaFromTestResources() (map[string]interface{}, error) { + // Load the RDE type schema + rdeFilePath := "../test-resources/rde_type.json" + _, err := os.Stat(rdeFilePath) + if os.IsNotExist(err) { + return nil, fmt.Errorf("unable to find RDE type file '%s': %s", rdeFilePath, err) + } + + rdeFile, err := os.OpenFile(filepath.Clean(rdeFilePath), os.O_RDONLY, 0400) + if err != nil { + return nil, fmt.Errorf("unable to open RDE type file '%s': %s", rdeFilePath, err) + } + defer safeClose(rdeFile) + + rdeSchema, err := io.ReadAll(rdeFile) + if err != nil { + return nil, fmt.Errorf("error reading RDE type file %s: %s", rdeFilePath, err) + } + + var unmarshaledJson map[string]interface{} + err = json.Unmarshal(rdeSchema, &unmarshaledJson) + if err != nil { + return nil, fmt.Errorf("could not unmarshal RDE type file %s: %s", rdeFilePath, err) + } + + return unmarshaledJson, nil +} + +// Test_RdeTypeBehavior tests the CRUD methods of RDE Types to create Behaviors, as a System administrator and tenant user. +// This test can be run with GOVCD_SKIP_VAPP_CREATION option enabled. +func (vcd *TestVCD) Test_RdeTypeBehavior(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointRdeTypeBehaviors) + + // Create a new RDE Type from scratch + unmarshaledRdeTypeSchema, err := loadRdeTypeSchemaFromTestResources() + check.Assert(err, IsNil) + check.Assert(true, Equals, len(unmarshaledRdeTypeSchema) > 0) + sanizitedTestName := strings.NewReplacer("_", "", ".", "").Replace(check.TestName()) + rdeType, err := vcd.client.CreateRdeType(&types.DefinedEntityType{ + Name: sanizitedTestName, + Description: "Created by " + check.TestName(), + Nss: "nss", + Version: "1.0.0", + Vendor: "vmware", + Schema: unmarshaledRdeTypeSchema, + Interfaces: []string{"urn:vcloud:interface:vmware:k8s:1.0.0"}, + }) + check.Assert(err, IsNil) + AddToCleanupListOpenApi(rdeType.DefinedEntityType.ID, check.TestName(), types.OpenApiPathVersion1_0_0+types.OpenApiEndpointRdeEntityTypes+rdeType.DefinedEntityType.ID) + defer func() { + err := rdeType.Delete() + check.Assert(err, IsNil) + }() + + // Get all the behaviors of the RDE Type. As it referenced the K8s Interface, it inherits its Behaviors, so it + // has one Behavior without anyone creating it. + originalBehaviors, err := rdeType.GetAllBehaviors(nil) + check.Assert(err, IsNil) + check.Assert(len(originalBehaviors), Equals, 1) + check.Assert(originalBehaviors[0].Name, Equals, "createKubeConfig") + check.Assert(originalBehaviors[0].Description, Equals, "Creates and returns a kubeconfig") + check.Assert(len(originalBehaviors[0].Execution), Equals, 2) + check.Assert(originalBehaviors[0].Execution["id"], Equals, "CreateKubeConfigActivity") + check.Assert(originalBehaviors[0].Execution["type"], Equals, "Activity") + + // Error getting non-existing Behaviors + _, err = rdeType.GetBehaviorById("urn:vcloud:behavior-type:notexist:notexist:notexist:9.9.9") + check.Assert(err, NotNil) + check.Assert(strings.Contains(err.Error(), "RDE_INVALID_BEHAVIOR_SCOPE"), Equals, true) + + _, err = rdeType.GetBehaviorByName("DoesNotExist") + check.Assert(err, NotNil) + check.Assert(strings.Contains(err.Error(), ErrorEntityNotFound.Error()), Equals, true) + + // Getting behaviors correctly + originalBehavior, err := rdeType.GetBehaviorById(originalBehaviors[0].ID) + check.Assert(err, IsNil) + check.Assert(originalBehavior, NotNil) + check.Assert(originalBehavior.Name, Equals, originalBehaviors[0].Name) + check.Assert(originalBehavior.Description, Equals, originalBehaviors[0].Description) + check.Assert(originalBehavior.Execution, DeepEquals, originalBehaviors[0].Execution) + + behavior2, err := rdeType.GetBehaviorByName(originalBehaviors[0].Name) + check.Assert(err, IsNil) + check.Assert(originalBehavior, NotNil) + check.Assert(originalBehavior, DeepEquals, behavior2) + + // Override the behavior + rdeTypeBehavior, err := rdeType.UpdateBehaviorOverride(types.Behavior{ + ID: originalBehavior.ID, + Description: originalBehavior.Description + "Overridden", + Execution: map[string]interface{}{ + "id": originalBehavior.Execution["id"].(string) + "Overridden", + "type": "noop", + }, + Name: "WillNotBeOverridden", + }) + check.Assert(err, IsNil) + check.Assert(rdeTypeBehavior.ID, Not(Equals), originalBehavior.ID) // Now that it is overridden, ID changes to behavior-type + check.Assert(rdeTypeBehavior.Ref, Equals, originalBehavior.ID) + check.Assert(rdeTypeBehavior.Description, Equals, originalBehavior.Description+"Overridden") + check.Assert(rdeTypeBehavior.Execution["id"], Equals, originalBehavior.Execution["id"].(string)+"Overridden") + check.Assert(rdeTypeBehavior.Execution["type"], Equals, "noop") + check.Assert(rdeTypeBehavior.Name, Equals, originalBehavior.Name) // Name can't be overridden + + // Check that it can be retrieved with new Behavior ID (generated by the override) and old one + retrOverridden, err := rdeType.GetBehaviorById(rdeTypeBehavior.ID) + check.Assert(err, IsNil) + check.Assert(retrOverridden, DeepEquals, rdeTypeBehavior) + + retrOverridden, err = rdeType.GetBehaviorById(rdeTypeBehavior.Ref) // Ref is the Interface Behavior ID + check.Assert(err, IsNil) + check.Assert(retrOverridden, DeepEquals, rdeTypeBehavior) + + testRdeTypeAccessControls(check, rdeType, retrOverridden) + + // We test Behavior invocation. Note that we invoke the overridden Behavior + // instead of the one from the Interface as the overridden is a dummy No-op that will finish OK all the time, + // as opposed as the original Activity. + testRdeBehaviorInvocation(check, rdeType, retrOverridden) + + // Delete the Behavior with original RDE Interface Behavior ID. It doesn't care if we use the original or the overridden ID, + // it is smart enough to delete the Behavior from the receiver Type. + err = rdeType.DeleteBehaviorOverride(originalBehavior.ID) + check.Assert(err, IsNil) + + // Check that the deletion was done correctly + originalBehaviors, err = rdeType.GetAllBehaviors(nil) + check.Assert(err, IsNil) + check.Assert(len(originalBehaviors), Equals, 1) // We still have 1: The original RDE Interface Behavior + check.Assert(originalBehaviors[0].ID, Not(Equals), retrOverridden.ID) // The ID should not be the overridden one as we deleted it + check.Assert(originalBehaviors[0].ID, Equals, originalBehavior.ID) // The ID should not be the overridden one as we deleted it +} + +// testRdeBehaviorInvocation tests that a Behavior can be invoked in a RDE of a given Type. +func testRdeBehaviorInvocation(check *C, rdeType *DefinedEntityType, behavior *types.Behavior) { + rdeEntityJson := []byte(` + { + "bar": "stringValue1", + "prop2": { + "subprop1": "stringValue2", + "subprop2": [ + "stringValue3", + "stringValue4" + ] + }, + "foo": { + "key": "stringValue5" + } + }`) + var unmarshaledRdeEntityJson map[string]interface{} + err := json.Unmarshal(rdeEntityJson, &unmarshaledRdeEntityJson) + check.Assert(err, IsNil) + + rde, err := rdeType.CreateRde(types.DefinedEntity{ + Name: check.TestName(), + Entity: unmarshaledRdeEntityJson, + }, nil) + check.Assert(err, IsNil) + + // RDE needs to be Resolved to ve invoked or deleted + err = rde.Resolve() + check.Assert(err, IsNil) + AddToCleanupListOpenApi(rde.DefinedEntity.ID, check.TestName(), types.OpenApiPathVersion1_0_0+types.OpenApiEndpointRdeEntities+rde.DefinedEntity.ID) + defer func() { + err := rde.Delete() + check.Assert(err, IsNil) + }() + + // We need to grant access to the Behavior to invoke it + err = rdeType.SetBehaviorAccessControls([]*types.BehaviorAccess{ + { + AccessLevelId: "urn:vcloud:accessLevel:FullControl", + BehaviorId: behavior.Ref, + }, + }) + check.Assert(err, IsNil) + + var invocation map[string]interface{} + err = rde.InvokeBehaviorAndMarshal(behavior.ID, types.BehaviorInvocation{ + Arguments: map[string]interface{}{}, + Metadata: map[string]interface{}{}, + }, &invocation) + check.Assert(err, IsNil) + check.Assert(len(invocation) > 0, Equals, true) + check.Assert(invocation["entityId"], Equals, rde.DefinedEntity.ID) +} + +func testRdeTypeAccessControls(check *C, rdeType *DefinedEntityType, behavior *types.Behavior) { + allAccCtrl, err := rdeType.GetAllBehaviorsAccessControls(nil) + check.Assert(err, IsNil) + check.Assert(len(allAccCtrl), Equals, 0) + + // Add the behavior access controls + behaviorAccess := &types.BehaviorAccess{ + AccessLevelId: "urn:vcloud:accessLevel:ReadOnly", + BehaviorId: behavior.ID, + } + err = rdeType.SetBehaviorAccessControls([]*types.BehaviorAccess{behaviorAccess}) + check.Assert(err, IsNil) + + allAccCtrl, err = rdeType.GetAllBehaviorsAccessControls(nil) + check.Assert(err, IsNil) + check.Assert(len(allAccCtrl), Equals, 1) + check.Assert(*allAccCtrl[0], DeepEquals, *behaviorAccess) + + // Update the behavior access controls + behaviorAccess = &types.BehaviorAccess{ + AccessLevelId: "urn:vcloud:accessLevel:ReadWrite", + BehaviorId: behavior.ID, + } + err = rdeType.SetBehaviorAccessControls([]*types.BehaviorAccess{behaviorAccess}) + check.Assert(err, IsNil) + + allAccCtrl, err = rdeType.GetAllBehaviorsAccessControls(nil) + check.Assert(err, IsNil) + check.Assert(len(allAccCtrl), Equals, 1) + check.Assert(*allAccCtrl[0], DeepEquals, *behaviorAccess) + + // Delete the behavior access controls + err = rdeType.SetBehaviorAccessControls([]*types.BehaviorAccess{}) + check.Assert(err, IsNil) + + var payload []*types.BehaviorAccess + // This one simulates a filtering that goes wrong and leaves "payload" nil + for _, acl := range allAccCtrl { + if acl.BehaviorId == "notExist" { + payload = append(payload, acl) + } + } + + err = rdeType.SetBehaviorAccessControls(payload) // payload is nil, it should be equivalent to set an empty slice of access controls + check.Assert(err, IsNil) + + allAccCtrl, err = rdeType.GetAllBehaviorsAccessControls(nil) + check.Assert(err, IsNil) + check.Assert(len(allAccCtrl), Equals, 0) +} diff --git a/govcd/defined_interface.go b/govcd/defined_interface.go new file mode 100644 index 000000000..f39ef4488 --- /dev/null +++ b/govcd/defined_interface.go @@ -0,0 +1,237 @@ +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "net/url" + "strings" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +const ( + labelDefinedInterface = "Defined Interface" + labelDefinedInterfaceBehavior = "Defined Interface Behavior" +) + +// DefinedInterface is a type for handling Defined Interfaces, from the Runtime Defined Entities framework, in VCD. +// This is often referred as Runtime Defined Entity Interface or RDE Interface in documentation. +type DefinedInterface struct { + DefinedInterface *types.DefinedInterface + client *Client +} + +// 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 (d DefinedInterface) wrap(inner *types.DefinedInterface) *DefinedInterface { + d.DefinedInterface = inner + return &d +} + +// CreateDefinedInterface creates a Defined Interface. +// Only System administrator can create Defined Interfaces. +func (vcdClient *VCDClient) CreateDefinedInterface(definedInterface *types.DefinedInterface) (*DefinedInterface, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeInterfaces, + entityLabel: labelDefinedInterface, + } + outerType := DefinedInterface{client: &vcdClient.Client} + return createOuterEntity(&vcdClient.Client, outerType, c, definedInterface) +} + +// GetAllDefinedInterfaces retrieves all Defined Interfaces. Query parameters can be supplied to perform additional filtering. +func (vcdClient *VCDClient) GetAllDefinedInterfaces(queryParameters url.Values) ([]*DefinedInterface, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeInterfaces, + entityLabel: labelDefinedInterface, + queryParameters: queryParameters, + } + + outerType := DefinedInterface{client: &vcdClient.Client} + return getAllOuterEntities(&vcdClient.Client, outerType, c) +} + +// GetDefinedInterface retrieves a single Defined Interface defined by its unique combination of vendor, nss and version. +func (vcdClient *VCDClient) GetDefinedInterface(vendor, nss, version string) (*DefinedInterface, error) { + queryParameters := url.Values{} + queryParameters.Add("filter", fmt.Sprintf("vendor==%s;nss==%s;version==%s", vendor, nss, version)) + interfaces, err := vcdClient.GetAllDefinedInterfaces(queryParameters) + if err != nil { + return nil, err + } + + if len(interfaces) == 0 { + return nil, fmt.Errorf("%s could not find the Defined Interface with vendor %s, nss %s and version %s", ErrorEntityNotFound, vendor, nss, version) + } + + if len(interfaces) > 1 { + return nil, fmt.Errorf("found more than 1 Defined Interface with vendor %s, nss %s and version %s", vendor, nss, version) + } + + return interfaces[0], nil +} + +// GetDefinedInterfaceById gets a Defined Interface identified by its unique URN. +func (vcdClient *VCDClient) GetDefinedInterfaceById(id string) (*DefinedInterface, error) { + c := crudConfig{ + entityLabel: labelDefinedInterface, + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeInterfaces, + endpointParams: []string{id}, + } + + outerType := DefinedInterface{client: &vcdClient.Client} + return getOuterEntity(&vcdClient.Client, outerType, c) +} + +// Update updates the receiver Defined Interface with the values given by the input. +// Only System administrator can update Defined Interfaces. +func (di *DefinedInterface) Update(definedInterface types.DefinedInterface) error { + if di.DefinedInterface.ID == "" { + return fmt.Errorf("ID of the receiver Defined Interface is empty") + } + + if definedInterface.ID != "" && definedInterface.ID != di.DefinedInterface.ID { + return fmt.Errorf("ID of the receiver Defined Interface and the input ID don't match") + } + + // We override these as they need to be always sent on updates + definedInterface.Version = di.DefinedInterface.Version + definedInterface.Nss = di.DefinedInterface.Nss + definedInterface.Vendor = di.DefinedInterface.Vendor + + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeInterfaces, + endpointParams: []string{di.DefinedInterface.ID}, + entityLabel: labelDefinedInterface, + } + resultDefinedInterface, err := updateInnerEntity(di.client, c, &definedInterface) + if err != nil { + return err + } + // Only if there was no error in request we overwrite pointer receiver as otherwise it would + // wipe out existing data + di.DefinedInterface = resultDefinedInterface + return err +} + +// Delete deletes the receiver Defined Interface. +// Only System administrator can delete Defined Interfaces. +func (di *DefinedInterface) Delete() error { + if di.DefinedInterface.ID == "" { + return fmt.Errorf("ID of the receiver Defined Interface is empty") + } + + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeInterfaces, + endpointParams: []string{di.DefinedInterface.ID}, + entityLabel: labelDefinedInterface, + } + + err := deleteEntityById(di.client, c) + if err != nil { + return err + } + + di.DefinedInterface = &types.DefinedInterface{} + return nil +} + +// AddBehavior adds a new Behavior to the receiver DefinedInterface. +// Only allowed if the Interface is not in use. +func (di *DefinedInterface) AddBehavior(behavior types.Behavior) (*types.Behavior, error) { + if di.DefinedInterface.ID == "" { + return nil, fmt.Errorf("ID of the receiver Defined Interface is empty") + } + + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeInterfaceBehaviors, + endpointParams: []string{di.DefinedInterface.ID}, + entityLabel: labelDefinedInterfaceBehavior, + } + return createInnerEntity(di.client, c, &behavior) +} + +// GetAllBehaviors retrieves all the Behaviors of the receiver Defined Interface. +func (di *DefinedInterface) GetAllBehaviors(queryParameters url.Values) ([]*types.Behavior, error) { + if di.DefinedInterface.ID == "" { + return nil, fmt.Errorf("ID of the receiver Defined Interface is empty") + } + return getAllBehaviors(di.client, di.DefinedInterface.ID, types.OpenApiEndpointRdeInterfaceBehaviors, queryParameters) +} + +// getAllBehaviors gets all the Behaviors from the object referenced by the input Object ID with the given OpenAPI endpoint. +func getAllBehaviors(client *Client, objectId, openApiEndpoint string, queryParameters url.Values) ([]*types.Behavior, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + openApiEndpoint, + entityLabel: labelDefinedInterfaceBehavior, + endpointParams: []string{objectId}, + queryParameters: queryParameters, + } + return getAllInnerEntities[types.Behavior](client, c) +} + +// GetBehaviorById retrieves a unique Behavior that belongs to the receiver Defined Interface and is determined by the +// input ID. +func (di *DefinedInterface) GetBehaviorById(id string) (*types.Behavior, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeInterfaceBehaviors, + endpointParams: []string{di.DefinedInterface.ID, id}, + entityLabel: labelDefinedInterfaceBehavior, + } + return getInnerEntity[types.Behavior](di.client, c) +} + +// GetBehaviorByName retrieves a unique Behavior that belongs to the receiver Defined Interface and is named after +// the input. +func (di *DefinedInterface) GetBehaviorByName(name string) (*types.Behavior, error) { + behaviors, err := di.GetAllBehaviors(nil) + if err != nil { + return nil, fmt.Errorf("could not get the Behaviors of the Defined Interface with ID '%s': %s", di.DefinedInterface.ID, err) + } + label := fmt.Sprintf("Defined Interface Behavior with name '%s' in Defined Interface with ID '%s': %s", name, di.DefinedInterface.ID, ErrorEntityNotFound) + return localFilterOneOrError(label, behaviors, "Name", name) +} + +// UpdateBehavior updates a Behavior specified by the input. +func (di *DefinedInterface) UpdateBehavior(behavior types.Behavior) (*types.Behavior, error) { + if di.DefinedInterface.ID == "" { + return nil, fmt.Errorf("ID of the receiver Defined Interface is empty") + } + if behavior.ID == "" { + return nil, fmt.Errorf("ID of the Behavior to update is empty") + } + + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeInterfaceBehaviors, + endpointParams: []string{di.DefinedInterface.ID, behavior.ID}, + entityLabel: labelDefinedInterfaceBehavior, + } + return updateInnerEntity(di.client, c, &behavior) +} + +// DeleteBehavior removes a Behavior specified by its ID from the receiver Defined Interface. +func (di *DefinedInterface) DeleteBehavior(behaviorId string) error { + if di.DefinedInterface.ID == "" { + return fmt.Errorf("ID of the receiver Defined Interface is empty") + } + + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeInterfaceBehaviors, + endpointParams: []string{di.DefinedInterface.ID, behaviorId}, + entityLabel: labelDefinedInterfaceBehavior, + } + return deleteEntityById(di.client, c) +} + +// amendRdeApiError fixes a wrong type of error returned by VCD API <= v36.0 on GET operations +// when the defined interface does not exist. +func amendRdeApiError(client *Client, err error) error { + if client.APIClientVersionIs("<= 36.0") && err != nil && strings.Contains(err.Error(), "does not exist") { + return fmt.Errorf("%s: %s", ErrorEntityNotFound.Error(), err) + } + return err +} diff --git a/govcd/defined_interface_test.go b/govcd/defined_interface_test.go new file mode 100644 index 000000000..2eb320765 --- /dev/null +++ b/govcd/defined_interface_test.go @@ -0,0 +1,225 @@ +//go:build functional || openapi || rde || ALL + +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "strings" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +// Test_DefinedInterface tests the CRUD behavior of Defined Interfaces as a System administrator and tenant user. +// This test can be run with GOVCD_SKIP_VAPP_CREATION option enabled. +func (vcd *TestVCD) Test_DefinedInterface(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointRdeInterfaces) + if len(vcd.config.Tenants) == 0 { + check.Skip("skipping as there is no configured tenant users") + } + + // Creates the clients for the System admin and the Tenant user + systemAdministratorClient := vcd.client + tenantUserClient := NewVCDClient(vcd.client.Client.VCDHREF, true) + err := tenantUserClient.Authenticate(vcd.config.Tenants[0].User, vcd.config.Tenants[0].Password, vcd.config.Tenants[0].SysOrg) + check.Assert(err, IsNil) + + // First, it checks how many exist already, as VCD contains some pre-defined ones. + allDefinedInterfacesBySysAdmin, err := systemAdministratorClient.GetAllDefinedInterfaces(nil) + check.Assert(err, IsNil) + alreadyPresentRDEs := len(allDefinedInterfacesBySysAdmin) + + allDefinedInterfacesByTenant, err := tenantUserClient.GetAllDefinedInterfaces(nil) + check.Assert(err, IsNil) + check.Assert(len(allDefinedInterfacesByTenant), Equals, len(allDefinedInterfacesBySysAdmin)) + + // Then we create a new Defined Interface with System administrator. We replace the dots in both + // nss and name as API is quirky at versions of VCD <= 10.3.0 + dummyRde := &types.DefinedInterface{ + Name: strings.ReplaceAll(check.TestName()+"name", ".", ""), + Nss: strings.ReplaceAll(check.TestName()+"nss", ".", ""), + Version: "1.2.3", + Vendor: "vmware", + } + newDefinedInterfaceFromSysAdmin, err := systemAdministratorClient.CreateDefinedInterface(dummyRde) + check.Assert(err, IsNil) + check.Assert(newDefinedInterfaceFromSysAdmin, NotNil) + check.Assert(newDefinedInterfaceFromSysAdmin.DefinedInterface.Name, Equals, dummyRde.Name) + check.Assert(newDefinedInterfaceFromSysAdmin.DefinedInterface.Nss, Equals, dummyRde.Nss) + check.Assert(newDefinedInterfaceFromSysAdmin.DefinedInterface.Version, Equals, dummyRde.Version) + check.Assert(newDefinedInterfaceFromSysAdmin.DefinedInterface.Vendor, Equals, dummyRde.Vendor) + check.Assert(newDefinedInterfaceFromSysAdmin.DefinedInterface.IsReadOnly, Equals, dummyRde.IsReadOnly) + AddToCleanupListOpenApi(newDefinedInterfaceFromSysAdmin.DefinedInterface.ID, check.TestName(), types.OpenApiPathVersion1_0_0+types.OpenApiEndpointRdeInterfaces+newDefinedInterfaceFromSysAdmin.DefinedInterface.ID) + + // Tenants can't create Defined Interfaces. We replace the dots in both + // nss and name as API is quirky at versions of VCD <= 10.3.0 + nilDefinedInterface, err := tenantUserClient.CreateDefinedInterface(&types.DefinedInterface{ + Name: strings.ReplaceAll(check.TestName()+"name2", ".", ""), + Nss: strings.ReplaceAll(check.TestName()+"name2", ".", ""), + Version: "4.5.6", + Vendor: "vmware", + }) + check.Assert(err, NotNil) + check.Assert(nilDefinedInterface, IsNil) + check.Assert(strings.Contains(err.Error(), "ACCESS_TO_RESOURCE_IS_FORBIDDEN"), Equals, true) + + // As we created a new one, we check the new count is correct in both System admin and Tenant user + allDefinedInterfacesBySysAdmin, err = systemAdministratorClient.GetAllDefinedInterfaces(nil) + check.Assert(err, IsNil) + check.Assert(len(allDefinedInterfacesBySysAdmin), Equals, alreadyPresentRDEs+1) + + allDefinedInterfacesByTenant, err = tenantUserClient.GetAllDefinedInterfaces(nil) + check.Assert(err, IsNil) + check.Assert(len(allDefinedInterfacesByTenant), Equals, len(allDefinedInterfacesBySysAdmin)) + + // Test the multiple ways of getting a Defined Interface in both users. + obtainedDefinedInterface, err := systemAdministratorClient.GetDefinedInterfaceById(newDefinedInterfaceFromSysAdmin.DefinedInterface.ID) + check.Assert(err, IsNil) + check.Assert(*obtainedDefinedInterface.DefinedInterface, DeepEquals, *newDefinedInterfaceFromSysAdmin.DefinedInterface) + + obtainedDefinedInterface, err = tenantUserClient.GetDefinedInterfaceById(newDefinedInterfaceFromSysAdmin.DefinedInterface.ID) + check.Assert(err, IsNil) + check.Assert(*obtainedDefinedInterface.DefinedInterface, DeepEquals, *newDefinedInterfaceFromSysAdmin.DefinedInterface) + + obtainedDefinedInterface2, err := systemAdministratorClient.GetDefinedInterface(obtainedDefinedInterface.DefinedInterface.Vendor, obtainedDefinedInterface.DefinedInterface.Nss, obtainedDefinedInterface.DefinedInterface.Version) + check.Assert(err, IsNil) + check.Assert(*obtainedDefinedInterface2.DefinedInterface, DeepEquals, *obtainedDefinedInterface.DefinedInterface) + + obtainedDefinedInterface2, err = tenantUserClient.GetDefinedInterface(obtainedDefinedInterface.DefinedInterface.Vendor, obtainedDefinedInterface.DefinedInterface.Nss, obtainedDefinedInterface.DefinedInterface.Version) + check.Assert(err, IsNil) + check.Assert(*obtainedDefinedInterface2.DefinedInterface, DeepEquals, *obtainedDefinedInterface.DefinedInterface) + + // Update the Defined Interface as System administrator + err = newDefinedInterfaceFromSysAdmin.Update(types.DefinedInterface{ + Name: dummyRde.Name + "3", // Only name can be updated + }) + check.Assert(err, IsNil) + check.Assert(newDefinedInterfaceFromSysAdmin.DefinedInterface.Name, Equals, dummyRde.Name+"3") + + // This one was obtained by the tenant, so it shouldn't be updatable + err = obtainedDefinedInterface2.Update(types.DefinedInterface{ + Name: dummyRde.Name + "4", + }) + check.Assert(err, NotNil) + check.Assert(strings.Contains(err.Error(), "ACCESS_TO_RESOURCE_IS_FORBIDDEN"), Equals, true) + + // This one was obtained by the tenant, so it shouldn't be deletable + err = obtainedDefinedInterface2.Delete() + check.Assert(err, NotNil) + check.Assert(strings.Contains(err.Error(), "ACCESS_TO_RESOURCE_IS_FORBIDDEN"), Equals, true) + + // We perform the actual removal with the System administrator + deletedId := newDefinedInterfaceFromSysAdmin.DefinedInterface.ID + err = newDefinedInterfaceFromSysAdmin.Delete() + check.Assert(err, IsNil) + check.Assert(*newDefinedInterfaceFromSysAdmin.DefinedInterface, DeepEquals, types.DefinedInterface{}) + + _, err = systemAdministratorClient.GetDefinedInterfaceById(deletedId) + check.Assert(err, NotNil) + check.Assert(strings.Contains(err.Error(), ErrorEntityNotFound.Error()), Equals, true) +} + +// Test_DefinedInterfaceBehavior tests the CRUD methods of Defined Interfaces to create Behaviors. +// This test can be run with GOVCD_SKIP_VAPP_CREATION option enabled. +func (vcd *TestVCD) Test_DefinedInterfaceBehavior(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointRdeInterfaceBehaviors) + + // Create a new Defined Interface with dummy values, so we can test behaviors on it + sanizitedTestName := strings.NewReplacer("_", "", ".", "").Replace(check.TestName()) + di, err := vcd.client.CreateDefinedInterface(&types.DefinedInterface{ + Name: sanizitedTestName, + Nss: "nss", + Version: "1.0.0", + Vendor: "vmware", + }) + check.Assert(err, IsNil) + AddToCleanupListOpenApi(di.DefinedInterface.ID, check.TestName(), types.OpenApiPathVersion1_0_0+types.OpenApiEndpointRdeInterfaces+di.DefinedInterface.ID) + defer func() { + err := di.Delete() + check.Assert(err, IsNil) + }() + + // Create a new Behavior payload with an Activity type. + behaviorPayload := types.Behavior{ + Name: sanizitedTestName, + Description: "Generated by " + check.TestName(), + Execution: map[string]interface{}{ + "id": "TestActivity", + "type": "Activity", + }, + } + behavior, err := di.AddBehavior(behaviorPayload) + check.Assert(err, IsNil) + check.Assert(behavior.Name, Equals, behaviorPayload.Name) + check.Assert(behavior.Description, Equals, behaviorPayload.Description) + check.Assert(behavior.Ref, Equals, fmt.Sprintf("urn:vcloud:behavior-interface:%s:%s:%s:%s", behaviorPayload.Name, di.DefinedInterface.Vendor, di.DefinedInterface.Nss, di.DefinedInterface.Version)) + check.Assert(behavior.ID, Equals, behavior.Ref) + + // Try to add the same behavior again. + _, err = di.AddBehavior(behaviorPayload) + check.Assert(err, NotNil) + check.Assert(strings.Contains(err.Error(), "RDE_BEHAVIOR_ALREADY_EXISTS"), Equals, true) + + // We check that the Behaviors can be retrieved + allBehaviors, err := di.GetAllBehaviors(nil) + check.Assert(err, IsNil) + check.Assert(1, Equals, len(allBehaviors)) + check.Assert(allBehaviors[0], DeepEquals, behavior) + + // Error getting non-existing Behaviors + _, err = di.GetBehaviorById("urn:vcloud:behavior-interface:notexist:notexist:notexist:9.9.9") + check.Assert(err, NotNil) + check.Assert(strings.Contains(err.Error(), ErrorEntityNotFound.Error()), Equals, true) + + _, err = di.GetBehaviorByName("DoesNotExist") + check.Assert(err, NotNil) + check.Assert(strings.Contains(err.Error(), ErrorEntityNotFound.Error()), Equals, true) + + // Getting behaviors correctly + retrievedBehavior, err := di.GetBehaviorById(behavior.ID) + check.Assert(err, IsNil) + check.Assert(retrievedBehavior, NotNil) + check.Assert(retrievedBehavior.Name, Equals, behavior.Name) + check.Assert(retrievedBehavior.Description, Equals, behavior.Description) + check.Assert(retrievedBehavior.Execution, DeepEquals, behavior.Execution) + + retrievedBehavior2, err := di.GetBehaviorByName(behavior.Name) + check.Assert(err, IsNil) + check.Assert(retrievedBehavior, NotNil) + check.Assert(retrievedBehavior, DeepEquals, retrievedBehavior2) + + updatePayload := types.Behavior{ + Description: "Updated description", + Execution: map[string]interface{}{ + "id": "TestActivityUpdated", + "type": "Activity", + }, + Ref: "notGoingToUpdate1", + Name: "notGoingToUpdate2", + } + _, err = di.UpdateBehavior(updatePayload) + check.Assert(err, NotNil) + check.Assert(err.Error(), Equals, "ID of the Behavior to update is empty") + + updatePayload.ID = retrievedBehavior.ID + updatedBehavior, err := di.UpdateBehavior(updatePayload) + check.Assert(err, IsNil) + check.Assert(updatedBehavior.ID, Equals, retrievedBehavior.ID) + check.Assert(updatedBehavior.Ref, Equals, retrievedBehavior.Ref) // This cannot be updated + check.Assert(updatedBehavior.Name, Equals, retrievedBehavior.Name) // This cannot be updated + check.Assert(updatedBehavior.Execution, DeepEquals, updatePayload.Execution) + check.Assert(updatedBehavior.Description, Equals, updatePayload.Description) + + err = di.DeleteBehavior(behavior.ID) + check.Assert(err, IsNil) +} diff --git a/govcd/disk.go b/govcd/disk.go index 67a3411c4..61f12f221 100644 --- a/govcd/disk.go +++ b/govcd/disk.go @@ -43,10 +43,6 @@ func NewDiskRecord(cli *Client) *DiskRecord { } } -// While theoretically we can use smaller amounts, there is an issue when updating -// disks with size < 1MB -const MinimumDiskSize int64 = 1048576 // = 1Mb - // Create an independent disk in VDC // Reference: vCloud API Programming Guide for Service Providers vCloud API 30.0 PDF Page 102 - 103, // https://vdc-download.vmware.com/vmwb-repository/dcr-public/1b6cf07d-adb3-4dba-8c47-9c1c92b04857/ @@ -54,17 +50,13 @@ const MinimumDiskSize int64 = 1048576 // = 1Mb func (vdc *Vdc) CreateDisk(diskCreateParams *types.DiskCreateParams) (Task, error) { util.Logger.Printf("[TRACE] Create disk, name: %s, size: %d \n", diskCreateParams.Disk.Name, - diskCreateParams.Disk.Size, + diskCreateParams.Disk.SizeMb, ) if diskCreateParams.Disk.Name == "" { return Task{}, fmt.Errorf("disk name is required") } - if diskCreateParams.Disk.Size < MinimumDiskSize { - return Task{}, fmt.Errorf("disk size should be greater than or equal to 1Mb") - } - var err error var createDiskLink *types.Link @@ -101,6 +93,9 @@ func (vdc *Vdc) CreateDisk(diskCreateParams *types.DiskCreateParams) (Task, erro return Task{}, errors.New("error cannot find disk creation task in API response") } task := NewTask(vdc.client) + if disk.Disk.Tasks == nil || len(disk.Disk.Tasks.Task) == 0 { + return Task{}, fmt.Errorf("no task found after disk %s creation", diskCreateParams.Disk.Name) + } task.Task = disk.Disk.Tasks.Task[0] util.Logger.Printf("[TRACE] AFTER CREATE DISK\n %s\n", prettyDisk(*disk.Disk)) @@ -119,7 +114,7 @@ func (vdc *Vdc) CreateDisk(diskCreateParams *types.DiskCreateParams) (Task, erro func (disk *Disk) Update(newDiskInfo *types.Disk) (Task, error) { util.Logger.Printf("[TRACE] Update disk, name: %s, size: %d, HREF: %s \n", newDiskInfo.Name, - newDiskInfo.Size, + newDiskInfo.SizeMb, disk.Disk.HREF, ) @@ -129,10 +124,6 @@ func (disk *Disk) Update(newDiskInfo *types.Disk) (Task, error) { return Task{}, fmt.Errorf("disk name is required") } - if newDiskInfo.Size < MinimumDiskSize { - return Task{}, fmt.Errorf("disk size should be greater than or equal to 1Mb") - } - // Verify the independent disk is not connected to any VM vmRef, err := disk.AttachedVM() if err != nil { @@ -166,7 +157,7 @@ func (disk *Disk) Update(newDiskInfo *types.Disk) (Task, error) { xmlPayload := &types.Disk{ Xmlns: types.XMLNamespaceVCloud, Description: newDiskInfo.Description, - Size: newDiskInfo.Size, + SizeMb: newDiskInfo.SizeMb, Name: newDiskInfo.Name, StorageProfile: newDiskInfo.StorageProfile, Owner: newDiskInfo.Owner, @@ -226,11 +217,10 @@ func (disk *Disk) Delete() (Task, error) { // Refresh the disk information by disk href func (disk *Disk) Refresh() error { - util.Logger.Printf("[TRACE] Disk refresh, HREF: %s\n", disk.Disk.HREF) - if disk.Disk == nil || disk.Disk.HREF == "" { return fmt.Errorf("cannot refresh, Object is empty") } + util.Logger.Printf("[TRACE] Disk refresh, HREF: %s\n", disk.Disk.HREF) unmarshalledDisk := &types.Disk{} @@ -286,12 +276,12 @@ func (disk *Disk) AttachedVM() (*types.Reference, error) { } // If disk is not attached to any VM - if vms.VmReference == nil { + if vms.VmReference == nil || len(vms.VmReference) == 0 { return nil, nil } // An independent disk can be attached to at most one virtual machine so return the first result of VM reference - return vms.VmReference, nil + return vms.VmReference[0], nil } // Find an independent disk by disk href in VDC @@ -329,7 +319,8 @@ func (vdc *Vdc) QueryDisk(diskName string) (DiskRecord, error) { typeMedia = "adminDisk" } - results, err := vdc.QueryWithNotEncodedParams(nil, map[string]string{"type": typeMedia, "filter": "name==" + url.QueryEscape(diskName), "filterEncoded": "true"}) + results, err := vdc.QueryWithNotEncodedParams(nil, map[string]string{"type": typeMedia, + "filter": "name==" + url.QueryEscape(diskName) + ";vdc==" + vdc.vdcId(), "filterEncoded": "true"}) if err != nil { return DiskRecord{}, fmt.Errorf("error querying disk %s", err) } @@ -362,7 +353,8 @@ func (vdc *Vdc) QueryDisks(diskName string) (*[]*types.DiskRecordType, error) { typeMedia = "adminDisk" } - results, err := vdc.QueryWithNotEncodedParams(nil, map[string]string{"type": typeMedia, "filter": "name==" + url.QueryEscape(diskName), "filterEncoded": "true"}) + results, err := vdc.QueryWithNotEncodedParams(nil, map[string]string{"type": typeMedia, + "filter": "name==" + url.QueryEscape(diskName) + ";vdc==" + vdc.vdcId(), "filterEncoded": "true"}) if err != nil { return nil, fmt.Errorf("error querying disks %s", err) } @@ -383,8 +375,8 @@ func (vdc *Vdc) GetDiskByHref(diskHref string) (*Disk, error) { Disk := NewDisk(vdc.client) _, err := vdc.client.ExecuteRequest(diskHref, http.MethodGet, - "", "error retrieving Disk: %#v", nil, Disk.Disk) - if err != nil && strings.Contains(err.Error(), "MajorErrorCode:403") { + "", "error retrieving Disk: %s", nil, Disk.Disk) + if err != nil && (strings.Contains(err.Error(), "MajorErrorCode:403") || strings.Contains(err.Error(), "does not exist")) { return nil, ErrorEntityNotFound } if err != nil { @@ -442,3 +434,50 @@ func (vdc *Vdc) GetDiskById(diskId string, refresh bool) (*Disk, error) { } return nil, ErrorEntityNotFound } + +// Get a VMs HREFs that is attached to the disk +// An independent disk can be attached to at most one virtual machine. +// If the disk isn't attached to any VM, return empty slice. +// Otherwise return the list of VMs HREFs. +func (disk *Disk) GetAttachedVmsHrefs() ([]string, error) { + util.Logger.Printf("[TRACE] GetAttachedVmsHrefs, HREF: %s\n", disk.Disk.HREF) + + var vmHrefs []string + + var attachedVMsLink *types.Link + + // Find the proper link for request + for _, diskLink := range disk.Disk.Link { + if diskLink.Type == types.MimeVMs { + util.Logger.Printf("[TRACE] GetAttachedVmsHrefs - found the proper link for request, HREF: %s, name: %s, type: %s,id: %s, rel: %s \n", + diskLink.HREF, diskLink.Name, diskLink.Type, diskLink.ID, diskLink.Rel) + + attachedVMsLink = diskLink + break + } + } + + if attachedVMsLink == nil { + return nil, fmt.Errorf("error GetAttachedVmsHrefs - could not find request URL for attached vm in disk Link") + } + + // Decode request + var vms = new(types.Vms) + + _, err := disk.client.ExecuteRequest(attachedVMsLink.HREF, http.MethodGet, + attachedVMsLink.Type, "error GetAttachedVmsHrefs - error getting attached VMs: %s", nil, vms) + if err != nil { + return nil, err + } + + // If disk is not attached to any VM + if vms.VmReference == nil || len(vms.VmReference) == 0 { + return nil, nil + } + + for _, value := range vms.VmReference { + vmHrefs = append(vmHrefs, value.HREF) + } + + return vmHrefs, nil +} diff --git a/govcd/disk_test.go b/govcd/disk_test.go index c787203aa..487be583c 100644 --- a/govcd/disk_test.go +++ b/govcd/disk_test.go @@ -1,4 +1,4 @@ -// +build disk functional ALL +//go:build disk || functional || ALL /* * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -8,6 +8,7 @@ package govcd import ( "fmt" + "strings" . "gopkg.in/check.v1" @@ -24,16 +25,12 @@ func (vcd *TestVCD) Test_NewDisk(check *C) { // Test create independent disk func (vcd *TestVCD) Test_CreateDisk(check *C) { - if vcd.config.VCD.Disk.Size == 0 { - check.Skip("skipping test because disk size is 0") - } - fmt.Printf("Running: %s\n", check.TestName()) // Create disk diskCreateParamsDisk := &types.Disk{ Name: TestCreateDisk, - Size: vcd.config.VCD.Disk.Size, + SizeMb: 11, Description: TestCreateDisk, } @@ -58,27 +55,24 @@ func (vcd *TestVCD) Test_CreateDisk(check *C) { disk, err := vcd.vdc.GetDiskByHref(diskHREF) check.Assert(err, IsNil) check.Assert(disk.Disk.Name, Equals, diskCreateParamsDisk.Name) - check.Assert(disk.Disk.Size, Equals, diskCreateParamsDisk.Size) + check.Assert(disk.Disk.SizeMb, Equals, diskCreateParamsDisk.SizeMb) check.Assert(disk.Disk.Description, Equals, diskCreateParamsDisk.Description) + if vcd.client.Client.APIVCDMaxVersionIs(">= 36") { + check.Assert(disk.Disk.UUID, Not(Equals), "") + check.Assert(disk.Disk.SharingType, Equals, "None") + check.Assert(disk.Disk.Encrypted, Equals, false) + } } // Test update independent disk func (vcd *TestVCD) Test_UpdateDisk(check *C) { - if vcd.config.VCD.Disk.Size == 0 { - check.Skip("skipping test because disk size is 0") - } - - if vcd.config.VCD.Disk.SizeForUpdate <= 0 { - check.Skip("skipping test because disk update size is <= 0") - } - fmt.Printf("Running: %s\n", check.TestName()) // Create disk diskCreateParamsDisk := &types.Disk{ Name: TestUpdateDisk, - Size: vcd.config.VCD.Disk.Size, + SizeMb: 99, Description: TestUpdateDisk, } @@ -103,13 +97,13 @@ func (vcd *TestVCD) Test_UpdateDisk(check *C) { disk, err := vcd.vdc.GetDiskByHref(diskHREF) check.Assert(err, IsNil) check.Assert(disk.Disk.Name, Equals, diskCreateParamsDisk.Name) - check.Assert(disk.Disk.Size, Equals, diskCreateParamsDisk.Size) + check.Assert(disk.Disk.SizeMb, Equals, diskCreateParamsDisk.SizeMb) check.Assert(disk.Disk.Description, Equals, diskCreateParamsDisk.Description) // Update disk newDiskInfo := &types.Disk{ Name: TestUpdateDisk, - Size: vcd.config.VCD.Disk.SizeForUpdate, + SizeMb: 102, Description: TestUpdateDisk + "_Update", } @@ -122,17 +116,13 @@ func (vcd *TestVCD) Test_UpdateDisk(check *C) { err = disk.Refresh() check.Assert(err, IsNil) check.Assert(disk.Disk.Name, Equals, newDiskInfo.Name) - check.Assert(disk.Disk.Size, Equals, newDiskInfo.Size) + check.Assert(disk.Disk.SizeMb, Equals, newDiskInfo.SizeMb) check.Assert(disk.Disk.Description, Equals, newDiskInfo.Description) } // Test delete independent disk func (vcd *TestVCD) Test_DeleteDisk(check *C) { - if vcd.config.VCD.Disk.Size == 0 { - check.Skip("skipping test because disk size is 0") - } - fmt.Printf("Running: %s\n", check.TestName()) var err error @@ -140,7 +130,7 @@ func (vcd *TestVCD) Test_DeleteDisk(check *C) { // Create disk diskCreateParamsDisk := &types.Disk{ Name: TestDeleteDisk, - Size: vcd.config.VCD.Disk.Size, + SizeMb: 1, Description: TestDeleteDisk, } @@ -165,7 +155,7 @@ func (vcd *TestVCD) Test_DeleteDisk(check *C) { disk, err := vcd.vdc.GetDiskByHref(diskHREF) check.Assert(err, IsNil) check.Assert(disk.Disk.Name, Equals, diskCreateParamsDisk.Name) - check.Assert(disk.Disk.Size, Equals, diskCreateParamsDisk.Size) + check.Assert(disk.Disk.SizeMb, Equals, diskCreateParamsDisk.SizeMb) check.Assert(disk.Disk.Description, Equals, diskCreateParamsDisk.Description) // Delete disk @@ -178,20 +168,12 @@ func (vcd *TestVCD) Test_DeleteDisk(check *C) { // Test refresh independent disk info func (vcd *TestVCD) Test_RefreshDisk(check *C) { - if vcd.config.VCD.Disk.Size <= 0 { - check.Skip("skipping test because disk size is 0") - } - - if vcd.config.VCD.Disk.SizeForUpdate <= 0 { - check.Skip("skipping test because disk update size is <= 0") - } - fmt.Printf("Running: %s\n", check.TestName()) // Create disk diskCreateParamsDisk := &types.Disk{ Name: TestRefreshDisk, - Size: vcd.config.VCD.Disk.Size, + SizeMb: 43, Description: TestRefreshDisk, } @@ -216,13 +198,13 @@ func (vcd *TestVCD) Test_RefreshDisk(check *C) { disk, err := vcd.vdc.GetDiskByHref(diskHREF) check.Assert(err, IsNil) check.Assert(disk.Disk.Name, Equals, diskCreateParamsDisk.Name) - check.Assert(disk.Disk.Size, Equals, diskCreateParamsDisk.Size) + check.Assert(disk.Disk.SizeMb, Equals, diskCreateParamsDisk.SizeMb) check.Assert(disk.Disk.Description, Equals, diskCreateParamsDisk.Description) // Update disk newDiskInfo := &types.Disk{ Name: TestRefreshDisk, - Size: vcd.config.VCD.Disk.SizeForUpdate, + SizeMb: 43, Description: TestRefreshDisk + "_Update", } @@ -235,18 +217,13 @@ func (vcd *TestVCD) Test_RefreshDisk(check *C) { err = disk.Refresh() check.Assert(err, IsNil) check.Assert(disk.Disk.Name, Equals, newDiskInfo.Name) - check.Assert(disk.Disk.Size, Equals, newDiskInfo.Size) + check.Assert(disk.Disk.SizeMb, Equals, newDiskInfo.SizeMb) check.Assert(disk.Disk.Description, Equals, newDiskInfo.Description) } // Test find disk attached VM func (vcd *TestVCD) Test_AttachedVMDisk(check *C) { - - if vcd.config.VCD.Disk.Size <= 0 { - check.Skip("skipping test because disk size is 0") - } - if vcd.skipVappTests { check.Skip("skipping test because vApp wasn't properly created") } @@ -273,7 +250,7 @@ func (vcd *TestVCD) Test_AttachedVMDisk(check *C) { // Create disk diskCreateParamsDisk := &types.Disk{ Name: TestAttachedVMDisk, - Size: vcd.config.VCD.Disk.Size, + SizeMb: 210, Description: TestAttachedVMDisk, } @@ -298,7 +275,7 @@ func (vcd *TestVCD) Test_AttachedVMDisk(check *C) { disk, err := vcd.vdc.GetDiskByHref(diskHREF) check.Assert(err, IsNil) check.Assert(disk.Disk.Name, Equals, diskCreateParamsDisk.Name) - check.Assert(disk.Disk.Size, Equals, diskCreateParamsDisk.Size) + check.Assert(disk.Disk.SizeMb, Equals, diskCreateParamsDisk.SizeMb) check.Assert(disk.Disk.Description, Equals, diskCreateParamsDisk.Description) // Attach disk @@ -318,6 +295,13 @@ func (vcd *TestVCD) Test_AttachedVMDisk(check *C) { check.Assert(vmRef, NotNil) check.Assert(vmRef.Name, Equals, vm.VM.Name) + // Get attached VM + vmHrefs, err := disk.GetAttachedVmsHrefs() + check.Assert(err, IsNil) + check.Assert(vmHrefs, NotNil) + check.Assert(len(vmHrefs), Equals, 1) + check.Assert(vmHrefs[0], Equals, vm.VM.HREF) + // Detach disk err = vcd.detachIndependentDisk(Disk{disk.Disk, &vcd.client.Client}) check.Assert(err, IsNil) @@ -325,16 +309,12 @@ func (vcd *TestVCD) Test_AttachedVMDisk(check *C) { // Test find Disk by Href in VDC struct func (vcd *TestVCD) Test_VdcFindDiskByHREF(check *C) { - if vcd.config.VCD.Disk.Size <= 0 { - check.Skip("skipping test because disk size is 0") - } - fmt.Printf("Running: %s\n", check.TestName()) // Create disk diskCreateParamsDisk := &types.Disk{ Name: TestVdcFindDiskByHREF, - Size: vcd.config.VCD.Disk.Size, + SizeMb: 2, Description: TestVdcFindDiskByHREF, } @@ -360,23 +340,19 @@ func (vcd *TestVCD) Test_VdcFindDiskByHREF(check *C) { check.Assert(err, IsNil) check.Assert(disk.Disk.Name, Equals, diskCreateParamsDisk.Name) - check.Assert(disk.Disk.Size, Equals, diskCreateParamsDisk.Size) + check.Assert(disk.Disk.SizeMb, Equals, diskCreateParamsDisk.SizeMb) check.Assert(disk.Disk.Description, Equals, diskCreateParamsDisk.Description) } // Test find disk by href and vdc client func (vcd *TestVCD) Test_FindDiskByHREF(check *C) { - if vcd.config.VCD.Disk.Size <= 0 { - check.Skip("skipping test because disk size is 0") - } - fmt.Printf("Running: %s\n", check.TestName()) // Create disk diskCreateParamsDisk := &types.Disk{ Name: TestFindDiskByHREF, - Size: vcd.config.VCD.Disk.Size, + SizeMb: 3, Description: TestFindDiskByHREF, } @@ -402,7 +378,7 @@ func (vcd *TestVCD) Test_FindDiskByHREF(check *C) { check.Assert(err, IsNil) check.Assert(disk.Disk.Name, Equals, diskCreateParamsDisk.Name) - check.Assert(disk.Disk.Size, Equals, diskCreateParamsDisk.Size) + check.Assert(disk.Disk.SizeMb, Equals, diskCreateParamsDisk.SizeMb) check.Assert(disk.Disk.Description, Equals, diskCreateParamsDisk.Description) // Find disk by href @@ -415,14 +391,6 @@ func (vcd *TestVCD) Test_FindDiskByHREF(check *C) { // Independent disk integration test func (vcd *TestVCD) Test_Disk(check *C) { - if vcd.config.VCD.Disk.Size <= 0 { - check.Skip("skipping test because disk size is 0") - } - - if vcd.config.VCD.Disk.SizeForUpdate <= 0 { - check.Skip("skipping test because disk update size is <= 0") - } - if vcd.skipVappTests { check.Skip("skipping test because vApp wasn't properly created") } @@ -449,7 +417,7 @@ func (vcd *TestVCD) Test_Disk(check *C) { // Create disk diskCreateParamsDisk := &types.Disk{ Name: TestDisk, - Size: vcd.config.VCD.Disk.Size, + SizeMb: 14, Description: TestDisk, } @@ -474,7 +442,7 @@ func (vcd *TestVCD) Test_Disk(check *C) { disk, err := vcd.vdc.GetDiskByHref(diskHREF) check.Assert(err, IsNil) check.Assert(disk.Disk.Name, Equals, diskCreateParamsDisk.Name) - check.Assert(disk.Disk.Size, Equals, diskCreateParamsDisk.Size) + check.Assert(disk.Disk.SizeMb, Equals, diskCreateParamsDisk.SizeMb) check.Assert(disk.Disk.Description, Equals, diskCreateParamsDisk.Description) // Attach disk @@ -508,7 +476,7 @@ func (vcd *TestVCD) Test_Disk(check *C) { // Update disk newDiskInfo := &types.Disk{ Name: TestDisk, - Size: vcd.config.VCD.Disk.SizeForUpdate, + SizeMb: 41, Description: TestDisk + "_Update", } @@ -528,11 +496,6 @@ func (vcd *TestVCD) Test_Disk(check *C) { // Test query disk func (vcd *TestVCD) Test_QueryDisk(check *C) { - - if vcd.config.VCD.Disk.Size <= 0 { - check.Skip("skipping test because disk size is 0") - } - fmt.Printf("Running: %s\n", check.TestName()) name := "TestQueryDisk" @@ -540,7 +503,7 @@ func (vcd *TestVCD) Test_QueryDisk(check *C) { // Create disk diskCreateParamsDisk := &types.Disk{ Name: name, - Size: vcd.config.VCD.Disk.Size, + SizeMb: 1, Description: name, } @@ -566,17 +529,12 @@ func (vcd *TestVCD) Test_QueryDisk(check *C) { check.Assert(err, IsNil) check.Assert(diskRecord.Disk.Name, Equals, diskCreateParamsDisk.Name) - check.Assert(diskRecord.Disk.SizeB, Equals, int64(diskCreateParamsDisk.Size)) + check.Assert(diskRecord.Disk.SizeMb, Equals, int64(diskCreateParamsDisk.SizeMb)) check.Assert(diskRecord.Disk.Description, Equals, diskCreateParamsDisk.Description) } // Test query disk func (vcd *TestVCD) Test_QueryDisks(check *C) { - - if vcd.config.VCD.Disk.Size <= 0 { - check.Skip("skipping test because disk size is 0") - } - fmt.Printf("Running: %s\n", check.TestName()) name := "TestQueryDisks" @@ -584,7 +542,7 @@ func (vcd *TestVCD) Test_QueryDisks(check *C) { // Create disk diskCreateParamsDisk := &types.Disk{ Name: name, - Size: vcd.config.VCD.Disk.Size, + SizeMb: 22, Description: name, } @@ -624,17 +582,17 @@ func (vcd *TestVCD) Test_QueryDisks(check *C) { check.Assert(err, IsNil) check.Assert(len(*diskRecords), Equals, 2) check.Assert((*diskRecords)[0].Name, Equals, diskCreateParamsDisk.Name) - check.Assert((*diskRecords)[0].SizeB, Equals, int64(diskCreateParamsDisk.Size)) + check.Assert((*diskRecords)[0].SizeMb, Equals, int64(diskCreateParamsDisk.SizeMb)) + if vcd.client.Client.APIVCDMaxVersionIs(">= 36") { + check.Assert((*diskRecords)[0].UUID, Not(Equals), "") + check.Assert((*diskRecords)[0].SharingType, Equals, "None") + check.Assert((*diskRecords)[0].Encrypted, Equals, false) + } } // Tests Disk list retrieval by name, by ID func (vcd *TestVCD) Test_GetDisks(check *C) { fmt.Printf("Running: %s\n", check.TestName()) - - if vcd.config.VCD.Disk.Size == 0 { - check.Skip("skipping test because disk size is 0") - } - if vcd.config.VCD.Vdc == "" { check.Skip("Test_GetDisk: VDC name not given") return @@ -644,7 +602,7 @@ func (vcd *TestVCD) Test_GetDisks(check *C) { // Create disk diskCreateParamsDisk := &types.Disk{ Name: diskName, - Size: vcd.config.VCD.Disk.Size, + SizeMb: 12, Description: diskName + "Description", } @@ -710,10 +668,6 @@ func (vcd *TestVCD) Test_GetDisks(check *C) { func (vcd *TestVCD) Test_GetDiskByHref(check *C) { fmt.Printf("Running: %s\n", check.TestName()) - if vcd.config.VCD.Disk.Size == 0 { - check.Skip("skipping test because disk size is 0") - } - if vcd.config.VCD.Vdc == "" { check.Skip("Test_GetDisk: VDC name not given") return @@ -723,7 +677,7 @@ func (vcd *TestVCD) Test_GetDiskByHref(check *C) { // Create disk diskCreateParamsDisk := &types.Disk{ Name: diskName, - Size: vcd.config.VCD.Disk.Size, + SizeMb: 2048, Description: diskName + "Description", } @@ -749,9 +703,17 @@ func (vcd *TestVCD) Test_GetDiskByHref(check *C) { check.Assert(disk.Disk.Name, Equals, diskName) check.Assert(disk.Disk.Description, Equals, diskName+"Description") - invalidDiskHREF := diskHREF + "1" + // Creating HREF with fake UUID + uuid, err := GetUuidFromHref(diskHREF, true) + check.Assert(err, IsNil) + invalidDiskHREF := strings.ReplaceAll(diskHREF, uuid, "1abcbdb3-1111-1111-a1c2-85d261e22fcf") disk, err = vcd.vdc.GetDiskByHref(invalidDiskHREF) check.Assert(err, NotNil) - check.Assert(IsNotFound(err), Equals, true) + if vcd.client.Client.IsSysAdmin { + check.Assert(IsNotFound(err), Equals, true) + } else { + // The errors returned for non-existing disk are different for system administrator and org user + check.Assert(strings.Contains(err.Error(), "API Error: 403:"), Equals, true) + } check.Assert(disk, IsNil) } diff --git a/govcd/dse_data_solution.go b/govcd/dse_data_solution.go new file mode 100644 index 000000000..3fbcd9f75 --- /dev/null +++ b/govcd/dse_data_solution.go @@ -0,0 +1,319 @@ +/* +* Copyright 2024 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "net/url" + "strings" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + "github.com/vmware/go-vcloud-director/v2/util" +) + +var dataSolutionRdeType = [3]string{"vmware", "dsConfig", "0.1"} +var dseRightsBundleName = "vmware:dataSolutionsRightsBundle" // Rights bundle name +// Name of Data Solutions Operator package. It cannot be published itself, but it is still seen in +// the list. +var defaultDsoName = "VCD Data Solutions" // Data Solutions Operator (DSO) name + +// DataSolution represents Data Solution entities and their repository configurations as can be seen +// in "Container Registry" UI view +type DataSolution struct { + DataSolution *types.DataSolution + DefinedEntity *DefinedEntity + vcdClient *VCDClient +} + +// GetAllDataSolutions retrieves all Data Solutions +func (vcdClient *VCDClient) GetAllDataSolutions(queryParameters url.Values) ([]*DataSolution, error) { + allDseInstances, err := vcdClient.GetAllRdes(dataSolutionRdeType[0], dataSolutionRdeType[1], dataSolutionRdeType[2], queryParameters) + if err != nil { + return nil, fmt.Errorf("error retrieving all Data Solutions: %s", err) + } + + results := make([]*DataSolution, len(allDseInstances)) + for index, rde := range allDseInstances { + dseConfig, err := convertRdeEntityToAny[types.DataSolution](rde.DefinedEntity.Entity) + if err != nil { + return nil, fmt.Errorf("error converting RDE to Data Solution: %s", err) + } + + results[index] = &DataSolution{ + vcdClient: vcdClient, + DefinedEntity: rde, + DataSolution: dseConfig, + } + } + + return results, nil +} + +// GetDataSolutionById retrieves Data Solution by ID +func (vcdClient *VCDClient) GetDataSolutionById(id string) (*DataSolution, error) { + if id == "" { + return nil, fmt.Errorf("id must be specified") + } + rde, err := getRdeById(&vcdClient.Client, id) + if err != nil { + return nil, fmt.Errorf("error retrieving Data Solution by ID: %s", err) + } + + result, err := convertRdeEntityToAny[types.DataSolution](rde.DefinedEntity.Entity) + if err != nil { + return nil, err + } + + packages := &DataSolution{ + DataSolution: result, + vcdClient: vcdClient, + DefinedEntity: rde, + } + + return packages, nil +} + +// GetDataSolutionByName retrieves Data Solution by Name +func (vcdClient *VCDClient) GetDataSolutionByName(name string) (*DataSolution, error) { + dseEntities, err := vcdClient.GetAllDataSolutions(nil) + if err != nil { + return nil, fmt.Errorf("error retrieving Data Solution with name '%s': %s", name, err) + } + + for _, instance := range dseEntities { + if instance.DataSolution.Spec.Artifacts[0]["name"].(string) == name { + return instance, nil + } + } + return nil, fmt.Errorf("%s Data Solution by name '%s' not found", ErrorEntityNotFound, name) +} + +// Update Data Solution with given configuration +func (ds *DataSolution) Update(cfg *types.DataSolution) (*DataSolution, error) { + unmarshalledRdeEntityJson, err := convertAnyToRdeEntity(cfg) + if err != nil { + return nil, err + } + + ds.DefinedEntity.DefinedEntity.Entity = unmarshalledRdeEntityJson + err = ds.DefinedEntity.Update(*ds.DefinedEntity.DefinedEntity) + if err != nil { + return nil, err + } + + result, err := convertRdeEntityToAny[types.DataSolution](ds.DefinedEntity.DefinedEntity.Entity) + if err != nil { + return nil, err + } + + packages := DataSolution{ + DataSolution: result, + vcdClient: ds.vcdClient, + DefinedEntity: ds.DefinedEntity, + } + + return &packages, nil +} + +// Publish Data Solution to a tenant +// It is a bundle of operations that mimics what UI does when performing "publish" operation +// * Publish rights bundle 'vmware:dataSolutionsRightsBundle' to the tenant +// * Provision access to particular Data Solution +// * Always provision access to special Data Solution 'VCD Data Solutions'. This is the package that +// will install Data Solutions Operator (DSO) to Kubernetes cluster +// * Publish all templates of the given instance. Note: UI will not show instance templates until the +// tenant installs Data Solutions Operator (DSO) +func (ds *DataSolution) Publish(tenantId string) (*types.DefinedEntityAccess, *types.DefinedEntityAccess, []*types.DefinedEntityAccess, error) { + if tenantId == "" { + return nil, nil, nil, fmt.Errorf("error - tenant ID empty") + } + + if ds.Name() == defaultDsoName { + return nil, nil, nil, fmt.Errorf("cannot publish %s", defaultDsoName) + } + + // The operation is idempotent and can be run multiple times which is what the UI does + err := ds.PublishRightsBundle([]string{tenantId}) + if err != nil { + return nil, nil, nil, fmt.Errorf("error publishing Rights Bundle: %s", err) + } + + // Publish ACLs to a given Data Solution + mainAcl, err := ds.PublishAccessControls(tenantId) + if err != nil { + return nil, nil, nil, fmt.Errorf("error publishing Access Controls to '%s': %s", ds.Name(), err) + } + + // Additionally set the same ACL for Data Solutions Operator (DSO) + dso, err := ds.vcdClient.GetDataSolutionByName(defaultDsoName) + if err != nil { + return mainAcl, nil, nil, err + } + + dsoAcl, err := dso.PublishAccessControls(tenantId) + if err != nil { + return mainAcl, nil, nil, fmt.Errorf("error publishing Access Controls to '%s': %s", dso.Name(), err) + } + + // PublishAllInstanceTemplates + templateAcls, err := ds.PublishAllInstanceTemplates(tenantId) + if err != nil { + return mainAcl, dsoAcl, nil, fmt.Errorf("error publishing all Data Solution Instance Templates: %s", err) + } + + return mainAcl, dsoAcl, templateAcls, nil +} + +// Unpublish Data Solution to a slice of tenants +// It is a bundle of operations that mimics what UI does when unpublishing and attempts to revert +// what is done in 'Publish' method +// * Remove access from given Data Solution +// +// Note. This method (and UI) is asymmetric in comparison to 'Publish' operation. It _does not_ do +// the following operations: +// * Unpublish all Data Solution Templates +// * Unpublish access for Data Solutions Operator (DSO) +// * Unpublish rights bundle 'vmware:dataSolutionsRightsBundle' +func (ds *DataSolution) Unpublish(tenantId string) error { + if tenantId == "" { + return fmt.Errorf("error - tenant ID empty") + } + + // ACLs + err := ds.UnpublishAccessControls(tenantId) + if err != nil { + return fmt.Errorf("failed unpublishing Access Controls for %s: %s", ds.Name(), err) + } + + return nil +} + +// PublishRightsBundle publishes "vmware:dataSolutionsRightsBundle" rights bundle +func (ds *DataSolution) PublishRightsBundle(tenantIds []string) error { + rightsBundle, err := ds.vcdClient.Client.GetRightsBundleByName(dseRightsBundleName) + if err != nil { + return fmt.Errorf("error retrieving Rights Bundle %s: %s", dseRightsBundleName, err) + } + + references := convertSliceOfStringsToOpenApiReferenceIds(tenantIds) + err = rightsBundle.PublishTenants(references) + if err != nil { + return fmt.Errorf("error publishing Rights Bundle '%s' to Tenants '%s': %s", + dseRightsBundleName, strings.Join(tenantIds, ","), err) + } + + return nil +} + +// UnpublishRightsBundle removes "vmware:dataSolutionsRightsBundle" rights bundle from tenant +func (ds *DataSolution) UnpublishRightsBundle(tenantIds []string) error { + rightsBundle, err := ds.vcdClient.Client.GetRightsBundleByName(dseRightsBundleName) + if err != nil { + return fmt.Errorf("error retrieving Rights Bundle %s: %s", dseRightsBundleName, err) + } + + references := convertSliceOfStringsToOpenApiReferenceIds(tenantIds) + err = rightsBundle.UnpublishTenants(references) + if err != nil { + return fmt.Errorf("error unpublishing %s for Tenants '%s': %s", + dseRightsBundleName, strings.Join(tenantIds, ","), err) + } + + return nil +} + +// PublishAccessControls provisions ACL for a given tenant +func (ds *DataSolution) PublishAccessControls(tenantId string) (*types.DefinedEntityAccess, error) { + acl := &types.DefinedEntityAccess{ + Tenant: types.OpenApiReference{ID: tenantId}, + GrantType: "MembershipAccessControlGrant", + AccessLevelID: "urn:vcloud:accessLevel:ReadOnly", + MemberID: tenantId, + } + + accessControl, err := ds.DefinedEntity.SetAccessControl(acl) + if err != nil { + return nil, fmt.Errorf("error setting Access Control for Data Solution '%s', Org ID %s: %s", tenantId, ds.Name(), err) + } + + return accessControl, nil +} + +// UnpublishAccessControls removes ACLs for a given tenant +func (ds *DataSolution) UnpublishAccessControls(tenantId string) error { + acls, err := ds.GetAllAccessControlsForTenant(tenantId) + if err != nil { + return fmt.Errorf("error retrieving all Access Controls for Tenant '%s': %s", tenantId, err) + } + + for _, acl := range acls { + err = ds.DefinedEntity.DeleteAccessControl(acl) + if err != nil { + return fmt.Errorf("error deleting Access Control: %s", err) + } + } + + return nil +} + +// GetAllAccessControls for a Data Solution +func (ds *DataSolution) GetAllAccessControls(queryParameters url.Values) ([]*types.DefinedEntityAccess, error) { + allAcls, err := ds.DefinedEntity.GetAllAccessControls(queryParameters) + if err != nil { + return nil, fmt.Errorf("error retrieving all Access Controls for Data Solution %s: %s", ds.Name(), err) + } + + return localFilter("Data Solution ACL", allAcls, "ObjectId", ds.RdeId()) +} + +// GetAccessControlById retrieves ACL by ID +func (ds *DataSolution) GetAccessControlById(id string) (*types.DefinedEntityAccess, error) { + return ds.DefinedEntity.GetAccessControlById(id) +} + +// GetAllAccessControlsForTenant retrieves all ACLs that apply for a specific Tenant +func (ds *DataSolution) GetAllAccessControlsForTenant(tenantId string) ([]*types.DefinedEntityAccess, error) { + util.Logger.Printf("[TRACE] Data Solution '%s' getting Access Controls for tenant '%s'", ds.Name(), tenantId) + allAcls, err := ds.GetAllAccessControls(nil) + if err != nil { + return nil, fmt.Errorf("error retrieving all Access Controls for Data Solution: %s", err) + } + + foundAcls := make([]*types.DefinedEntityAccess, 0) + util.Logger.Printf("[TRACE] Data Solution '%s' looking for Access Controls for tenant '%s'", ds.Name(), tenantId) + for _, acl := range allAcls { + util.Logger.Printf("[TRACE] Data Solution '%s' checking Access Control ID '%s'", ds.Name(), acl.Id) + if acl.Tenant.ID == tenantId { + util.Logger.Printf("[TRACE] Data Solution '%s' Access Control '%s' matches tenant '%s'", ds.Name(), acl.Id, tenantId) + foundAcls = append(foundAcls, acl) + } + } + + return foundAcls, nil +} + +// RdeId is a shorthand function to retrieve parent RDE ID for a Data Solution. +func (dsCfg *DataSolution) RdeId() string { + if dsCfg == nil || dsCfg.DefinedEntity == nil || dsCfg.DefinedEntity.DefinedEntity == nil { + return "" + } + + return dsCfg.DefinedEntity.DefinedEntity.ID +} + +// Name extracts the name from inside RDE configuration. This name is used in UI and is not always +// the same as RDE name. It is guaranteed to persist. +func (dsCfg *DataSolution) Name() string { + if dsCfg.DataSolution == nil || dsCfg.DataSolution.Spec.Artifacts == nil || len(dsCfg.DataSolution.Spec.Artifacts) < 1 { + return "" + } + + nameString, ok := dsCfg.DataSolution.Spec.Artifacts[0]["name"].(string) + if !ok { + return "" + } + + return nameString +} diff --git a/govcd/dse_data_solution_instance_template.go b/govcd/dse_data_solution_instance_template.go new file mode 100644 index 000000000..239c8b523 --- /dev/null +++ b/govcd/dse_data_solution_instance_template.go @@ -0,0 +1,180 @@ +/* +* Copyright 2024 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" + "github.com/vmware/go-vcloud-director/v2/util" +) + +var dataSolutionTemplateInstanceRdeType = [3]string{"vmware", "dsInstanceTemplate", "0.1"} + +// DataSolutionInstanceTemplate represents Data Solution Instance Templates that come with Data Solutions +type DataSolutionInstanceTemplate struct { + DataSolutionInstanceTemplate *types.DataSolutionInstanceTemplate + DefinedEntity *DefinedEntity + vcdClient *VCDClient +} + +// GetAllInstanceTemplates retrieves all Data Solution Instance Templates that are available in the +// system +func (vcdClient *VCDClient) GetAllInstanceTemplates(queryParameters url.Values) ([]*DataSolutionInstanceTemplate, error) { + allDseInstanceTemplates, err := vcdClient.GetAllRdes(dataSolutionTemplateInstanceRdeType[0], dataSolutionTemplateInstanceRdeType[1], dataSolutionTemplateInstanceRdeType[2], queryParameters) + if err != nil { + return nil, fmt.Errorf("error retrieving all Data Solution Instance Templates: %s", err) + } + + results := make([]*DataSolutionInstanceTemplate, len(allDseInstanceTemplates)) + for index, rde := range allDseInstanceTemplates { + dseConfig, err := convertRdeEntityToAny[types.DataSolutionInstanceTemplate](rde.DefinedEntity.Entity) + if err != nil { + return nil, fmt.Errorf("error converting RDE to Data Solution Instance Template: %s", err) + } + + results[index] = &DataSolutionInstanceTemplate{ + vcdClient: vcdClient, + DefinedEntity: rde, + DataSolutionInstanceTemplate: dseConfig, + } + } + + return results, nil +} + +// GetAllInstanceTemplates retrieves all Data Solution Instance Templates for a given Data Solution +func (ds *DataSolution) GetAllInstanceTemplates() ([]*DataSolutionInstanceTemplate, error) { + if ds == nil || ds.DataSolution == nil { + return nil, fmt.Errorf("error - Data Solution structure is empty") + } + + queryParams := copyOrNewUrlValues(nil) + queryParams = queryParameterFilterAnd(fmt.Sprintf("entity.spec.solutionType==%s", ds.DataSolution.Spec.SolutionType), queryParams) + queryParams = queryParameterFilterAnd("state==RESOLVED", queryParams) + + return ds.vcdClient.GetAllInstanceTemplates(queryParams) +} + +// PublishAllInstanceTemplates creates Access Controls for all available Data Solution Instance Templates +func (ds *DataSolution) PublishAllInstanceTemplates(tenantId string) ([]*types.DefinedEntityAccess, error) { + allTemplates, err := ds.GetAllInstanceTemplates() + if err != nil { + return nil, fmt.Errorf("error retrieving all Data Solution Instance Templates: %s", err) + } + + definedEntityAccess := make([]*types.DefinedEntityAccess, len(allTemplates)) + for templateIndex, template := range allTemplates { + access, err := template.Publish(tenantId) + if err != nil { + return nil, fmt.Errorf("error setting ACL for Data Solution Instance Template '%s': %s", + template.DefinedEntity.DefinedEntity.Name, err) + } + + definedEntityAccess[templateIndex] = access + } + + return definedEntityAccess, nil +} + +// UnPublishAllInstanceTemplates removes all ACLs of a given Data Solution from specified tenantId +func (ds *DataSolution) UnPublishAllInstanceTemplates(tenantId string) error { + allTemplates, err := ds.GetAllInstanceTemplates() + if err != nil { + return fmt.Errorf("error retrieving all Data Solution Instance Templates: %s", err) + } + + for _, template := range allTemplates { + err := template.Unpublish(tenantId) + if err != nil { + return fmt.Errorf("error removing ACL for Data Solution Instance Template '%s': %s", + template.DefinedEntity.DefinedEntity.Name, err) + } + + } + + return nil +} + +// Name extracts the name from inside RDE configuration. This name is used in UI. +func (dst *DataSolutionInstanceTemplate) Name() string { + if dst.DefinedEntity == nil || dst.DefinedEntity.DefinedEntity == nil { + return "" + } + + return dst.DefinedEntity.DefinedEntity.Name +} + +// GetAllAccessControls retrieves all Access Controls for a given Data Solution Instance Template +func (dst *DataSolutionInstanceTemplate) GetAllAccessControls(queryParameters url.Values) ([]*types.DefinedEntityAccess, error) { + return dst.DefinedEntity.GetAllAccessControls(queryParameters) +} + +// GetAllAccessControlsForTenant retrieves all Access Controls for a given tenant +func (dst *DataSolutionInstanceTemplate) GetAllAccessControlsForTenant(tenantId string) ([]*types.DefinedEntityAccess, error) { + util.Logger.Printf("[TRACE] Data Solution Instance Template '%s' getting Access Controls for tenant '%s'", dst.Name(), tenantId) + allAcls, err := dst.GetAllAccessControls(nil) + if err != nil { + return nil, fmt.Errorf("error retrieving all Access Controls for Data Solution Solution Instance Template: %s", err) + } + + foundAcls := make([]*types.DefinedEntityAccess, 0) + util.Logger.Printf("[TRACE] Data Solution Instance Template '%s' looking for Access Controls for tenant '%s'", dst.Name(), tenantId) + for _, acl := range allAcls { + util.Logger.Printf("[TRACE] Data Solution Instance Template '%s' checking Access Control ID '%s'", dst.Name(), acl.Id) + if acl.Tenant.ID == tenantId { + util.Logger.Printf("[TRACE] Data Solution Instance Template '%s' Access Control '%s' matches tenant '%s'", dst.Name(), acl.Id, tenantId) + foundAcls = append(foundAcls, acl) + } + } + + return foundAcls, nil +} + +// Publish a single Data Solution Instance Template to a given tenant +func (dst *DataSolutionInstanceTemplate) Publish(tenantId string) (*types.DefinedEntityAccess, error) { + acl := &types.DefinedEntityAccess{ + Tenant: types.OpenApiReference{ID: tenantId}, + GrantType: "MembershipAccessControlGrant", + AccessLevelID: "urn:vcloud:accessLevel:ReadOnly", + MemberID: tenantId, + } + + accessControl, err := dst.DefinedEntity.SetAccessControl(acl) + if err != nil { + return nil, fmt.Errorf("error setting Access Control for Data Solution %s: %s", dst.DefinedEntity.DefinedEntity.Name, err) + } + + return accessControl, nil +} + +// Unpublish a single Data Solution Instance Template for a given tenant +func (dst *DataSolutionInstanceTemplate) Unpublish(tenantId string) error { + queryParams := copyOrNewUrlValues(nil) + queryParams = queryParameterFilterAnd(fmt.Sprintf("tenant.id==%s", tenantId), queryParams) + acls, err := dst.DefinedEntity.GetAllAccessControls(queryParams) + if err != nil { + return fmt.Errorf("error getting Access Control for Data Solution Instance Template %s: %s", dst.DefinedEntity.DefinedEntity.Name, err) + } + + for _, acl := range acls { + err = dst.DefinedEntity.DeleteAccessControl(acl) + if err != nil { + return fmt.Errorf("error deleting Access Control: %s", err) + } + } + + return nil +} + +// RdeId is a shortcut of SolutionEntity.DefinedEntity.DefinedEntity.ID +func (dst *DataSolutionInstanceTemplate) RdeId() string { + if dst == nil || dst.DefinedEntity == nil || dst.DefinedEntity.DefinedEntity == nil { + return "" + } + + return dst.DefinedEntity.DefinedEntity.ID +} diff --git a/govcd/dse_data_solution_org_config.go b/govcd/dse_data_solution_org_config.go new file mode 100644 index 000000000..de3879ca4 --- /dev/null +++ b/govcd/dse_data_solution_org_config.go @@ -0,0 +1,167 @@ +/* +* Copyright 2024 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "net/url" + "strings" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +var dataSolutionOrgConfig = [3]string{"vmware", "dsOrgConfig", "0.1.1"} + +// DataSolutionOrgConfig structure represents Data Solution Org Configuration. This configuration +// can carry additional information to Data Solutions if they require it. At the moment of +// implementation the only known Data Solution that requires this configuration is `Confluent +// Platform` that uses this structure to store licensing data. +type DataSolutionOrgConfig struct { + DataSolutionOrgConfig *types.DataSolutionOrgConfig + DefinedEntity *DefinedEntity + vcdClient *VCDClient +} + +// CreateDataSolutionOrgConfig creates Data Solution Org Configuration for a defined orgId +func (vcdClient *VCDClient) CreateDataSolutionOrgConfig(orgId string, cfg *types.DataSolutionOrgConfig) (*DataSolutionOrgConfig, error) { + rdeType, err := vcdClient.GetRdeType(dataSolutionOrgConfig[0], dataSolutionOrgConfig[1], dataSolutionOrgConfig[2]) + if err != nil { + return nil, fmt.Errorf("error retrieving RDE Type for VCD Data Solution Org Configuration: %s", err) + } + + // 2. Convert more precise structure to fit DefinedEntity.DefinedEntity.Entity + unmarshalledRdeEntityJson, err := convertAnyToRdeEntity(cfg) + if err != nil { + return nil, err + } + + // 3. Construct payload + entityCfg := &types.DefinedEntity{ + EntityType: "urn:vcloud:type:" + strings.Join(dataSolutionOrgConfig[:], ":"), + Name: orgId, // Receiving Org ID is used as name + State: addrOf("PRE_CREATED"), + Entity: unmarshalledRdeEntityJson, + } + + // 4. Create RDE + createdRdeEntity, err := rdeType.CreateRde(*entityCfg, nil) + if err != nil { + return nil, fmt.Errorf("error creating RDE entity: %s", err) + } + + // 5. Resolve RDE + err = createdRdeEntity.Resolve() + if err != nil { + return nil, fmt.Errorf("error resolving Solutions add-on after creating: %s", err) + } + + // 6. Reload RDE + err = createdRdeEntity.Refresh() + if err != nil { + return nil, fmt.Errorf("error refreshing RDE after resolving: %s", err) + } + + result, err := convertRdeEntityToAny[types.DataSolutionOrgConfig](createdRdeEntity.DefinedEntity.Entity) + if err != nil { + return nil, err + } + + returnType := DataSolutionOrgConfig{ + DataSolutionOrgConfig: result, + vcdClient: vcdClient, + DefinedEntity: createdRdeEntity, + } + + return &returnType, nil +} + +// GetAllDataSolutionOrgConfigs retrieves all available Data Solution Org Configs +func (vcdClient *VCDClient) GetAllDataSolutionOrgConfigs(queryParameters url.Values) ([]*DataSolutionOrgConfig, error) { + allDseInstances, err := vcdClient.GetAllRdes(dataSolutionOrgConfig[0], dataSolutionOrgConfig[1], dataSolutionOrgConfig[2], queryParameters) + if err != nil { + return nil, fmt.Errorf("error retrieving all Data Solution Org Configs: %s", err) + } + + results := make([]*DataSolutionOrgConfig, len(allDseInstances)) + for index, rde := range allDseInstances { + dsOrgConfig, err := convertRdeEntityToAny[types.DataSolutionOrgConfig](rde.DefinedEntity.Entity) + if err != nil { + return nil, fmt.Errorf("error converting RDE to Solution Add-on Instance: %s", err) + } + + results[index] = &DataSolutionOrgConfig{ + vcdClient: vcdClient, + DefinedEntity: rde, + DataSolutionOrgConfig: dsOrgConfig, + } + } + + return results, nil +} + +// GetAllDataSolutionOrgConfigs retrieves all available Data Solution Org Configs for a given Data +// Solution +func (ds *DataSolution) GetAllDataSolutionOrgConfigs() ([]*DataSolutionOrgConfig, error) { + if ds == nil || ds.DataSolution == nil { + return nil, fmt.Errorf("error - Data Solution structure is empty") + } + + queryParams := copyOrNewUrlValues(nil) + queryParams = queryParameterFilterAnd(fmt.Sprintf("entity.spec.solutionType==%s", ds.DataSolution.Spec.SolutionType), queryParams) + queryParams = queryParameterFilterAnd("state==RESOLVED", queryParams) + + return ds.vcdClient.GetAllDataSolutionOrgConfigs(queryParams) +} + +// GetDataSolutionOrgConfigForTenant retrieves all available Data Solution Org Configs for a given +// Data Solution and then uses local filter to find the one for given tenantId +func (ds *DataSolution) GetDataSolutionOrgConfigForTenant(tenantId string) (*DataSolutionOrgConfig, error) { + if ds == nil || ds.DataSolution == nil { + return nil, fmt.Errorf("error - Data Solution structure is empty") + } + + if tenantId == "" { + return nil, fmt.Errorf("tenant ID is required") + } + + allOrgConfigs, err := ds.GetAllDataSolutionOrgConfigs() + if err != nil { + return nil, fmt.Errorf("error retrieving all Data Solution Org Configs: %s", err) + } + + var foundOrgCfg *DataSolutionOrgConfig + for _, orgCfg := range allOrgConfigs { + // tenant ID is stored in RDE Name + if orgCfg.DefinedEntity.DefinedEntity.Name == tenantId { + foundOrgCfg = orgCfg + break + } + } + + if foundOrgCfg == nil { + return nil, fmt.Errorf("%s: could not find Data Solution '%s' Org Config for a given tenant '%s'", + ErrorEntityNotFound, ds.Name(), tenantId) + } + + return foundOrgCfg, nil + +} + +// Delete Data Solution Org Config +func (dsOrgCfg *DataSolutionOrgConfig) Delete() error { + if dsOrgCfg.DefinedEntity == nil { + return fmt.Errorf("error - parent Defined Entity is nil") + } + return dsOrgCfg.DefinedEntity.Delete() +} + +// RdeId is a shortcut of SolutionEntity.DefinedEntity.DefinedEntity.ID +func (dsOrgCfg *DataSolutionOrgConfig) RdeId() string { + if dsOrgCfg == nil || dsOrgCfg.DefinedEntity == nil || dsOrgCfg.DefinedEntity.DefinedEntity == nil { + return "" + } + + return dsOrgCfg.DefinedEntity.DefinedEntity.ID +} diff --git a/govcd/dse_data_solution_test.go b/govcd/dse_data_solution_test.go new file mode 100644 index 000000000..ffe856aa0 --- /dev/null +++ b/govcd/dse_data_solution_test.go @@ -0,0 +1,255 @@ +//go:build slz || functional || ALL + +/* +* Copyright 2024 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "slices" + "strings" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +// Test_Dse attempts to perform a lot of checks for code in one function because it is quite expensive +// to establish a Solution Add-On (measured to roughly 30mins) +func (vcd *TestVCD) Test_Dse(check *C) { + vcd.skipIfNotSysAdmin(check) + if vcd.client.Client.APIVCDMaxVersionIs("< 37.1") { + check.Skip("Solution Landing Zones are supported in VCD 10.4.1+") + } + + if vcd.config.SolutionAddOn.Org == "" || vcd.config.SolutionAddOn.Catalog == "" || len(vcd.config.SolutionAddOn.DseSolutions) < 1 { + check.Skip("DSE configuration is not present") + } + + // Prerequisites - Data Solution Add-On instance must be created and published + // Note this block can be commented out to get more rapid testing if one already has DSE + // instantiated and deployed. + slz, addOn, addOnInstance := createDseAddonInstanceAndPublish(vcd, check) + + defer func() { + fmt.Println("# Cleaning up prerequisites") + _, err := addOnInstance.Publishing(nil, false) + check.Assert(err, IsNil) + + deleteInputs := make(map[string]interface{}) + deleteInputs["name"] = addOnInstance.SolutionAddOnInstance.AddonInstanceSolutionName + deleteInputs["input-force-delete"] = true + + _, err = addOnInstance.Delete(deleteInputs) + check.Assert(err, IsNil) + + err = addOn.Delete() + check.Assert(err, IsNil) + + err = slz.Delete() + check.Assert(err, IsNil) + }() + // End of prerequisites + + fmt.Println("# Prerequisites created, starting test") + + // Create new client session because the original one will not be able to query Data Solutions + orgName := vcd.config.Provider.SysOrg + userName := vcd.config.Provider.User + password := vcd.config.Provider.Password + vcdClient := NewVCDClient(vcd.client.Client.VCDHREF, true) + err := vcdClient.Authenticate(userName, password, orgName) + check.Assert(err, IsNil) + + recipientOrg, err := vcdClient.GetOrgByName(vcd.config.Cse.TenantOrg) + check.Assert(err, IsNil) + + dsNames := make([]string, 0) + for dsName := range vcd.config.SolutionAddOn.DseSolutions { + dsNames = append(dsNames, dsName) + } + + // Lookup testing + allDataSolutions, err := vcdClient.GetAllDataSolutions(nil) + check.Assert(err, IsNil) + check.Assert(len(allDataSolutions), Equals, len(dsNames)+1) // +1 because of default "VCD Data Solutions" + + for _, ds := range allDataSolutions { + printVerbose("# Testing Data Solution '%s' retrieval methods\n", ds.Name()) + if ds.Name() != defaultDsoName { + check.Assert(slices.Contains(dsNames, ds.Name()), Equals, true) + } + check.Assert(strings.HasPrefix(ds.RdeId(), "urn:vcloud:entity:vmware:dsConfig:"), Equals, true) + + byId, err := vcdClient.GetDataSolutionById(ds.RdeId()) + check.Assert(err, IsNil) + check.Assert(byId.DataSolution, DeepEquals, ds.DataSolution) + + byName, err := vcdClient.GetDataSolutionByName(ds.Name()) + check.Assert(err, IsNil) + check.Assert(byName.DataSolution, DeepEquals, ds.DataSolution) + } + + // Configure all Data Solutions except DSO + for dsName, dsConfig := range vcd.config.SolutionAddOn.DseSolutions { + printVerbose("# Configuring Data Solution '%s'\n", dsName) + + byName, err := vcdClient.GetDataSolutionByName(dsName) + check.Assert(err, IsNil) + + cfg := byName.DataSolution + + if value, ok := dsConfig["chart_repository"]; ok { + cfg.Spec.Artifacts[0]["chartRepository"] = value + } + if value, ok := dsConfig["version"]; ok { + cfg.Spec.Artifacts[0]["version"] = value + } + if value, ok := dsConfig["package_name"]; ok { + cfg.Spec.Artifacts[0]["packageName"] = value + } + + if value, ok := dsConfig["package_repository"]; ok { + cfg.Spec.Artifacts[0]["image"] = value + } + + updatedDs, err := byName.Update(cfg) + check.Assert(err, IsNil) + + if updatedDs.DefinedEntity.State() != "RESOLVED" { + err = updatedDs.DefinedEntity.Resolve() + check.Assert(err, IsNil) + } + } + + // Configure DSO + printVerbose("# Configuring Default Data Solution '%s'\n", defaultDsoName) + dsoByName, err := vcdClient.GetDataSolutionByName(defaultDsoName) + check.Assert(err, IsNil) + + // Simulate using default values, but also configure registry + cfg := dsoByName.DataSolution + artifacts := cfg.Spec.Artifacts[0] + + if artifacts["defaultImage"] != nil { + cfg.Spec.Artifacts[0]["image"] = artifacts["defaultImage"].(string) + } + + if artifacts["defaultChartRepository"] != nil { + cfg.Spec.Artifacts[0]["chartRepository"] = artifacts["defaultChartRepository"].(string) + } + if artifacts["defaultVersion"] != nil { + cfg.Spec.Artifacts[0]["version"] = artifacts["defaultVersion"].(string) + } + + if artifacts["defaultPackageName"] != nil { + cfg.Spec.Artifacts[0]["packageName"] = artifacts["defaultPackageName"].(string) + } + + auths := make(map[string]types.DseDockerAuth) + auths[check.TestName()+"1"] = types.DseDockerAuth{Username: "user1", Password: "pass1", Description: "Test 1"} + auths[check.TestName()+"2"] = types.DseDockerAuth{Username: "user2", Password: "pass2", Description: "Test 2"} + cfg.Spec.DockerConfig = &types.DseDockerConfig{Auths: auths} + + updatedDs, err := dsoByName.Update(cfg) + check.Assert(err, IsNil) + + if updatedDs.DefinedEntity.State() != "RESOLVED" { + err = updatedDs.DefinedEntity.Resolve() + check.Assert(err, IsNil) + } + + // Publish to tenant + for dsName := range vcd.config.SolutionAddOn.DseSolutions { + printVerbose("# Publishing Data Solution '%s' to tenant '%s'\n", dsName, recipientOrg.Org.Name) + + ds, err := vcdClient.GetDataSolutionByName(dsName) + check.Assert(err, IsNil) + + dsAcl, dsoAcl, templateAcls, err := ds.Publish(recipientOrg.Org.ID) + check.Assert(err, IsNil) + check.Assert(dsAcl, NotNil) + check.Assert(dsoAcl, NotNil) + check.Assert(templateAcls, NotNil) + check.Assert(len(templateAcls) > 1, Equals, true) + + printVerbose("# Unpublishing Data Solution '%s'\n", dsName) + err = ds.Unpublish(recipientOrg.Org.ID) + check.Assert(err, IsNil) + } + + for dsName := range vcd.config.SolutionAddOn.DseSolutions { + printVerbose("# Retrieve Data Solution '%s' Instance Templates\n", dsName) + + ds, err := vcdClient.GetDataSolutionByName(dsName) + check.Assert(err, IsNil) + + allDst, err := ds.GetAllInstanceTemplates() + check.Assert(err, IsNil) + for _, dst := range allDst { + printVerbose("## Got Template '%s' for Data Solution '%s'\n", dst.Name(), dsName) + check.Assert(strings.HasPrefix(dst.RdeId(), "urn:vcloud:entity:vmware:dsInstanceTemplate:"), Equals, true) + + // Publishing / unpublishing to tenant + printVerbose("# Publishing Template '%s' for Data Solution '%s' to tenant '%s'\n", dst.Name(), dsName, recipientOrg.Org.Name) + createdAcl, err := dst.Publish(recipientOrg.Org.ID) + check.Assert(err, IsNil) + + // Checking that ACLs can be found + allAcls, err := dst.GetAllAccessControls(nil) + check.Assert(err, IsNil) + + var foundAcl bool + for _, singleAcl := range allAcls { + if singleAcl.Id == createdAcl.Id { + foundAcl = true + break + } + } + check.Assert(foundAcl, Equals, true) + + allTenantAcls, err := dst.GetAllAccessControlsForTenant(recipientOrg.Org.ID) + check.Assert(err, IsNil) + + foundAcl = false + for _, singleAcl := range allTenantAcls { + if singleAcl.Id == createdAcl.Id { + foundAcl = true + break + } + } + check.Assert(foundAcl, Equals, true) + + printVerbose("# Unpublishing Template '%s' for Data Solution '%s' for tenant '%s'\n", dst.Name(), dsName, recipientOrg.Org.Name) + err = dst.Unpublish(recipientOrg.Org.ID) + check.Assert(err, IsNil) + + // Check that ACL is removed after unpublishing the template + tenantAclsAfterRemoval, err := dst.GetAllAccessControlsForTenant(recipientOrg.Org.ID) + check.Assert(err, IsNil) + check.Assert(len(tenantAclsAfterRemoval), Equals, 0) + } + } + + // cleanup is deferred at the top +} + +func createDseAddonInstanceAndPublish(vcd *TestVCD, check *C) (*SolutionLandingZone, *SolutionAddOn, *SolutionAddOnInstance) { + slz, addOn := createSlzAddOn(vcd, check) + + inputs := make(map[string]interface{}) + inputs["name"] = check.TestName() + inputs["input-delete-previous-uiplugin-versions"] = false + + addOnInstance, _, err := addOn.CreateSolutionAddOnInstance(inputs) + check.Assert(err, IsNil) + + PrependToCleanupListOpenApi(addOnInstance.DefinedEntity.DefinedEntity.ID, check.TestName(), types.OpenApiPathVersion1_0_0+types.OpenApiEndpointRdeEntities+addOnInstance.DefinedEntity.DefinedEntity.ID) + + scope := []string{vcd.config.Cse.TenantOrg} + _, err = addOnInstance.Publishing(scope, false) + check.Assert(err, IsNil) + + return slz, addOn, addOnInstance +} diff --git a/govcd/edgegateway.go b/govcd/edgegateway.go index ed9a41ebb..2f3f6c9b2 100644 --- a/govcd/edgegateway.go +++ b/govcd/edgegateway.go @@ -120,7 +120,7 @@ func (egw *EdgeGateway) AddDhcpPool(network *types.OrgVDCNetwork, dhcppool []int for { buffer := bytes.NewBufferString(xml.Header + string(output)) - apiEndpoint, _ := url.ParseRequestURI(egw.EdgeGateway.HREF) + apiEndpoint := urlParseRequestURI(egw.EdgeGateway.HREF) apiEndpoint.Path += "/action/configureServices" req := egw.client.NewRequest(map[string]string{}, http.MethodPost, *apiEndpoint, buffer) @@ -197,7 +197,7 @@ func (egw *EdgeGateway) RemoveNATPortMapping(natType, externalIP, externalPort, NatService: newNatService, } - apiEndpoint, _ := url.ParseRequestURI(egw.EdgeGateway.HREF) + apiEndpoint := urlParseRequestURI(egw.EdgeGateway.HREF) apiEndpoint.Path += "/action/configureServices" // Return the task @@ -263,7 +263,7 @@ func (egw *EdgeGateway) RemoveNATRuleAsync(id string) (Task, error) { NatService: natServiceToUpdate, } - egwConfigureHref, _ := url.ParseRequestURI(egw.EdgeGateway.HREF) + egwConfigureHref := urlParseRequestURI(egw.EdgeGateway.HREF) egwConfigureHref.Path += "/action/configureServices" // Return the task @@ -428,7 +428,7 @@ func (egw *EdgeGateway) UpdateNatRuleAsync(natRule *types.NatRule) (Task, error) NatService: natServiceToUpdate, } - egwConfigureHref, _ := url.ParseRequestURI(egw.EdgeGateway.HREF) + egwConfigureHref := urlParseRequestURI(egw.EdgeGateway.HREF) egwConfigureHref.Path += "/action/configureServices" // Return the task @@ -484,7 +484,7 @@ func (egw *EdgeGateway) AddNATRuleAsync(ruleDetails NatRule) (Task, error) { //construct new rule natRule := &types.NatRule{ RuleType: ruleDetails.NatType, - IsEnabled: takeBoolPointer(true), + IsEnabled: addrOf(true), Description: ruleDetails.Description, GatewayNatRule: &types.GatewayNatRule{ Interface: &types.Reference{ @@ -506,7 +506,7 @@ func (egw *EdgeGateway) AddNATRuleAsync(ruleDetails NatRule) (Task, error) { NatService: newNatService, } - egwConfigureHref, _ := url.ParseRequestURI(egw.EdgeGateway.HREF) + egwConfigureHref := urlParseRequestURI(egw.EdgeGateway.HREF) egwConfigureHref.Path += "/action/configureServices" // Return the task @@ -632,7 +632,7 @@ func (egw *EdgeGateway) AddNATPortMappingWithUplink(network *types.OrgVDCNetwork //add rule natRule := &types.NatRule{ RuleType: natType, - IsEnabled: takeBoolPointer(true), + IsEnabled: addrOf(true), GatewayNatRule: &types.GatewayNatRule{ Interface: &types.Reference{ HREF: uplinkRef, @@ -654,7 +654,7 @@ func (egw *EdgeGateway) AddNATPortMappingWithUplink(network *types.OrgVDCNetwork NatService: newNatService, } - apiEndpoint, _ := url.ParseRequestURI(egw.EdgeGateway.HREF) + apiEndpoint := urlParseRequestURI(egw.EdgeGateway.HREF) apiEndpoint.Path += "/action/configureServices" // Return the task @@ -687,7 +687,7 @@ func (egw *EdgeGateway) CreateFirewallRules(defaultAction string, rules []*types for { buffer := bytes.NewBufferString(xml.Header + string(output)) - apiEndpoint, _ := url.ParseRequestURI(egw.EdgeGateway.HREF) + apiEndpoint := urlParseRequestURI(egw.EdgeGateway.HREF) apiEndpoint.Path += "/action/configureServices" req := egw.client.NewRequest(map[string]string{}, http.MethodPost, *apiEndpoint, buffer) @@ -837,7 +837,7 @@ func (egw *EdgeGateway) Remove1to1Mapping(internal, external string) (Task, erro // Fix newEdgeConfig.NatService.IsEnabled = true - apiEndpoint, _ := url.ParseRequestURI(egw.EdgeGateway.HREF) + apiEndpoint := urlParseRequestURI(egw.EdgeGateway.HREF) apiEndpoint.Path += "/action/configureServices" // Return the task @@ -866,7 +866,7 @@ func (egw *EdgeGateway) Create1to1Mapping(internal, external, description string snat := &types.NatRule{ Description: description, RuleType: "SNAT", - IsEnabled: takeBoolPointer(true), + IsEnabled: addrOf(true), GatewayNatRule: &types.GatewayNatRule{ Interface: &types.Reference{ HREF: uplinkif, @@ -885,7 +885,7 @@ func (egw *EdgeGateway) Create1to1Mapping(internal, external, description string dnat := &types.NatRule{ Description: description, RuleType: "DNAT", - IsEnabled: takeBoolPointer(true), + IsEnabled: addrOf(true), GatewayNatRule: &types.GatewayNatRule{ Interface: &types.Reference{ HREF: uplinkif, @@ -932,7 +932,7 @@ func (egw *EdgeGateway) Create1to1Mapping(internal, external, description string newEdgeConfig.FirewallService.FirewallRule = append(newEdgeConfig.FirewallService.FirewallRule, fwout) - apiEndpoint, _ := url.ParseRequestURI(egw.EdgeGateway.HREF) + apiEndpoint := urlParseRequestURI(egw.EdgeGateway.HREF) apiEndpoint.Path += "/action/configureServices" // Return the task @@ -950,7 +950,7 @@ func (egw *EdgeGateway) AddIpsecVPN(ipsecVPNConfig *types.EdgeGatewayServiceConf ipsecVPNConfig.Xmlns = types.XMLNamespaceVCloud - apiEndpoint, _ := url.ParseRequestURI(egw.EdgeGateway.HREF) + apiEndpoint := urlParseRequestURI(egw.EdgeGateway.HREF) apiEndpoint.Path += "/action/configureServices" // Return the task @@ -1174,7 +1174,7 @@ func (egw *EdgeGateway) UpdateLBGeneralParams(enabled, accelerationEnabled, logg } // GetFirewallConfig retrieves firewall configuration and can be used -// to alter master configuration options. These are 3 fields only: +// to alter main configuration options. These are 3 fields only: // FirewallConfigWithXml.Enabled, FirewallConfigWithXml.DefaultPolicy.LoggingEnabled and // FirewallConfigWithXml.DefaultPolicy.Action func (egw *EdgeGateway) GetFirewallConfig() (*types.FirewallConfigWithXml, error) { diff --git a/govcd/edgegateway_test.go b/govcd/edgegateway_test.go index ec4905541..406cd5ea2 100644 --- a/govcd/edgegateway_test.go +++ b/govcd/edgegateway_test.go @@ -1,4 +1,4 @@ -// +build gateway functional ALL +//go:build gateway || functional || ALL /* * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -285,6 +285,7 @@ func (vcd *TestVCD) TestEdgeGateway_GetNetworks(check *C) { } func (vcd *TestVCD) Test_AddSNATRule(check *C) { + vcd.skipIfNotSysAdmin(check) if vcd.config.VCD.ExternalIp == "" || vcd.config.VCD.InternalIp == "" { check.Skip("Skipping test because no valid ip given") } @@ -356,6 +357,7 @@ func (vcd *TestVCD) Test_AddSNATRule(check *C) { } func (vcd *TestVCD) Test_AddDNATRule(check *C) { + vcd.skipIfNotSysAdmin(check) if vcd.config.VCD.ExternalIp == "" || vcd.config.VCD.InternalIp == "" { check.Skip("Skipping test because no valid ip given") } @@ -436,6 +438,7 @@ func (vcd *TestVCD) Test_AddDNATRule(check *C) { } func (vcd *TestVCD) Test_UpdateNATRule(check *C) { + vcd.skipIfNotSysAdmin(check) if vcd.config.VCD.ExternalIp == "" || vcd.config.VCD.InternalIp == "" { check.Skip("Skipping test because no valid ip given") } @@ -551,6 +554,7 @@ func (vcd *TestVCD) Test_UpdateNATRule(check *C) { // 4. Compare the XML text and structs before configuration and after configuration - they should be // identical except tag which is versioning the configuration func (vcd *TestVCD) TestEdgeGateway_UpdateLBGeneralParams(check *C) { + vcd.skipIfNotSysAdmin(check) if vcd.config.VCD.EdgeGateway == "" { check.Skip("Skipping test because no edge gateway given") } @@ -624,6 +628,7 @@ func (vcd *TestVCD) TestEdgeGateway_UpdateFwGeneralParams(check *C) { } func (vcd *TestVCD) TestEdgeGateway_GetVdcNetworks(check *C) { + vcd.skipIfNotSysAdmin(check) if vcd.config.VCD.EdgeGateway == "" { check.Skip("Skipping test because no edge gateway given") } @@ -745,6 +750,7 @@ func (vcd *TestVCD) TestListEdgeGateway(check *C) { } func (vcd *TestVCD) Test_UpdateEdgeGateway(check *C) { + vcd.skipIfNotSysAdmin(check) if vcd.config.VCD.EdgeGateway == "" { check.Skip("Skipping test because no edge gateway given") } diff --git a/govcd/edgegateway_unit_test.go b/govcd/edgegateway_unit_test.go index 303c93390..0a9a43607 100644 --- a/govcd/edgegateway_unit_test.go +++ b/govcd/edgegateway_unit_test.go @@ -1,4 +1,4 @@ -// +build unit ALL +//go:build unit || ALL /* * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -240,10 +240,10 @@ func Test_getVnicIndexFromNetworkNameType(t *testing.T) { hasError bool expectedError error }{ - {"ExtNetwork", "my-ext-network", types.EdgeGatewayVnicTypeUplink, takeIntAddress(0), false, nil}, - {"OrgNetwork", "my-vdc-int-net", types.EdgeGatewayVnicTypeInternal, takeIntAddress(1), false, nil}, - {"WithSubinterfaces", "subinterfaced-net", types.EdgeGatewayVnicTypeSubinterface, takeIntAddress(10), false, nil}, - {"WithSubinterfaces2", "subinterface2", types.EdgeGatewayVnicTypeSubinterface, takeIntAddress(11), false, nil}, + {"ExtNetwork", "my-ext-network", types.EdgeGatewayVnicTypeUplink, addrOf(0), false, nil}, + {"OrgNetwork", "my-vdc-int-net", types.EdgeGatewayVnicTypeInternal, addrOf(1), false, nil}, + {"WithSubinterfaces", "subinterfaced-net", types.EdgeGatewayVnicTypeSubinterface, addrOf(10), false, nil}, + {"WithSubinterfaces2", "subinterface2", types.EdgeGatewayVnicTypeSubinterface, addrOf(11), false, nil}, {"NonExistingUplink", "invalid-network-name", types.EdgeGatewayVnicTypeUplink, nil, true, ErrorEntityNotFound}, {"NonExistingInternal", "invalid-network-name", types.EdgeGatewayVnicTypeInternal, nil, true, ErrorEntityNotFound}, {"NonExistingSubinterface", "invalid-network-name", types.EdgeGatewayVnicTypeSubinterface, nil, true, ErrorEntityNotFound}, diff --git a/govcd/entity.go b/govcd/entity.go index e05f70974..972784b88 100644 --- a/govcd/entity.go +++ b/govcd/entity.go @@ -11,19 +11,19 @@ type genericGetter func(string, bool) (interface{}, error) // On failure, returns a nil pointer and an error // Example usage: // -// func (org *Org) GetCatalogByNameOrId(identifier string, refresh bool) (*Catalog, error) { -// getByName := func(name string, refresh bool) (interface{}, error) { -// return org.GetCatalogByName(name, refresh) -// } -// getById := func(id string, refresh bool) (interface{}, error) { -// return org.GetCatalogById(id, refresh) -// } -// entity, err := getEntityByNameOrId(getByName, getById, identifier, refresh) -// if entity != nil { -// return nil, err -// } -// return entity.(*Catalog), err -// } +// func (org *Org) GetCatalogByNameOrId(identifier string, refresh bool) (*Catalog, error) { +// getByName := func(name string, refresh bool) (interface{}, error) { +// return org.GetCatalogByName(name, refresh) +// } +// getById := func(id string, refresh bool) (interface{}, error) { +// return org.GetCatalogById(id, refresh) +// } +// entity, err := getEntityByNameOrId(getByName, getById, identifier, refresh) +// if entity != nil { +// return nil, err +// } +// return entity.(*Catalog), err +// } func getEntityByNameOrId(getByName, getById genericGetter, identifier string, refresh bool) (interface{}, error) { var byNameErr, byIdErr error @@ -43,3 +43,29 @@ func getEntityByNameOrId(getByName, getById genericGetter, identifier string, re return nil, byIdErr } } + +// getEntityByNameOrIdSkipNonId is like getEntityByNameOrId, but it does not even attempt to lookup "ById" if the +// identifier does not look like URN or UUID +func getEntityByNameOrIdSkipNonId(getByName, getById genericGetter, identifier string, refresh bool) (interface{}, error) { + + var byNameErr, byIdErr error + var entity interface{} + + // Only check by Id if it is an ID or an URN + if isUrn(identifier) || IsUuid(identifier) { + entity, byIdErr = getById(identifier, refresh) + if byIdErr == nil { + // Found by ID + return entity, nil + } + } + + if IsNotFound(byIdErr) || byIdErr == nil { + // Not found by ID, try by name + entity, byNameErr = getByName(identifier, false) + return entity, byNameErr + } else { + // On any other error, we return it + return nil, byIdErr + } +} diff --git a/govcd/entity_test.go b/govcd/entity_test.go index 112a8e64d..b45619e07 100644 --- a/govcd/entity_test.go +++ b/govcd/entity_test.go @@ -1,4 +1,4 @@ -// +build api functional catalog org extnetwork vm vdc system user nsxv network vapp vm affinity ALL +//go:build api || functional || catalog || org || extnetwork || vm || vdc || system || user || nsxv || network || vapp || vm || affinity || ALL /* * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -90,31 +90,31 @@ func (vmar *VmAffinityRule) id() string { return vmar.VmAffinityRule.ID } // and within the caller it must define the getter functions // Example usage: // -// func (vcd *TestVCD) Test_OrgGetVdc(check *C) { -// if vcd.config.VCD.Org == "" { -// check.Skip("Test_OrgGetVdc: Org name not given.") -// return -// } -// org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) -// check.Assert(err, IsNil) -// check.Assert(org, NotNil) +// func (vcd *TestVCD) Test_OrgGetVdc(check *C) { +// if vcd.config.VCD.Org == "" { +// check.Skip("Test_OrgGetVdc: Org name not given.") +// return +// } +// org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) +// check.Assert(err, IsNil) +// check.Assert(org, NotNil) // -// getByName := func(name string, refresh bool) (genericEntity, error) { return org.GetVDCByName(name, refresh) } -// getById := func(id string, refresh bool) (genericEntity, error) { return org.GetVDCById(id, refresh) } -// getByNameOrId := func(id string, refresh bool) (genericEntity, error) { return org.GetVDCByNameOrId(id, refresh) } +// getByName := func(name string, refresh bool) (genericEntity, error) { return org.GetVDCByName(name, refresh) } +// getById := func(id string, refresh bool) (genericEntity, error) { return org.GetVDCById(id, refresh) } +// getByNameOrId := func(id string, refresh bool) (genericEntity, error) { return org.GetVDCByNameOrId(id, refresh) } // -// var def = getterTestDefinition{ -// parentType: "Org", -// parentName: vcd.config.VCD.Org, -// entityType: "Vdc", -// getterPrefix: "VDC", -// entityName: vcd.config.VCD.Vdc, -// getByName: getByName, -// getById: getById, -// getByNameOrId: getByNameOrId, +// var def = getterTestDefinition{ +// parentType: "Org", +// parentName: vcd.config.VCD.Org, +// entityType: "Vdc", +// getterPrefix: "VDC", +// entityName: vcd.config.VCD.Vdc, +// getByName: getByName, +// getById: getById, +// getByNameOrId: getByNameOrId, +// } +// vcd.testFinderGetGenericEntity(def, check) // } -// vcd.testFinderGetGenericEntity(def, check) -// } func (vcd *TestVCD) testFinderGetGenericEntity(def getterTestDefinition, check *C) { entityName := def.entityName if entityName == "" { @@ -142,7 +142,7 @@ func (vcd *TestVCD) testFinderGetGenericEntity(def getterTestDefinition, check * check.Skip(fmt.Sprintf("testFinderGetGenericEntity: %s %s not found.", def.entityType, def.entityName)) return } - entity1 := ge.(genericEntity) + entity1 := ge wantedType := fmt.Sprintf("*govcd.%s", def.entityType) if testVerbose { @@ -157,12 +157,12 @@ func (vcd *TestVCD) testFinderGetGenericEntity(def getterTestDefinition, check * // 2. Get the entity by ID if testVerbose { - fmt.Printf("#Testing %s.Get%sById\n", def.parentType, def.getterPrefix) + fmt.Printf("#Testing %s.Get%sById (using ID '%s')\n", def.parentType, def.getterPrefix, entityId) } ge, err = def.getById(entityId, false) check.Assert(err, IsNil) check.Assert(ge, NotNil) - entity2 := ge.(genericEntity) + entity2 := ge check.Assert(entity2, NotNil) check.Assert(entity2.name(), Equals, entityName) check.Assert(entity2.id(), Equals, entityId) @@ -175,7 +175,7 @@ func (vcd *TestVCD) testFinderGetGenericEntity(def getterTestDefinition, check * ge, err = def.getByNameOrId(entityId, false) check.Assert(err, IsNil) check.Assert(ge, NotNil) - entity3 := ge.(genericEntity) + entity3 := ge check.Assert(entity3, NotNil) check.Assert(entity3.name(), Equals, entityName) check.Assert(entity3.id(), Equals, entityId) @@ -183,12 +183,13 @@ func (vcd *TestVCD) testFinderGetGenericEntity(def getterTestDefinition, check * // 4. Get the entity by Name or ID, using the entity name if testVerbose { - fmt.Printf("#Testing %s.Get%sByNameOrId\n", def.parentType, def.getterPrefix) + fmt.Printf("#Testing %s.Get%sByNameOrId (name '%s', ID '%s')\n", + def.parentType, def.getterPrefix, entityName, entityId) } ge, err = def.getByNameOrId(entityName, false) - check.Assert(ge, NotNil) - entity4 := ge.(genericEntity) check.Assert(err, IsNil) + check.Assert(ge, NotNil) + entity4 := ge check.Assert(entity4, NotNil) check.Assert(entity4.name(), Equals, entityName) check.Assert(entity4.id(), Equals, entityId) diff --git a/govcd/external_network_v2.go b/govcd/external_network_v2.go index b5d58bb4b..89128eff1 100644 --- a/govcd/external_network_v2.go +++ b/govcd/external_network_v2.go @@ -23,7 +23,7 @@ type ExternalNetworkV2 struct { // provided. types.ExternalNetworkV2 has documented fields. func CreateExternalNetworkV2(vcdClient *VCDClient, newExtNet *types.ExternalNetworkV2) (*ExternalNetworkV2, error) { endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointExternalNetworks - minimumApiVersion, err := vcdClient.Client.checkOpenApiEndpointCompatibility(endpoint) + apiVersion, err := vcdClient.Client.getOpenApiHighestElevatedVersion(endpoint) if err != nil { return nil, err } @@ -38,7 +38,7 @@ func CreateExternalNetworkV2(vcdClient *VCDClient, newExtNet *types.ExternalNetw client: &vcdClient.Client, } - err = vcdClient.Client.OpenApiPostItem(minimumApiVersion, urlRef, nil, newExtNet, returnExtNet.ExternalNetwork) + err = vcdClient.Client.OpenApiPostItem(apiVersion, urlRef, nil, newExtNet, returnExtNet.ExternalNetwork, nil) if err != nil { return nil, fmt.Errorf("error creating external network: %s", err) } @@ -49,7 +49,7 @@ func CreateExternalNetworkV2(vcdClient *VCDClient, newExtNet *types.ExternalNetw // GetExternalNetworkV2ById retrieves external network by given ID using OpenAPI endpoint func GetExternalNetworkV2ById(vcdClient *VCDClient, id string) (*ExternalNetworkV2, error) { endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointExternalNetworks - minimumApiVersion, err := vcdClient.Client.checkOpenApiEndpointCompatibility(endpoint) + apiVersion, err := vcdClient.Client.getOpenApiHighestElevatedVersion(endpoint) if err != nil { return nil, err } @@ -68,7 +68,7 @@ func GetExternalNetworkV2ById(vcdClient *VCDClient, id string) (*ExternalNetwork client: &vcdClient.Client, } - err = vcdClient.Client.OpenApiGetItem(minimumApiVersion, urlRef, nil, extNet.ExternalNetwork) + err = vcdClient.Client.OpenApiGetItem(apiVersion, urlRef, nil, extNet.ExternalNetwork, nil) if err != nil { return nil, err } @@ -107,7 +107,7 @@ func GetExternalNetworkV2ByName(vcdClient *VCDClient, name string) (*ExternalNet // perform additional filtering func GetAllExternalNetworksV2(vcdClient *VCDClient, queryParameters url.Values) ([]*ExternalNetworkV2, error) { endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointExternalNetworks - minimumApiVersion, err := vcdClient.Client.checkOpenApiEndpointCompatibility(endpoint) + apiVersion, err := vcdClient.Client.getOpenApiHighestElevatedVersion(endpoint) if err != nil { return nil, err } @@ -118,7 +118,7 @@ func GetAllExternalNetworksV2(vcdClient *VCDClient, queryParameters url.Values) } typeResponses := []*types.ExternalNetworkV2{{}} - err = vcdClient.Client.OpenApiGetAllItems(minimumApiVersion, urlRef, queryParameters, &typeResponses) + err = vcdClient.Client.OpenApiGetAllItems(apiVersion, urlRef, queryParameters, &typeResponses, nil) if err != nil { return nil, err } @@ -138,7 +138,7 @@ func GetAllExternalNetworksV2(vcdClient *VCDClient, queryParameters url.Values) // Update updates existing external network using OpenAPI endpoint func (extNet *ExternalNetworkV2) Update() (*ExternalNetworkV2, error) { endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointExternalNetworks - minimumApiVersion, err := extNet.client.checkOpenApiEndpointCompatibility(endpoint) + apiVersion, err := extNet.client.getOpenApiHighestElevatedVersion(endpoint) if err != nil { return nil, err } @@ -157,7 +157,7 @@ func (extNet *ExternalNetworkV2) Update() (*ExternalNetworkV2, error) { client: extNet.client, } - err = extNet.client.OpenApiPutItem(minimumApiVersion, urlRef, nil, extNet.ExternalNetwork, returnExtNet.ExternalNetwork) + err = extNet.client.OpenApiPutItem(apiVersion, urlRef, nil, extNet.ExternalNetwork, returnExtNet.ExternalNetwork, nil) if err != nil { return nil, fmt.Errorf("error updating external network: %s", err) } @@ -168,7 +168,7 @@ func (extNet *ExternalNetworkV2) Update() (*ExternalNetworkV2, error) { // Delete deletes external network using OpenAPI endpoint func (extNet *ExternalNetworkV2) Delete() error { endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointExternalNetworks - minimumApiVersion, err := extNet.client.checkOpenApiEndpointCompatibility(endpoint) + apiVersion, err := extNet.client.getOpenApiHighestElevatedVersion(endpoint) if err != nil { return err } @@ -182,7 +182,7 @@ func (extNet *ExternalNetworkV2) Delete() error { return err } - err = extNet.client.OpenApiDeleteItem(minimumApiVersion, urlRef, nil) + err = extNet.client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) if err != nil { return fmt.Errorf("error deleting extNet: %s", err) diff --git a/govcd/external_network_v2_test.go b/govcd/external_network_v2_test.go index b31ed4b5a..975d468e6 100644 --- a/govcd/external_network_v2_test.go +++ b/govcd/external_network_v2_test.go @@ -1,4 +1,4 @@ -// +build extnetwork network functional openapi ALL +//go:build extnetwork || network || nsxt || functional || openapi || ALL /* * Copyright 2020 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -14,22 +14,21 @@ import ( ) func (vcd *TestVCD) Test_CreateExternalNetworkV2Nsxt(check *C) { - vcd.testCreateExternalNetworkV2Nsxt(check, vcd.config.VCD.Nsxt.Tier0router, types.ExternalNetworkBackingTypeNsxtTier0Router) + vcd.testCreateExternalNetworkV2Nsxt(check, vcd.config.VCD.Nsxt.Tier0router, types.ExternalNetworkBackingTypeNsxtTier0Router, false, "", "", "") } func (vcd *TestVCD) Test_CreateExternalNetworkV2NsxtVrf(check *C) { - // The documented backing type of NSX-T VRF router is "NSXT_VRF_TIER0" (types.ExternalNetworkBackingTypeNsxtVrfTier0Router) - // but although it is documented - it fails and requires the same "NSXT_TIER0" (types.ExternalNetworkBackingTypeNsxtTier0Router) - // backing type to be specified - // As of 10.1.2 release it is not officially supported (support only introduced in 10.2.0) therefore skipping this test for - // 10.1.X. 10.1.1 allowed to create it, but 10.1.2 introduced a validator and throws error. - if vcd.client.Client.APIVCDMaxVersionIs("< 35") { - check.Skip("NSX-T VRF-Lite backed external networks are officially supported only in 10.2.0+") - } - vcd.testCreateExternalNetworkV2Nsxt(check, vcd.config.VCD.Nsxt.Tier0routerVrf, types.ExternalNetworkBackingTypeNsxtTier0Router) + vcd.testCreateExternalNetworkV2Nsxt(check, vcd.config.VCD.Nsxt.Tier0routerVrf, types.ExternalNetworkBackingTypeNsxtTier0Router, false, "", "", "") } -func (vcd *TestVCD) testCreateExternalNetworkV2Nsxt(check *C, nsxtTier0Router, backingType string) { +func (vcd *TestVCD) Test_CreateExternalNetworkV2NsxtSegment(check *C) { + vcd.testCreateExternalNetworkV2Nsxt(check, vcd.config.VCD.Nsxt.NsxtImportSegment, types.ExternalNetworkBackingTypeNsxtSegment, false, "", "", "") +} + +func (vcd *TestVCD) testCreateExternalNetworkV2Nsxt(check *C, backingName, backingType string, useIpSpace bool, ownerOrgId, natAndFwIntention, raIntention string) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointExternalNetworks skipOpenApiEndpointTest(vcd, check, endpoint) skipNoNsxtConfiguration(vcd, check) @@ -42,11 +41,10 @@ func (vcd *TestVCD) testCreateExternalNetworkV2Nsxt(check *C, nsxtTier0Router, b nsxtManagerId, err := BuildUrnWithUuid("urn:vcloud:nsxtmanager:", extractUuid(man[0].HREF)) check.Assert(err, IsNil) - tier0RouterVrf, err := vcd.client.GetImportableNsxtTier0RouterByName(nsxtTier0Router, nsxtManagerId) - check.Assert(err, IsNil) + backingId := getBackingIdByNameAndType(check, backingName, backingType, vcd, nsxtManagerId) // Create network and test CRUD capabilities - netNsxt := testExternalNetworkV2(check.TestName(), backingType, tier0RouterVrf.NsxtTier0Router.ID, nsxtManagerId) + netNsxt := testExternalNetworkV2(check.TestName(), backingType, backingId, nsxtManagerId, useIpSpace, ownerOrgId, natAndFwIntention, raIntention) createdNet, err := CreateExternalNetworkV2(vcd.client, netNsxt) check.Assert(err, IsNil) @@ -85,7 +83,28 @@ func (vcd *TestVCD) testCreateExternalNetworkV2Nsxt(check *C, nsxtTier0Router, b check.Assert(ContainsNotFound(err), Equals, true) } +// getBackingIdByNameAndType looks up Backing ID by name and type +func getBackingIdByNameAndType(check *C, backingName string, backingType string, vcd *TestVCD, nsxtManagerId string) string { + var backingId string + switch { + case backingType == types.ExternalNetworkBackingTypeNsxtTier0Router || backingType == types.ExternalNetworkBackingTypeNsxtVrfTier0Router: // Lookup T0 or T0 VRF + tier0RouterVrf, err := vcd.client.GetImportableNsxtTier0RouterByName(backingName, nsxtManagerId) + check.Assert(err, IsNil) + backingId = tier0RouterVrf.NsxtTier0Router.ID + case backingType == types.ExternalNetworkBackingTypeNsxtSegment: // Lookup segment ID + bareNsxtManagerId, err := getBareEntityUuid(nsxtManagerId) + check.Assert(err, IsNil) + filter := map[string]string{"nsxTManager": bareNsxtManagerId} + + nsxtSegment, err := vcd.client.GetFilteredNsxtImportableSwitches(filter) + check.Assert(err, IsNil) + backingId = nsxtSegment[0].NsxtImportableSwitch.ID + } + return backingId +} + func (vcd *TestVCD) Test_CreateExternalNetworkV2Nsxv(check *C) { + vcd.skipIfNotSysAdmin(check) endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointExternalNetworks skipOpenApiEndpointTest(vcd, check, endpoint) @@ -108,14 +127,14 @@ func (vcd *TestVCD) Test_CreateExternalNetworkV2Nsxv(check *C) { // Query vcHref, err := getVcenterHref(vcd.client, vcd.config.VCD.VimServer) check.Assert(err, IsNil) - vcuuid := extractUuid(vcHref) + vcUuid := extractUuid(vcHref) - vcUrn, err := BuildUrnWithUuid("urn:vcloud:vimserver:", vcuuid) + vcUrn, err := BuildUrnWithUuid("urn:vcloud:vimserver:", vcUuid) check.Assert(err, IsNil) - neT := testExternalNetworkV2(check.TestName(), vcd.config.VCD.ExternalNetworkPortGroupType, pgs[0].MoRef, vcUrn) + net := testExternalNetworkV2(check.TestName(), vcd.config.VCD.ExternalNetworkPortGroupType, pgs[0].MoRef, vcUrn, false, "", "", "") - r, err := CreateExternalNetworkV2(vcd.client, neT) + r, err := CreateExternalNetworkV2(vcd.client, net) check.Assert(err, IsNil) // Use generic "OpenApiEntity" resource cleanup type @@ -131,19 +150,21 @@ func (vcd *TestVCD) Test_CreateExternalNetworkV2Nsxv(check *C) { check.Assert(err, IsNil) } -func testExternalNetworkV2(name, backingType, backingId, NetworkProviderId string) *types.ExternalNetworkV2 { - neT := &types.ExternalNetworkV2{ - ID: "", - Name: name, - Description: "", - Subnets: types.ExternalNetworkV2Subnets{[]types.ExternalNetworkV2Subnet{ +func testExternalNetworkV2(name, backingType, backingId, NetworkProviderId string, useIpSpace bool, ownerOrgId, natAndFwIntention, raIntention string) *types.ExternalNetworkV2 { + net := &types.ExternalNetworkV2{ + ID: "", + Name: name, + Description: "", + NatAndFirewallServiceIntention: natAndFwIntention, + NetworkRouteAdvertisementIntention: raIntention, + Subnets: types.ExternalNetworkV2Subnets{Values: []types.ExternalNetworkV2Subnet{ { Gateway: "1.1.1.1", PrefixLength: 24, DNSSuffix: "", DNSServer1: "", DNSServer2: "", - IPRanges: types.ExternalNetworkV2IPRanges{[]types.ExternalNetworkV2IPRange{ + IPRanges: types.ExternalNetworkV2IPRanges{Values: []types.ExternalNetworkV2IPRange{ { StartAddress: "1.1.1.3", EndAddress: "1.1.1.50", @@ -154,20 +175,77 @@ func testExternalNetworkV2(name, backingType, backingId, NetworkProviderId strin TotalIPCount: 0, }, }}, - NetworkBackings: types.ExternalNetworkV2Backings{[]types.ExternalNetworkV2Backing{ + NetworkBackings: types.ExternalNetworkV2Backings{Values: []types.ExternalNetworkV2Backing{ { BackingID: backingId, - // Name: tier0Router.NsxtTier0Router.DisplayName, - BackingType: backingType, NetworkProvider: types.NetworkProvider{ - // Name: vcd.config.Nsxt.Manager, ID: NetworkProviderId, }, + BackingTypeValue: backingType, }, }}, } - return neT + if useIpSpace { + // removing subnet definition when using IP Spaces + net.Subnets = types.ExternalNetworkV2Subnets{} + net.UsingIpSpace = &useIpSpace + } + + if ownerOrgId != "" { + net.DedicatedOrg = &types.OpenApiReference{ID: ownerOrgId} + } + + return net +} + +func (vcd *TestVCD) Test_CreateExternalNetworkV2NsxtIpSpaceT0(check *C) { + if vcd.client.Client.APIVCDMaxVersionIs("< 37.1") { + check.Skip("IP Spaces are supported in VCD 10.4.1+") + } + vcd.testCreateExternalNetworkV2Nsxt(check, vcd.config.VCD.Nsxt.Tier0router, types.ExternalNetworkBackingTypeNsxtTier0Router, true, "", "", "") +} + +func (vcd *TestVCD) Test_CreateExternalNetworkV2NsxtIpSpaceVrf(check *C) { + if vcd.client.Client.APIVCDMaxVersionIs("< 37.1") { + check.Skip("IP Spaces are supported in VCD 10.4.1+") + } + vcd.testCreateExternalNetworkV2Nsxt(check, vcd.config.VCD.Nsxt.Tier0routerVrf, types.ExternalNetworkBackingTypeNsxtTier0Router, true, "", "", "") +} + +func (vcd *TestVCD) Test_CreateExternalNetworkV2NsxtIpSpaceT0DedicatedOrg(check *C) { + if vcd.client.Client.APIVCDMaxVersionIs("< 37.1") { + check.Skip("IP Spaces are supported in VCD 10.4.1+") + } + vcd.testCreateExternalNetworkV2Nsxt(check, vcd.config.VCD.Nsxt.Tier0router, types.ExternalNetworkBackingTypeNsxtTier0Router, true, vcd.org.Org.ID, "", "") +} + +func (vcd *TestVCD) Test_CreateExternalNetworkV2NsxtIpSpaceVrfDedicatedOrg(check *C) { + if vcd.client.Client.APIVCDMaxVersionIs("< 37.1") { + check.Skip("IP Spaces are supported in VCD 10.4.1+") + } + vcd.testCreateExternalNetworkV2Nsxt(check, vcd.config.VCD.Nsxt.Tier0routerVrf, types.ExternalNetworkBackingTypeNsxtTier0Router, true, vcd.org.Org.ID, "", "") +} + +func (vcd *TestVCD) Test_CreateExternalNetworkV2NsxtNatAndFwIntentionProviderGateway(check *C) { + if vcd.client.Client.APIVCDMaxVersionIs("< 38.1") { + check.Skip("NAT and Firewall intentions are supported in VCD 10.5.1+") + } + vcd.testCreateExternalNetworkV2Nsxt(check, vcd.config.VCD.Nsxt.Tier0routerVrf, types.ExternalNetworkBackingTypeNsxtTier0Router, true, vcd.org.Org.ID, "PROVIDER_GATEWAY", "IP_SPACE_UPLINKS_ADVERTISED_FLEXIBLE") +} + +func (vcd *TestVCD) Test_CreateExternalNetworkV2NsxtNatAndFwIntentionProviderAndEdgeGateway(check *C) { + if vcd.client.Client.APIVCDMaxVersionIs("< 38.1") { + check.Skip("NAT and Firewall intentions are supported in VCD 10.5.1+") + } + vcd.testCreateExternalNetworkV2Nsxt(check, vcd.config.VCD.Nsxt.Tier0routerVrf, types.ExternalNetworkBackingTypeNsxtTier0Router, true, vcd.org.Org.ID, "PROVIDER_AND_EDGE_GATEWAY", "ALL_NETWORKS_ADVERTISED") +} + +func (vcd *TestVCD) Test_CreateExternalNetworkV2NsxtNatAndFwIntentionEdgeGateway(check *C) { + if vcd.client.Client.APIVCDMaxVersionIs("< 38.1") { + check.Skip("NAT and Firewall intentions are supported in VCD 10.5.1+") + } + vcd.testCreateExternalNetworkV2Nsxt(check, vcd.config.VCD.Nsxt.Tier0routerVrf, types.ExternalNetworkBackingTypeNsxtTier0Router, true, vcd.org.Org.ID, "EDGE_GATEWAY", "IP_SPACE_UPLINKS_ADVERTISED_STRICT") } func getVcenterHref(vcdClient *VCDClient, name string) (string, error) { diff --git a/govcd/externalnetwork_test.go b/govcd/externalnetwork_test.go index 6d1ffcca7..dbaf6b25a 100644 --- a/govcd/externalnetwork_test.go +++ b/govcd/externalnetwork_test.go @@ -1,4 +1,4 @@ -// +build extnetwork network functional ALL +//go:build extnetwork || network || functional || ALL /* * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. diff --git a/govcd/filter_condition.go b/govcd/filter_condition.go index ac9e0861c..72e64f42c 100644 --- a/govcd/filter_condition.go +++ b/govcd/filter_condition.go @@ -51,12 +51,13 @@ type parentIdCondition struct { // matchParent matches the wanted parent name (passed in 'stored') to the parent of the queryItem // Input: -// * stored: the data of the condition (a parentCondition) -// * item: a QueryItem +// - stored: the data of the condition (a parentCondition) +// - item: a QueryItem +// // Returns: -// * bool: the result of the comparison -// * string: a description of the operation -// * error: an error when the input is not as expected +// - bool: the result of the comparison +// - string: a description of the operation +// - error: an error when the input is not as expected func matchParent(stored, item interface{}) (bool, string, error) { condition, ok := stored.(parentCondition) if !ok { @@ -74,12 +75,13 @@ func matchParent(stored, item interface{}) (bool, string, error) { // matchParentId matches the wanted parent ID (passed in 'stored') to the parent ID of the queryItem // The IDs being compared are filtered through extractUuid, to make them homogeneous // Input: -// * stored: the data of the condition (a parentCondition) -// * item: a QueryItem +// - stored: the data of the condition (a parentCondition) +// - item: a QueryItem +// // Returns: -// * bool: the result of the comparison -// * string: a description of the operation -// * error: an error when the input is not as expected +// - bool: the result of the comparison +// - string: a description of the operation +// - error: an error when the input is not as expected func matchParentId(stored, item interface{}) (bool, string, error) { condition, ok := stored.(parentIdCondition) if !ok { @@ -98,12 +100,13 @@ func matchParentId(stored, item interface{}) (bool, string, error) { // matchName matches a name (passed in 'stored') to the name of the queryItem // Input: -// * stored: the data of the condition (a nameCondition) -// * item: a QueryItem +// - stored: the data of the condition (a nameCondition) +// - item: a QueryItem +// // Returns: -// * bool: the result of the comparison -// * string: a description of the operation -// * error: an error when the input is not as expected +// - bool: the result of the comparison +// - string: a description of the operation +// - error: an error when the input is not as expected func matchName(stored, item interface{}) (bool, string, error) { re, ok := stored.(nameCondition) if !ok { @@ -118,12 +121,13 @@ func matchName(stored, item interface{}) (bool, string, error) { // matchIp matches an IP (passed in 'stored') to the IP of the queryItem // Input: -// * stored: the data of the condition (an ipCondition) -// * item: a QueryItem +// - stored: the data of the condition (an ipCondition) +// - item: a QueryItem +// // Returns: -// * bool: the result of the comparison -// * string: a description of the operation -// * error: an error when the input is not as expected +// - bool: the result of the comparison +// - string: a description of the operation +// - error: an error when the input is not as expected func matchIp(stored, item interface{}) (bool, string, error) { re, ok := stored.(ipCondition) if !ok { @@ -142,12 +146,13 @@ func matchIp(stored, item interface{}) (bool, string, error) { // matchDate matches a date (passed in 'stored') to the date of the queryItem // Input: -// * stored: the data of the condition (a dateCondition) -// * item: a QueryItem +// - stored: the data of the condition (a dateCondition) +// - item: a QueryItem +// // Returns: -// * bool: the result of the comparison -// * string: a description of the operation -// * error: an error when the input is not as expected +// - bool: the result of the comparison +// - string: a description of the operation +// - error: an error when the input is not as expected func matchDate(stored, item interface{}) (bool, string, error) { expr, ok := stored.(dateCondition) if !ok { @@ -167,12 +172,13 @@ func matchDate(stored, item interface{}) (bool, string, error) { // matchMetadata matches a value (passed in 'stored') to the metadata value retrieved from queryItem // Input: -// * stored: the data of the condition (a metadataRegexpCondition) -// * item: a QueryItem +// - stored: the data of the condition (a metadataRegexpCondition) +// - item: a QueryItem +// // Returns: -// * bool: the result of the comparison -// * string: a description of the operation -// * error: an error when the input is not as expected +// - bool: the result of the comparison +// - string: a description of the operation +// - error: an error when the input is not as expected func matchMetadata(stored, item interface{}) (bool, string, error) { re, ok := stored.(metadataRegexpCondition) if !ok { diff --git a/govcd/filter_engine.go b/govcd/filter_engine.go index 94729099c..d091acfee 100644 --- a/govcd/filter_engine.go +++ b/govcd/filter_engine.go @@ -250,7 +250,7 @@ func searchByFilter(queryByMetadata queryByMetadataFunc, queryWithMetadataFields util.Logger.Printf("[SearchByFilter] result %v: ", greater) if greater { latestDate = candidate.GetDate() - candidateByLatest = candidate.(QueryItem) + candidateByLatest = candidate } } if candidateByLatest != nil { @@ -281,7 +281,7 @@ func searchByFilter(queryByMetadata queryByMetadataFunc, queryWithMetadataFields util.Logger.Printf("[SearchByFilter] result %v: ", greater) if greater { earliestDate = candidate.GetDate() - candidateByEarliest = candidate.(QueryItem) + candidateByEarliest = candidate } } if candidateByEarliest != nil { @@ -291,10 +291,6 @@ func searchByFilter(queryByMetadata queryByMetadataFunc, queryWithMetadataFields return nil, explanation, fmt.Errorf("search for oldest item failed. Empty dates found for items %v", emptyDatesFound) } } - if searchEarliest || searchLatest { - // We should never reach this point, as a failure for newest or oldest item was caught above, but just in case - return nil, explanation, fmt.Errorf("search for oldest or earliest item failed. No reason found") - } return candidatesByConditions, explanation, nil } diff --git a/govcd/filter_engine_test.go b/govcd/filter_engine_test.go index 7e051314e..0a19a86e0 100644 --- a/govcd/filter_engine_test.go +++ b/govcd/filter_engine_test.go @@ -1,4 +1,4 @@ -// +build search functional ALL +//go:build search || functional || ALL /* * Copyright 2020 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -309,3 +309,46 @@ func (vcd *TestVCD) Test_SearchMediaItem(check *C) { } } } + +func (vcd *TestVCD) Test_SearchOrgVdc(check *C) { + vcd.skipIfNotSysAdmin(check) // this test creates another VDC + if vcd.config.VCD.Vdc == "" { + check.Skip("no VDC provided. Skipping test") + } + client := vcd.client + // Fetching organization and VDC + org, err := client.GetOrgByName(vcd.org.Org.Name) + check.Assert(err, IsNil) + check.Assert(org, NotNil) + + anotherVdc := spawnTestVdc(vcd, check, vcd.org.Org.Name) + // Add some metadata to the newly created VDC + _, err = anotherVdc.AddMetadata("key1", "value1") + check.Assert(err, IsNil) + _, err = anotherVdc.AddMetadata("key2", "value2") + check.Assert(err, IsNil) + _, err = anotherVdc.AddMetadata("key3", "value3") + check.Assert(err, IsNil) + + // Get existing vdc, and create sample filters to retrieve them + filters, err := HelperMakeFiltersFromOrgVdc(org) + check.Assert(err, IsNil) + check.Assert(filters, NotNil) + + queryType := client.Client.GetQueryType(types.QtOrgVdc) + + for _, fm := range filters { + queryItems, explanation, err := org.SearchByFilter(queryType, fm.Criteria) + check.Assert(err, IsNil) + printVerbose("%s\n", explanation) + check.Assert(len(queryItems), Equals, 1) + check.Assert(queryItems[0].GetName(), Equals, fm.ExpectedName) + for i, item := range queryItems { + printVerbose("( I) %2d %-10s %-20s %s\n\n", i, item.GetType(), item.GetParentName(), item.GetName()) + } + } + task, err := anotherVdc.Delete(true, true) + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) +} diff --git a/govcd/filter_engine_unit_test.go b/govcd/filter_engine_unit_test.go index cdd4d5116..f852a7957 100644 --- a/govcd/filter_engine_unit_test.go +++ b/govcd/filter_engine_unit_test.go @@ -1,4 +1,4 @@ -// +build unit +//go:build unit /* * Copyright 2020 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. diff --git a/govcd/filter_helpers.go b/govcd/filter_helpers.go index 17876db91..65ed2455c 100644 --- a/govcd/filter_helpers.go +++ b/govcd/filter_helpers.go @@ -12,6 +12,7 @@ import ( "time" "github.com/vmware/go-vcloud-director/v2/types/v56" + "github.com/vmware/go-vcloud-director/v2/util" ) // This file contains functions that help create tests for filtering. @@ -98,7 +99,7 @@ func HelperMakeFiltersFromNetworks(vdc *Vdc) ([]FilterMatch, error) { return nil, err } - filter, err = vdc.client.metadataToFilter(net.HREF, filter) + filter, err = vdc.client.metadataToFilter(net.HREF, net.Name, filter) if err != nil { return nil, err } @@ -152,18 +153,33 @@ func makeDateFilter(items []DateItem) ([]FilterMatch, error) { earliestFound = true } exactFilter := NewFilterDef() - _ = exactFilter.AddFilter(types.FilterDate, "=="+item.Date) + err = exactFilter.AddFilter(types.FilterDate, "=="+item.Date) + if err != nil { + return nil, fmt.Errorf("error adding filter '%s' '%s': %s", types.FilterDate, "=="+item.Date, err) + } filters = append(filters, FilterMatch{exactFilter, item.Name, item.Entity, item.EntityType}) } if earliestFound && latestFound && earliestDate != latestDate { earlyFilter := NewFilterDef() - _ = earlyFilter.AddFilter(types.FilterDate, "<"+latestDate) - _ = earlyFilter.AddFilter(types.FilterEarliest, "true") + err := earlyFilter.AddFilter(types.FilterDate, "<"+latestDate) + if err != nil { + return nil, err + } + err = earlyFilter.AddFilter(types.FilterEarliest, "true") + if err != nil { + return nil, err + } lateFilter := NewFilterDef() - _ = lateFilter.AddFilter(types.FilterDate, ">"+earliestDate) - _ = lateFilter.AddFilter(types.FilterLatest, "true") + err = lateFilter.AddFilter(types.FilterDate, ">"+earliestDate) + if err != nil { + return nil, err + } + err = lateFilter.AddFilter(types.FilterLatest, "true") + if err != nil { + return nil, err + } filters = append(filters, FilterMatch{earlyFilter, earliestName, earliestEntity, entityType}) filters = append(filters, FilterMatch{lateFilter, latestName, latestEntity, entityType}) @@ -191,7 +207,7 @@ func HelperMakeFiltersFromCatalogs(org *AdminOrg) ([]FilterMatch, error) { dateInfo = append(dateInfo, dInfo...) - filter, err = org.client.metadataToFilter(cat.HREF, filter) + filter, err = org.client.metadataToFilter(cat.HREF, cat.Name, filter) if err != nil { return nil, err } @@ -229,7 +245,7 @@ func HelperMakeFiltersFromMedia(vdc *Vdc, catalogName string) ([]FilterMatch, er dateInfo = append(dateInfo, dInfo...) - filter, err = vdc.client.metadataToFilter(item.HREF, filter) + filter, err = vdc.client.metadataToFilter(item.HREF, item.Name, filter) if err != nil { return nil, err } @@ -286,7 +302,7 @@ func HelperMakeFiltersFromCatalogItem(catalog *Catalog) ([]FilterMatch, error) { dateInfo = append(dateInfo, dInfo...) - filter, err = catalog.client.metadataToFilter(item.HREF, filter) + filter, err = catalog.client.metadataToFilter(item.HREF, item.Name, filter) if err != nil { return nil, err } @@ -322,7 +338,7 @@ func HelperMakeFiltersFromVappTemplate(catalog *Catalog) ([]FilterMatch, error) dateInfo = append(dateInfo, dInfo...) - filter, err = catalog.client.metadataToFilter(item.HREF, filter) + filter, err = catalog.client.metadataToFilter(item.HREF, item.Name, filter) if err != nil { return nil, err } @@ -415,6 +431,32 @@ func HelperCreateMultipleCatalogItems(catalog *Catalog, requestData []VappTempla return data, nil } +func HelperMakeFiltersFromOrgVdc(org *Org) ([]FilterMatch, error) { + var filters []FilterMatch + items, err := org.QueryOrgVdcList() + if err != nil { + return filters, err + } + for _, item := range items { + localItem := QueryOrgVdc(*item) + qItem := QueryItem(localItem) + + filter, _, err := queryItemToFilter(qItem, "QueryOrgVdc") + if err != nil { + return nil, err + } + + filter, err = org.client.metadataToFilter(item.HREF, item.Name, filter) + if err != nil { + return nil, err + } + + filters = append(filters, FilterMatch{filter, item.Name, localItem, "QueryOrgVdc"}) + } + + return filters, nil +} + // ipToRegex creates a regular expression that matches an IP without the last element func ipToRegex(ip string) string { elements := strings.Split(ip, ".") @@ -428,15 +470,25 @@ func ipToRegex(ip string) string { // strToRegex creates a regular expression that matches perfectly with the input query func strToRegex(s string) string { var result strings.Builder - result.WriteString("^") + var err error + _, err = result.WriteString("^") + if err != nil { + util.Logger.Printf("[DEBUG - strToRegex] error writing to string: %s", err) + } for _, ch := range s { if ch == '.' { - result.WriteString(fmt.Sprintf("\\%c", ch)) + _, err = result.WriteString(fmt.Sprintf("\\%c", ch)) } else { - result.WriteString(fmt.Sprintf("[%c]", ch)) + _, err = result.WriteString(fmt.Sprintf("[%c]", ch)) + } + if err != nil { + util.Logger.Printf("[DEBUG - strToRegex] error writing to string: %s", err) } } - result.WriteString("$") + _, err = result.WriteString("$") + if err != nil { + util.Logger.Printf("[DEBUG - strToRegex] error writing to string: %s", err) + } return result.String() } @@ -444,7 +496,7 @@ func strToRegex(s string) string { // If the value looks like a number, or a true/false value, the corresponding type is returned // Otherwise, we assume it's a string. // We do this because the API doesn't return the metadata type -// (it would if the field TypedValue.XsiType were defined as `xml:"type,attr"`, but then metadata updates would fail.) +// (it would if the field MetadataTypedValue.XsiType were defined as `xml:"type,attr"`, but then metadata updates would fail.) func guessMetadataType(value string) string { fType := "STRING" reNumber := regexp.MustCompile(`^[0-9]+$`) @@ -461,14 +513,18 @@ func guessMetadataType(value string) string { // metadataToFilter adds metadata elements to an existing filter // href is the address of the entity for which we want to retrieve metadata // filter is an existing filter to which we want to add metadata elements -func (client *Client) metadataToFilter(href string, filter *FilterDef) (*FilterDef, error) { +// objectName is the name of the entity for which we want to retrieve metadata +func (client *Client) metadataToFilter(href, objectName string, filter *FilterDef) (*FilterDef, error) { if filter == nil { filter = &FilterDef{} } - metadata, err := getMetadata(client, href) + metadata, err := getMetadata(client, href, objectName) if err == nil && metadata != nil && len(metadata.MetadataEntry) > 0 { for _, md := range metadata.MetadataEntry { - isSystem := md.Domain == "SYSTEM" + isSystem := false + if md.Domain != nil && md.Domain.Domain == "SYSTEM" { + isSystem = true + } var fType string var ok bool if md.TypedValue.XsiType == "" { diff --git a/govcd/filter_interface.go b/govcd/filter_interface.go index d45b99d9a..e3ba84966 100644 --- a/govcd/filter_interface.go +++ b/govcd/filter_interface.go @@ -34,6 +34,10 @@ type ( QueryMedia types.MediaRecordType QueryVapp types.QueryResultVAppRecordType QueryVm types.QueryResultVMRecordType + QueryOrgVdc types.QueryResultOrgVdcRecordType + QueryTask types.QueryResultTaskRecordType + QueryAdminTask types.QueryResultTaskRecordType + QueryOrg types.QueryResultOrgRecordType ) // getMetadataValue is a generic metadata lookup for all query items @@ -49,6 +53,20 @@ func getMetadataValue(metadata *types.Metadata, key string) string { return "" } +// -------------------------------------------------------------- +// Org VDC +// -------------------------------------------------------------- +func (orgVdc QueryOrgVdc) GetHref() string { return orgVdc.HREF } +func (orgVdc QueryOrgVdc) GetName() string { return orgVdc.Name } +func (orgVdc QueryOrgVdc) GetType() string { return "org_vdc" } +func (orgVdc QueryOrgVdc) GetIp() string { return "" } // IP does not apply to VDC +func (orgVdc QueryOrgVdc) GetDate() string { return "" } // Date does not aply to VDC +func (orgVdc QueryOrgVdc) GetParentName() string { return orgVdc.OrgName } +func (orgVdc QueryOrgVdc) GetParentId() string { return orgVdc.Org } +func (orgVdc QueryOrgVdc) GetMetadataValue(key string) string { + return getMetadataValue(orgVdc.Metadata, key) +} + // -------------------------------------------------------------- // vApp template // -------------------------------------------------------------- @@ -157,6 +175,34 @@ func (network QueryOrgVdcNetwork) GetMetadataValue(key string) string { return getMetadataValue(network.Metadata, key) } +// -------------------------------------------------------------- +// Task +// -------------------------------------------------------------- +func (task QueryTask) GetHref() string { return task.HREF } +func (task QueryTask) GetName() string { return task.Name } +func (task QueryTask) GetType() string { return "Task" } +func (task QueryTask) GetIp() string { return "" } +func (task QueryTask) GetDate() string { return task.StartDate } +func (task QueryTask) GetParentName() string { return task.OwnerName } +func (task QueryTask) GetParentId() string { return task.Org } +func (task QueryTask) GetMetadataValue(key string) string { + return getMetadataValue(task.Metadata, key) +} + +// -------------------------------------------------------------- +// AdminTask +// -------------------------------------------------------------- +func (task QueryAdminTask) GetHref() string { return task.HREF } +func (task QueryAdminTask) GetName() string { return task.Name } +func (task QueryAdminTask) GetType() string { return "Task" } +func (task QueryAdminTask) GetIp() string { return "" } +func (task QueryAdminTask) GetDate() string { return task.StartDate } +func (task QueryAdminTask) GetParentName() string { return task.OwnerName } +func (task QueryAdminTask) GetParentId() string { return task.Org } +func (task QueryAdminTask) GetMetadataValue(key string) string { + return getMetadataValue(task.Metadata, key) +} + // -------------------------------------------------------------- // vApp // -------------------------------------------------------------- @@ -185,6 +231,20 @@ func (vm QueryVm) GetMetadataValue(key string) string { return getMetadataValue(vm.MetaData, key) } +// -------------------------------------------------------------- +// Organization +// -------------------------------------------------------------- +func (org QueryOrg) GetHref() string { return org.HREF } +func (org QueryOrg) GetName() string { return org.Name } +func (org QueryOrg) GetType() string { return "organization" } +func (org QueryOrg) GetDate() string { return "" } +func (org QueryOrg) GetIp() string { return "" } +func (org QueryOrg) GetParentId() string { return "" } +func (org QueryOrg) GetParentName() string { return "" } +func (org QueryOrg) GetMetadataValue(key string) string { + return getMetadataValue(org.Metadata, key) +} + // -------------------------------------------------------------- // result conversion // -------------------------------------------------------------- @@ -228,6 +288,10 @@ func resultToQueryItems(queryType string, results Results) ([]QueryItem, error) for i, item := range results.Results.OrgVdcNetworkRecord { items[i] = QueryOrgVdcNetwork(*item) } + case types.QtOrg: + for i, item := range results.Results.OrgRecord { + items[i] = QueryOrg(*item) + } case types.QtCatalog: for i, item := range results.Results.CatalogRecord { items[i] = QueryCatalog(*item) @@ -252,6 +316,23 @@ func resultToQueryItems(queryType string, results Results) ([]QueryItem, error) for i, item := range results.Results.AdminVAppRecord { items[i] = QueryVapp(*item) } + case types.QtOrgVdc: + for i, item := range results.Results.OrgVdcRecord { + items[i] = QueryOrgVdc(*item) + } + case types.QtAdminOrgVdc: + for i, item := range results.Results.OrgVdcAdminRecord { + items[i] = QueryOrgVdc(*item) + } + case types.QtTask: + for i, item := range results.Results.TaskRecord { + items[i] = QueryTask(*item) + } + case types.QtAdminTask: + for i, item := range results.Results.TaskRecord { + items[i] = QueryAdminTask(*item) + } + } if len(items) > 0 { return items, nil diff --git a/govcd/filter_util.go b/govcd/filter_util.go index fa519593e..500967c8a 100644 --- a/govcd/filter_util.go +++ b/govcd/filter_util.go @@ -117,7 +117,8 @@ func (fd *FilterDef) AddMetadataFilter(key, value, valueType string, isSystem, u // stringToBool converts a string to a bool // The following values are recognized as TRUE: -// t, true, y, yes, ok +// +// t, true, y, yes, ok func stringToBool(s string) bool { switch strings.ToLower(s) { case "t", "true", "y", "yes", "ok": diff --git a/govcd/filter_util_unit_test.go b/govcd/filter_util_unit_test.go index 9c60b09b2..8703bde04 100644 --- a/govcd/filter_util_unit_test.go +++ b/govcd/filter_util_unit_test.go @@ -1,4 +1,4 @@ -// +build unit ALL +//go:build unit || ALL /* * Copyright 2020 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. diff --git a/govcd/generic_functions.go b/govcd/generic_functions.go new file mode 100644 index 000000000..db50d74c5 --- /dev/null +++ b/govcd/generic_functions.go @@ -0,0 +1,130 @@ +package govcd + +import ( + "encoding/json" + "fmt" + "reflect" +) + +// 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[E any](key, value string, entitySlice []*E) (*E, error) { + if len(entitySlice) > 1 { + return nil, fmt.Errorf("got more than one entity by %s '%s' %d", key, value, 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, value) + } + + return entitySlice[0], nil +} + +// localFilter performs filtering of a type E based on a field name `fieldName` and its +// expected string value `expectedFieldValue`. Common use case for GetAllX methods where API does +// not support filtering and it must be done on the client side. +// +// Note. The field name `fieldName` must be present in a given type E (letter casing is important) +func localFilter[E any](entityLabel string, entities []*E, fieldName, expectedFieldValue string) ([]*E, error) { + if len(entities) == 0 { + return nil, fmt.Errorf("zero entities provided for filtering") + } + + filteredValues := make([]*E, 0) + for _, entity := range entities { + + // Need to deference pointer because `reflect` package requires to work with types and not + // pointers to types + var entityValue E + if entity != nil { + entityValue = *entity + } else { + return nil, fmt.Errorf("given entity for %s is a nil pointer", entityLabel) + } + + value := reflect.ValueOf(entityValue) + field := value.FieldByName(fieldName) + + if !field.IsValid() { + return nil, fmt.Errorf("the struct for %s does not have the field '%s'", entityLabel, fieldName) + } + + if field.Type().Name() != "string" { + return nil, fmt.Errorf("field '%s' is not string type, it has type '%s'", fieldName, field.Type().Name()) + } + + if field.String() == expectedFieldValue { + filteredValues = append(filteredValues, entity) + } + } + + return filteredValues, nil +} + +// localFilterOneOrError performs local filtering using `genericLocalFilter()` and +// additionally verifies that only a single result is present using `oneOrError()`. Common use case +// for GetXByName methods where API does not support filtering and it must be done on client side. +func localFilterOneOrError[E any](entityLabel string, entities []*E, fieldName, expectedFieldValue string) (*E, error) { + if fieldName == "" || expectedFieldValue == "" { + return nil, fmt.Errorf("expected field name and value must be specified to filter %s", entityLabel) + } + + filteredValues, err := localFilter(entityLabel, entities, fieldName, expectedFieldValue) + if err != nil { + return nil, err + } + + return oneOrError(fieldName, expectedFieldValue, filteredValues) +} + +// convertAnyToRdeEntity unmarshals any entity to map[string]interface{} +func convertAnyToRdeEntity[E any](entityCfg *E) (map[string]interface{}, error) { + jsonText, err := json.Marshal(entityCfg) + if err != nil { + return nil, fmt.Errorf("error marshalling configuration :%s", err) + } + + var unmarshalledRdeEntityJson map[string]interface{} + err = json.Unmarshal(jsonText, &unmarshalledRdeEntityJson) + if err != nil { + return nil, fmt.Errorf("error unmarshalling configuration :%s", err) + } + + return unmarshalledRdeEntityJson, nil +} + +func convertRdeEntityToAny[E any](content map[string]interface{}) (*E, error) { + jsonText2, err := json.Marshal(content) + if err != nil { + return nil, fmt.Errorf("error converting entity to type: %s", err) + } + + result := new(E) + err = json.Unmarshal(jsonText2, result) + if err != nil { + return nil, fmt.Errorf("error converting entity to type: %s", err) + } + + return result, nil +} diff --git a/govcd/generic_functions_unit_test.go b/govcd/generic_functions_unit_test.go new file mode 100644 index 000000000..0dc39ba5f --- /dev/null +++ b/govcd/generic_functions_unit_test.go @@ -0,0 +1,101 @@ +//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 `json:"name"` +} diff --git a/govcd/global_role.go b/govcd/global_role.go new file mode 100644 index 000000000..0bea4ce38 --- /dev/null +++ b/govcd/global_role.go @@ -0,0 +1,383 @@ +/* + * Copyright 2021 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" +) + +type GlobalRole struct { + GlobalRole *types.GlobalRole + client *Client +} + +// GetAllGlobalRoles retrieves all global roles. Query parameters can be supplied to perform additional filtering +// Only System administrator can handle global roles +func (client *Client) GetAllGlobalRoles(queryParameters url.Values) ([]*GlobalRole, error) { + if !client.IsSysAdmin { + return nil, fmt.Errorf("only system administrator can handle global roles") + } + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointGlobalRoles + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + typeResponses := []*types.GlobalRole{{}} + err = client.OpenApiGetAllItems(minimumApiVersion, urlRef, queryParameters, &typeResponses, nil) + if err != nil { + return nil, err + } + + // Wrap all typeResponses into GlobalRole types with client + returnGlobalRoles := make([]*GlobalRole, len(typeResponses)) + for sliceIndex := range typeResponses { + returnGlobalRoles[sliceIndex] = &GlobalRole{ + GlobalRole: typeResponses[sliceIndex], + client: client, + } + } + + return returnGlobalRoles, nil +} + +// GetGlobalRoleByName retrieves a global role by given name +func (client *Client) GetGlobalRoleByName(name string) (*GlobalRole, error) { + queryParams := url.Values{} + queryParams.Add("filter", "name=="+name) + globalRoles, err := client.GetAllGlobalRoles(queryParams) + if err != nil { + return nil, err + } + if len(globalRoles) == 0 { + return nil, ErrorEntityNotFound + } + if len(globalRoles) > 1 { + return nil, fmt.Errorf("more than one global role found with name '%s'", name) + } + return globalRoles[0], nil +} + +// GetGlobalRoleById retrieves global role by given ID +func (client *Client) GetGlobalRoleById(id string) (*GlobalRole, error) { + if !client.IsSysAdmin { + return nil, fmt.Errorf("only system administrator can handle global roles") + } + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointGlobalRoles + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + if id == "" { + return nil, fmt.Errorf("empty GlobalRole id") + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, id) + if err != nil { + return nil, err + } + + globalRole := &GlobalRole{ + GlobalRole: &types.GlobalRole{}, + client: client, + } + + err = client.OpenApiGetItem(minimumApiVersion, urlRef, nil, globalRole.GlobalRole, nil) + if err != nil { + return nil, err + } + + return globalRole, nil +} + +// CreateGlobalRole creates a new global role as a system administrator +func (client *Client) CreateGlobalRole(newGlobalRole *types.GlobalRole) (*GlobalRole, error) { + if !client.IsSysAdmin { + return nil, fmt.Errorf("only system administrator can handle global roles") + } + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointGlobalRoles + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + if newGlobalRole.BundleKey == "" { + newGlobalRole.BundleKey = types.VcloudUndefinedKey + } + if newGlobalRole.PublishAll == nil { + newGlobalRole.PublishAll = addrOf(false) + } + returnGlobalRole := &GlobalRole{ + GlobalRole: &types.GlobalRole{}, + client: client, + } + + err = client.OpenApiPostItem(minimumApiVersion, urlRef, nil, newGlobalRole, returnGlobalRole.GlobalRole, nil) + if err != nil { + return nil, fmt.Errorf("error creating global role: %s", err) + } + + return returnGlobalRole, nil +} + +// Update updates existing global role +func (globalRole *GlobalRole) Update() (*GlobalRole, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointGlobalRoles + minimumApiVersion, err := globalRole.client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + if globalRole.GlobalRole.Id == "" { + return nil, fmt.Errorf("cannot update role without id") + } + + urlRef, err := globalRole.client.OpenApiBuildEndpoint(endpoint, globalRole.GlobalRole.Id) + if err != nil { + return nil, err + } + + returnGlobalRole := &GlobalRole{ + GlobalRole: &types.GlobalRole{}, + client: globalRole.client, + } + + err = globalRole.client.OpenApiPutItem(minimumApiVersion, urlRef, nil, globalRole.GlobalRole, returnGlobalRole.GlobalRole, nil) + if err != nil { + return nil, fmt.Errorf("error updating global role: %s", err) + } + + return returnGlobalRole, nil +} + +// Delete deletes global role +func (globalRole *GlobalRole) Delete() error { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointGlobalRoles + minimumApiVersion, err := globalRole.client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return err + } + + if globalRole.GlobalRole.Id == "" { + return fmt.Errorf("cannot delete global role without id") + } + + urlRef, err := globalRole.client.OpenApiBuildEndpoint(endpoint, globalRole.GlobalRole.Id) + if err != nil { + return err + } + + err = globalRole.client.OpenApiDeleteItem(minimumApiVersion, urlRef, nil, nil) + + if err != nil { + return fmt.Errorf("error deleting global role: %s", err) + } + + return nil +} + +// getContainerTenants retrieves all tenants associated with a given rights container (Global Role, Rights Bundle). +// Query parameters can be supplied to perform additional filtering +func getContainerTenants(client *Client, rightsContainerId, endpoint string, queryParameters url.Values) ([]types.OpenApiReference, error) { + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint + rightsContainerId + "/tenants") + if err != nil { + return nil, err + } + + typeResponses := types.OpenApiItems{ + Values: []types.OpenApiReference{}, + } + + err = client.OpenApiGetAllItems(minimumApiVersion, urlRef, queryParameters, &typeResponses.Values, nil) + if err != nil { + return nil, err + } + + return typeResponses.Values, nil +} + +// publishContainerToTenants is a generic function that publishes or unpublishes a rights collection (Global Role, or Rights bundle) to tenants +// containerType is an informative string (one of "GlobalRole", "RightsBundle") +// name and id are the name and ID of the collection +// endpoint is the API endpoint used as a basis for the POST operation +// tenants is a collection of tenants (ID+name) to be added +// publishType can be one of "add", "remove", "replace" +func publishContainerToTenants(client *Client, containerType, name, id, endpoint string, tenants []types.OpenApiReference, publishType string) error { + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return err + } + + if id == "" { + return fmt.Errorf("cannot update %s without id", containerType) + } + if name == "" { + return fmt.Errorf("empty name given for %s %s", containerType, id) + } + + var operation string + + var action func(apiVersion string, urlRef *url.URL, params url.Values, payload, outType interface{}, additionalHeader map[string]string) error + + switch publishType { + case "add": + operation = "/tenants/publish" + action = client.OpenApiPostItem + case "replace": + operation = "/tenants" + action = client.OpenApiPutItem + case "remove": + operation = "/tenants/unpublish" + action = client.OpenApiPostItem + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, id, operation) + if err != nil { + return err + } + + var input types.OpenApiItems + + for _, tenant := range tenants { + input.Values = append(input.Values, types.OpenApiReference{ + Name: tenant.Name, + ID: tenant.ID, + }) + } + var pages types.OpenApiPages + + err = action(minimumApiVersion, urlRef, nil, &input, &pages, nil) + + if err != nil { + return fmt.Errorf("error publishing %s %s to tenants: %s", containerType, name, err) + } + + return nil +} + +// publishContainerToAllTenants is a generic function that publishes or unpublishes a rights collection ( Global Role, or Rights bundle) to all tenants +// containerType is an informative string (one of "GlobalRole", "RightsBundle") +// name and id are the name and ID of the collection +// endpoint is the API endpoint used as a basis for the POST operation +// If "publish" is false, it will revert the operation +func publishContainerToAllTenants(client *Client, containerType, name, id, endpoint string, publish bool) error { + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return err + } + + if id == "" { + return fmt.Errorf("cannot update %s without id", containerType) + } + if name == "" { + return fmt.Errorf("empty name given for %s %s", containerType, id) + } + + operation := "/tenants/publishAll" + if !publish { + operation = "/tenants/unpublishAll" + } + urlRef, err := client.OpenApiBuildEndpoint(endpoint, id, operation) + if err != nil { + return err + } + + var pages types.OpenApiPages + + err = client.OpenApiPostItem(minimumApiVersion, urlRef, nil, &pages, &pages, nil) + + if err != nil { + return fmt.Errorf("error publishing %s %s to tenants: %s", containerType, name, err) + } + + return nil +} + +// AddRights adds a collection of rights to a global role +func (globalRole *GlobalRole) AddRights(newRights []types.OpenApiReference) error { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointGlobalRoles + return addRightsToRole(globalRole.client, "GlobalRole", globalRole.GlobalRole.Name, globalRole.GlobalRole.Id, endpoint, newRights, nil) +} + +// UpdateRights replaces existing rights with the given collection of rights +func (globalRole *GlobalRole) UpdateRights(newRights []types.OpenApiReference) error { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointGlobalRoles + return updateRightsInRole(globalRole.client, "GlobalRole", globalRole.GlobalRole.Name, globalRole.GlobalRole.Id, endpoint, newRights, nil) +} + +// RemoveRights removes specific rights from a global role +func (globalRole *GlobalRole) RemoveRights(removeRights []types.OpenApiReference) error { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointGlobalRoles + return removeRightsFromRole(globalRole.client, "GlobalRole", globalRole.GlobalRole.Name, globalRole.GlobalRole.Id, endpoint, removeRights, nil) +} + +// RemoveAllRights removes all rights from a global role +func (globalRole *GlobalRole) RemoveAllRights() error { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointGlobalRoles + return removeAllRightsFromRole(globalRole.client, "GlobalRole", globalRole.GlobalRole.Name, globalRole.GlobalRole.Id, endpoint, nil) +} + +// GetRights retrieves all rights belonging to a given Global Role. Query parameters can be supplied to perform additional +// filtering +func (globalRole *GlobalRole) GetRights(queryParameters url.Values) ([]*types.Right, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointGlobalRoles + return getRights(globalRole.client, globalRole.GlobalRole.Id, endpoint, queryParameters, nil) +} + +// GetTenants retrieves all tenants associated to a given Global Role. Query parameters can be supplied to perform additional +// filtering +func (globalRole *GlobalRole) GetTenants(queryParameters url.Values) ([]types.OpenApiReference, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointGlobalRoles + return getContainerTenants(globalRole.client, globalRole.GlobalRole.Id, endpoint, queryParameters) +} + +// PublishTenants publishes a global role to one or more tenants, adding to tenants that may already been there +func (globalRole *GlobalRole) PublishTenants(tenants []types.OpenApiReference) error { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointGlobalRoles + return publishContainerToTenants(globalRole.client, "GlobalRole", globalRole.GlobalRole.Name, globalRole.GlobalRole.Id, endpoint, tenants, "add") +} + +// ReplacePublishedTenants publishes a global role to one or more tenants, removing the tenants already present +func (globalRole *GlobalRole) ReplacePublishedTenants(tenants []types.OpenApiReference) error { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointGlobalRoles + return publishContainerToTenants(globalRole.client, "GlobalRole", globalRole.GlobalRole.Name, globalRole.GlobalRole.Id, endpoint, tenants, "replace") +} + +// UnpublishTenants remove tenats from a global role +func (globalRole *GlobalRole) UnpublishTenants(tenants []types.OpenApiReference) error { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointGlobalRoles + return publishContainerToTenants(globalRole.client, "GlobalRole", globalRole.GlobalRole.Name, globalRole.GlobalRole.Id, endpoint, tenants, "remove") +} + +// PublishAllTenants publishes a global role to all tenants +func (globalRole *GlobalRole) PublishAllTenants() error { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointGlobalRoles + return publishContainerToAllTenants(globalRole.client, "GlobalRole", globalRole.GlobalRole.Name, globalRole.GlobalRole.Id, endpoint, true) +} + +// UnpublishAllTenants remove publication status of a global role from all tenants +func (globalRole *GlobalRole) UnpublishAllTenants() error { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointGlobalRoles + return publishContainerToAllTenants(globalRole.client, "GlobalRole", globalRole.GlobalRole.Name, globalRole.GlobalRole.Id, endpoint, false) +} diff --git a/govcd/global_role_test.go b/govcd/global_role_test.go new file mode 100644 index 000000000..6e8ec0401 --- /dev/null +++ b/govcd/global_role_test.go @@ -0,0 +1,259 @@ +//go:build functional || openapi || role || ALL + +/* + * Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "net/url" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +type rightsProviderCollection interface { + PublishAllTenants() error + UnpublishAllTenants() error + PublishTenants([]types.OpenApiReference) error + UnpublishTenants([]types.OpenApiReference) error + GetTenants(queryParameters url.Values) ([]types.OpenApiReference, error) + ReplacePublishedTenants([]types.OpenApiReference) error +} + +func (vcd *TestVCD) Test_GlobalRoles(check *C) { + client := vcd.client.Client + if !client.IsSysAdmin { + check.Skip("test Test_GlobalRoles requires system administrator privileges") + } + vcd.checkSkipWhenApiToken(check) + + // Step 1 - Get all global roles + allExistingGlobalRoles, err := client.GetAllGlobalRoles(nil) + check.Assert(err, IsNil) + check.Assert(allExistingGlobalRoles, NotNil) + + // Step 2 - Get all roles using query filters + for _, oneGlobalRole := range allExistingGlobalRoles { + + // Step 2.1 - retrieve specific global role by using FIQL filter + queryParams := url.Values{} + queryParams.Add("filter", "id=="+oneGlobalRole.GlobalRole.Id) + + expectOneGlobalRoleResultById, err := client.GetAllGlobalRoles(queryParams) + check.Assert(err, IsNil) + check.Assert(len(expectOneGlobalRoleResultById) == 1, Equals, true) + + // Step 2.2 - retrieve specific global role by using endpoint + exactItem, err := client.GetGlobalRoleById(oneGlobalRole.GlobalRole.Id) + check.Assert(err, IsNil) + + check.Assert(err, IsNil) + check.Assert(exactItem, NotNil) + + // Step 2.3 - compare struct retrieved by using filter and the one retrieved by exact endpoint ID + check.Assert(oneGlobalRole, DeepEquals, expectOneGlobalRoleResultById[0]) + + } + + // Step 3 - Create a new global role and ensure it is created as specified by doing deep comparison + + newGR := &types.GlobalRole{ + Name: check.TestName(), + Description: "Global Role created by test", + // This BundleKey is being set by VCD even if it is not sent + BundleKey: types.VcloudUndefinedKey, + ReadOnly: false, + } + + createdGlobalRole, err := client.CreateGlobalRole(newGR) + check.Assert(err, IsNil) + AddToCleanupListOpenApi(createdGlobalRole.GlobalRole.Name, check.TestName(), + types.OpenApiPathVersion1_0_0+types.OpenApiEndpointGlobalRoles+createdGlobalRole.GlobalRole.Id) + + // Ensure supplied and created structs differ only by ID + newGR.Id = createdGlobalRole.GlobalRole.Id + check.Assert(createdGlobalRole.GlobalRole, DeepEquals, newGR) + + // Step 4 - updated created global role + createdGlobalRole.GlobalRole.Description = "Updated description" + updatedGlobalRole, err := createdGlobalRole.Update() + check.Assert(err, IsNil) + check.Assert(updatedGlobalRole.GlobalRole, DeepEquals, createdGlobalRole.GlobalRole) + + // Step 5 - add rights to global role + + // These rights include 5 implied rights + rightNames := []string{ + "Catalog: Add vApp from My Cloud", + "Catalog: Edit Properties", + } + // Add an intentional duplicate to test the validity of getRightsSet and FindMissingImpliedRights + rightNames = append(rightNames, rightNames[1]) + + rightSet, err := getRightsSet(&client, rightNames) + check.Assert(err, IsNil) + + err = updatedGlobalRole.AddRights(rightSet) + check.Assert(err, IsNil) + + // Calculate the total amount of rights we should expect to be added to the global role + rights, err := updatedGlobalRole.GetRights(nil) + check.Assert(err, IsNil) + check.Assert(len(rights), Equals, len(rightSet)) + + // Step 6 - remove 1 right from global role + + err = updatedGlobalRole.RemoveRights([]types.OpenApiReference{rightSet[0]}) + check.Assert(err, IsNil) + rights, err = updatedGlobalRole.GetRights(nil) + check.Assert(err, IsNil) + check.Assert(len(rights), Equals, len(rightSet)-1) + + testRightsContainerTenants(vcd, check, updatedGlobalRole) + + // Step 7 - remove all rights from global role + err = updatedGlobalRole.RemoveAllRights() + check.Assert(err, IsNil) + + rights, err = updatedGlobalRole.GetRights(nil) + check.Assert(err, IsNil) + check.Assert(len(rights), Equals, 0) + + // Step 8 - delete created global role + err = updatedGlobalRole.Delete() + check.Assert(err, IsNil) + + // Step 9 - try to read deleted global role and expect error to contain 'ErrorEntityNotFound' + // Read is tricky - it throws an error ACCESS_TO_RESOURCE_IS_FORBIDDEN when the resource with ID does not + // exist therefore one cannot know what kind of error occurred. + deletedGlobalRole, err := client.GetGlobalRoleById(createdGlobalRole.GlobalRole.Id) + check.Assert(ContainsNotFound(err), Equals, true) + check.Assert(deletedGlobalRole, IsNil) +} + +func foundOrg(name, id string, items []types.OpenApiReference) bool { + for _, item := range items { + if item.ID == id && item.Name == name { + return true + } + } + return false +} + +// testRightsContainerTenants is a sub-test that checks the validity of the tenants +// registered to the container +func testRightsContainerTenants(vcd *TestVCD, check *C, rpc rightsProviderCollection) { + + newOrgName := check.TestName() + "-org" + task, err := CreateOrg(vcd.client, newOrgName, newOrgName, newOrgName, &types.OrgSettings{}, true) + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) + + newOrg, err := vcd.client.GetAdminOrgByName(newOrgName) + check.Assert(err, IsNil) + AddToCleanupList(newOrgName, "org", "", "testRightsContainerTenants") + + err = rpc.PublishTenants([]types.OpenApiReference{ + {ID: vcd.org.Org.ID, Name: vcd.org.Org.Name}, + {ID: newOrg.AdminOrg.ID, Name: newOrg.AdminOrg.Name}, + }) + check.Assert(err, IsNil) + + tenants, err := rpc.GetTenants(nil) + check.Assert(err, IsNil) + check.Assert(len(tenants), Equals, 2) + + check.Assert(foundOrg(vcd.org.Org.Name, vcd.org.Org.ID, tenants), Equals, true) + check.Assert(foundOrg(newOrg.AdminOrg.Name, newOrg.AdminOrg.ID, tenants), Equals, true) + + err = rpc.UnpublishTenants(tenants) + check.Assert(err, IsNil) + tenants, err = rpc.GetTenants(nil) + check.Assert(err, IsNil) + check.Assert(len(tenants), Equals, 0) + + err = rpc.PublishTenants([]types.OpenApiReference{ + {ID: vcd.org.Org.ID, Name: vcd.org.Org.Name}, + }) + check.Assert(err, IsNil) + + tenants, err = rpc.GetTenants(nil) + check.Assert(err, IsNil) + check.Assert(len(tenants), Equals, 1) + + check.Assert(foundOrg(vcd.org.Org.Name, vcd.org.Org.ID, tenants), Equals, true) + + err = rpc.ReplacePublishedTenants([]types.OpenApiReference{ + {ID: vcd.org.Org.ID, Name: vcd.org.Org.Name}, + {ID: newOrg.AdminOrg.ID, Name: newOrg.AdminOrg.Name}, + }) + check.Assert(err, IsNil) + tenants, err = rpc.GetTenants(nil) + check.Assert(err, IsNil) + check.Assert(len(tenants), Equals, 2) + + check.Assert(foundOrg(vcd.org.Org.Name, vcd.org.Org.ID, tenants), Equals, true) + check.Assert(foundOrg(newOrg.AdminOrg.Name, newOrg.AdminOrg.ID, tenants), Equals, true) + + err = rpc.UnpublishTenants(tenants) + check.Assert(err, IsNil) + tenants, err = rpc.GetTenants(nil) + check.Assert(err, IsNil) + check.Assert(len(tenants), Equals, 0) + + err = rpc.PublishAllTenants() + check.Assert(err, IsNil) + + tenants, err = rpc.GetTenants(nil) + check.Assert(err, IsNil) + check.Assert(len(tenants), Not(Equals), 0) + + check.Assert(foundOrg(vcd.org.Org.Name, vcd.org.Org.ID, tenants), Equals, true) + check.Assert(foundOrg(newOrg.AdminOrg.Name, newOrg.AdminOrg.ID, tenants), Equals, true) + + err = rpc.UnpublishAllTenants() + check.Assert(err, IsNil) + tenants, err = rpc.GetTenants(nil) + check.Assert(err, IsNil) + check.Assert(len(tenants), Equals, 0) + err = newOrg.Delete(true, true) + check.Assert(err, IsNil) +} + +// getRightsSet is a convenience function that retrieves a list of rights +// from a list of right names, and adds the implied rights +func getRightsSet(client *Client, rightNames []string) ([]types.OpenApiReference, error) { + var rightList []types.OpenApiReference + var uniqueNames = make(map[string]bool) + + for _, name := range rightNames { + _, seen := uniqueNames[name] + if seen { + continue + } + right, err := client.GetRightByName(name) + if err != nil { + return nil, err + } + rightList = append(rightList, types.OpenApiReference{ + Name: right.Name, + ID: right.ID, + }) + uniqueNames[name] = true + } + implied, err := FindMissingImpliedRights(client, rightList) + if err != nil { + return nil, err + } + for _, ir := range implied { + _, seen := uniqueNames[ir.Name] + if seen { + continue + } + rightList = append(rightList, ir) + } + return rightList, nil +} diff --git a/govcd/group.go b/govcd/group.go index 263dd418f..6b64b3add 100644 --- a/govcd/group.go +++ b/govcd/group.go @@ -131,7 +131,7 @@ func (group *OrgGroup) Update() error { util.Logger.Printf("[TRACE] Url for updating group : %s and name: %s", groupHREF.String(), group.Group.Name) _, err = group.client.ExecuteRequest(groupHREF.String(), http.MethodPut, - types.MimeAdminGroup, "error updating group : %s", group.Group, nil) + types.MimeAdminGroup, "error updating group : %s", copyWithoutUserList(group.Group), nil) return err } @@ -183,3 +183,21 @@ func validateDeleteGroup(group *types.Group) error { return nil } + +// copyWithoutUserList returns a copy of the given group, with the UserList attribute set to nil. +// This can and should be used to interact with VCD after a group read from the LDAP, +// as having this list populated will return an error 400 as VCD doesn't expect this list to be updatable. +func copyWithoutUserList(group *types.Group) *types.Group { + return &types.Group{ + XMLName: group.XMLName, + Xmlns: group.Xmlns, + ID: group.ID, + Href: group.Href, + Type: group.Type, + Description: group.Description, + Name: group.Name, + ProviderType: group.ProviderType, + Role: group.Role, + UsersList: nil, + } +} diff --git a/govcd/group_test.go b/govcd/group_test.go index 9e1f24591..d2fa0acb3 100644 --- a/govcd/group_test.go +++ b/govcd/group_test.go @@ -1,4 +1,4 @@ -// +build user functional ALL +//go:build user || functional || ALL /* * Copyright 2020 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -14,7 +14,7 @@ import ( ) // test_GroupCRUD tests out CRUD capabilities for Org Groups. -// Note. Because it requires LDAP to be functional - this test is run from main test "Test_LDAP" +// Note: Because it requires LDAP to be functional - this test is run from main test "Test_LDAP" // which sets up LDAP configuration. func (vcd *TestVCD) test_GroupCRUD(check *C) { fmt.Printf("Running: %s\n", "test_GroupCRUD") @@ -109,8 +109,6 @@ func (vcd *TestVCD) test_GroupCRUD(check *C) { check.Assert(err, IsNil) foundGroup2, err := adminOrg.GetGroupByName(gd.name, true) - check.Assert(err, IsNil) - check.Assert(err, IsNil) check.Assert(foundGroup2.Group.Href, Equals, createdGroup.Group.Href) check.Assert(foundGroup2.Group.Name, Equals, createdGroup.Group.Name) @@ -125,7 +123,7 @@ func (vcd *TestVCD) test_GroupCRUD(check *C) { // test_GroupFinderGetGenericEntity uses testFinderGetGenericEntity to validate that ByName, ById // ByNameOrId method work properly. -// Note. Because it requires LDAP to be functional - this test is run from main test "Test_LDAP" +// Note: Because it requires LDAP to be functional - this test is run from main test "Test_LDAP" // which sets up LDAP configuration. func (vcd *TestVCD) test_GroupFinderGetGenericEntity(check *C) { fmt.Printf("Running: %s\n", "test_GroupFinderGetGenericEntity") @@ -178,3 +176,71 @@ func (vcd *TestVCD) test_GroupFinderGetGenericEntity(check *C) { err = grp.Delete() check.Assert(err, IsNil) } + +// test_GroupUserListIsPopulated checks that when retrieving the existing groups from the testing LDAP, +// the user list reference is populated. +// Note: Because it requires LDAP to be functional - this test is run from main test "Test_LDAP" +// which sets up LDAP configuration. +func (vcd *TestVCD) test_GroupUserListIsPopulated(check *C) { + fmt.Printf("Running: %s\n", "test_GroupUserListIsPopulated") + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.org.Org.Name) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + + roleRef, err := adminOrg.GetRoleReference(OrgUserRoleOrganizationAdministrator) + check.Assert(err, IsNil) + + group := NewGroup(adminOrg.client, adminOrg) + const groupName = "ship_crew" + group.Group = &types.Group{ + Name: groupName, + Role: roleRef, + ProviderType: OrgUserProviderIntegrated, + } + + _, err = adminOrg.CreateGroup(group.Group) + check.Assert(err, IsNil) + AddToCleanupList(groupName, "group", group.AdminOrg.AdminOrg.Name, check.TestName()) + + user := NewUser(adminOrg.client, adminOrg) + const userName = "fry" + user.User = &types.User{ + Name: userName, + Password: userName, + Role: roleRef, + IsExternal: true, + IsEnabled: true, + ProviderType: OrgUserProviderIntegrated, + } + _, err = adminOrg.CreateUser(user.User) + check.Assert(err, IsNil) + AddToCleanupList(userName, "user", group.AdminOrg.AdminOrg.Name, check.TestName()) + + grp, err := adminOrg.GetGroupByName(group.Group.Name, true) + check.Assert(err, IsNil) + check.Assert(grp.Group.UsersList, NotNil) + check.Assert(grp.Group.UsersList.UserReference[0], NotNil) + + // We check here that usersList doesn't make VCD fail, they should be sent as nil + err = grp.Update() + check.Assert(err, IsNil) + + user, err = adminOrg.GetUserByHref(grp.Group.UsersList.UserReference[0].HREF) + check.Assert(err, IsNil) + check.Assert(user.User.Name, Equals, userName) + check.Assert(len(user.User.GroupReferences.GroupReference), Equals, 1) + + // We check here that the user used for update is the same as we had originally, except the user list + grp.Group.UsersList = nil + check.Assert(copyWithoutUserList(grp.Group), DeepEquals, grp.Group) + + // We check here that groupReferences doesn't make VCD fail, they should be sent as nil + err = user.Update() + check.Assert(err, IsNil) + + // Cleanup + err = user.Delete(false) + check.Assert(err, IsNil) + err = grp.Delete() + check.Assert(err, IsNil) +} diff --git a/govcd/importable_dvpg.go b/govcd/importable_dvpg.go new file mode 100644 index 000000000..8218d3f6d --- /dev/null +++ b/govcd/importable_dvpg.go @@ -0,0 +1,148 @@ +/* + * 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 +} + +// Parent returns the port group parent switch +func (dvpg *VcenterImportableDvpg) Parent() *types.OpenApiReference { + return dvpg.VcenterImportableDvpg.DvSwitch.BackingRef +} + +// UsableWith tells whether a given port group can be used with others to create a network pool +func (dvpg *VcenterImportableDvpg) UsableWith(others ...*VcenterImportableDvpg) bool { + // No items provided: assume false + if len(others) == 0 { + return false + } + // Only one item provided, and it is the same as the current port group: assume false + if len(others) == 1 && dvpg.VcenterImportableDvpg.BackingRef.ID == others[0].VcenterImportableDvpg.BackingRef.ID { + return false + } + for _, other := range others { + if dvpg.VcenterImportableDvpg.BackingRef.ID == others[0].VcenterImportableDvpg.BackingRef.ID { + continue + } + if dvpg.Parent().ID != other.Parent().ID { + return false + } + } + return true +} diff --git a/govcd/importable_dvpg_test.go b/govcd/importable_dvpg_test.go new file mode 100644 index 000000000..b87d820ee --- /dev/null +++ b/govcd/importable_dvpg_test.go @@ -0,0 +1,87 @@ +//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" + "strings" +) + +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) + + var compatibleDVPG []*VcenterImportableDvpg + for _, dvpg := range dvpgs { + // make a list of the port groups created with 'count' during the VCD configuration + if strings.HasPrefix(dvpg.VcenterImportableDvpg.BackingRef.Name, vcd.config.VCD.Nsxt.NsxtDvpg) { + compatibleDVPG = append(compatibleDVPG, dvpg) + } + } + + // 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) + + check.Assert(len(compatibleDVPG) > 1, Equals, true, + Commentf("The VCD %s was not configured with multiple vsphere_distributed_port_group", vcd.config.Provider.Url)) + // test that port group created with the same switch ID are compatible + foundSameParent := false + for _, dvpg := range dvpgs { + for _, other := range dvpgs { + if other == dvpg { + continue + } + if dvpg.Parent().ID == other.Parent().ID { + foundSameParent = true + break + } + } + if foundSameParent { + break + } + } + check.Assert(foundSameParent, Equals, true) + foundCompatible := false + for _, dvpg := range dvpgs { + if dvpg.UsableWith(compatibleDVPG...) { + foundCompatible = true + break + } + } + check.Assert(len(compatibleDVPG), Equals, 3) + check.Assert(foundCompatible, Equals, true) +} diff --git a/govcd/internal/udf/cdrom.go b/govcd/internal/udf/cdrom.go new file mode 100644 index 000000000..b2e36d873 --- /dev/null +++ b/govcd/internal/udf/cdrom.go @@ -0,0 +1,114 @@ +package udf + +import ( + "fmt" + "slices" +) + +const ( + cdromVolumeDescriptorSectorNumber = 16 + + cdromVolumeIdentifierBEA01 = "BEA01" + cdromVolumeIdentifierBOOT2 = "BOOT2" + cdromVolumeIdentifierCD001 = "CD001" + cdromVolumeIdentifierCDW02 = "CDW02" + cdromVolumeIdentifierNSR02 = "NSR02" + cdromVolumeIdentifierNSR03 = "NSR03" + cdromVolumeIdentifierTEA01 = "TEA01" +) + +type CdromDescriptor interface { + GetHeader() *CdromVolumeDescriptorHeader +} + +type CdromDescriptorList []CdromDescriptor + +func (list CdromDescriptorList) hasAnyIdentifier(identifiers ...string) bool { + for idx := 0; idx < len(list); idx++ { + identifier := list[idx].GetHeader().Identifier + if slices.Contains(identifiers, identifier) { + return true + } + } + return false +} + +func (list CdromDescriptorList) getByType(descType uint8) CdromDescriptor { + if desc := list.findByType(descType); desc != nil { + return desc + } else { + panic(fmt.Sprintf("CDROM descriptor with type %d does not exist in sequence", descType)) + } +} + +func (list CdromDescriptorList) findByType(descType uint8) CdromDescriptor { + for idx := 0; idx < len(list); idx++ { + if list[idx].GetHeader().Type == descType { + return list[idx] + } + } + return nil +} + +func (list CdromDescriptorList) getByIdentifier(identifier string) CdromDescriptor { + if desc := list.findByIdentifier(identifier); desc != nil { + return desc + } else { + panic(fmt.Sprintf("CDROM descriptor with identifier %s does not exist in sequence", identifier)) + } +} + +func (list CdromDescriptorList) findByIdentifier(identifier string) CdromDescriptor { + for idx := 0; idx < len(list); idx++ { + if list[idx].GetHeader().Identifier == identifier { + return list[idx] + } + } + return nil +} + +type CdromVolumeDescriptorHeader struct { + Type uint8 + Identifier string + Version uint8 +} + +type CdromExtendedAreaVolumeDescriptor struct { + Header CdromVolumeDescriptorHeader +} + +func (d *CdromExtendedAreaVolumeDescriptor) GetHeader() *CdromVolumeDescriptorHeader { + return &d.Header +} + +type CdromBootVolumeDescriptor struct { + Header CdromVolumeDescriptorHeader +} + +func (d *CdromBootVolumeDescriptor) GetHeader() *CdromVolumeDescriptorHeader { + return &d.Header +} + +type CdromCdwVolumeDescriptor struct { + Header CdromVolumeDescriptorHeader +} + +func (d *CdromCdwVolumeDescriptor) GetHeader() *CdromVolumeDescriptorHeader { + return &d.Header +} + +type CdromNsrVolumeDescriptor struct { + Header CdromVolumeDescriptorHeader +} + +func (d *CdromNsrVolumeDescriptor) GetHeader() *CdromVolumeDescriptorHeader { + return &d.Header +} + +type CdromTerminalVolumeDescriptor struct { + Header CdromVolumeDescriptorHeader +} + +func (d *CdromTerminalVolumeDescriptor) GetHeader() *CdromVolumeDescriptorHeader { + return &d.Header +} diff --git a/govcd/internal/udf/perms.go b/govcd/internal/udf/perms.go new file mode 100644 index 000000000..740acb617 --- /dev/null +++ b/govcd/internal/udf/perms.go @@ -0,0 +1,124 @@ +package udf + +import "io/fs" + +// Permissions (BP 44) +// Bit 0: Other: If set to ZERO, shall mean that the user may not execute the file; If set to ONE, shall mean that +// the user may execute the file. +// Bit 1: Other: If set to ZERO, shall mean that the user may not write the file; If set to ONE, shall mean that +// the user may write the file. +// Bit 2: Other: If set to ZERO, shall mean that the user may not read the file; If set to ONE, shall mean that the +// user may read the file. +// Bit 3: Other: If set to ZERO, shall mean that the user may not change any attributes of the file; If set to +// ONE, shall mean that the user may change attributes of the file. +// Bit 4: Other: If set to ZERO, shall mean that the user may not delete the file; If set to ONE, shall mean that +// the user may delete the file. +// Bit 5: Group: If set to ZERO, shall mean that the user may not execute the file; If set to ONE, shall mean +// that the user may execute the file. +// Bit 6: Group: If set to ZERO, shall mean that the user may not write the file; If set to ONE, shall mean that +// the user may write the file. +// Bit 7: Group: If set to ZERO, shall mean that the user may not read the file; If set to ONE, shall mean that +// the user may read the file. +// Bit 8: Group: If set to ZERO, shall mean that the user may not change any attributes of the file; If set to +// ONE, shall mean that the user may change attributes of the file. +// Bit 9: Group: If set to ZERO, shall mean that the user may not delete the file; If set to ONE, shall mean that +// the user may delete the file. +// Bit 10: Owner: If set to ZERO, shall mean that the user may not execute the file; If set to ONE, shall mean +// that the user may execute the file. +// Bit 11: Owner: If set to ZERO, shall mean that the user may not write the file; If set to ONE, shall mean that +// the user may write the file. +// Bit 12: Owner: If set to ZERO, shall mean that the user may not read the file; If set to ONE, shall mean that +// the user may read the file. +// Bit 13: Owner: If set to ZERO, shall mean that the user may not change any attributes of the file; If set to +// ONE, shall mean that the user may change attributes of the file. +// Bit 14: Owner: If set to ZERO, shall mean that the user may not delete the file; If set to ONE, shall mean that +// the user may delete the file. +// 15-31 Reserved: Shall be set to ZERO. + +type FilePerm uint32 + +const ( + FilePermExecute FilePerm = 1 << iota + FilePermWrite + FilePermRead + FilePermChange + FilePermDelete +) + +const ( + FileModeOtherOffset = 5 * iota + FileModeGroupOffset + FileModeOwnerOffset +) + +const ( + FileModeOtherMask = ((1 << 5) - 1) << (5 * iota) + FileModeGroupMask + FileModeOwnerMask +) + +type FileMode uint32 + +func ToFileMode(mode fs.FileMode) FileMode { + mode = mode.Perm() + var r FileMode + r |= FileMode(mode & 7) // Other + r |= FileMode((mode >> 3) & 7 << 5) // Group + r |= FileMode((mode >> 6) & 7 << 10) // Owner + return r +} + +func FromFileMode(mode FileMode) fs.FileMode { + var r fs.FileMode + r |= fs.FileMode(mode & 7) // Other + r |= fs.FileMode(((mode >> 5) & 7) << 3) // Group + r |= fs.FileMode(((mode >> 10) & 7) << 6) // Owner + return r +} + +func (m FileMode) Other() FileMode { + return m & FileModeOtherMask +} + +func (m FileMode) HasOther(perms FilePerm) bool { + return (m>>FileModeOtherOffset)&FileMode(perms) == FileMode(perms) +} + +func (m FileMode) SetOther(perms FilePerm) FileMode { + return m | (FileMode(perms) << FileModeOtherOffset) +} + +func (m FileMode) UnsetOther(perms FilePerm) FileMode { + return m &^ (FileMode(perms) << FileModeOtherOffset) +} + +func (m FileMode) Group() FileMode { + return m & FileModeGroupMask +} + +func (m FileMode) HasGroup(perms FilePerm) bool { + return (m>>FileModeGroupOffset)&FileMode(perms) == FileMode(perms) +} + +func (m FileMode) SetGroup(perms FilePerm) FileMode { + return m | (FileMode(perms) << FileModeGroupOffset) +} + +func (m FileMode) UnsetGroup(perms FilePerm) FileMode { + return m &^ (FileMode(perms) << FileModeGroupOffset) +} +func (m FileMode) Owner() FileMode { + return m & FileModeOwnerMask +} + +func (m FileMode) HasOwner(perms FilePerm) bool { + return (m>>FileModeOwnerOffset)&FileMode(perms) == FileMode(perms) +} + +func (m FileMode) SetOwner(perms FilePerm) FileMode { + return m | (FileMode(perms) << FileModeOwnerOffset) +} + +func (m FileMode) UnsetOwner(perms FilePerm) FileMode { + return m &^ (FileMode(perms) << FileModeOwnerOffset) +} diff --git a/govcd/internal/udf/reader.go b/govcd/internal/udf/reader.go new file mode 100644 index 000000000..4d32b147b --- /dev/null +++ b/govcd/internal/udf/reader.go @@ -0,0 +1,736 @@ +package udf + +import ( + "encoding/binary" + "fmt" + "io" + "io/fs" + "path/filepath" + "time" + + "golang.org/x/text/encoding/charmap" + "golang.org/x/text/encoding/unicode" + "golang.org/x/text/transform" +) + +func Open(reader io.ReaderAt) (r *ImageReader, err error) { + defer func() { + if fatalErr := recover(); fatalErr != nil { + err = fmt.Errorf("%v", fatalErr) + } + }() + r = &ImageReader{inner: reader} + + r.cdromVolumeDescriptors = r.readCdromVolumeDescriptorSequence(cdromVolumeDescriptorSectorNumber) + + // Validate image is UDF + if !r.cdromVolumeDescriptors.hasAnyIdentifier(cdromVolumeIdentifierNSR02, cdromVolumeIdentifierNSR03) { + err = fmt.Errorf("unsupported image format") + return + } + + r.anchorVolumeDescriptor = r.readDescriptorFromSectorWithTag( + anchorVolumeDescriptorSectorNumber, tagAnchorVolumeDescriptorPointer).(*AnchorVolumeDescriptorPointer) + r.mainVolumeDescriptors = r.readDescriptorSequence(&r.anchorVolumeDescriptor.MainVolumeDescriptorSequence) + r.primaryVolume = r.mainVolumeDescriptors.get(tagPrimaryVolumeDescriptor).(*PrimaryVolumeDescriptor) + r.partition = r.mainVolumeDescriptors.get(tagPartitionDescriptor).(*PartitionDescriptor) + r.logicalVolume = r.mainVolumeDescriptors.get(tagLogicalVolumeDescriptor).(*LogicalVolumeDescriptor) + r.fileSet = r.readDescriptorFromSectorWithTag(int64(r.partition.PartitionStartingLocation), tagFileSetDescriptor).(*FileSetDescriptor) + return +} + +type descriptorReader interface { + Descriptor + read(reader *bufferReader) +} + +type ImageReader struct { + inner io.ReaderAt + cdromVolumeDescriptors CdromDescriptorList + anchorVolumeDescriptor *AnchorVolumeDescriptorPointer + mainVolumeDescriptors DescriptorList + primaryVolume *PrimaryVolumeDescriptor + partition *PartitionDescriptor + logicalVolume *LogicalVolumeDescriptor + fileSet *FileSetDescriptor +} + +func (r *ImageReader) RootDir() (fi *FileInfo, err error) { + defer func() { + if fatalErr := recover(); fatalErr != nil { + err = fmt.Errorf("unable to read UDF root directory: %v", fatalErr) + } + }() + partitionStart := int64(r.partition.PartitionStartingLocation) + fileEntrySector := partitionStart + int64(r.fileSet.RootDirectoryICB.Location) + fileEntry := r.readDescriptorFromSectorWithTag(fileEntrySector, tagFileEntry).(*FileEntryDescriptor) + fi = &FileInfo{ + entry: fileEntry, + logicalVolume: r.logicalVolume, + } + return +} + +func (r *ImageReader) ReadDir(parent *FileInfo) (children []FileInfo, err error) { + defer func() { + if fatalErr := recover(); fatalErr != nil { + err = fmt.Errorf("unable to read UDF directory: %v", fatalErr) + } + }() + + if !parent.IsDir() { + err = fmt.Errorf("entry %s is not a directory", parent.Name()) + return + } + + if len(parent.entry.AllocationDescriptors) == 0 { + children = []FileInfo{} + return + } + + children = make([]FileInfo, 0, 8) + + for idx := range parent.entry.AllocationDescriptors { + allocDesc := &parent.entry.AllocationDescriptors[idx] + bufSize := int(allocDesc.Length) + partitionStart := int64(r.partition.PartitionStartingLocation) + fileBufReader := r.readSectors(partitionStart+int64(allocDesc.Location), (bufSize+sectorSize-1)/sectorSize) + + for fileBufReader.off < bufSize { + fid := r.readDescriptorWithTag(fileBufReader, tagFileIdentifierDescriptor).(*FileIdentifierDescriptor) + if len(fid.FileIdentifier) > 0 { + fileEntrySector := partitionStart + int64(fid.ICB.Location) + fileEntry := r.readDescriptorFromSectorWithTag(fileEntrySector, tagFileEntry).(*FileEntryDescriptor) + + var filePath string + if parent.IsRoot() { + filePath = fid.FileIdentifier + } else { + filePath = filepath.Join(parent.path, fid.FileIdentifier) + } + + children = append(children, FileInfo{ + entry: fileEntry, + fid: fid, + parent: parent, + path: filePath, + }) + } + } + } + + return +} + +func (r *ImageReader) NewFileReader(file *FileInfo) (fileReader io.Reader, err error) { + if file.IsDir() { + err = fmt.Errorf("entry %s is not a file", file.Name()) + return + } + + defer func() { + if fatalErr := recover(); fatalErr != nil { + err = fmt.Errorf("unable to open UDF file: %v", fatalErr) + } + }() + + partitionStart := int64(r.partition.PartitionStartingLocation) + readers := make([]io.Reader, len(file.entry.AllocationDescriptors)) + + for idx := range file.entry.AllocationDescriptors { + allocDesc := &file.entry.AllocationDescriptors[idx] + location := int64(allocDesc.Location) + offset := sectorSize * (location + partitionStart) + readers[idx] = io.NewSectionReader(r.inner, offset, int64(allocDesc.Length)) + } + + fileReader = io.MultiReader(readers...) + return +} + +func (r *ImageReader) readDescriptorSequence(ref *Extent) DescriptorList { + descriptors := make(DescriptorList, 0, 8) + for sector := int64(ref.Location); ; sector++ { + descriptor := r.readDescriptorFromSector(sector) + descriptors = append(descriptors, descriptor) + if descriptor.GetIdentifier() == tagTerminatingDescriptor { + break + } + } + return descriptors +} + +func (r *ImageReader) readDescriptor(bufferReader *bufferReader) Descriptor { + tagId := bufferReader.peekUint16() + descriptor := NewDescriptor(tagId) + descriptor.(descriptorReader).read(bufferReader) + return descriptor +} + +func (r *ImageReader) readDescriptorWithTag(bufferReader *bufferReader, tagId int) Descriptor { + descriptor := r.readDescriptor(bufferReader) + if descriptor.GetIdentifier() != tagId { + panic(fmt.Errorf("expected descriptor with tag ID %d but was %d", tagId, descriptor.GetIdentifier())) + } + return descriptor +} + +func (r *ImageReader) readDescriptorFromSector(sector int64) Descriptor { + return r.readDescriptor(r.readSector(sector)) +} + +func (r *ImageReader) readDescriptorFromSectorWithTag(sector int64, tagId int) Descriptor { + descriptor := r.readDescriptorFromSector(sector) + if descriptor.GetIdentifier() != tagId { + panic(fmt.Errorf("expected descriptor with tag ID %d but was %d at sector %d", + tagId, descriptor.GetIdentifier(), sector)) + } + return descriptor +} + +func (r *ImageReader) readCdromVolumeDescriptorSequence(sector int64) CdromDescriptorList { + descriptors := make(CdromDescriptorList, 0, 8) + for n := sector; ; n++ { + descriptor := r.readCdromVolumeDescriptorFromSector(n) + descriptors = append(descriptors, descriptor) + if descriptor.GetHeader().Identifier == cdromVolumeIdentifierTEA01 { + break + } + } + return descriptors +} + +func (r *ImageReader) readCdromVolumeDescriptor(bufferReader *bufferReader) CdromDescriptor { + header := CdromVolumeDescriptorHeader{} + header.read(bufferReader) + + switch header.Identifier { + case cdromVolumeIdentifierBEA01: + return &CdromExtendedAreaVolumeDescriptor{ + Header: header, + } + case cdromVolumeIdentifierBOOT2: + return &CdromBootVolumeDescriptor{ + Header: header, + } + case cdromVolumeIdentifierCD001: + return &CdromCdwVolumeDescriptor{ + Header: header, + } + case cdromVolumeIdentifierCDW02: + return &CdromCdwVolumeDescriptor{ + Header: header, + } + case cdromVolumeIdentifierNSR02: + return &CdromNsrVolumeDescriptor{ + Header: header, + } + case cdromVolumeIdentifierNSR03: + return &CdromNsrVolumeDescriptor{ + Header: header, + } + case cdromVolumeIdentifierTEA01: + return &CdromTerminalVolumeDescriptor{ + Header: header, + } + default: + panic(fmt.Sprintf("unrecognized file system '%s'", header.Identifier)) + } +} + +func (r *ImageReader) readCdromVolumeDescriptorFromSector(sector int64) CdromDescriptor { + return r.readCdromVolumeDescriptor(r.readSector(sector)) +} + +func (r *ImageReader) readSector(sector int64) *bufferReader { + return r.read(sector*sectorSize, sectorSize) +} + +func (r *ImageReader) readSectors(sector int64, count int) *bufferReader { + return r.read(sector*sectorSize, sectorSize*count) +} + +func (r *ImageReader) read(offset int64, size int) *bufferReader { + data := make([]byte, size) + if n, err := r.inner.ReadAt(data, offset); err != nil { + panic(err) + } else if n != size { + panic(io.ErrUnexpectedEOF) + } + return &bufferReader{buf: data} +} + +func (e *Extent) read(reader *bufferReader) { + e.Length = reader.readUint32() + e.Location = reader.readUint32() +} + +func (e *ExtentLong) read(reader *bufferReader) { + e.Length = reader.readUint32() + e.Location = reader.readUint48() + reader.skipBytes(6) // Reserved +} + +func (e *EntityID) read(reader *bufferReader) { + e.Flags = reader.readUint8() + e.Identifier = reader.readString(23) + e.IdentifierSuffix = reader.readString(8) +} + +func (iu *ImplementationUse) read(reader *bufferReader, size int) { + iu.Entity.read(reader) + size = size - entityIdSize // ImplementationUse - EntityID + if size > 0 { + iu.Implementation = reader.readBytes(size) + } +} + +func (c *Charspec) read(reader *bufferReader) { + c.CharacterSetType = reader.readUint8() + c.CharacterSetInfo = reader.readBytes(63) +} + +func (tag *DescriptorTag) read(reader *bufferReader) { + tag.TagIdentifier = reader.readUint16() + tag.DescriptorVersion = reader.readUint16() + tag.TagChecksum = reader.readUint8() + reader.skipBytes(1) // Reserved + tag.TagSerialNumber = reader.readUint16() + tag.DescriptorCRC = reader.readUint16() + tag.DescriptorCRCLength = reader.readUint16() + tag.TagLocation = reader.readUint32() +} + +func (d *AnchorVolumeDescriptorPointer) read(reader *bufferReader) { + d.Tag.read(reader) + d.MainVolumeDescriptorSequence.read(reader) + d.ReserveVolumeDescriptorSequence.read(reader) + reader.skipBytes(480) // Reserved +} + +func (d *VolumeDescriptorPointer) read(reader *bufferReader) { + d.Tag.read(reader) + d.VolumeDescriptorSequenceNumber = reader.readUint32() + d.NextVolumeDescriptorSequenceExtent.read(reader) + // This field shall be reserved for future standardisation and all bytes shall be set to #00 + reader.skipBytes(484) +} + +func (d *PrimaryVolumeDescriptor) read(reader *bufferReader) { + d.Tag.read(reader) + d.VolumeDescriptorSequenceNumber = reader.readUint32() + d.PrimaryVolumeDescriptorNumber = reader.readUint32() + d.VolumeIdentifier = reader.readDString(32) + d.VolumeSequenceNumber = reader.readUint16() + d.MaximumVolumeSequenceNumber = reader.readUint16() + d.InterchangeLevel = reader.readUint16() + d.MaximumInterchangeLevel = reader.readUint16() + d.CharacterSetList = reader.readUint32() + d.MaximumCharacterSetList = reader.readUint32() + d.VolumeSetIdentifier = reader.readDString(128) + d.DescriptorCharacterSet.read(reader) + d.ExplanatoryCharacterSet.read(reader) + d.VolumeAbstract.read(reader) + d.VolumeCopyrightNoticeExtent.read(reader) + d.ApplicationIdentifier.read(reader) + d.RecordingDateTime = reader.readTimestamp() + d.ImplementationIdentifier.read(reader) + d.ImplementationUse = reader.readBytes(64) + d.PredecessorVolumeDescriptorSequenceLocation = reader.readUint32() + d.Flags = reader.readUint16() + reader.skipBytes(22) // Reserved +} + +func (d *ImplementationUseVolumeDescriptor) read(reader *bufferReader) { + d.Tag.read(reader) + d.VolumeDescriptorSequenceNumber = reader.readUint32() + d.ImplementationIdentifier.read(reader) + d.ImplementationUse.read(reader) +} + +func (lvi *LVInformation) read(reader *bufferReader) { + lvi.LVICharset.read(reader) + lvi.LogicalVolumeIdentifier = reader.readDString(128) + lvi.LVInfo1 = reader.readDString(36) + lvi.LVInfo2 = reader.readDString(36) + lvi.LVInfo3 = reader.readDString(36) + lvi.ImplementationID.read(reader) + lvi.ImplementationUse = reader.readBytes(128) +} + +func (d *PartitionDescriptor) read(reader *bufferReader) { + d.Tag.read(reader) + d.VolumeDescriptorSequenceNumber = reader.readUint32() + d.PartitionFlags = reader.readUint16() + d.PartitionNumber = reader.readUint16() + d.PartitionContents.read(reader) + d.PartitionContentsUse = reader.readBytes(128) + d.AccessType = reader.readUint32() + d.PartitionStartingLocation = reader.readUint32() + d.PartitionLength = reader.readUint32() + d.ImplementationIdentifier.read(reader) + d.ImplementationUse = reader.readBytes(128) + reader.skipBytes(156) +} + +func (d *LogicalVolumeDescriptor) read(reader *bufferReader) { + d.Tag.read(reader) + d.VolumeDescriptorSequenceNumber = reader.readUint32() + d.DescriptorCharacterSet.read(reader) + d.LogicalVolumeIdentifier = reader.readDString(128) + d.LogicalBlockSize = reader.readUint32() + d.DomainIdentifier.read(reader) + d.LogicalVolumeContentsUse = reader.readBytes(16) + d.MapTableLength = reader.readUint32() + d.NumberOfPartitionMaps = reader.readUint32() + d.ImplementationIdentifier.read(reader) + d.ImplementationUse = reader.readBytes(128) + d.IntegritySequenceExtent.read(reader) + d.PartitionMaps = make([]PartitionMap, d.NumberOfPartitionMaps) + for idx := 0; idx < int(d.NumberOfPartitionMaps); idx++ { + d.PartitionMaps[idx].read(reader) + } +} + +func (d *LogicalVolumeIntegrityDescriptor) read(reader *bufferReader) { + d.Tag.read(reader) + d.RecordingDateTime = reader.readTimestamp() + d.IntegrityType = reader.readUint32() + d.NextIntegrityExtent.read(reader) + d.LogicalVolumeContentsUse.read(reader) + d.NumberOfPartitions = reader.readUint32() + d.LengthOfImplementationUse = reader.readUint32() + d.FreeSpaceTable = make([]uint32, d.NumberOfPartitions) + for idx := range d.FreeSpaceTable { + d.FreeSpaceTable[idx] = reader.readUint32() + } + d.SizeTable = make([]uint32, d.NumberOfPartitions) + for idx := range d.SizeTable { + d.SizeTable[idx] = reader.readUint32() + } + d.ImplementationUse.read(reader, int(d.LengthOfImplementationUse)) +} + +func (h *LogicalVolumeHeaderDescriptor) read(reader *bufferReader) { + h.UniqueID = reader.readUint64() + reader.skipBytes(24) // Reserved +} + +func (pm *PartitionMap) read(reader *bufferReader) { + pm.PartitionMapType = reader.readUint8() + if pm.PartitionMapType != 1 { + panic(fmt.Sprintf("expected partition map 1 but got %d", pm.PartitionMapType)) + } + pm.PartitionMapLength = reader.readUint8() + if pm.PartitionMapLength != 6 { + panic(fmt.Sprintf("expected partition map 1 to be 6 bytes long but was %d", pm.PartitionMapLength)) + } + pm.VolumeSequenceNumber = reader.readUint16() + pm.PartitionNumber = reader.readUint16() +} + +func (d *UnallocatedSpaceDescriptor) read(reader *bufferReader) { + d.Tag.read(reader) + d.VolumeDescriptorSequenceNumber = reader.readUint32() + d.NumberOfAllocationDescriptors = reader.readUint32() + d.AllocationDescriptors = make([]Extent, d.NumberOfAllocationDescriptors) + for idx := 0; idx < int(d.NumberOfAllocationDescriptors); idx++ { + d.AllocationDescriptors[idx].read(reader) + } +} + +func (d *TerminatingDescriptor) read(reader *bufferReader) { + d.Tag.read(reader) + reader.skipBytes(496) // Reserved +} + +func (d *FileSetDescriptor) read(reader *bufferReader) { + d.Tag.read(reader) + d.RecordingDateTime = reader.readTimestamp() + d.InterchangeLevel = reader.readUint16() + d.MaximumInterchangeLevel = reader.readUint16() + d.CharacterSetList = reader.readUint32() + d.MaximumCharacterSetList = reader.readUint32() + d.FileSetNumber = reader.readUint32() + d.FileSetDescriptorNumber = reader.readUint32() + d.LogicalVolumeIdentifierCharacterSet.read(reader) + d.LogicalVolumeIdentifier = reader.readDString(128) + d.FileSetCharacterSet.read(reader) + d.FileSetIdentifier = reader.readDString(32) + d.CopyrightFileIdentifier = reader.readDString(32) + d.AbstractFileIdentifier = reader.readDString(32) + d.RootDirectoryICB.read(reader) + d.DomainIdentifier.read(reader) + d.NextExtent.read(reader) + d.SystemStreamDirectoryICB.read(reader) + reader.skipBytes(32) // Reserved +} + +func (d *FileEntryDescriptor) read(reader *bufferReader) { + d.Tag.read(reader) + d.ICBTag.read(reader) + d.Uid = reader.readUint32() + d.Gid = reader.readUint32() + d.Permissions = FileMode(reader.readUint32()) + d.FileLinkCount = reader.readUint16() + d.RecordFormat = reader.readUint8() + d.RecordDisplayAttributes = reader.readUint8() + d.RecordLength = reader.readUint32() + d.InformationLength = reader.readUint64() + d.LogicalBlocksRecorded = reader.readUint64() + d.AccessTime = reader.readTimestamp() + d.ModificationTime = reader.readTimestamp() + d.AttributeTime = reader.readTimestamp() + d.Checkpoint = reader.readUint32() + d.ExtendedAttributeICB.read(reader) + d.ImplementationIdentifier.read(reader) + d.UniqueID = reader.readUint64() + d.LengthOfExtendedAttributes = reader.readUint32() + d.LengthOfAllocationDescriptors = reader.readUint32() + d.ExtendedAttributes = reader.readBytes(int(d.LengthOfExtendedAttributes)) + d.AllocationDescriptors = make([]Extent, d.LengthOfAllocationDescriptors/8) + for i := range d.AllocationDescriptors { + d.AllocationDescriptors[i].read(reader) + } +} + +func (icb *ICBTag) read(reader *bufferReader) { + icb.PriorRecordedNumberOfDirectEntries = reader.readUint32() + icb.StrategyType = reader.readUint16() + icb.StrategyParameter = reader.readUint16() + icb.MaximumNumberOfEntries = reader.readUint16() + reader.skipBytes(1) + icb.FileType = reader.readUint8() + icb.ParentICBLocation.read(reader) + icb.Flags = reader.readUint16() +} + +func (lba *LogicalBlockAddress) read(reader *bufferReader) { + lba.LogicalBlockNumber = reader.readUint32() + lba.PartitionReferenceNumber = reader.readUint16() +} + +func (d *FileIdentifierDescriptor) read(reader *bufferReader) { + start := reader.off + d.Tag.read(reader) + d.FileVersionNumber = reader.readUint16() + d.FileCharacteristics = FileCharacteristics(reader.readUint8()) + d.LengthOfFileIdentifier = reader.readUint8() + d.ICB.read(reader) + d.LengthOfImplementationUse = reader.readUint16() + d.ImplementationUse = reader.readBytes(int(d.LengthOfImplementationUse)) + d.FileIdentifier = reader.readDCharacters(int(d.LengthOfFileIdentifier)) + + // Padding: 4 x ip((L_FI+L_IU+38+3)/4) - (L_FI+L_IU+38) bytes long and shall contain all #00 bytes. + // L_FI: Length of File Identifier + // L_IU: Length of Implementation Use + currentSize := reader.off - start + paddingLen := 4*((currentSize+3)/4) - currentSize + reader.skipBytes(paddingLen) +} + +func (d *CdromVolumeDescriptorHeader) read(reader *bufferReader) { + d.Type = reader.readUint8() + d.Identifier = string(reader.readBytes(5)) + d.Version = reader.readUint8() +} + +type bufferReader struct { + off int + buf []byte +} + +func (r *bufferReader) eof() bool { + return r.off >= len(r.buf) +} + +func (r *bufferReader) skipBytes(len int) { + r.off += len +} + +func (r *bufferReader) readBytes(len int) []byte { + start := r.off + r.off += len + return r.buf[start:r.off] +} + +func (r *bufferReader) readInt8() int8 { + return int8(r.readUint8()) +} + +func (r *bufferReader) readUint8() uint8 { + start := r.off + r.off += 1 + return r.buf[start] +} + +func (r *bufferReader) readInt16() int16 { + return int16(r.readUint16()) +} + +func (r *bufferReader) readUint16() uint16 { + start := r.off + r.off += 2 + return binary.LittleEndian.Uint16(r.buf[start:r.off]) +} + +func (r *bufferReader) peekUint16() uint16 { + return binary.LittleEndian.Uint16(r.buf[r.off : r.off+2]) +} + +func (r *bufferReader) readInt32() int32 { + return int32(r.readUint32()) +} + +func (r *bufferReader) readUint32() uint32 { + start := r.off + r.off += 4 + return binary.LittleEndian.Uint32(r.buf[start:r.off]) +} + +func (r *bufferReader) readUint48() uint64 { + start := r.off + r.off += 6 + data := make([]byte, 8) + copy(data, r.buf[start:r.off]) + return binary.LittleEndian.Uint64(data) +} + +func (r *bufferReader) readInt64() int64 { + return int64(r.readUint64()) +} + +func (r *bufferReader) readUint64() uint64 { + start := r.off + r.off += 8 + return binary.LittleEndian.Uint64(r.buf[start:r.off]) +} + +func (r *bufferReader) readString(size int) string { + if size == 0 { + return "" + } + start := r.off + r.off += size + return string(r.buf[start:r.off]) +} + +func (r *bufferReader) readDString(size int) string { + if size == 0 { + return "" + } + start := r.off + r.off += size + length := int(r.buf[r.off-1]) + if length == 0 { + return "" + } + + // The CompressionID shall identify the compression algorithm used to compress the CompressedBitStream field. + // 8: Value indicates there are 8 bits per character in the CompressedBitStream. + // 16: Value indicates there are 16 bits per character in the CompressedBitStream. + compressionId := r.buf[start] + if compressionId != 8 { + panic("expecting character length to be 8 bit long") + } + + return string(r.buf[start+1 : start+length]) +} + +func (r *bufferReader) readDCharacters(length int) string { + if length == 0 { + return "" + } + encodingType := r.readUint8() + length-- + switch encodingType { + case dcharEncodingType8: + win1252Dec := charmap.Windows1252.NewDecoder() + s, _, err := transform.Bytes(win1252Dec, r.readBytes(length)) + if err != nil { + panic(err) + } + return string(s) + case dcharEncodingType16: + utf16Dec := unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM).NewDecoder() + s, _, err := transform.Bytes(utf16Dec, r.readBytes(length)) + if err != nil { + panic(err) + } + return string(s) + default: + panic(fmt.Sprintf("unsupported string encoding type %d", encodingType)) + } +} + +func (r *bufferReader) readTimestamp() time.Time { + r.skipBytes(2) // TypeAndTimezone + year := r.readUint16() + month := r.readUint8() + day := r.readUint8() + hour := r.readUint8() + minute := r.readUint8() + second := r.readUint8() + r.skipBytes(3) // Centiseconds+HundredsofMicroseconds+Microseconds + return time.Date(int(year), time.Month(month), int(day), int(hour), int(minute), int(second), 0, time.UTC) +} + +type FileInfo struct { + fid *FileIdentifierDescriptor + logicalVolume *LogicalVolumeDescriptor + entry *FileEntryDescriptor + parent *FileInfo + path string +} + +func (f *FileInfo) Name() string { + if f.IsRoot() { + return f.logicalVolume.LogicalVolumeIdentifier + } else { + return f.fid.FileIdentifier + } +} + +func (f *FileInfo) Path() string { + return f.path +} + +func (f *FileInfo) Size() int64 { + return int64(f.entry.InformationLength) +} + +func (f *FileInfo) Mode() FileMode { + return f.entry.Permissions +} + +func (f *FileInfo) FileMode() fs.FileMode { + mode := FromFileMode(f.entry.Permissions) + if f.IsDir() { + mode |= fs.ModeDir + } + return mode +} + +func (f *FileInfo) ModTime() time.Time { + return f.entry.ModificationTime +} + +func (f *FileInfo) IsRoot() bool { + return f.fid == nil +} + +func (f *FileInfo) IsDir() bool { + return f.entry.ICBTag.FileType == fileTypeDirectory +} + +func (f *FileInfo) Uid() uint32 { + return f.entry.Uid +} + +func (f *FileInfo) Gid() uint32 { + return f.entry.Gid +} + +func (f *FileInfo) Sys() any { + return f +} diff --git a/govcd/internal/udf/udf.go b/govcd/internal/udf/udf.go new file mode 100644 index 000000000..ddbcb9e37 --- /dev/null +++ b/govcd/internal/udf/udf.go @@ -0,0 +1,585 @@ +package udf + +import ( + "fmt" + "time" + + "golang.org/x/exp/utf8string" + "golang.org/x/text/encoding/charmap" + "golang.org/x/text/encoding/unicode" + "golang.org/x/text/transform" +) + +const ( + sectorSize = 2048 + anchorVolumeDescriptorSectorNumber = 256 + tagSize = 16 + tagVersion2 = 2 // ECMA-167/2 + entityIdSize = 32 +) + +const ( + dcharEncodingType8 = 8 + dcharEncodingType16 = 16 +) + +const ( + // Indicates the contents of the specified logical volume or file set + // is complaint with domain defined by this document. + entityIdentifierOSTACompliant = "*OSTA UDF Compliant" + // Contains additional Logical Volume identification information. + entityIdentifierLVInfo = "*UDF LV Info" + // Contains free unused space within the implementation extended attribute space. + entityIdentifierFreeEASpace = "*UDF FreeEASpace" + entityIdentifierDvdCGMSInfo = "*UDF DVD CGMS Info" +) + +const ( + osClassUndefined uint8 = 0 + osClassMac = 3 + osClassUnix = 4 + osClassWindows = 6 +) + +const ( + tagPrimaryVolumeDescriptor = 0x0001 + tagAnchorVolumeDescriptorPointer = 0x0002 + tagVolumeDescriptorPointer = 0x0003 + tagImplementationUseVolumeDescriptor = 0x0004 + tagPartitionDescriptor = 0x0005 + tagLogicalVolumeDescriptor = 0x0006 + tagUnallocatedSpaceDescriptor = 0x0007 + tagTerminatingDescriptor = 0x0008 + tagLogicalVolumeIntegrityDescriptor = 0x0009 + tagFileSetDescriptor = 0x0100 + tagFileIdentifierDescriptor = 0x0101 + tagAllocationExtentDescriptor = 0x0102 + tagIndirectEntry = 0x0103 + tagTerminalEntry = 0x0104 + tagFileEntry = 0x0105 + tagExtendedAttributeHeaderDescriptor = 0x0106 + tagUnallocatedSpaceEntry = 0x0107 + tagSpaceBitmapDescriptor = 0x0108 + tagPartitionIntegrityEntry = 0x0109 + tagExtendedFileEntry = 0x010a +) + +const ( + // Shall mean that this is an Unallocated Space Entry + fileTypeUnallocated uint8 = 1 + // Shall mean that this is a Partition Integrity Entry + fileTypePartitionIntegrity = 2 + // Shall mean that this is an Indirect Entry + fileTypeIndirect = 3 + // Shall mean that the file is a directory + fileTypeDirectory = 4 + // Shall mean that the file shall be interpreted as a sequence of bytes, each of which may be randomly accessed + fileTypeBytes = 5 + // Shall mean that the file is a block special device file as specified by ISO/IEC 9945-1 + fileTypeBlockDevice = 6 +) + +type FileCharacteristics uint8 + +const ( + // FileCharacteristicHidden If set to ZERO, shall mean that the existence of the file shall be made known + // to the user; If set to ONE, shall mean that the existence of the file need not be made known to the user. + FileCharacteristicHidden FileCharacteristics = 1 << (0 + iota) + + // FileCharacteristicDirectory If set to ZERO, shall mean that the file is not a directory (see 4/14.6.6); + // If set to ONE, shall mean that the file is a directory. + FileCharacteristicDirectory + + // FileCharacteristicDeleted If set to ONE, shall mean this File Identifier Descriptor identifies a file that + // has been deleted; If set to ZERO, shall mean that this File Identifier Descriptor identifies a file that + // has not been deleted. + FileCharacteristicDeleted + + // FileCharacteristicParent If set to ONE, shall mean that the ICB field of this descriptor identifies the + // ICB associated with the file in which is recorded the parent directory of the directory that this + // descriptor is recorded in; + // If set to ZERO, shall mean that the ICB field identifies the ICB associated with the file specified + // by this descriptor + FileCharacteristicParent + + // FileCharacteristicMetadata If this File Identifier Descriptor is not in a stream directory, + // this bit shall be set to ZERO. If this File Identifier Descriptor is in a stream directory, + // a value of ZERO shall indicate that this stream contains user data. A value of ONE shall indicate that the + // stream contains implementation use data. + FileCharacteristicMetadata +) + +func (c FileCharacteristics) HasFlags(mask FileCharacteristics) bool { + return c&mask == mask +} + +type Extent struct { + Length uint32 + Location uint32 +} + +type ExtentLong struct { + Length uint32 + Location uint64 +} + +type EntityID struct { + Flags uint8 + Identifier string + IdentifierSuffix string +} + +type ImplementationUse struct { + Entity EntityID + // Additional vendor-specific data + Implementation []byte +} + +type Charspec struct { + // CharacterSetType field shall have the value of 0 to indicate the CS0 coded character set. + CharacterSetType uint8 + // CharacterSetInfo field shall contain the following byte values with the remainder of the field set to a value of 0: + // #4F, #53, #54, #41, #20, #43, #6F, #6D, #70, #72, #65, #73, #73, + // #65, #64, #20, #55, #6E, #69, #63, #6F, #64, #65 + CharacterSetInfo []byte +} + +func NewDescriptor(tagId uint16) Descriptor { + switch tagId { + case tagPrimaryVolumeDescriptor: + return &PrimaryVolumeDescriptor{} + case tagAnchorVolumeDescriptorPointer: + return &AnchorVolumeDescriptorPointer{} + case tagVolumeDescriptorPointer: + return &VolumeDescriptorPointer{} + case tagImplementationUseVolumeDescriptor: + return &ImplementationUseVolumeDescriptor{} + case tagPartitionDescriptor: + return &PartitionDescriptor{} + case tagLogicalVolumeDescriptor: + return &LogicalVolumeDescriptor{} + case tagUnallocatedSpaceDescriptor: + return &UnallocatedSpaceDescriptor{} + case tagTerminatingDescriptor: + return &TerminatingDescriptor{} + case tagLogicalVolumeIntegrityDescriptor: + return &LogicalVolumeIntegrityDescriptor{} + case tagFileSetDescriptor: + return &FileSetDescriptor{} + case tagFileIdentifierDescriptor: + return &FileIdentifierDescriptor{} + case tagFileEntry: + return &FileEntryDescriptor{} + default: + panic(fmt.Errorf("unexpected tag identifier %d", tagId)) + } +} + +type Descriptor interface { + GetIdentifier() int + GetTag() *DescriptorTag +} + +type DescriptorList []Descriptor + +func (list DescriptorList) get(tagId int) Descriptor { + if desc := list.find(tagId); desc != nil { + return desc + } else { + panic(fmt.Sprintf("descriptor with ID %d does not exist in sequence", tagId)) + } +} + +func (list DescriptorList) find(tagId int) Descriptor { + for idx := 0; idx < len(list); idx++ { + if list[idx].GetIdentifier() == tagId { + return list[idx] + } + } + return nil +} + +// DescriptorTag Certain descriptors specified in Part 3 have a 16 byte structure, or tag, +// recorded at the start of the descriptor. +type DescriptorTag struct { + TagIdentifier uint16 + DescriptorVersion uint16 + // This field shall specify the sum modulo 256 of bytes 0-3 and 5-15 of the tag. + TagChecksum uint8 + TagSerialNumber uint16 + // This field shall specify the CRC of the bytes of the descriptor starting at the first byte after the descriptor tag. + // The CRC shall be 16 bits long and be generated by the CRC-ITU-T polynomial + DescriptorCRC uint16 + // The number of bytes shall be specified by the Descriptor CRC Length field. + DescriptorCRCLength uint16 + TagLocation uint32 +} + +// AnchorVolumeDescriptorPointer structure shall only be recorded at 2 of the following 3 locations on the media: +// * Logical Sector 256. +// * Logical Sector (N - 256). +// * N +type AnchorVolumeDescriptorPointer struct { + Tag DescriptorTag + MainVolumeDescriptorSequence Extent + ReserveVolumeDescriptorSequence Extent +} + +func (d *AnchorVolumeDescriptorPointer) GetIdentifier() int { + return tagAnchorVolumeDescriptorPointer +} + +func (d *AnchorVolumeDescriptorPointer) GetTag() *DescriptorTag { + return &d.Tag +} + +type VolumeDescriptorPointer struct { + Tag DescriptorTag + // This field shall specify the Volume Descriptor Sequence Number for this descriptor. + VolumeDescriptorSequenceNumber uint32 + // This field shall specify the next extent in the Volume Descriptor Sequence. If the extent's length is 0, no such + //extent is specified. + NextVolumeDescriptorSequenceExtent Extent +} + +func (d *VolumeDescriptorPointer) GetIdentifier() int { + return tagVolumeDescriptorPointer +} + +func (d *VolumeDescriptorPointer) GetTag() *DescriptorTag { + return &d.Tag +} + +// PrimaryVolumeDescriptor There shall be exactly one prevailing Primary Volume Descriptor recorded per volume. +type PrimaryVolumeDescriptor struct { + Tag DescriptorTag + VolumeDescriptorSequenceNumber uint32 + PrimaryVolumeDescriptorNumber uint32 + VolumeIdentifier string + VolumeSequenceNumber uint16 + MaximumVolumeSequenceNumber uint16 + InterchangeLevel uint16 + MaximumInterchangeLevel uint16 + CharacterSetList uint32 + MaximumCharacterSetList uint32 + VolumeSetIdentifier string + DescriptorCharacterSet Charspec + ExplanatoryCharacterSet Charspec + VolumeAbstract Extent + VolumeCopyrightNoticeExtent Extent + ApplicationIdentifier EntityID + RecordingDateTime time.Time + ImplementationIdentifier EntityID + ImplementationUse []byte + PredecessorVolumeDescriptorSequenceLocation uint32 + Flags uint16 +} + +func (d *PrimaryVolumeDescriptor) GetIdentifier() int { + return tagPrimaryVolumeDescriptor +} + +func (d *PrimaryVolumeDescriptor) GetTag() *DescriptorTag { + return &d.Tag +} + +// ImplementationUseVolumeDescriptor shall be recorded on every Volume of a Volume Set. +// The Volume may also contain additional Implementation Use Volume Descriptors which are implementation specific. +// The intended purpose of this descriptor is to aid in the identification of a Volume within a Volume Set +// that belongs to a specific Logical Volume. +type ImplementationUseVolumeDescriptor struct { + Tag DescriptorTag + VolumeDescriptorSequenceNumber uint32 + ImplementationIdentifier EntityID + ImplementationUse LVInformation +} + +func (d *ImplementationUseVolumeDescriptor) GetIdentifier() int { + return tagImplementationUseVolumeDescriptor +} + +func (d *ImplementationUseVolumeDescriptor) GetTag() *DescriptorTag { + return &d.Tag +} + +type LVInformation struct { + LVICharset Charspec + LogicalVolumeIdentifier string + LVInfo1 string + LVInfo2 string + LVInfo3 string + ImplementationID EntityID + ImplementationUse []byte +} + +// PartitionDescriptor A Partition Access Type of Read-Only , Rewritable, Overwritable and WORM shall be supported. +// There shall be exactly one prevailing Partition Descriptor recorded per volume, with one exception. +// For Volume Sets that consist of single volume, the volume may contain 2 Partitions with 2 prevailing +// Partition Descriptors only if one has an access type of read only and the other has an access type of Rewritable or Overwritable. +// The Logical Volume for this volume would consist of the contents of both partitions. +type PartitionDescriptor struct { + Tag DescriptorTag + VolumeDescriptorSequenceNumber uint32 + PartitionFlags uint16 + PartitionNumber uint16 + PartitionContents EntityID + PartitionContentsUse []byte + AccessType uint32 + PartitionStartingLocation uint32 + PartitionLength uint32 + ImplementationIdentifier EntityID + ImplementationUse []byte +} + +func (d *PartitionDescriptor) GetIdentifier() int { + return tagPartitionDescriptor +} + +func (d *PartitionDescriptor) GetTag() *DescriptorTag { + return &d.Tag +} + +type LogicalVolumeDescriptor struct { + Tag DescriptorTag + VolumeDescriptorSequenceNumber uint32 + DescriptorCharacterSet Charspec + LogicalVolumeIdentifier string + LogicalBlockSize uint32 + DomainIdentifier EntityID + LogicalVolumeContentsUse []byte + MapTableLength uint32 + NumberOfPartitionMaps uint32 + ImplementationIdentifier EntityID + ImplementationUse []byte + IntegritySequenceExtent Extent + PartitionMaps []PartitionMap +} + +func (d *LogicalVolumeDescriptor) GetIdentifier() int { + return tagLogicalVolumeDescriptor +} + +func (d *LogicalVolumeDescriptor) GetTag() *DescriptorTag { + return &d.Tag +} + +type LogicalVolumeHeaderDescriptor struct { + UniqueID uint64 +} + +type PartitionMap struct { + PartitionMapType uint8 + PartitionMapLength uint8 + VolumeSequenceNumber uint16 + PartitionNumber uint16 +} + +// UnallocatedSpaceDescriptor shall be recorded, even if there is no free volume space. +// The first 32768 bytes of the Volume space shall not be used for the recording of ECMA 167 structures. +// This area shall not be referenced by the Unallocated Space Descriptor or any other ECMA 167 descriptor. +type UnallocatedSpaceDescriptor struct { + Tag DescriptorTag + VolumeDescriptorSequenceNumber uint32 + NumberOfAllocationDescriptors uint32 + AllocationDescriptors []Extent +} + +func (d *UnallocatedSpaceDescriptor) GetIdentifier() int { + return tagUnallocatedSpaceDescriptor +} + +func (d *UnallocatedSpaceDescriptor) GetTag() *DescriptorTag { + return &d.Tag +} + +// TerminatingDescriptor may be recorded to terminate a Volume Descriptor Sequence +type TerminatingDescriptor struct { + Tag DescriptorTag +} + +func (d *TerminatingDescriptor) GetIdentifier() int { + return tagTerminatingDescriptor +} + +func (d *TerminatingDescriptor) GetTag() *DescriptorTag { + return &d.Tag +} + +type LogicalVolumeIntegrityDescriptor struct { + Tag DescriptorTag + RecordingDateTime time.Time + IntegrityType uint32 + NextIntegrityExtent Extent + LogicalVolumeContentsUse LogicalVolumeHeaderDescriptor + NumberOfPartitions uint32 + LengthOfImplementationUse uint32 + FreeSpaceTable []uint32 + SizeTable []uint32 + ImplementationUse ImplementationUse +} + +func (d *LogicalVolumeIntegrityDescriptor) GetIdentifier() int { + return tagLogicalVolumeIntegrityDescriptor +} + +func (d *LogicalVolumeIntegrityDescriptor) GetTag() *DescriptorTag { + return &d.Tag +} + +// FileSetDescriptor Only one File Set Descriptor shall be recorded. On WORM media, multiple File Sets may be recorded. +type FileSetDescriptor struct { + Tag DescriptorTag + RecordingDateTime time.Time + InterchangeLevel uint16 + MaximumInterchangeLevel uint16 + CharacterSetList uint32 + MaximumCharacterSetList uint32 + FileSetNumber uint32 + FileSetDescriptorNumber uint32 + LogicalVolumeIdentifierCharacterSet Charspec + LogicalVolumeIdentifier string + FileSetCharacterSet Charspec + FileSetIdentifier string + CopyrightFileIdentifier string + AbstractFileIdentifier string + RootDirectoryICB ExtentLong + DomainIdentifier EntityID + NextExtent ExtentLong + SystemStreamDirectoryICB ExtentLong +} + +func (d *FileSetDescriptor) GetIdentifier() int { + return tagFileSetDescriptor +} + +func (d *FileSetDescriptor) GetTag() *DescriptorTag { + return &d.Tag +} + +type FileEntryDescriptor struct { + Tag DescriptorTag + ICBTag ICBTag + Uid uint32 + Gid uint32 + Permissions FileMode + FileLinkCount uint16 + RecordFormat uint8 + RecordDisplayAttributes uint8 + RecordLength uint32 + InformationLength uint64 + LogicalBlocksRecorded uint64 + AccessTime time.Time + ModificationTime time.Time + AttributeTime time.Time + Checkpoint uint32 + ExtendedAttributeICB ExtentLong + ImplementationIdentifier EntityID + UniqueID uint64 + LengthOfExtendedAttributes uint32 + LengthOfAllocationDescriptors uint32 + ExtendedAttributes []byte + AllocationDescriptors []Extent +} + +func (d *FileEntryDescriptor) GetIdentifier() int { + return tagFileEntry +} + +func (d *FileEntryDescriptor) GetTag() *DescriptorTag { + return &d.Tag +} + +type ICBTag struct { + PriorRecordedNumberOfDirectEntries uint32 + StrategyType uint16 + StrategyParameter uint16 + MaximumNumberOfEntries uint16 + FileType uint8 + ParentICBLocation LogicalBlockAddress + Flags uint16 +} + +type LogicalBlockAddress struct { + LogicalBlockNumber uint32 + PartitionReferenceNumber uint16 +} + +// FileIdentifierDescriptor ECMA 167 4/14.4 +type FileIdentifierDescriptor struct { + Tag DescriptorTag + FileVersionNumber uint16 + FileCharacteristics FileCharacteristics + LengthOfFileIdentifier uint8 + ICB ExtentLong + LengthOfImplementationUse uint16 + ImplementationUse []byte + FileIdentifier string +} + +func (d *FileIdentifierDescriptor) GetIdentifier() int { + return tagFileIdentifierDescriptor +} + +func (d *FileIdentifierDescriptor) GetTag() *DescriptorTag { + return &d.Tag +} + +func (d *FileIdentifierDescriptor) calculateSize() int { + fileIdentifierLen := len(encodeDCharacters(d.FileIdentifier)) + size := 38 + int(d.LengthOfImplementationUse) + fileIdentifierLen + paddingLen := 4*((size+3)/4) - size + if paddingLen > 0 { + size += paddingLen + } + return size +} + +type ExtendedAttributeHeaderDescriptor struct { + Tag DescriptorTag + ImplementationAttributesLocation uint32 + ApplicationAttributesLocation uint32 +} + +func (d *ExtendedAttributeHeaderDescriptor) GetIdentifier() int { + return tagExtendedAttributeHeaderDescriptor +} + +func (d *ExtendedAttributeHeaderDescriptor) GetTag() *DescriptorTag { + return &d.Tag +} + +type ImplementationUseExtendedAttribute struct { + AttributeType uint32 + AttributeSubtype uint8 + AttributeLength uint32 + ImplementationUseLength uint32 + ImplementationIdentifier EntityID + ImplementationData []byte +} + +func encodeDCharacters(value string) []byte { + if len(value) == 0 { + return []byte{} + } + var encodingType int + var buf []byte + var err error + if utf8string.NewString(value).IsASCII() { + encodingType = dcharEncodingType8 + win1252Enc := charmap.Windows1252.NewEncoder() + if buf, _, err = transform.Bytes(win1252Enc, []byte(value)); err != nil { + panic(err) + } + } else { + encodingType = dcharEncodingType16 + utf16Enc := unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM).NewEncoder() + if buf, _, err = transform.Bytes(utf16Enc, []byte(value)); err != nil { + panic(err) + } + } + + res := make([]byte, len(buf)+1) + res[0] = byte(encodingType) + copy(res[1:], buf) + return res +} diff --git a/govcd/ip_space.go b/govcd/ip_space.go new file mode 100644 index 000000000..79c7944c3 --- /dev/null +++ b/govcd/ip_space.go @@ -0,0 +1,142 @@ +/* + * Copyright 2024 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" +) + +const labelIpSpace = "IP Space" + +// IpSpace provides structured approach to allocating public and private IP addresses by preventing +// the use of overlapping IP addresses across organizations and organization VDCs. +// +// An IP space consists of a set of defined non-overlapping IP ranges and small CIDR blocks that are +// reserved and used during the consumption aspect of the IP space life cycle. An IP space can be +// either IPv4 or IPv6, but not both. +// +// Every IP space has an internal scope and an external scope. The internal scope of an IP space is +// a list of CIDR notations that defines the exact span of IP addresses in which all ranges and +// blocks must be contained in. The external scope defines the total span of IP addresses to which +// the IP space has access, for example the internet or a WAN. +type IpSpace struct { + IpSpace *types.IpSpace + 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 IpSpace) wrap(inner *types.IpSpace) *IpSpace { + g.IpSpace = inner + return &g +} + +// CreateIpSpace creates IP Space with desired configuration +func (vcdClient *VCDClient) CreateIpSpace(ipSpaceConfig *types.IpSpace) (*IpSpace, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaces, + entityLabel: labelIpSpace, + } + outerType := IpSpace{vcdClient: vcdClient} + return createOuterEntity(&vcdClient.Client, outerType, c, ipSpaceConfig) +} + +// GetAllIpSpaceSummaries retrieve summaries of all IP Spaces with an optional filter +// Note. There is no API endpoint to get multiple IP Spaces with their full definitions. Only +// "summaries" endpoint exists, but it does not include all fields. To retrieve complete structure +// one can use `GetIpSpaceById` or `GetIpSpaceByName` +func (vcdClient *VCDClient) GetAllIpSpaceSummaries(queryParameters url.Values) ([]*IpSpace, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceSummaries, + entityLabel: labelIpSpace, + queryParameters: queryParameters, + } + + outerType := IpSpace{vcdClient: vcdClient} + return getAllOuterEntities[IpSpace, types.IpSpace](&vcdClient.Client, outerType, c) +} + +// GetIpSpaceByName retrieves IP Space with a given name +// Note. It will return an error if multiple IP Spaces exist with the same name +func (vcdClient *VCDClient) GetIpSpaceByName(name string) (*IpSpace, error) { + if name == "" { + return nil, fmt.Errorf("IP Space lookup requires name") + } + + queryParams := url.Values{} + queryParams.Add("filter", "name=="+name) + + filteredEntities, err := vcdClient.GetAllIpSpaceSummaries(queryParams) + if err != nil { + return nil, err + } + + singleIpSpace, err := oneOrError("name", name, filteredEntities) + if err != nil { + return nil, err + } + + return vcdClient.GetIpSpaceById(singleIpSpace.IpSpace.ID) +} + +func (vcdClient *VCDClient) GetIpSpaceById(id string) (*IpSpace, error) { + c := crudConfig{ + entityLabel: labelIpSpace, + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaces, + endpointParams: []string{id}, + } + + outerType := IpSpace{vcdClient: vcdClient} + return getOuterEntity[IpSpace, types.IpSpace](&vcdClient.Client, outerType, c) +} + +// GetIpSpaceByNameAndOrgId retrieves IP Space with a given name in a particular Org +// Note. Only PRIVATE IP spaces belong to Orgs +func (vcdClient *VCDClient) GetIpSpaceByNameAndOrgId(name, orgId string) (*IpSpace, error) { + if name == "" || orgId == "" { + return nil, fmt.Errorf("IP Space lookup requires name and Org ID") + } + + queryParams := url.Values{} + queryParams.Add("filter", "name=="+name) + queryParams = queryParameterFilterAnd("orgRef.id=="+orgId, queryParams) + + filteredEntities, err := vcdClient.GetAllIpSpaceSummaries(queryParams) + if err != nil { + return nil, err + } + + singleIpSpace, err := oneOrError("name", name, filteredEntities) + if err != nil { + return nil, err + } + + return vcdClient.GetIpSpaceById(singleIpSpace.IpSpace.ID) +} + +// Update updates IP Space with new config +func (ipSpace *IpSpace) Update(ipSpaceConfig *types.IpSpace) (*IpSpace, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaces, + endpointParams: []string{ipSpace.IpSpace.ID}, + entityLabel: labelIpSpace, + } + outerType := IpSpace{vcdClient: ipSpace.vcdClient} + return updateOuterEntity(&ipSpace.vcdClient.Client, outerType, c, ipSpaceConfig) +} + +// Delete deletes IP Space +func (ipSpace *IpSpace) Delete() error { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaces, + endpointParams: []string{ipSpace.IpSpace.ID}, + entityLabel: labelIpSpace, + } + return deleteEntityById(&ipSpace.vcdClient.Client, c) +} diff --git a/govcd/ip_space_allocation.go b/govcd/ip_space_allocation.go new file mode 100644 index 000000000..1940eed10 --- /dev/null +++ b/govcd/ip_space_allocation.go @@ -0,0 +1,310 @@ +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "encoding/json" + "fmt" + "net/url" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +const labelIpSpaceFloatingIpSuggestion = "IP Space floating IP suggestions" + +// IpSpaceIpAllocation handles IP Space IP allocation requests +type IpSpaceIpAllocation struct { + IpSpaceIpAllocation *types.IpSpaceIpAllocation + IpSpaceId string + + client *Client + // Org context must be sent with requests + parent organization +} + +// AllocateIp performs IP Allocation request for a specific Org and returns the result +func (ipSpace *IpSpace) AllocateIp(orgId, orgName string, ipAllocationConfig *types.IpSpaceIpAllocationRequest) ([]types.IpSpaceIpAllocationRequestResult, error) { + return allocateIpSpaceIp(&ipSpace.vcdClient.Client, orgId, orgName, ipSpace.IpSpace.ID, ipAllocationConfig) +} + +// IpSpaceAllocateIp performs IP allocation request for a specific IP Space +func (org *Org) IpSpaceAllocateIp(ipSpaceId string, ipAllocationConfig *types.IpSpaceIpAllocationRequest) ([]types.IpSpaceIpAllocationRequestResult, error) { + return allocateIpSpaceIp(org.client, org.Org.ID, org.Org.Name, ipSpaceId, ipAllocationConfig) +} + +// GetIpSpaceAllocationByTypeAndValue retrieves IP Space allocation by its type and value +// allocationType can be 'FLOATING_IP' (types.IpSpaceIpAllocationTypeFloatingIp) or 'IP_PREFIX' +// (types.IpSpaceIpAllocationTypeIpPrefix) +func (org *Org) GetIpSpaceAllocationByTypeAndValue(ipSpaceId string, allocationType, value string, queryParameters url.Values) (*IpSpaceIpAllocation, error) { + queryParams := queryParameterFilterAnd(fmt.Sprintf("value==%s;type==%s", value, allocationType), queryParameters) + results, err := getAllIpSpaceAllocations(org.client, ipSpaceId, org, queryParams) + if err != nil { + return nil, fmt.Errorf("error retrieving IP allocations: %s", err) + } + + singleResult, err := oneOrError("value", value, results) + if err != nil { + return nil, err + } + + return singleResult, nil +} + +// GetAllIpSpaceAllocations retrieves all IP Allocations for a particular IP Space +// allocationType can be 'FLOATING_IP' (types.IpSpaceIpAllocationTypeFloatingIp) or 'IP_PREFIX' +// (types.IpSpaceIpAllocationTypeIpPrefix) +func (ipSpace *IpSpace) GetAllIpSpaceAllocations(allocationType string, queryParameters url.Values) ([]*IpSpaceIpAllocation, error) { + if allocationType == "" { + return nil, fmt.Errorf("allocationType is mandatory and must be 'FLOATING_IP' or 'IP_PREFIX'") + } + + client := ipSpace.vcdClient.Client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceIpAllocations + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, ipSpace.IpSpace.ID)) + if err != nil { + return nil, err + } + + queryParams := queryParameterFilterAnd(fmt.Sprintf("type==%s", allocationType), queryParameters) + typeResponses := []*types.IpSpaceIpAllocation{{}} + err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParams, &typeResponses, nil) + if err != nil { + return nil, err + } + + // Wrap all typeResponses into IpSpaceIpAllocation types with client + results := make([]*IpSpaceIpAllocation, len(typeResponses)) + for sliceIndex := range typeResponses { + results[sliceIndex] = &IpSpaceIpAllocation{ + IpSpaceIpAllocation: typeResponses[sliceIndex], + client: &client, + IpSpaceId: ipSpace.IpSpace.ID, + parent: &Org{ + Org: &types.Org{ + ID: typeResponses[sliceIndex].OrgRef.ID, + Name: typeResponses[sliceIndex].OrgRef.Name}, + }, + } + } + + return results, nil +} + +// GetIpSpaceAllocationById retrieves IP Allocation in a given IP Space by IDs +func (org *Org) GetIpSpaceAllocationById(ipSpaceId, allocationId string) (*IpSpaceIpAllocation, error) { + if ipSpaceId == "" || allocationId == "" { + return nil, fmt.Errorf("ipSpaceId and allocationId cannot be empty") + } + client := org.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceIpAllocations + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, ipSpaceId), allocationId) + if err != nil { + return nil, err + } + + response := &IpSpaceIpAllocation{ + IpSpaceIpAllocation: &types.IpSpaceIpAllocation{}, + parent: org, + IpSpaceId: ipSpaceId, + client: client, + } + + err = client.OpenApiGetItem(apiVersion, urlRef, nil, response.IpSpaceIpAllocation, nil) + if err != nil { + return nil, err + } + + return response, nil + +} + +// Update updates IP Allocation with a given configuration +func (ipSpaceAllocation *IpSpaceIpAllocation) Update(ipSpaceAllocationConfig *types.IpSpaceIpAllocation) (*IpSpaceIpAllocation, error) { + client := ipSpaceAllocation.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceIpAllocations + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, ipSpaceAllocation.IpSpaceId), ipSpaceAllocation.IpSpaceIpAllocation.ID) + if err != nil { + return nil, err + } + + returnIpSpaceAllocation := &IpSpaceIpAllocation{ + IpSpaceIpAllocation: &types.IpSpaceIpAllocation{}, + client: client, + parent: ipSpaceAllocation.parent, + IpSpaceId: ipSpaceAllocation.IpSpaceId, + } + + tenantContext, err := ipSpaceAllocation.getTenantContext() + if err != nil { + return nil, err + } + + err = client.OpenApiPutItem(apiVersion, urlRef, nil, ipSpaceAllocationConfig, returnIpSpaceAllocation.IpSpaceIpAllocation, getTenantContextHeader(tenantContext)) + if err != nil { + return nil, fmt.Errorf("error updating IP Space IP Allocation: %s", err) + } + + return returnIpSpaceAllocation, nil +} + +// Delete removes IP Allocation +func (ipSpaceAllocation *IpSpaceIpAllocation) Delete() error { + if ipSpaceAllocation == nil || ipSpaceAllocation.IpSpaceIpAllocation == nil || ipSpaceAllocation.IpSpaceIpAllocation.ID == "" { + return fmt.Errorf("IP Space IP Allocation must have ID") + } + + if ipSpaceAllocation.IpSpaceId == "" || ipSpaceAllocation.parent == nil { + return fmt.Errorf("incomplete IpSpaceIpAllocation type") + } + + client := ipSpaceAllocation.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceIpAllocations + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return err + } + + tenantContext, err := ipSpaceAllocation.getTenantContext() + if err != nil { + return err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, ipSpaceAllocation.IpSpaceId), ipSpaceAllocation.IpSpaceIpAllocation.ID) + if err != nil { + return err + } + + err = client.OpenApiDeleteItem(apiVersion, urlRef, nil, getTenantContextHeader(tenantContext)) + if err != nil { + return fmt.Errorf("error deleting IP Space IP Allocation: %s", err) + } + + return nil +} + +func allocateIpSpaceIp(client *Client, orgId, orgName, ipSpaceId string, ipAllocationConfig *types.IpSpaceIpAllocationRequest) ([]types.IpSpaceIpAllocationRequestResult, error) { + if orgId == "" || orgName == "" || ipSpaceId == "" { + return nil, fmt.Errorf("IP Space must have all values Org ID, Org Name and IP Space ID populated") + } + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceUplinksAllocate + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, ipSpaceId)) + if err != nil { + return nil, err + } + + tenantContext := &TenantContext{ + OrgId: orgId, + OrgName: orgName, + } + + task, err := client.OpenApiPostItemAsyncWithHeaders(apiVersion, urlRef, nil, ipAllocationConfig, getTenantContextHeader(tenantContext)) + if err != nil { + return nil, fmt.Errorf("error triggering IP Allocation task for IP Space '%s': %s", ipSpaceId, err) + } + + err = task.WaitTaskCompletion() + if err != nil { + return nil, fmt.Errorf("error waiting for task completion: %s", err) + } + + // Result of the task should contain a JSON with allocated IP details + if task.Task == nil || task.Task.Result == nil || task.Task.Result.ResultContent.Text == "" { + return nil, fmt.Errorf("error finding allocated IP result in task") + } + result := task.Task.Result.ResultContent.Text + + unmarshalStorage := []types.IpSpaceIpAllocationRequestResult{} + err = json.Unmarshal([]byte(result), &unmarshalStorage) + if err != nil { + return nil, fmt.Errorf("error unmarshalling task result: %s", err) + } + + return unmarshalStorage, nil +} + +func getAllIpSpaceAllocations(client *Client, ipSpaceId string, org *Org, queryParameters url.Values) ([]*IpSpaceIpAllocation, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceIpAllocations + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, ipSpaceId)) + if err != nil { + return nil, err + } + + tenantContext, err := org.getTenantContext() + if err != nil { + return nil, fmt.Errorf("error getting tenant context: %s", err) + } + + typeResponses := []*types.IpSpaceIpAllocation{{}} + err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParameters, &typeResponses, getTenantContextHeader(tenantContext)) + if err != nil { + return nil, err + } + + // Wrap all typeResponses into IpSpaceIpAllocation types with client + results := make([]*IpSpaceIpAllocation, len(typeResponses)) + for sliceIndex := range typeResponses { + results[sliceIndex] = &IpSpaceIpAllocation{ + IpSpaceIpAllocation: typeResponses[sliceIndex], + client: client, + parent: org, + IpSpaceId: ipSpaceId, + } + } + + return results, nil +} + +// GetAllIpSpaceFloatingIpSuggestions suggests IP addresses to use for networking services on Edge +// Gateway or Provider Gateway. 'gatewayId' is mandatory. Based on the specified Gateway, VCD will +// query all the applicable IP Spaces and suggest some IP addresses which can be utilized to +// configure the network services on the Gateway. Allocated IP Space's IP addresses, but not +// currently used for any network services are returned. Results can also be filtered by IPV4 or +// IPV6 IP address types. +// +// Filter examples:(filter=gatewayId==URN), (filter=gatewayId==URN;ipType==IPV6) +// Go code: +// queryParams := url.Values{} +// queryParams.Set("filter", "ipType==IPV4") +func (vcdClient *VCDClient) GetAllIpSpaceFloatingIpSuggestions(gatewayId string, queryParameters url.Values) ([]*types.IpSpaceFloatingIpSuggestion, error) { + if gatewayId == "" { + return nil, fmt.Errorf("edge gateway ID is mandatory") + } + + queryParams := copyOrNewUrlValues(queryParameters) + queryParams = queryParameterFilterAnd("gatewayId=="+gatewayId, queryParams) + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceFloatingIpSuggestions, + entityLabel: labelIpSpaceFloatingIpSuggestion, + queryParameters: queryParams, + } + + return getAllInnerEntities[types.IpSpaceFloatingIpSuggestion](&vcdClient.Client, c) +} diff --git a/govcd/ip_space_allocation_test.go b/govcd/ip_space_allocation_test.go new file mode 100644 index 000000000..dd3a97fc4 --- /dev/null +++ b/govcd/ip_space_allocation_test.go @@ -0,0 +1,214 @@ +//go:build network || nsxt || functional || openapi || ALL + +package govcd + +import ( + "fmt" + "net/url" + "strings" + "time" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +// Test_IpSpaceIpAllocation tests out IP Space integration with other components +func (vcd *TestVCD) Test_IpSpaceIpAllocation(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointIpSpaceIpAllocations) + + ipSpace := createIpSpace(vcd, check) + extNet := createExternalNetwork(vcd, check) + + // IP Space uplink (not directly referenced anywhere, but is required to make IP allocations) + ipSpaceUplink := createIpSpaceUplink(vcd, check, extNet.ExternalNetwork.ID, ipSpace.IpSpace.ID) + + // Create NSX-T Edge Gateway + edgeGw := createNsxtEdgeGateway(vcd, check, extNet.ExternalNetwork.ID) + + // Floating IP Allocation request + floatingIpAllocationRequest := &types.IpSpaceIpAllocationRequest{ + Type: "FLOATING_IP", + Quantity: addrOf(1), + } + performIpAllocationChecks(vcd, check, ipSpace.IpSpace.ID, edgeGw.EdgeGateway.ID, floatingIpAllocationRequest) + + // Prefix allocation request + prefixAllocationRequest := &types.IpSpaceIpAllocationRequest{ + Type: "IP_PREFIX", + Quantity: addrOf(1), + PrefixLength: addrOf(31), + } + performIpAllocationChecks(vcd, check, ipSpace.IpSpace.ID, edgeGw.EdgeGateway.ID, prefixAllocationRequest) + + // Cleanup + err := edgeGw.Delete() + check.Assert(err, IsNil) + + err = ipSpaceUplink.Delete() + check.Assert(err, IsNil) + + err = extNet.Delete() + check.Assert(err, IsNil) + + err = ipSpace.Delete() + check.Assert(err, IsNil) +} + +func performIpAllocationChecks(vcd *TestVCD, check *C, ipSpaceId, edgeGatewayId string, ipSpaceAllocationRequest *types.IpSpaceIpAllocationRequest) { + // resulting slice must have 1 IP as the requested quantity is 1 + ipAllocationResult, err := vcd.org.IpSpaceAllocateIp(ipSpaceId, ipSpaceAllocationRequest) + check.Assert(err, IsNil) + check.Assert(len(ipAllocationResult), Equals, 1) + + openApiEndpoint := types.OpenApiPathVersion1_0_0 + fmt.Sprintf(types.OpenApiEndpointIpSpaceIpAllocations, ipSpaceId) + ipAllocationResult[0].ID + PrependToCleanupListOpenApi("NSX-T IP Space IP Allocation", check.TestName(), openApiEndpoint) + + // Check IP allocation suggestion endpoint + performIpSuggestionChecks(vcd, check, ipSpaceId, edgeGatewayId, ipSpaceAllocationRequest) + + // Get IP Allocation + ipAllocation, err := vcd.org.GetIpSpaceAllocationByTypeAndValue(ipSpaceId, ipSpaceAllocationRequest.Type, ipAllocationResult[0].Value, nil) + check.Assert(err, IsNil) + check.Assert(ipAllocation, NotNil) + check.Assert(ipAllocation.IpSpaceIpAllocation.UsageState, Equals, types.IpSpaceIpAllocationUnused) + + // Get IP Allocation by ID + ipAllocationById, err := vcd.org.GetIpSpaceAllocationById(ipSpaceId, ipAllocation.IpSpaceIpAllocation.ID) + check.Assert(err, IsNil) + check.Assert(ipAllocationById, NotNil) + check.Assert(ipAllocationById.IpSpaceIpAllocation, DeepEquals, ipAllocation.IpSpaceIpAllocation) + + // Set the IP for manual usage + ipAllocation.IpSpaceIpAllocation.UsageState = types.IpSpaceIpAllocationUsedManual + ipAllocation.IpSpaceIpAllocation.Description = "Manual usage description" + updatedIpAllocationManual, err := ipAllocation.Update(ipAllocation.IpSpaceIpAllocation) + check.Assert(err, IsNil) + check.Assert(updatedIpAllocationManual.IpSpaceIpAllocation.ID, Equals, ipAllocation.IpSpaceIpAllocation.ID) + check.Assert(updatedIpAllocationManual.IpSpaceIpAllocation.UsageState, Equals, types.IpSpaceIpAllocationUsedManual) + + // Removal manual allocation + ipAllocation.IpSpaceIpAllocation.UsageState = types.IpSpaceIpAllocationUnused + ipAllocation.IpSpaceIpAllocation.Description = "" + releasedIpAllocation, err := updatedIpAllocationManual.Update(ipAllocation.IpSpaceIpAllocation) + check.Assert(err, IsNil) + check.Assert(releasedIpAllocation, NotNil) + check.Assert(releasedIpAllocation.IpSpaceIpAllocation.UsageState, Equals, types.IpSpaceIpAllocationUnused) + + err = updatedIpAllocationManual.Delete() + check.Assert(err, IsNil) + + // Get IP Space by ID + ipSpace, err := vcd.client.GetIpSpaceById(ipSpaceId) + check.Assert(err, IsNil) + + // Attempt to search for allocations when none exist + allAllocations, err := ipSpace.GetAllIpSpaceAllocations(ipSpaceAllocationRequest.Type, nil) + check.Assert(err, IsNil) + check.Assert(len(allAllocations), Equals, 0) + + // allocate IP + allocationByIpSpaceResult, err := ipSpace.AllocateIp(vcd.org.Org.ID, vcd.org.Org.Name, ipSpaceAllocationRequest) + check.Assert(err, IsNil) + check.Assert(len(allocationByIpSpaceResult), Equals, 1) + + // Remove + ipAllocationByIpSpaceResult, err := vcd.org.GetIpSpaceAllocationByTypeAndValue(ipSpaceId, ipSpaceAllocationRequest.Type, allocationByIpSpaceResult[0].Value, nil) + check.Assert(err, IsNil) + check.Assert(ipAllocation, NotNil) + + err = ipAllocationByIpSpaceResult.Delete() + check.Assert(err, IsNil) + +} + +func performIpSuggestionChecks(vcd *TestVCD, check *C, ipSpaceId, edgeGatewayId string, ipSpaceAllocationRequest *types.IpSpaceIpAllocationRequest) { + // Get IP suggestions without additional filters + floatingIpSuggestions, err := vcd.client.GetAllIpSpaceFloatingIpSuggestions(edgeGatewayId, nil) + check.Assert(err, IsNil) + check.Assert(len(floatingIpSuggestions) > 0, Equals, true) + if ipSpaceAllocationRequest.Type == "FLOATING_IP" { + check.Assert(len(floatingIpSuggestions[0].UnusedValues), Equals, 1) + } + + // Get IP suggestions only for IPv4 + queryParams := url.Values{} + queryParams.Set("filter", "ipType==IPV4") + floatingIpSuggestionsWithFilterIpv4, err := vcd.client.GetAllIpSpaceFloatingIpSuggestions(edgeGatewayId, queryParams) + check.Assert(err, IsNil) + check.Assert(len(floatingIpSuggestionsWithFilterIpv4) > 0, Equals, true) + if ipSpaceAllocationRequest.Type == "FLOATING_IP" { + check.Assert(len(floatingIpSuggestionsWithFilterIpv4[0].UnusedValues), Equals, 1) + } + + // Get IP suggestions only for IPv6 + queryParams.Set("filter", "ipType==IPV6") + floatingIpSuggestionsWithFilterIpv6, err := vcd.client.GetAllIpSpaceFloatingIpSuggestions(edgeGatewayId, queryParams) + check.Assert(err, IsNil) + check.Assert(len(floatingIpSuggestionsWithFilterIpv6) > 0, Equals, false) + + // check IP suggestions with invalid Edge Gateway - it returns ACCESS_TO_RESOURCE_IS_FORBIDDEN + // queryParams.Set("filter", fmt.Sprintf("gatewayId==%s", "urn:vcloud:gateway:00000000-0000-0000-0000-000000000000")) + floatingIpSuggestions2, err := vcd.client.GetAllIpSpaceFloatingIpSuggestions("urn:vcloud:gateway:00000000-0000-0000-0000-000000000000", nil) + check.Assert(strings.Contains(err.Error(), "ACCESS_TO_RESOURCE_IS_FORBIDDEN"), Equals, true) + check.Assert(floatingIpSuggestions2, IsNil) + + // check with empty filter - it cannot be used this way (edge gateway is mandatory) + floatingIpSuggestions3, err := vcd.client.GetAllIpSpaceFloatingIpSuggestions("", nil) + check.Assert(strings.Contains(err.Error(), "edge gateway ID is mandatory"), Equals, true) + check.Assert(floatingIpSuggestions3, IsNil) +} + +func createIpSpaceUplink(vcd *TestVCD, check *C, extNetId, ipSpaceId string) *IpSpaceUplink { + // Create Uplink configuration + uplinkConfig := &types.IpSpaceUplink{ + Name: check.TestName(), + Description: "IP SPace Uplink for External Network (Provider Gateway)", + ExternalNetworkRef: &types.OpenApiReference{ID: extNetId}, + IPSpaceRef: &types.OpenApiReference{ID: ipSpaceId}, + } + + createdIpSpaceUplink, err := vcd.client.CreateIpSpaceUplink(uplinkConfig) + check.Assert(err, IsNil) + check.Assert(createdIpSpaceUplink, NotNil) + + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceUplinks + createdIpSpaceUplink.IpSpaceUplink.ID + AddToCleanupListOpenApi(createdIpSpaceUplink.IpSpaceUplink.Name, check.TestName(), openApiEndpoint) + + time.Sleep(3 * time.Second) + err = vcd.client.Client.WaitForRouteAdvertisementTasks() + check.Assert(err, IsNil) + + return createdIpSpaceUplink +} + +func createNsxtEdgeGateway(vcd *TestVCD, check *C, extNetId string) *NsxtEdgeGateway { + egwDefinition := &types.OpenAPIEdgeGateway{ + Name: check.TestName(), + Description: "nsx-t-edge-description", + OrgVdc: &types.OpenApiReference{ + ID: vcd.nsxtVdc.Vdc.ID, + }, + EdgeGatewayUplinks: []types.EdgeGatewayUplinks{ + types.EdgeGatewayUplinks{ + UplinkID: extNetId, + }, + }, + } + + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + + createdEdge, err := adminOrg.CreateNsxtEdgeGateway(egwDefinition) + check.Assert(err, IsNil) + check.Assert(createdEdge.EdgeGateway.Name, Equals, egwDefinition.Name) + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGateways + createdEdge.EdgeGateway.ID + // Using Prepend function so that Edge Gateway is removed before parent External Network is being removed + PrependToCleanupListOpenApi("NSX-T Edge Gateway", check.TestName(), openApiEndpoint) + + return createdEdge +} diff --git a/govcd/ip_space_org_assignment.go b/govcd/ip_space_org_assignment.go new file mode 100644 index 000000000..7b272c717 --- /dev/null +++ b/govcd/ip_space_org_assignment.go @@ -0,0 +1,160 @@ +/* + * 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" +) + +// IpSpaceOrgAssignment handles Custom Quotas (name in UI) for a particular Org. They complement +// default quotas which are being set in IP Space itself. +// The behavior of IpSpaceOrgAssignment is specific - whenever an NSX-T Edge Gateway backed by +// Provider gateway using IP Spaces is being created - Org Assignment is created implicitly. One can +// look up that assignment by IP Space and Org to update `types.IpSpaceOrgAssignment.CustomQuotas` +// field +type IpSpaceOrgAssignment struct { + IpSpaceOrgAssignment *types.IpSpaceOrgAssignment + IpSpaceId string + + vcdClient *VCDClient +} + +// GetAllOrgAssignments retrieves all IP Space Org assignments within an IP Space +// +// Note. Org assignments are implicitly created after NSX-T Edge Gateway backed by Provider gateway +// using IP Spaces is being created. +func (ipSpace *IpSpace) GetAllOrgAssignments(queryParameters url.Values) ([]*IpSpaceOrgAssignment, error) { + client := ipSpace.vcdClient.Client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceOrgAssignments + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + queryParams := queryParameterFilterAnd(fmt.Sprintf("ipSpaceRef.id==%s", ipSpace.IpSpace.ID), queryParameters) + typeResponses := []*types.IpSpaceOrgAssignment{{}} + err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParams, &typeResponses, nil) + if err != nil { + return nil, err + } + + // Wrap all typeResponses into IpSpaceOrgAssignment types with client + results := make([]*IpSpaceOrgAssignment, len(typeResponses)) + for sliceIndex := range typeResponses { + results[sliceIndex] = &IpSpaceOrgAssignment{ + IpSpaceOrgAssignment: typeResponses[sliceIndex], + IpSpaceId: ipSpace.IpSpace.ID, + vcdClient: ipSpace.vcdClient, + } + } + + return results, nil +} + +// GetOrgAssignmentById retrieves IP Space Org Assignment with a given ID +func (ipSpace *IpSpace) GetOrgAssignmentById(id string) (*IpSpaceOrgAssignment, error) { + if id == "" { + return nil, fmt.Errorf("IP Space Org Assignment lookup requires ID") + } + + client := ipSpace.vcdClient.Client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceOrgAssignments + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, id) + if err != nil { + return nil, err + } + + response := &IpSpaceOrgAssignment{ + IpSpaceOrgAssignment: &types.IpSpaceOrgAssignment{}, + IpSpaceId: ipSpace.IpSpace.ID, + vcdClient: ipSpace.vcdClient, + } + + err = client.OpenApiGetItem(apiVersion, urlRef, nil, response.IpSpaceOrgAssignment, nil) + if err != nil { + return nil, err + } + + return response, nil +} + +// GetOrgAssignmentById retrieves IP Space Org Assignment with a given Org Name +func (ipSpace *IpSpace) GetOrgAssignmentByOrgName(orgName string) (*IpSpaceOrgAssignment, error) { + if orgName == "" { + return nil, fmt.Errorf("name of Org is required") + } + queryParams := queryParameterFilterAnd(fmt.Sprintf("orgRef.name==%s", orgName), nil) + results, err := ipSpace.GetAllOrgAssignments(queryParams) + if err != nil { + return nil, fmt.Errorf("error retrieving IP Space Org Assignments by Org Name: %s", err) + } + + singleResult, err := oneOrError("Org Name", orgName, results) + if err != nil { + return nil, err + } + + return singleResult, nil +} + +// GetOrgAssignmentById retrieves IP Space Org Assignment with a given Org ID +func (ipSpace *IpSpace) GetOrgAssignmentByOrgId(orgId string) (*IpSpaceOrgAssignment, error) { + if orgId == "" { + return nil, fmt.Errorf("organization ID is required") + } + queryParams := queryParameterFilterAnd(fmt.Sprintf("orgRef.id==%s", orgId), nil) + results, err := ipSpace.GetAllOrgAssignments(queryParams) + if err != nil { + return nil, fmt.Errorf("error retrieving IP Space Org Assignments by Org ID: %s", err) + } + + singleResult, err := oneOrError("Org ID", orgId, results) + if err != nil { + return nil, err + } + + return singleResult, nil +} + +// Update Org Assignment +func (ipSpaceOrgAssignment *IpSpaceOrgAssignment) Update(ipSpaceOrgAssignmentConfig *types.IpSpaceOrgAssignment) (*IpSpaceOrgAssignment, error) { + client := ipSpaceOrgAssignment.vcdClient.Client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceOrgAssignments + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + ipSpaceOrgAssignmentConfig.ID = ipSpaceOrgAssignment.IpSpaceOrgAssignment.ID + urlRef, err := client.OpenApiBuildEndpoint(endpoint, ipSpaceOrgAssignmentConfig.ID) + if err != nil { + return nil, err + } + + result := &IpSpaceOrgAssignment{ + IpSpaceOrgAssignment: &types.IpSpaceOrgAssignment{}, + IpSpaceId: ipSpaceOrgAssignment.IpSpaceId, + vcdClient: ipSpaceOrgAssignment.vcdClient, + } + + err = client.OpenApiPutItem(apiVersion, urlRef, nil, ipSpaceOrgAssignmentConfig, result.IpSpaceOrgAssignment, nil) + if err != nil { + return nil, fmt.Errorf("error updating IP Space Org Assignment: %s", err) + } + + return result, nil +} diff --git a/govcd/ip_space_org_assignment_test.go b/govcd/ip_space_org_assignment_test.go new file mode 100644 index 000000000..7140644c5 --- /dev/null +++ b/govcd/ip_space_org_assignment_test.go @@ -0,0 +1,87 @@ +//go:build network || nsxt || functional || openapi || ALL + +package govcd + +import ( + "fmt" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_IpSpaceOrgAssignment(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointIpSpaceOrgAssignments) + + ipSpace := createIpSpace(vcd, check) + extNet := createExternalNetwork(vcd, check) + + // IP Space uplink (not directly referenced anywhere, but is required to make IP allocations) + ipSpaceUplink := createIpSpaceUplink(vcd, check, extNet.ExternalNetwork.ID, ipSpace.IpSpace.ID) + + // Check if any Org assignments are found before Edge Gateway creation - there should be none as + // Org assignments are implicitly created during Edge Gateway creation + allOrgAssignments, err := ipSpace.GetAllOrgAssignments(nil) + check.Assert(err, IsNil) + check.Assert(len(allOrgAssignments), Equals, 0) + + // Create NSX-T Edge Gateway + edgeGw := createNsxtEdgeGateway(vcd, check, extNet.ExternalNetwork.ID) + + // After the Edge Gateway is created - one can find an implicitly created IP Space Org Assignment + orgAssignmentByOrgName, err := ipSpace.GetOrgAssignmentByOrgName(vcd.org.Org.Name) + check.Assert(err, IsNil) + check.Assert(orgAssignmentByOrgName, NotNil) + + orgAssignmentByOrgId, err := ipSpace.GetOrgAssignmentByOrgId(vcd.org.Org.ID) + check.Assert(err, IsNil) + check.Assert(orgAssignmentByOrgId, NotNil) + + // Get Org Assignment by ID + orgAssignmentById, err := ipSpace.GetOrgAssignmentById(orgAssignmentByOrgId.IpSpaceOrgAssignment.ID) + check.Assert(err, IsNil) + check.Assert(orgAssignmentById.IpSpaceOrgAssignment, DeepEquals, orgAssignmentByOrgId.IpSpaceOrgAssignment) + + // Get All org Assignments and check that there is exactly one - matching other lookup methods + allOrgAssignments, err = ipSpace.GetAllOrgAssignments(nil) + check.Assert(err, IsNil) + check.Assert(len(allOrgAssignments), Equals, 1) + check.Assert(allOrgAssignments[0].IpSpaceOrgAssignment, DeepEquals, orgAssignmentByOrgId.IpSpaceOrgAssignment) + check.Assert(allOrgAssignments[0].IpSpaceOrgAssignment, DeepEquals, orgAssignmentByOrgName.IpSpaceOrgAssignment) + + // Update + orgAssignmentById.IpSpaceOrgAssignment.CustomQuotas = &types.IpSpaceOrgAssignmentQuotas{ + FloatingIPQuota: addrOf(10), + IPPrefixQuotas: []types.IpSpaceOrgAssignmentIPPrefixQuotas{ + { + PrefixLength: addrOf(31), + Quota: addrOf(11), + }, + { + PrefixLength: addrOf(30), + Quota: addrOf(12), + }, + }, + } + + updatedOrgAssignmentCustomQuota, err := orgAssignmentById.Update(orgAssignmentById.IpSpaceOrgAssignment) + check.Assert(err, IsNil) + check.Assert(updatedOrgAssignmentCustomQuota.IpSpaceOrgAssignment.CustomQuotas.FloatingIPQuota, DeepEquals, orgAssignmentById.IpSpaceOrgAssignment.CustomQuotas.FloatingIPQuota) + check.Assert(len(updatedOrgAssignmentCustomQuota.IpSpaceOrgAssignment.CustomQuotas.IPPrefixQuotas), DeepEquals, len(orgAssignmentById.IpSpaceOrgAssignment.CustomQuotas.IPPrefixQuotas)) + + // Cleanup + err = edgeGw.Delete() + check.Assert(err, IsNil) + + err = ipSpaceUplink.Delete() + check.Assert(err, IsNil) + + err = extNet.Delete() + check.Assert(err, IsNil) + + err = ipSpace.Delete() + check.Assert(err, IsNil) +} diff --git a/govcd/ip_space_test.go b/govcd/ip_space_test.go new file mode 100644 index 000000000..a2c536351 --- /dev/null +++ b/govcd/ip_space_test.go @@ -0,0 +1,240 @@ +//go:build network || nsxt || functional || openapi || ALL + +package govcd + +import ( + "fmt" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_IpSpacePublic(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointIpSpaces) + + ipSpaceConfig := &types.IpSpace{ + Name: check.TestName(), + IPSpaceInternalScope: []string{"22.0.0.0/24"}, + IPSpaceExternalScope: "200.0.0.1/24", + Type: types.IpSpacePublic, + RouteAdvertisementEnabled: false, + IPSpacePrefixes: []types.IPSpacePrefixes{ + { + DefaultQuotaForPrefixLength: -1, + IPPrefixSequence: []types.IPPrefixSequence{ + { + StartingPrefixIPAddress: "22.0.0.200", + PrefixLength: 31, + TotalPrefixCount: 3, + }, + }, + }, + { + DefaultQuotaForPrefixLength: 2, + IPPrefixSequence: []types.IPPrefixSequence{ + { + StartingPrefixIPAddress: "22.0.0.100", + PrefixLength: 30, + TotalPrefixCount: 3, + }, + }, + }, + }, + IPSpaceRanges: types.IPSpaceRanges{ + DefaultFloatingIPQuota: 3, + IPRanges: []types.IpSpaceRangeValues{ + { + StartIPAddress: "22.0.0.10", + EndIPAddress: "22.0.0.30", + }, + { + StartIPAddress: "22.0.0.32", + EndIPAddress: "22.0.0.34", + }, + }, + }, + } + + ipSpaceChecks(vcd, check, ipSpaceConfig) +} + +func (vcd *TestVCD) Test_IpSpaceShared(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointIpSpaces) + + ipSpaceConfig := &types.IpSpace{ + Name: check.TestName(), + IPSpaceInternalScope: []string{"22.0.0.1/24"}, + IPSpaceExternalScope: "200.0.0.1/24", + Type: types.IpSpaceShared, + RouteAdvertisementEnabled: false, + IPSpacePrefixes: []types.IPSpacePrefixes{ + { + DefaultQuotaForPrefixLength: -1, + IPPrefixSequence: []types.IPPrefixSequence{ + { + StartingPrefixIPAddress: "22.0.0.200", + PrefixLength: 31, + TotalPrefixCount: 3, + }, + }, + }, + { + DefaultQuotaForPrefixLength: 2, + IPPrefixSequence: []types.IPPrefixSequence{ + { + StartingPrefixIPAddress: "22.0.0.100", + PrefixLength: 30, + TotalPrefixCount: 3, + }, + }, + }, + }, + IPSpaceRanges: types.IPSpaceRanges{ + DefaultFloatingIPQuota: 3, + IPRanges: []types.IpSpaceRangeValues{ + { + StartIPAddress: "22.0.0.10", + EndIPAddress: "22.0.0.30", + }, + { + StartIPAddress: "22.0.0.32", + EndIPAddress: "22.0.0.34", + }, + }, + }, + } + ipSpaceChecks(vcd, check, ipSpaceConfig) +} + +func (vcd *TestVCD) Test_IpSpacePrivate(check *C) { + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointIpSpaces) + + ipSpaceConfig := &types.IpSpace{ + OrgRef: &types.OpenApiReference{ + ID: vcd.org.Org.ID, // Private IP Space requires Org + }, + Name: check.TestName(), + IPSpaceInternalScope: []string{"22.0.0.1/24"}, + IPSpaceExternalScope: "200.0.0.1/24", + Type: types.IpSpacePrivate, + RouteAdvertisementEnabled: false, + IPSpacePrefixes: []types.IPSpacePrefixes{ + { + DefaultQuotaForPrefixLength: -1, + IPPrefixSequence: []types.IPPrefixSequence{ + { + StartingPrefixIPAddress: "22.0.0.200", + PrefixLength: 31, + TotalPrefixCount: 3, + }, + }, + }, + { + DefaultQuotaForPrefixLength: 2, + IPPrefixSequence: []types.IPPrefixSequence{ + { + StartingPrefixIPAddress: "22.0.0.100", + PrefixLength: 30, + TotalPrefixCount: 3, + }, + }, + }, + }, + IPSpaceRanges: types.IPSpaceRanges{ + DefaultFloatingIPQuota: 3, + IPRanges: []types.IpSpaceRangeValues{ + { + StartIPAddress: "22.0.0.10", + EndIPAddress: "22.0.0.30", + }, + { + StartIPAddress: "22.0.0.32", + EndIPAddress: "22.0.0.34", + }, + }, + }, + } + + ipSpaceChecks(vcd, check, ipSpaceConfig) +} + +func ipSpaceChecks(vcd *TestVCD, check *C, ipSpaceConfig *types.IpSpace) { + createdIpSpace, err := vcd.client.CreateIpSpace(ipSpaceConfig) + check.Assert(err, IsNil) + check.Assert(createdIpSpace, NotNil) + + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaces + createdIpSpace.IpSpace.ID + AddToCleanupListOpenApi(createdIpSpace.IpSpace.Name, check.TestName(), openApiEndpoint) + + // Get by ID + byId, err := vcd.client.GetIpSpaceById(createdIpSpace.IpSpace.ID) + check.Assert(err, IsNil) + check.Assert(byId.IpSpace, DeepEquals, createdIpSpace.IpSpace) + + // Get by Name + byName, err := vcd.client.GetIpSpaceByName(createdIpSpace.IpSpace.Name) + check.Assert(err, IsNil) + check.Assert(byName.IpSpace, DeepEquals, createdIpSpace.IpSpace) + + // Get all and make sure it is found + allIpSpaces, err := vcd.client.GetAllIpSpaceSummaries(nil) + check.Assert(err, IsNil) + check.Assert(len(allIpSpaces) > 0, Equals, true) + var found bool + for i := range allIpSpaces { + if allIpSpaces[i].IpSpace.ID == byId.IpSpace.ID { + found = true + break + } + } + check.Assert(found, Equals, true) + + // If an Org is assigned - attempt to lookup by name and Org ID + if byId.IpSpace.OrgRef != nil && byId.IpSpace.OrgRef.ID != "" { + byNameAndOrgId, err := vcd.client.GetIpSpaceByNameAndOrgId(byId.IpSpace.Name, byId.IpSpace.OrgRef.ID) + check.Assert(err, IsNil) + check.Assert(byNameAndOrgId, NotNil) + check.Assert(byNameAndOrgId.IpSpace, DeepEquals, createdIpSpace.IpSpace) + + } + + // Check an update + ipSpaceConfig.RouteAdvertisementEnabled = true + ipSpaceConfig.IPSpaceInternalScope = append(ipSpaceConfig.IPSpaceInternalScope, "32.0.0.0/24") + + updatedIpSpace, err := createdIpSpace.Update(ipSpaceConfig) + check.Assert(err, IsNil) + check.Assert(updatedIpSpace, NotNil) + check.Assert(len(ipSpaceConfig.IPSpaceInternalScope), Equals, len(updatedIpSpace.IpSpace.IPSpaceInternalScope)) + + if vcd.client.Client.APIVCDMaxVersionIs(">= 38.0") { + fmt.Println("# Testing NAT and Firewall rule autocreation flags for VCD 10.5.0+") + ipSpaceConfig.Name = check.TestName() + "-GatewayServiceConfig" + ipSpaceConfig.DefaultGatewayServiceConfig = &types.IpSpaceDefaultGatewayServiceConfig{ + EnableDefaultFirewallRuleCreation: true, + EnableDefaultNoSnatRuleCreation: true, + EnableDefaultSnatRuleCreation: true, + } + + updatedIpSpace, err = updatedIpSpace.Update(ipSpaceConfig) + check.Assert(err, IsNil) + check.Assert(updatedIpSpace.IpSpace.DefaultGatewayServiceConfig, DeepEquals, ipSpaceConfig.DefaultGatewayServiceConfig) + } + + err = createdIpSpace.Delete() + check.Assert(err, IsNil) + + // Check that the entity is not found + notFoundById, err := vcd.client.GetIpSpaceById(byId.IpSpace.ID) + check.Assert(ContainsNotFound(err), Equals, true) + check.Assert(notFoundById, IsNil) +} diff --git a/govcd/ip_space_uplink.go b/govcd/ip_space_uplink.go new file mode 100644 index 000000000..5c5190e49 --- /dev/null +++ b/govcd/ip_space_uplink.go @@ -0,0 +1,109 @@ +/* + * 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" +) + +const labelIpSpaceUplink = "IP Space Uplink" + +// IpSpaceUplink provides the capability to assign one or more IP Spaces as Uplinks to External +// Networks +type IpSpaceUplink struct { + IpSpaceUplink *types.IpSpaceUplink + 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 (i IpSpaceUplink) wrap(inner *types.IpSpaceUplink) *IpSpaceUplink { + i.IpSpaceUplink = inner + return &i +} + +// CreateIpSpaceUplink creates an IP Space Uplink with a given configuration +func (vcdClient *VCDClient) CreateIpSpaceUplink(ipSpaceUplinkConfig *types.IpSpaceUplink) (*IpSpaceUplink, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceUplinks, + entityLabel: labelIpSpaceUplink, + } + + outerType := IpSpaceUplink{vcdClient: vcdClient} + return createOuterEntity(&vcdClient.Client, outerType, c, ipSpaceUplinkConfig) +} + +// GetAllIpSpaceUplinks retrieves all IP Space Uplinks for a given External Network ID +// +// externalNetworkId is mandatory +func (vcdClient *VCDClient) GetAllIpSpaceUplinks(externalNetworkId string, queryParameters url.Values) ([]*IpSpaceUplink, error) { + if externalNetworkId == "" { + return nil, fmt.Errorf("mandatory External Network ID is empty") + } + + queryparams := queryParameterFilterAnd(fmt.Sprintf("externalNetworkRef.id==%s", externalNetworkId), queryParameters) + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceUplinks, + entityLabel: labelIpSpaceUplink, + queryParameters: queryparams, + } + + outerType := IpSpaceUplink{vcdClient: vcdClient} + return getAllOuterEntities[IpSpaceUplink, types.IpSpaceUplink](&vcdClient.Client, outerType, c) +} + +// GetIpSpaceUplinkByName retrieves a single IP Space Uplink by Name in a given External Network +func (vcdClient *VCDClient) GetIpSpaceUplinkByName(externalNetworkId, name string) (*IpSpaceUplink, error) { + queryParams := queryParameterFilterAnd(fmt.Sprintf("name==%s", name), nil) + allIpSpaceUplinks, err := vcdClient.GetAllIpSpaceUplinks(externalNetworkId, queryParams) + if err != nil { + return nil, fmt.Errorf("error getting IP Space Uplink by Name '%s':%s", name, err) + } + + return oneOrError("name", name, allIpSpaceUplinks) +} + +// GetIpSpaceUplinkById retrieves IP Space Uplink with a given ID +func (vcdClient *VCDClient) GetIpSpaceUplinkById(id string) (*IpSpaceUplink, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceUplinks, + endpointParams: []string{id}, + entityLabel: labelIpSpaceUplink, + } + + outerType := IpSpaceUplink{vcdClient: vcdClient} + return getOuterEntity[IpSpaceUplink, types.IpSpaceUplink](&vcdClient.Client, outerType, c) +} + +// Update IP Space Uplink +func (ipSpaceUplink *IpSpaceUplink) Update(ipSpaceUplinkConfig *types.IpSpaceUplink) (*IpSpaceUplink, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceUplinks, + endpointParams: []string{ipSpaceUplink.IpSpaceUplink.ID}, + entityLabel: labelIpSpaceUplink, + } + + outerType := IpSpaceUplink{vcdClient: ipSpaceUplink.vcdClient} + return updateOuterEntity(&ipSpaceUplink.vcdClient.Client, outerType, c, ipSpaceUplinkConfig) +} + +// Delete IP Space Uplink +func (ipSpaceUplink *IpSpaceUplink) Delete() error { + if ipSpaceUplink == nil || ipSpaceUplink.IpSpaceUplink == nil || ipSpaceUplink.IpSpaceUplink.ID == "" { + return fmt.Errorf("IP Space Uplink must have ID") + } + + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceUplinks, + endpointParams: []string{ipSpaceUplink.IpSpaceUplink.ID}, + entityLabel: labelIpSpaceUplink, + } + + return deleteEntityById(&ipSpaceUplink.vcdClient.Client, c) +} diff --git a/govcd/ip_space_uplink_test.go b/govcd/ip_space_uplink_test.go new file mode 100644 index 000000000..75e2f9b1f --- /dev/null +++ b/govcd/ip_space_uplink_test.go @@ -0,0 +1,186 @@ +//go:build network || nsxt || functional || openapi || ALL + +package govcd + +import ( + "fmt" + "time" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_IpSpaceUplink(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointIpSpaceUplinks) + + // Create External Network (Provider Gateway) + extNet := createExternalNetwork(vcd, check) + + // Create IP Space + ipSpace := createIpSpace(vcd, check) + + // Create Uplink configuration + uplinkConfig := &types.IpSpaceUplink{ + Name: check.TestName(), + Description: "IP SPace Uplink for External Network (Provider Gateway)", + ExternalNetworkRef: &types.OpenApiReference{ID: extNet.ExternalNetwork.ID}, + IPSpaceRef: &types.OpenApiReference{ID: ipSpace.IpSpace.ID}, + } + + createdIpSpaceUplink, err := vcd.client.CreateIpSpaceUplink(uplinkConfig) + check.Assert(err, IsNil) + check.Assert(createdIpSpaceUplink, NotNil) + + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceUplinks + createdIpSpaceUplink.IpSpaceUplink.ID + AddToCleanupListOpenApi(createdIpSpaceUplink.IpSpaceUplink.Name, check.TestName(), openApiEndpoint) + + // Operations on IP Space related entities trigger a separate task + // 'ipSpaceUplinkRouteAdvertisementSync' which is better to finish before any other operations + // as it might cause an error: busy completing an operation IP_SPACE_UPLINK_ROUTE_ADVERTISEMENT_SYNC + // Sleeping a few seconds because the task is not immediately seen sometimes. + time.Sleep(3 * time.Second) + err = vcd.client.Client.WaitForRouteAdvertisementTasks() + check.Assert(err, IsNil) + + // Get all IP Space Uplinks + allIpSpaceUplinks, err := vcd.client.GetAllIpSpaceUplinks(extNet.ExternalNetwork.ID, nil) + check.Assert(err, IsNil) + check.Assert(len(allIpSpaceUplinks) > 0, Equals, true) + + // Get by ID + byId, err := vcd.client.GetIpSpaceUplinkById(createdIpSpaceUplink.IpSpaceUplink.ID) + check.Assert(err, IsNil) + check.Assert(byId, NotNil) + check.Assert(byId.IpSpaceUplink, DeepEquals, createdIpSpaceUplink.IpSpaceUplink) + + // Get by Name + byName, err := vcd.client.GetIpSpaceUplinkByName(extNet.ExternalNetwork.ID, byId.IpSpaceUplink.Name) + check.Assert(err, IsNil) + check.Assert(byName, NotNil) + check.Assert(byName.IpSpaceUplink, DeepEquals, byId.IpSpaceUplink) + + // Update + uplinkConfig.Name = check.TestName() + "updated" + uplinkConfig.Description = uplinkConfig.Description + "updated" + updatedUplinkConfig, err := createdIpSpaceUplink.Update(uplinkConfig) + check.Assert(err, IsNil) + check.Assert(updatedUplinkConfig.IpSpaceUplink.ID, Equals, byId.IpSpaceUplink.ID) + check.Assert(updatedUplinkConfig.IpSpaceUplink.ID, Equals, createdIpSpaceUplink.IpSpaceUplink.ID) + check.Assert(updatedUplinkConfig.IpSpaceUplink.Name, Equals, uplinkConfig.Name) + check.Assert(updatedUplinkConfig.IpSpaceUplink.Description, Equals, uplinkConfig.Description) + check.Assert(updatedUplinkConfig.IpSpaceUplink.ExternalNetworkRef.ID, Equals, createdIpSpaceUplink.IpSpaceUplink.ExternalNetworkRef.ID) + check.Assert(updatedUplinkConfig.IpSpaceUplink.IPSpaceRef.ID, Equals, createdIpSpaceUplink.IpSpaceUplink.IPSpaceRef.ID) + + // Read-only variables + check.Assert(updatedUplinkConfig.IpSpaceUplink.IPSpaceType, Equals, types.IpSpacePublic) + check.Assert(updatedUplinkConfig.IpSpaceUplink.Status, Equals, "REALIZED") + + time.Sleep(3 * time.Second) + err = vcd.client.Client.WaitForRouteAdvertisementTasks() + check.Assert(err, IsNil) + + err = createdIpSpaceUplink.Delete() + check.Assert(err, IsNil) + + // Check that IP Space Uplink was deleted + _, err = vcd.client.GetIpSpaceUplinkById(updatedUplinkConfig.IpSpaceUplink.ID) + check.Assert(ContainsNotFound(err), Equals, true) + + err = extNet.Delete() + check.Assert(err, IsNil) + + err = ipSpace.Delete() + check.Assert(err, IsNil) +} + +func createIpSpace(vcd *TestVCD, check *C) *IpSpace { + ipSpaceConfig := &types.IpSpace{ + Name: check.TestName(), + IPSpaceInternalScope: []string{"22.0.0.0/24"}, + IPSpaceExternalScope: "200.0.0.1/24", + Type: types.IpSpacePublic, + RouteAdvertisementEnabled: false, + IPSpacePrefixes: []types.IPSpacePrefixes{ + { + DefaultQuotaForPrefixLength: -1, + IPPrefixSequence: []types.IPPrefixSequence{ + { + StartingPrefixIPAddress: "22.0.0.200", + PrefixLength: 31, + TotalPrefixCount: 3, + }, + }, + }, + { + DefaultQuotaForPrefixLength: 2, + IPPrefixSequence: []types.IPPrefixSequence{ + { + StartingPrefixIPAddress: "22.0.0.100", + PrefixLength: 30, + TotalPrefixCount: 3, + }, + }, + }, + }, + IPSpaceRanges: types.IPSpaceRanges{ + DefaultFloatingIPQuota: 3, + IPRanges: []types.IpSpaceRangeValues{ + { + StartIPAddress: "22.0.0.10", + EndIPAddress: "22.0.0.30", + }, + { + StartIPAddress: "22.0.0.32", + EndIPAddress: "22.0.0.34", + }, + }, + }, + } + + createdIpSpace, err := vcd.client.CreateIpSpace(ipSpaceConfig) + check.Assert(err, IsNil) + check.Assert(createdIpSpace, NotNil) + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaces + createdIpSpace.IpSpace.ID + AddToCleanupListOpenApi(createdIpSpace.IpSpace.Name, check.TestName(), openApiEndpoint) + + return createdIpSpace +} + +func createExternalNetwork(vcd *TestVCD, check *C) *ExternalNetworkV2 { + // NSX-T details + man, err := vcd.client.QueryNsxtManagerByName(vcd.config.VCD.Nsxt.Manager) + check.Assert(err, IsNil) + nsxtManagerId, err := BuildUrnWithUuid("urn:vcloud:nsxtmanager:", extractUuid(man[0].HREF)) + check.Assert(err, IsNil) + + backingId := getBackingIdByNameAndType(check, vcd.config.VCD.Nsxt.Tier0router, types.ExternalNetworkBackingTypeNsxtTier0Router, vcd, nsxtManagerId) + + net := &types.ExternalNetworkV2{ + Name: check.TestName(), + Description: "", + NetworkBackings: types.ExternalNetworkV2Backings{Values: []types.ExternalNetworkV2Backing{ + { + BackingID: backingId, + NetworkProvider: types.NetworkProvider{ + ID: nsxtManagerId, + }, + BackingTypeValue: types.ExternalNetworkBackingTypeNsxtTier0Router, + }, + }}, + UsingIpSpace: addrOf(true), + } + + createdNet, err := CreateExternalNetworkV2(vcd.client, net) + check.Assert(err, IsNil) + check.Assert(createdNet, NotNil) + + // Use generic "OpenApiEntity" resource cleanup type + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointExternalNetworks + createdNet.ExternalNetwork.ID + AddToCleanupListOpenApi(createdNet.ExternalNetwork.Name, check.TestName(), openApiEndpoint) + + return createdNet +} diff --git a/govcd/lb_test.go b/govcd/lb_test.go index 5629b3e97..f1f4791ad 100644 --- a/govcd/lb_test.go +++ b/govcd/lb_test.go @@ -1,5 +1,4 @@ -// +build lb functional integration ALL -// +build !skipLong +//go:build (lb || functional || integration || ALL) && !skipLong /* * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -9,7 +8,7 @@ package govcd import ( "fmt" - "io/ioutil" + "io" "net/http" "strings" "time" @@ -202,8 +201,11 @@ func checkLb(queryUrl string, expectedResponses []string, maxRetryTimeout int) e if err == nil { fmt.Printf(".") // progress bar when waiting for responses from all nodes - body, _ := ioutil.ReadAll(resp.Body) - resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + err = resp.Body.Close() + if err != nil { + return err + } // check if the element is in the list for index, value := range expectedResponses { if value == string(body) { diff --git a/govcd/lb_unit_test.go b/govcd/lb_unit_test.go index 88cf72aff..e284b6c93 100644 --- a/govcd/lb_unit_test.go +++ b/govcd/lb_unit_test.go @@ -1,4 +1,4 @@ -// +build unit lb lbAppProfile functional ALL +//go:build unit || lb || lbAppProfile || ALL /* * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. diff --git a/govcd/lbappprofile_test.go b/govcd/lbappprofile_test.go index ee881209b..829162748 100644 --- a/govcd/lbappprofile_test.go +++ b/govcd/lbappprofile_test.go @@ -1,4 +1,4 @@ -// +build lb lbAppProfile nsxv functional ALL +//go:build lb || lbAppProfile || nsxv || functional || ALL /* * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. diff --git a/govcd/lbapprule_test.go b/govcd/lbapprule_test.go index e62cbe6c8..6a7fc8977 100644 --- a/govcd/lbapprule_test.go +++ b/govcd/lbapprule_test.go @@ -1,4 +1,4 @@ -// +build lb lbAppRule nsxv functional ALL +//go:build lb || lbAppRule || nsxv || functional || ALL /* * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. diff --git a/govcd/lbserverpool_test.go b/govcd/lbserverpool_test.go index 9bb48f03a..127d55943 100644 --- a/govcd/lbserverpool_test.go +++ b/govcd/lbserverpool_test.go @@ -1,4 +1,4 @@ -// +build lb lbServerPool nsxv functional ALL +//go:build lb || lbServerPool || nsxv || functional || ALL /* * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. diff --git a/govcd/lbservicemonitor_test.go b/govcd/lbservicemonitor_test.go index 5824a187a..67b4174b6 100644 --- a/govcd/lbservicemonitor_test.go +++ b/govcd/lbservicemonitor_test.go @@ -1,4 +1,4 @@ -// +build lb lbServiceMonitor nsxv functional ALL +//go:build lb || lbServiceMonitor || nsxv || functional || ALL /* * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. diff --git a/govcd/lbvirtualserver_test.go b/govcd/lbvirtualserver_test.go index ec5ed765e..eb358c214 100644 --- a/govcd/lbvirtualserver_test.go +++ b/govcd/lbvirtualserver_test.go @@ -1,4 +1,4 @@ -// +build lb lbVirtualServer nsxv functional ALL +//go:build lb || lbVirtualServer || nsxv || functional || ALL /* * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. diff --git a/govcd/media.go b/govcd/media.go index 549dbd089..3be745437 100644 --- a/govcd/media.go +++ b/govcd/media.go @@ -12,6 +12,7 @@ import ( "net/http" "net/url" "os" + "path/filepath" "strconv" "strings" "time" @@ -132,7 +133,17 @@ func executeUpload(client *Client, media *types.Media, mediaFilePath, mediaName uploadError: &uploadError, } - go uploadFile(client, mediaFilePath, details) + // sending upload process to background, this allows not to lock and return task to client + // The error should be captured in details.uploadError, but just in case, we add a logging for the + // main error + go func() { + _, err = uploadFile(client, mediaFilePath, details) + if err != nil { + util.Logger.Println(strings.Repeat("*", 80)) + util.Logger.Printf("*** [DEBUG - executeUpload] error calling uploadFile: %s\n", err) + util.Logger.Println(strings.Repeat("*", 80)) + } + }() var task Task for _, item := range media.Tasks.Task { @@ -173,7 +184,12 @@ func createMedia(client *Client, link, mediaName, mediaDescription string, fileS if err != nil { return nil, err } - defer response.Body.Close() + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + util.Logger.Printf("error closing response Body [createMedia]: %s", err) + } + }(response.Body) mediaForUpload := &types.Media{} if err = decodeBody(types.BodyTypeXML, response, mediaForUpload); err != nil { @@ -251,12 +267,11 @@ func queryMedia(client *Client, mediaUrl string, newItemName string) (*types.Med // Verifies provided file header matches standard func verifyIso(filePath string) (bool, error) { - // #nosec G304 - linter does not like 'filePath' to be a variable. However this is necessary for file uploads. - file, err := os.Open(filePath) + file, err := os.Open(filepath.Clean(filePath)) if err != nil { return false, err } - defer file.Close() + defer safeClose(file) return readHeader(file) } @@ -274,21 +289,32 @@ func readHeader(reader io.Reader) (bool, error) { if headerOk { return true, nil } else { - return false, errors.New("file header didn't match ISO standard") + return false, errors.New("file header didn't match ISO or UDF standard") } } -// Verify file header info: https://www.garykessler.net/library/file_sigs.html +// Verify file header for ISO or UDF type. Info: https://www.garykessler.net/library/file_sigs.html func verifyHeader(buf []byte) bool { - // search for CD001(43 44 30 30 31) in specific file places. - //This signature usually occurs at byte offset 32769 (0x8001), - //34817 (0x8801), or 36865 (0x9001). + // ISO verification - search for CD001(43 44 30 30 31) in specific file places. + // This signature usually occurs at byte offset 32769 (0x8001), + // 34817 (0x8801), or 36865 (0x9001). + // UDF verification - search for BEA01(42 45 41 30 31) in specific file places. + // This signature usually occurs at byte offset 32769 (0x8001), + // 34817 (0x8801), or 36865 (0x9001). + return (buf[32769] == 0x43 && buf[32770] == 0x44 && buf[32771] == 0x30 && buf[32772] == 0x30 && buf[32773] == 0x31) || (buf[34817] == 0x43 && buf[34818] == 0x44 && buf[34819] == 0x30 && buf[34820] == 0x30 && buf[34821] == 0x31) || (buf[36865] == 0x43 && buf[36866] == 0x44 && - buf[36867] == 0x30 && buf[36868] == 0x30 && buf[36869] == 0x31) + buf[36867] == 0x30 && buf[36868] == 0x30 && buf[36869] == 0x31) || + (buf[32769] == 0x42 && buf[32770] == 0x45 && + buf[32771] == 0x41 && buf[32772] == 0x30 && buf[32773] == 0x31) || + (buf[34817] == 0x42 && buf[34818] == 0x45 && + buf[34819] == 41 && buf[34820] == 0x30 && buf[34821] == 0x31) || + (buf[36865] == 42 && buf[36866] == 45 && + buf[36867] == 41 && buf[36868] == 0x30 && buf[36869] == 0x31) + } // Reference for API usage http://pubs.vmware.com/vcloud-api-1-5/wwhelp/wwhimpl/js/html/wwhelp.htm#href=api_prog/GUID-9356B99B-E414-474A-853C-1411692AF84C.html @@ -526,6 +552,7 @@ func (cat *Catalog) GetMediaByNameOrId(identifier string, refresh bool) (*Media, func (adminCatalog *AdminCatalog) GetMediaByHref(mediaHref string) (*Media, error) { catalog := NewCatalog(adminCatalog.client) catalog.Catalog = &adminCatalog.AdminCatalog.Catalog + catalog.parent = adminCatalog.parent return catalog.GetMediaByHref(mediaHref) } @@ -535,6 +562,7 @@ func (adminCatalog *AdminCatalog) GetMediaByHref(mediaHref string) (*Media, erro func (adminCatalog *AdminCatalog) GetMediaByName(mediaName string, refresh bool) (*Media, error) { catalog := NewCatalog(adminCatalog.client) catalog.Catalog = &adminCatalog.AdminCatalog.Catalog + catalog.parent = adminCatalog.parent return catalog.GetMediaByName(mediaName, refresh) } @@ -544,6 +572,7 @@ func (adminCatalog *AdminCatalog) GetMediaByName(mediaName string, refresh bool) func (adminCatalog *AdminCatalog) GetMediaById(mediaId string) (*Media, error) { catalog := NewCatalog(adminCatalog.client) catalog.Catalog = &adminCatalog.AdminCatalog.Catalog + catalog.parent = adminCatalog.parent return catalog.GetMediaById(mediaId) } @@ -553,6 +582,7 @@ func (adminCatalog *AdminCatalog) GetMediaById(mediaId string) (*Media, error) { func (adminCatalog *AdminCatalog) GetMediaByNameOrId(identifier string, refresh bool) (*Media, error) { catalog := NewCatalog(adminCatalog.client) catalog.Catalog = &adminCatalog.AdminCatalog.Catalog + catalog.parent = adminCatalog.parent return catalog.GetMediaByNameOrId(identifier, refresh) } @@ -606,9 +636,46 @@ func (catalog *Catalog) QueryMedia(mediaName string) (*MediaRecord, error) { func (adminCatalog *AdminCatalog) QueryMedia(mediaName string) (*MediaRecord, error) { catalog := NewCatalog(adminCatalog.client) catalog.Catalog = &adminCatalog.AdminCatalog.Catalog + catalog.parent = adminCatalog.parent return catalog.QueryMedia(mediaName) } +// QueryMediaById returns a MediaRecord associated to the given media item URN. Returns ErrorEntityNotFound +// if it is not found, or an error if there's more than one result. +func (vcdClient *VCDClient) QueryMediaById(mediaId string) (*MediaRecord, error) { + if mediaId == "" { + return nil, fmt.Errorf("media ID is empty") + } + + filterType := types.QtMedia + if vcdClient.Client.IsSysAdmin { + filterType = types.QtAdminMedia + } + results, err := vcdClient.Client.QueryWithNotEncodedParams(nil, map[string]string{ + "type": filterType, + "filter": fmt.Sprintf("id==%s", url.QueryEscape(mediaId)), + "filterEncoded": "true"}) + if err != nil { + return nil, fmt.Errorf("error querying medias %s", err) + } + newMediaRecord := NewMediaRecord(&vcdClient.Client) + + mediaResults := results.Results.MediaRecord + if vcdClient.Client.IsSysAdmin { + mediaResults = results.Results.AdminMediaRecord + } + + if len(mediaResults) == 0 { + return nil, ErrorEntityNotFound + } + if len(mediaResults) > 1 { + return nil, fmt.Errorf("found %#v results with media ID %s", len(mediaResults), mediaId) + } + + newMediaRecord.MediaRecord = mediaResults[0] + return newMediaRecord, nil +} + // Refresh refreshes the media information by href func (mediaRecord *MediaRecord) Refresh() error { @@ -680,3 +747,99 @@ func (vdc *Vdc) QueryAllMedia(mediaName string) ([]*MediaRecord, error) { util.Logger.Printf("[TRACE] Found media records by name: %#v \n", mediaResults) return newMediaRecords, nil } + +// enableDownload prepares a media item for download and returns a download link +// Note: depending on the size of the item, it may take a long time. +func (media *Media) enableDownload() (string, error) { + downloadUrl := getUrlFromLink(media.Media.Link, "enable", "") + if downloadUrl == "" { + return "", fmt.Errorf("no enable URL found") + } + // The result of this operation is the creation of an entry in the 'Files' field of the media structure + // Inside that field, there will be a Link entry with the URL for the download + // e.g. + // + // + // + // + // + task, err := media.client.executeTaskRequest( + downloadUrl, + http.MethodPost, + types.MimeTask, + "error enabling download: %s", + nil, + media.client.APIVersion) + if err != nil { + return "", err + } + err = task.WaitTaskCompletion() + if err != nil { + return "", err + } + + err = media.Refresh() + if err != nil { + return "", err + } + + if media.Media.Files == nil || len(media.Media.Files.File) == 0 { + return "", fmt.Errorf("no downloadable file info found") + } + downloadHref := "" + for _, f := range media.Media.Files.File { + for _, l := range f.Link { + if l.Rel == "download:default" { + downloadHref = l.HREF + break + } + if downloadHref != "" { + break + } + } + } + + if downloadHref == "" { + return "", fmt.Errorf("no download URL found") + } + + return downloadHref, nil +} + +// Download gets the contents of a media item as a byte stream +// NOTE: the whole item will be saved in local memory. Do not attempt this operation for very large items +func (media *Media) Download() ([]byte, error) { + + downloadHref, err := media.enableDownload() + if err != nil { + return nil, err + } + + downloadUrl, err := url.ParseRequestURI(downloadHref) + if err != nil { + return nil, fmt.Errorf("error getting download URL: %s", err) + } + + request := media.client.NewRequest(map[string]string{}, http.MethodGet, *downloadUrl, nil) + resp, err := media.client.Http.Do(request) + if err != nil { + return nil, fmt.Errorf("error getting media download: %s", err) + } + + if !isSuccessStatus(resp.StatusCode) { + return nil, fmt.Errorf("error downloading media: %s", resp.Status) + } + body, err := io.ReadAll(resp.Body) + + defer func() { + err = resp.Body.Close() + if err != nil { + panic(fmt.Sprintf("error closing body: %s", err)) + } + }() + + if err != nil { + return nil, err + } + return body, nil +} diff --git a/govcd/media_test.go b/govcd/media_test.go index ecdd46ba5..4414b6e71 100644 --- a/govcd/media_test.go +++ b/govcd/media_test.go @@ -1,4 +1,4 @@ -// +build catalog functional ALL +//go:build catalog || functional || ALL /* * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -8,8 +8,10 @@ package govcd import ( "fmt" - . "gopkg.in/check.v1" + "os" + "path" + "runtime" ) // Tests System function Delete by creating media item and @@ -40,13 +42,80 @@ func (vcd *TestVCD) Test_DeleteMedia(check *C) { err = uploadTask.WaitTaskCompletion() check.Assert(err, IsNil) - AddToCleanupList(itemName, "mediaCatalogImage", vcd.org.Org.Name+"|"+vcd.config.VCD.Catalog.Name, "Test_DeleteMediaImage") + AddToCleanupList(itemName, "mediaCatalogImage", vcd.org.Org.Name+"|"+vcd.config.VCD.Catalog.Name, check.TestName()) + + media, err := catalog.GetMediaByName(itemName, true) + check.Assert(err, IsNil) + check.Assert(media, NotNil) + check.Assert(media.Media.Name, Equals, itemName) + + task, err := media.Delete() + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) + + _, err = catalog.GetMediaByName(itemName, true) + check.Assert(err, NotNil) + check.Assert(IsNotFound(err), Equals, true) +} + +func (vcd *TestVCD) Test_UploadAnyMediaFile(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + + if vcd.config.VCD.Org == "" { + check.Skip("Test_UploadAnyMediaFile: Org name not given") + return + } + if vcd.config.VCD.Catalog.Name == "" { + check.Skip("Test_UploadAnyMediaFile: Catalog name not given") + return + } + org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(org, NotNil) + + catalog, err := org.GetCatalogByName(vcd.config.VCD.Catalog.Name, false) + check.Assert(err, IsNil) + check.Assert(catalog, NotNil) + + _, sourceFile, _, _ := runtime.Caller(0) + sourceFile = path.Clean(sourceFile) + itemName := check.TestName() + itemPath := sourceFile + + // Upload the source file of the current test as a media item + uploadTask, err := catalog.UploadMediaFile(itemName, "Text file uploaded from test", itemPath, 1024, false) + check.Assert(err, IsNil) + err = uploadTask.WaitTaskCompletion() + check.Assert(err, IsNil) + + AddToCleanupList(itemName, "mediaCatalogImage", vcd.org.Org.Name+"|"+vcd.config.VCD.Catalog.Name, check.TestName()) + // Retrieve the media item media, err := catalog.GetMediaByName(itemName, true) check.Assert(err, IsNil) check.Assert(media, NotNil) check.Assert(media.Media.Name, Equals, itemName) + // Repeat the download a few times. Make sure that a repeated download works as well as the first one + for i := 0; i < 2; i++ { + // Download the media item from VCD as a byte slice + contents, err := media.Download() + check.Assert(err, IsNil) + check.Assert(len(contents), Not(Equals), 0) + check.Assert(media.Media.Files, NotNil) + check.Assert(media.Media.Files.File, NotNil) + check.Assert(media.Media.Files.File[0].Name, Not(Equals), "") + check.Assert(len(media.Media.Files.File[0].Link), Not(Equals), 0) + + // Read the source file from disk + fromFile, err := os.ReadFile(path.Clean(sourceFile)) + check.Assert(err, IsNil) + // Make sure that what we downloaded from VCD corresponds to the file contents. + check.Assert(len(fromFile), Equals, len(contents)) + check.Assert(fromFile, DeepEquals, contents) + } + task, err := media.Delete() check.Assert(err, IsNil) err = task.WaitTaskCompletion() diff --git a/govcd/metadata.go b/govcd/metadata.go index 7c7013c04..d3f1bf361 100644 --- a/govcd/metadata.go +++ b/govcd/metadata.go @@ -1,5 +1,5 @@ /* - * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + * Copyright 2022 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. */ package govcd @@ -7,142 +7,1028 @@ package govcd import ( "fmt" "net/http" - "net/url" "strings" "github.com/vmware/go-vcloud-director/v2/types/v56" ) -// GetMetadata calls private function getMetadata() with vm.client and vm.VM.HREF -// which returns a *types.Metadata struct for provided VM input. -func (vm *VM) GetMetadata() (*types.Metadata, error) { - return getMetadata(vm.client, vm.VM.HREF) +// All functions here should not be used as they are deprecated in favor of those present in "metadata_v2". +// Remove this file once go-vcloud-director v3.0 is released. + +// AddMetadataEntryByHref adds metadata typedValue and key/value pair provided as input to the given resource reference, +// then waits for the task to finish. +// Deprecated: Use VCDClient.AddMetadataEntryWithVisibilityByHref instead +func (vcdClient *VCDClient) AddMetadataEntryByHref(href, typedValue, key, value string) error { + task, err := vcdClient.AddMetadataEntryByHrefAsync(href, typedValue, key, value) + if err != nil { + return err + } + return task.WaitTaskCompletion() +} + +// AddMetadataEntryByHrefAsync adds metadata typedValue and key/value pair provided as input to the given resource reference +// and returns the task. +// Deprecated: Use VCDClient.AddMetadataEntryWithVisibilityByHrefAsync instead. +func (vcdClient *VCDClient) AddMetadataEntryByHrefAsync(href, typedValue, key, value string) (Task, error) { + return addMetadataDeprecated(&vcdClient.Client, typedValue, key, value, href) +} + +// MergeMetadataByHrefAsync merges metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// and returns the task. +// Deprecated: Use VCDClient.MergeMetadataWithVisibilityByHrefAsync instead. +func (vcdClient *VCDClient) MergeMetadataByHrefAsync(href, typedValue string, metadata map[string]interface{}) (Task, error) { + return mergeAllMetadataDeprecated(&vcdClient.Client, typedValue, metadata, href) +} + +// MergeMetadataByHref merges metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// then waits for the task to complete. +func (vcdClient *VCDClient) MergeMetadataByHref(href, typedValue string, metadata map[string]interface{}) error { + task, err := vcdClient.MergeMetadataByHrefAsync(href, typedValue, metadata) + if err != nil { + return err + } + return task.WaitTaskCompletion() +} + +// DeleteMetadataEntryByHref deletes metadata from the given resource reference, depending on key provided as input +// and waits for the task to finish. +// Deprecated: Use VCDClient.DeleteMetadataEntryWithDomainByHref +func (vcdClient *VCDClient) DeleteMetadataEntryByHref(href, key string) error { + task, err := vcdClient.DeleteMetadataEntryByHrefAsync(href, key) + if err != nil { + return err + } + + return task.WaitTaskCompletion() +} + +// DeleteMetadataEntryByHrefAsync deletes metadata from the given resource reference, depending on key provided as input +// and returns a task. +// Deprecated: Use VCDClient.DeleteMetadataEntryWithDomainByHrefAsync +func (vcdClient *VCDClient) DeleteMetadataEntryByHrefAsync(href, key string) (Task, error) { + return deleteMetadata(&vcdClient.Client, href, "", key, false) +} + +// AddMetadataEntry adds VM metadata typedValue and key/value pair provided as input +// and waits for the task to finish. +// Deprecated: Use VM.AddMetadataEntryWithVisibility instead +func (vm *VM) AddMetadataEntry(typedValue, key, value string) error { + task, err := vm.AddMetadataEntryAsync(typedValue, key, value) + if err != nil { + return err + } + + err = task.WaitTaskCompletion() + if err != nil { + return err + } + + err = vm.Refresh() + if err != nil { + return err + } + + return nil +} + +// AddMetadataEntryAsync adds VM metadata typedValue and key/value pair provided as input +// and returns the task. +// Deprecated: Use VM.AddMetadataEntryWithVisibilityAsync instead +func (vm *VM) AddMetadataEntryAsync(typedValue, key, value string) (Task, error) { + return addMetadataDeprecated(vm.client, typedValue, key, value, vm.VM.HREF) +} + +// MergeMetadataAsync merges VM metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// then returns the task. +// Deprecated: Use VM.MergeMetadataWithMetadataValuesAsync instead +func (vm *VM) MergeMetadataAsync(typedValue string, metadata map[string]interface{}) (Task, error) { + return mergeAllMetadataDeprecated(vm.client, typedValue, metadata, vm.VM.HREF) +} + +// MergeMetadata merges VM metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// then waits for the task to complete. +// Deprecated: Use VM.MergeMetadataWithMetadataValues +func (vm *VM) MergeMetadata(typedValue string, metadata map[string]interface{}) error { + task, err := vm.MergeMetadataAsync(typedValue, metadata) + if err != nil { + return err + } + return task.WaitTaskCompletion() +} + +// DeleteMetadataEntry deletes VM metadata by key provided as input and waits for the task to finish. +// Deprecated: Use VM.DeleteMetadataEntryWithDomain instead +func (vm *VM) DeleteMetadataEntry(key string) error { + task, err := vm.DeleteMetadataEntryAsync(key) + if err != nil { + return err + } + + err = task.WaitTaskCompletion() + if err != nil { + return err + } + + err = vm.Refresh() + if err != nil { + return err + } + + return nil +} + +// DeleteMetadataEntryAsync deletes VM metadata depending on key provided as input +// and returns the task. +// Deprecated: Use VM.DeleteMetadataEntryWithDomainAsync instead +func (vm *VM) DeleteMetadataEntryAsync(key string) (Task, error) { + return deleteMetadata(vm.client, vm.VM.HREF, vm.VM.Name, key, false) +} + +// AddMetadataEntry adds VDC metadata typedValue and key/value pair provided as input +// and waits for the task to finish. +// Note: Requires system administrator privileges. +// Deprecated: Use AdminVdc.AddMetadataEntryWithVisibility instead +func (vdc *Vdc) AddMetadataEntry(typedValue, key, value string) error { + task, err := vdc.AddMetadataEntryAsync(typedValue, key, value) + if err != nil { + return err + } + + err = task.WaitTaskCompletion() + if err != nil { + return err + } + + err = vdc.Refresh() + if err != nil { + return err + } + + return nil +} + +// AddMetadataEntry adds VDC metadata typedValue and key/value pair provided as input +// and waits for the task to finish. +// Deprecated: Use AdminVdc.AddMetadataEntryWithVisibility instead +func (adminVdc *AdminVdc) AddMetadataEntry(typedValue, key, value string) error { + task, err := adminVdc.AddMetadataEntryAsync(typedValue, key, value) + if err != nil { + return err + } + + return task.WaitTaskCompletion() +} + +// AddMetadataEntryAsync adds VDC metadata typedValue and key/value pair provided as input and returns the task. +// Note: Requires system administrator privileges. +// Deprecated: Use AdminVdc.AddMetadataEntryWithVisibilityAsync instead +func (vdc *Vdc) AddMetadataEntryAsync(typedValue, key, value string) (Task, error) { + return addMetadataDeprecated(vdc.client, typedValue, key, value, getAdminURL(vdc.Vdc.HREF)) +} + +// AddMetadataEntryAsync adds AdminVdc metadata typedValue and key/value pair provided as input and returns the task. +// Deprecated: Use AdminVdc.AddMetadataEntryWithVisibilityAsync instead +func (adminVdc *AdminVdc) AddMetadataEntryAsync(typedValue, key, value string) (Task, error) { + return addMetadataDeprecated(adminVdc.client, typedValue, key, value, adminVdc.AdminVdc.HREF) +} + +// MergeMetadataAsync merges VDC metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// then waits for the task to complete. +// Note: Requires system administrator privileges. +// Deprecated: Use AdminVdc.MergeMetadataWithMetadataValuesAsync +func (vdc *Vdc) MergeMetadataAsync(typedValue string, metadata map[string]interface{}) (Task, error) { + return mergeAllMetadataDeprecated(vdc.client, typedValue, metadata, getAdminURL(vdc.Vdc.HREF)) +} + +// MergeMetadataAsync merges AdminVdc metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// then waits for the task to complete. +// Deprecated: Use AdminVdc.MergeMetadataWithMetadataValuesAsync +func (adminVdc *AdminVdc) MergeMetadataAsync(typedValue string, metadata map[string]interface{}) (Task, error) { + return mergeAllMetadataDeprecated(adminVdc.client, typedValue, metadata, adminVdc.AdminVdc.HREF) +} + +// MergeMetadata merges VDC metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// then waits for the task to complete. +// Note: Requires system administrator privileges. +// Deprecated: Use AdminVdc.MergeMetadataWithMetadataValues +func (vdc *Vdc) MergeMetadata(typedValue string, metadata map[string]interface{}) error { + task, err := vdc.MergeMetadataAsync(typedValue, metadata) + if err != nil { + return err + } + return task.WaitTaskCompletion() +} + +// MergeMetadata merges AdminVdc metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// then waits for the task to complete. +// Deprecated: Use AdminVdc.MergeMetadataWithMetadataValues +func (adminVdc *AdminVdc) MergeMetadata(typedValue string, metadata map[string]interface{}) error { + task, err := adminVdc.MergeMetadataAsync(typedValue, metadata) + if err != nil { + return err + } + return task.WaitTaskCompletion() +} + +// DeleteMetadataEntry deletes VDC metadata by key provided as input and waits for +// the task to finish. +// Note: Requires system administrator privileges. +// Deprecated: Use AdminVdc.DeleteMetadataEntryWithDomain +func (vdc *Vdc) DeleteMetadataEntry(key string) error { + task, err := vdc.DeleteMetadataEntryAsync(key) + if err != nil { + return err + } + + err = task.WaitTaskCompletion() + if err != nil { + return err + } + + err = vdc.Refresh() + if err != nil { + return err + } + + return nil +} + +// DeleteMetadataEntry deletes AdminVdc metadata by key provided as input and waits for +// the task to finish. +// Note: Requires system administrator privileges. +// Deprecated: Use AdminVdc.DeleteMetadataEntryWithDomain +func (adminVdc *AdminVdc) DeleteMetadataEntry(key string) error { + task, err := adminVdc.DeleteMetadataEntryAsync(key) + if err != nil { + return err + } + + err = task.WaitTaskCompletion() + if err != nil { + return err + } + + err = adminVdc.Refresh() + if err != nil { + return err + } + + return nil +} + +// DeleteMetadataEntryAsync deletes VDC metadata depending on key provided as input and returns the task. +// Note: Requires system administrator privileges. +// Deprecated: Use AdminVdc.DeleteMetadataEntryWithDomainAsync +func (vdc *Vdc) DeleteMetadataEntryAsync(key string) (Task, error) { + return deleteMetadata(vdc.client, getAdminURL(vdc.Vdc.HREF), vdc.Vdc.Name, key, false) +} + +// DeleteMetadataEntryAsync deletes VDC metadata depending on key provided as input and returns the task. +// Note: Requires system administrator privileges. +// Deprecated: Use AdminVdc.DeleteMetadataEntryWithDomainAsync +func (adminVdc *AdminVdc) DeleteMetadataEntryAsync(key string) (Task, error) { + return deleteMetadata(adminVdc.client, adminVdc.AdminVdc.HREF, adminVdc.AdminVdc.Name, key, false) +} + +// AddMetadataEntry adds Provider VDC metadata typedValue and key/value pair provided as input +// and waits for the task to finish. +// Note: Requires system administrator privileges. +// Deprecated: Use ProviderVdc.AddMetadataEntryWithVisibility instead +func (providerVdc *ProviderVdc) AddMetadataEntry(typedValue, key, value string) error { + task, err := providerVdc.AddMetadataEntryAsync(typedValue, key, value) + if err != nil { + return err + } + + err = task.WaitTaskCompletion() + if err != nil { + return err + } + + err = providerVdc.Refresh() + if err != nil { + return err + } + + return nil +} + +// AddMetadataEntryAsync adds Provider VDC metadata typedValue and key/value pair provided as input and returns the task. +// Note: Requires system administrator privileges. +// Deprecated: Use ProviderVdc.AddMetadataEntryWithVisibilityAsync instead +func (providerVdc *ProviderVdc) AddMetadataEntryAsync(typedValue, key, value string) (Task, error) { + return addMetadataDeprecated(providerVdc.client, typedValue, key, value, providerVdc.ProviderVdc.HREF) +} + +// MergeMetadataAsync merges Provider VDC metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// then waits for the task to complete. +// Note: Requires system administrator privileges. +// Deprecated: Use ProviderVdc.MergeMetadataWithMetadataValuesAsync +func (providerVdc *ProviderVdc) MergeMetadataAsync(typedValue string, metadata map[string]interface{}) (Task, error) { + return mergeAllMetadataDeprecated(providerVdc.client, typedValue, metadata, providerVdc.ProviderVdc.HREF) +} + +// MergeMetadata merges Provider VDC metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// then waits for the task to complete. +// Note: Requires system administrator privileges. +// Deprecated: Use ProviderVdc.MergeMetadataWithMetadataValues +func (providerVdc *ProviderVdc) MergeMetadata(typedValue string, metadata map[string]interface{}) error { + task, err := providerVdc.MergeMetadataAsync(typedValue, metadata) + if err != nil { + return err + } + return task.WaitTaskCompletion() +} + +// DeleteMetadataEntry deletes Provider VDC metadata by key provided as input and waits for +// the task to finish. +// Note: Requires system administrator privileges. +// Deprecated: Use ProviderVdc.DeleteMetadataEntryWithDomain +func (providerVdc *ProviderVdc) DeleteMetadataEntry(key string) error { + task, err := providerVdc.DeleteMetadataEntryAsync(key) + if err != nil { + return err + } + + err = task.WaitTaskCompletion() + if err != nil { + return err + } + + err = providerVdc.Refresh() + if err != nil { + return err + } + + return nil +} + +// DeleteMetadataEntryAsync deletes Provider VDC metadata depending on key provided as input and returns the task. +// Note: Requires system administrator privileges. +// Deprecated: Use ProviderVdc.DeleteMetadataEntryWithDomainAsync +func (providerVdc *ProviderVdc) DeleteMetadataEntryAsync(key string) (Task, error) { + return deleteMetadata(providerVdc.client, providerVdc.ProviderVdc.HREF, providerVdc.ProviderVdc.Name, key, false) +} + +// AddMetadataEntry adds VApp metadata typedValue and key/value pair provided as input +// and waits for the task to finish. +// Deprecated: Use VApp.AddMetadataEntryWithVisibility instead +func (vapp *VApp) AddMetadataEntry(typedValue, key, value string) error { + task, err := vapp.AddMetadataEntryAsync(typedValue, key, value) + if err != nil { + return err + } + + err = task.WaitTaskCompletion() + if err != nil { + return err + } + + err = vapp.Refresh() + if err != nil { + return err + } + + return nil +} + +// AddMetadataEntryAsync adds VApp metadata typedValue and key/value pair provided as input and returns the task. +// Deprecated: Use VApp.AddMetadataEntryWithVisibilityAsync instead +func (vapp *VApp) AddMetadataEntryAsync(typedValue, key, value string) (Task, error) { + return addMetadataDeprecated(vapp.client, typedValue, key, value, vapp.VApp.HREF) +} + +// MergeMetadataAsync merges VApp metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// then waits for the task to complete. +// Deprecated: Use VApp.MergeMetadataWithMetadataValuesAsync +func (vapp *VApp) MergeMetadataAsync(typedValue string, metadata map[string]interface{}) (Task, error) { + return mergeAllMetadataDeprecated(vapp.client, typedValue, metadata, vapp.VApp.HREF) +} + +// MergeMetadata merges VApp metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// then waits for the task to complete. +// Deprecated: Use VApp.MergeMetadataWithMetadataValues +func (vapp *VApp) MergeMetadata(typedValue string, metadata map[string]interface{}) error { + task, err := vapp.MergeMetadataAsync(typedValue, metadata) + if err != nil { + return err + } + return task.WaitTaskCompletion() +} + +// DeleteMetadataEntry deletes VApp metadata by key provided as input and waits for +// the task to finish. +// Deprecated: Use VApp.DeleteMetadataEntryWithDomain instead +func (vapp *VApp) DeleteMetadataEntry(key string) error { + task, err := vapp.DeleteMetadataEntryAsync(key) + if err != nil { + return err + } + + err = task.WaitTaskCompletion() + if err != nil { + return err + } + + err = vapp.Refresh() + if err != nil { + return err + } + + return nil +} + +// DeleteMetadataEntryAsync deletes VApp metadata depending on key provided as input and returns the task. +// Deprecated: Use VApp.DeleteMetadataEntryWithDomainAsync instead +func (vapp *VApp) DeleteMetadataEntryAsync(key string) (Task, error) { + return deleteMetadata(vapp.client, vapp.VApp.HREF, vapp.VApp.Name, key, false) +} + +// AddMetadataEntry adds VAppTemplate metadata typedValue and key/value pair provided as input and +// waits for the task to finish. +// Deprecated: Use VAppTemplate.AddMetadataEntryWithVisibility instead +func (vAppTemplate *VAppTemplate) AddMetadataEntry(typedValue, key, value string) error { + task, err := vAppTemplate.AddMetadataEntryAsync(typedValue, key, value) + if err != nil { + return err + } + err = task.WaitTaskCompletion() + if err != nil { + return err + } + + err = vAppTemplate.Refresh() + if err != nil { + return err + } + + return nil +} + +// AddMetadataEntryAsync adds VAppTemplate metadata typedValue and key/value pair provided as input +// and returns the task. +// Deprecated: Use VAppTemplate.AddMetadataEntryWithVisibilityAsync instead +func (vAppTemplate *VAppTemplate) AddMetadataEntryAsync(typedValue, key, value string) (Task, error) { + return addMetadataDeprecated(vAppTemplate.client, typedValue, key, value, vAppTemplate.VAppTemplate.HREF) +} + +// MergeMetadataAsync merges VAppTemplate metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// then waits for the task to complete. +// Deprecated: Use VAppTemplate.MergeMetadataWithMetadataValuesAsync +func (vAppTemplate *VAppTemplate) MergeMetadataAsync(typedValue string, metadata map[string]interface{}) (Task, error) { + return mergeAllMetadataDeprecated(vAppTemplate.client, typedValue, metadata, vAppTemplate.VAppTemplate.HREF) +} + +// MergeMetadata merges VAppTemplate metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// then waits for the task to complete. +// Deprecated: Use VAppTemplate.MergeMetadataWithMetadataValues +func (vAppTemplate *VAppTemplate) MergeMetadata(typedValue string, metadata map[string]interface{}) error { + task, err := vAppTemplate.MergeMetadataAsync(typedValue, metadata) + if err != nil { + return err + } + return task.WaitTaskCompletion() +} + +// DeleteMetadataEntry deletes VAppTemplate metadata depending on key provided as input +// and waits for the task to finish. +// Deprecated: Use VAppTemplate.DeleteMetadataEntryWithDomain instead +func (vAppTemplate *VAppTemplate) DeleteMetadataEntry(key string) error { + task, err := vAppTemplate.DeleteMetadataEntryAsync(key) + if err != nil { + return err + } + + err = task.WaitTaskCompletion() + if err != nil { + return err + } + + err = vAppTemplate.Refresh() + if err != nil { + return err + } + + return nil +} + +// DeleteMetadataEntryAsync deletes VAppTemplate metadata depending on key provided as input +// and returns the task. +// Deprecated: Use VAppTemplate.DeleteMetadataEntryWithDomainAsync instead +func (vAppTemplate *VAppTemplate) DeleteMetadataEntryAsync(key string) (Task, error) { + return deleteMetadata(vAppTemplate.client, vAppTemplate.VAppTemplate.HREF, vAppTemplate.VAppTemplate.Name, key, false) +} + +// AddMetadataEntry adds MediaRecord metadata typedValue and key/value pair provided as input and +// waits for the task to finish. +// Deprecated: Use MediaRecord.AddMetadataEntryWithVisibility instead +func (mediaRecord *MediaRecord) AddMetadataEntry(typedValue, key, value string) error { + task, err := mediaRecord.AddMetadataEntryAsync(typedValue, key, value) + if err != nil { + return err + } + err = task.WaitTaskCompletion() + if err != nil { + return err + } + + err = mediaRecord.Refresh() + if err != nil { + return err + } + + return nil +} + +// AddMetadataEntryAsync adds MediaRecord metadata typedValue and key/value pair provided as input +// and returns the task. +// Deprecated: Use MediaRecord.AddMetadataEntryWithVisibilityAsync instead +func (mediaRecord *MediaRecord) AddMetadataEntryAsync(typedValue, key, value string) (Task, error) { + return addMetadataDeprecated(mediaRecord.client, typedValue, key, value, mediaRecord.MediaRecord.HREF) +} + +// MergeMetadataAsync merges MediaRecord metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// then waits for the task to complete. +// Deprecated: Use MediaRecord.MergeMetadataWithMetadataValuesAsync +func (mediaRecord *MediaRecord) MergeMetadataAsync(typedValue string, metadata map[string]interface{}) (Task, error) { + return mergeAllMetadataDeprecated(mediaRecord.client, typedValue, metadata, mediaRecord.MediaRecord.HREF) +} + +// MergeMetadata merges MediaRecord metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// then waits for the task to complete. +// Deprecated: Use MediaRecord.MergeMetadataWithMetadataValues +func (mediaRecord *MediaRecord) MergeMetadata(typedValue string, metadata map[string]interface{}) error { + task, err := mediaRecord.MergeMetadataAsync(typedValue, metadata) + if err != nil { + return err + } + return task.WaitTaskCompletion() +} + +// DeleteMetadataEntry deletes MediaRecord metadata depending on key provided as input +// and waits for the task to finish. +// Deprecated: Use MediaRecord.DeleteMetadataEntryWithDomain instead +func (mediaRecord *MediaRecord) DeleteMetadataEntry(key string) error { + task, err := mediaRecord.DeleteMetadataEntryAsync(key) + if err != nil { + return err + } + err = task.WaitTaskCompletion() + if err != nil { + return err + } + + err = mediaRecord.Refresh() + if err != nil { + return err + } + + return nil +} + +// DeleteMetadataEntryAsync deletes MediaRecord metadata depending on key provided as input +// and returns the task. +// Deprecated: Use MediaRecord.DeleteMetadataEntryWithDomainAsync instead +func (mediaRecord *MediaRecord) DeleteMetadataEntryAsync(key string) (Task, error) { + return deleteMetadata(mediaRecord.client, mediaRecord.MediaRecord.HREF, mediaRecord.MediaRecord.Name, key, false) +} + +// AddMetadataEntry adds Media metadata typedValue and key/value pair provided as input +// and waits for the task to finish. +// Deprecated: Use Media.AddMetadataEntryWithVisibility instead +func (media *Media) AddMetadataEntry(typedValue, key, value string) error { + task, err := media.AddMetadataEntryAsync(typedValue, key, value) + if err != nil { + return err + } + err = task.WaitTaskCompletion() + if err != nil { + return err + } + + err = media.Refresh() + if err != nil { + return err + } + + return nil +} + +// AddMetadataEntryAsync adds Media metadata typedValue and key/value pair provided as input +// and returns the task. +// Deprecated: Use Media.AddMetadataEntryWithVisibilityAsync instead +func (media *Media) AddMetadataEntryAsync(typedValue, key, value string) (Task, error) { + return addMetadataDeprecated(media.client, typedValue, key, value, media.Media.HREF) +} + +// MergeMetadataAsync merges Media metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// then waits for the task to complete. +// Deprecated: Use Media.MergeMetadataWithMetadataValuesAsync +func (media *Media) MergeMetadataAsync(typedValue string, metadata map[string]interface{}) (Task, error) { + return mergeAllMetadataDeprecated(media.client, typedValue, metadata, media.Media.HREF) +} + +// MergeMetadata merges Media metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// then waits for the task to complete. +// Deprecated: Use Media.MergeMetadataWithMetadataValues +func (media *Media) MergeMetadata(typedValue string, metadata map[string]interface{}) error { + task, err := media.MergeMetadataAsync(typedValue, metadata) + if err != nil { + return err + } + return task.WaitTaskCompletion() +} + +// DeleteMetadataEntry deletes Media metadata depending on key provided as input +// and waits for the task to finish. +// Deprecated: Use Media.DeleteMetadataEntryWithDomain instead +func (media *Media) DeleteMetadataEntry(key string) error { + task, err := media.DeleteMetadataEntryAsync(key) + if err != nil { + return err + } + + err = task.WaitTaskCompletion() + if err != nil { + return err + } + + err = media.Refresh() + if err != nil { + return err + } + + return nil } -// DeleteMetadata() function calls private function deleteMetadata() with vm.client and vm.VM.HREF -// which deletes metadata depending on key provided as input from VM. -func (vm *VM) DeleteMetadata(key string) (Task, error) { - return deleteMetadata(vm.client, key, vm.VM.HREF) +// DeleteMetadataEntryAsync deletes Media metadata depending on key provided as input +// and returns the task. +// Deprecated: Use Media.DeleteMetadataEntryWithDomainAsync instead +func (media *Media) DeleteMetadataEntryAsync(key string) (Task, error) { + return deleteMetadata(media.client, media.Media.HREF, media.Media.Name, key, false) } -// AddMetadata calls private function addMetadata() with vm.client and vm.VM.HREF -// which adds metadata key/value pair provided as input to VM. -func (vm *VM) AddMetadata(key string, value string) (Task, error) { - return addMetadata(vm.client, key, value, vm.VM.HREF) +// AddMetadataEntry adds AdminCatalog metadata typedValue and key/value pair provided as input +// and waits for the task to finish. +// Deprecated: Use AdminCatalog.AddMetadataEntryWithVisibility instead +func (adminCatalog *AdminCatalog) AddMetadataEntry(typedValue, key, value string) error { + task, err := adminCatalog.AddMetadataEntryAsync(typedValue, key, value) + if err != nil { + return err + } + err = task.WaitTaskCompletion() + if err != nil { + return err + } + + err = adminCatalog.Refresh() + if err != nil { + return err + } + + return nil } -// GetMetadata returns meta data for VDC. -func (vdc *Vdc) GetMetadata() (*types.Metadata, error) { - return getMetadata(vdc.client, getAdminVdcURL(vdc.Vdc.HREF)) +// AddMetadataEntryAsync adds AdminCatalog metadata typedValue and key/value pair provided as input +// and returns the task. +// Deprecated: Use AdminCatalog.AddMetadataEntryWithVisibilityAsync instead +func (adminCatalog *AdminCatalog) AddMetadataEntryAsync(typedValue, key, value string) (Task, error) { + return addMetadataDeprecated(adminCatalog.client, typedValue, key, value, adminCatalog.AdminCatalog.HREF) } -// DeleteMetadata() function deletes metadata by key provided as input -func (vdc *Vdc) DeleteMetadata(key string) (Vdc, error) { - task, err := deleteMetadata(vdc.client, key, getAdminVdcURL(vdc.Vdc.HREF)) +// MergeMetadataAsync merges AdminCatalog metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// then waits for the task to complete. +// Deprecated: Use AdminCatalog.MergeMetadataWithMetadataValuesAsync +func (adminCatalog *AdminCatalog) MergeMetadataAsync(typedValue string, metadata map[string]interface{}) (Task, error) { + return mergeAllMetadataDeprecated(adminCatalog.client, typedValue, metadata, adminCatalog.AdminCatalog.HREF) +} + +// MergeMetadata merges AdminCatalog metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// then waits for the task to complete. +// Deprecated: Use AdminCatalog.MergeMetadataWithMetadataValues +func (adminCatalog *AdminCatalog) MergeMetadata(typedValue string, metadata map[string]interface{}) error { + task, err := adminCatalog.MergeMetadataAsync(typedValue, metadata) if err != nil { - return Vdc{}, err + return err + } + return task.WaitTaskCompletion() +} + +// DeleteMetadataEntry deletes AdminCatalog metadata depending on key provided as input +// and waits for the task to finish. +// Deprecated: Use AdminCatalog.DeleteMetadataEntryWithDomain instead +func (adminCatalog *AdminCatalog) DeleteMetadataEntry(key string) error { + task, err := adminCatalog.DeleteMetadataEntryAsync(key) + if err != nil { + return err } err = task.WaitTaskCompletion() if err != nil { - return Vdc{}, err + return err } - err = vdc.Refresh() + err = adminCatalog.Refresh() if err != nil { - return Vdc{}, err + return err } - return *vdc, nil + return nil } -// AddMetadata adds metadata key/value pair provided as input to VDC. -func (vdc *Vdc) AddMetadata(key string, value string) (Vdc, error) { - task, err := addMetadata(vdc.client, key, value, getAdminVdcURL(vdc.Vdc.HREF)) +// DeleteMetadataEntryAsync deletes AdminCatalog metadata depending on key provided as input +// and returns a task. +// Deprecated: Use AdminCatalog.DeleteMetadataEntryWithDomainAsync instead +func (adminCatalog *AdminCatalog) DeleteMetadataEntryAsync(key string) (Task, error) { + return deleteMetadata(adminCatalog.client, adminCatalog.AdminCatalog.HREF, adminCatalog.AdminCatalog.Name, key, false) +} + +// AddMetadataEntry adds AdminOrg metadata key/value pair provided as input to the corresponding organization seen as administrator +// and waits for completion. +// Deprecated: Use AdminOrg.AddMetadataEntryWithVisibility instead +func (adminOrg *AdminOrg) AddMetadataEntry(typedValue, key, value string) error { + task, err := adminOrg.AddMetadataEntryAsync(typedValue, key, value) if err != nil { - return Vdc{}, err + return err + } + return task.WaitTaskCompletion() +} + +// AddMetadataEntryAsync adds AdminOrg metadata key/value pair provided as input to the corresponding organization seen as administrator +// and returns a task. +// Deprecated: Use AdminOrg.AddMetadataEntryWithVisibilityAsync instead +func (adminOrg *AdminOrg) AddMetadataEntryAsync(typedValue, key, value string) (Task, error) { + return addMetadataDeprecated(adminOrg.client, typedValue, key, value, adminOrg.AdminOrg.HREF) +} + +// MergeMetadataAsync merges AdminOrg metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// then waits for the task to complete. +// Deprecated: Use AdminOrg.MergeMetadataWithMetadataValuesAsync +func (adminOrg *AdminOrg) MergeMetadataAsync(typedValue string, metadata map[string]interface{}) (Task, error) { + return mergeAllMetadataDeprecated(adminOrg.client, typedValue, metadata, adminOrg.AdminOrg.HREF) +} + +// MergeMetadata merges AdminOrg metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// then waits for the task to complete. +// Deprecated: Use AdminOrg.MergeMetadataWithMetadataValues +func (adminOrg *AdminOrg) MergeMetadata(typedValue string, metadata map[string]interface{}) error { + task, err := adminOrg.MergeMetadataAsync(typedValue, metadata) + if err != nil { + return err } + return task.WaitTaskCompletion() +} +// DeleteMetadataEntry deletes metadata of the corresponding organization with the given key, and waits for completion +// Deprecated: Use AdminOrg.DeleteMetadataEntryWithDomain instead +func (adminOrg *AdminOrg) DeleteMetadataEntry(key string) error { + task, err := adminOrg.DeleteMetadataEntryAsync(key) + if err != nil { + return err + } err = task.WaitTaskCompletion() if err != nil { - return Vdc{}, err + return fmt.Errorf("error completing delete metadata for organization task: %s", err) } - err = vdc.Refresh() + return nil +} + +// DeleteMetadataEntryAsync deletes metadata of the corresponding organization with the given key, and returns +// a task. +// Deprecated: Use AdminOrg.DeleteMetadataEntryWithDomainAsync instead +func (adminOrg *AdminOrg) DeleteMetadataEntryAsync(key string) (Task, error) { + return deleteMetadata(adminOrg.client, adminOrg.AdminOrg.HREF, adminOrg.AdminOrg.Name, key, false) +} + +// AddMetadataEntry adds metadata key/value pair provided as input to the corresponding independent disk and waits for completion. +// Deprecated: Use Disk.AddMetadataEntryWithVisibility instead +func (disk *Disk) AddMetadataEntry(typedValue, key, value string) error { + task, err := disk.AddMetadataEntryAsync(typedValue, key, value) if err != nil { - return Vdc{}, err + return err } + return task.WaitTaskCompletion() +} - return *vdc, nil +// AddMetadataEntryAsync adds metadata key/value pair provided as input to the corresponding independent disk and returns a task. +// Deprecated: Use Disk.AddMetadataEntryWithVisibilityAsync instead +func (disk *Disk) AddMetadataEntryAsync(typedValue, key, value string) (Task, error) { + return addMetadataDeprecated(disk.client, typedValue, key, value, disk.Disk.HREF) } -// AddMetadata adds metadata key/value pair provided as input to VDC. -// and returns task -func (vdc *Vdc) AddMetadataAsync(key string, value string) (Task, error) { - return addMetadata(vdc.client, key, value, getAdminVdcURL(vdc.Vdc.HREF)) +// MergeMetadataAsync merges Disk metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// then waits for the task to complete. +// Deprecated: Use Disk.MergeMetadataWithMetadataValuesAsync +func (disk *Disk) MergeMetadataAsync(typedValue string, metadata map[string]interface{}) (Task, error) { + return mergeAllMetadataDeprecated(disk.client, typedValue, metadata, disk.Disk.HREF) } -// DeleteMetadata() function deletes metadata by key provided as input -// and returns task -func (vdc *Vdc) DeleteMetadataAsync(key string) (Task, error) { - return deleteMetadata(vdc.client, key, getAdminVdcURL(vdc.Vdc.HREF)) +// MergeMetadata merges Disk metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// then waits for the task to complete. +// Deprecated: Use Disk.MergeMetadataWithMetadataValues +func (disk *Disk) MergeMetadata(typedValue string, metadata map[string]interface{}) error { + task, err := disk.MergeMetadataAsync(typedValue, metadata) + if err != nil { + return err + } + return task.WaitTaskCompletion() } -func getAdminVdcURL(vdcURL string) string { - return strings.Split(vdcURL, "/api/vdc/")[0] + "/api/admin/vdc/" + strings.Split(vdcURL, "/api/vdc/")[1] +// DeleteMetadataEntry deletes metadata of the corresponding independent disk with the given key, and waits for completion +// Deprecated: Use Disk.DeleteMetadataEntryWithDomain instead +func (disk *Disk) DeleteMetadataEntry(key string) error { + task, err := disk.DeleteMetadataEntryAsync(key) + if err != nil { + return err + } + err = task.WaitTaskCompletion() + if err != nil { + return fmt.Errorf("error completing delete metadata for independent disk task: %s", err) + } + + return nil } -// GetMetadata calls private function getMetadata() with vapp.client and vapp.VApp.HREF -// which returns a *types.Metadata struct for provided vapp input. -func (vapp *VApp) GetMetadata() (*types.Metadata, error) { - return getMetadata(vapp.client, vapp.VApp.HREF) +// DeleteMetadataEntryAsync deletes metadata of the corresponding independent disk with the given key, and returns +// a task. +// Deprecated: Use Disk.DeleteMetadataEntryWithDomainAsync instead +func (disk *Disk) DeleteMetadataEntryAsync(key string) (Task, error) { + return deleteMetadata(disk.client, disk.Disk.HREF, disk.Disk.Name, key, false) } -func getMetadata(client *Client, requestUri string) (*types.Metadata, error) { - metadata := &types.Metadata{} +// AddMetadataEntry adds OrgVDCNetwork metadata typedValue and key/value pair provided as input +// and waits for the task to finish. +// Note: Requires system administrator privileges. +// Deprecated: Use OrgVDCNetwork.AddMetadataEntryWithVisibility instead +func (orgVdcNetwork *OrgVDCNetwork) AddMetadataEntry(typedValue, key, value string) error { + task, err := orgVdcNetwork.AddMetadataEntryAsync(typedValue, key, value) + if err != nil { + return err + } + return task.WaitTaskCompletion() +} - _, err := client.ExecuteRequest(requestUri+"/metadata/", http.MethodGet, - types.MimeMetaData, "error retrieving metadata: %s", nil, metadata) +// AddMetadataEntryAsync adds OrgVDCNetwork metadata typedValue and key/value pair provided as input +// and returns the task. +// Note: Requires system administrator privileges. +// Deprecated: Use OrgVDCNetwork.AddMetadataEntryWithVisibilityAsync instead +func (orgVdcNetwork *OrgVDCNetwork) AddMetadataEntryAsync(typedValue, key, value string) (Task, error) { + return addMetadataDeprecated(orgVdcNetwork.client, typedValue, key, value, getAdminURL(orgVdcNetwork.OrgVDCNetwork.HREF)) +} - return metadata, err +// MergeMetadataAsync merges OrgVDCNetwork metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// then waits for the task to complete. +// Note: Requires system administrator privileges. +// Deprecated: Use OrgVDCNetwork.MergeMetadataWithMetadataValuesAsync +func (orgVdcNetwork *OrgVDCNetwork) MergeMetadataAsync(typedValue string, metadata map[string]interface{}) (Task, error) { + return mergeAllMetadataDeprecated(orgVdcNetwork.client, typedValue, metadata, getAdminURL(orgVdcNetwork.OrgVDCNetwork.HREF)) } -// DeleteMetadata() function calls private function deleteMetadata() with vapp.client and vapp.VApp.HREF -// which deletes metadata depending on key provided as input from vApp. -func (vapp *VApp) DeleteMetadata(key string) (Task, error) { - return deleteMetadata(vapp.client, key, vapp.VApp.HREF) +// MergeMetadata merges OrgVDCNetwork metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// then waits for the task to complete. +// Note: Requires system administrator privileges. +// Deprecated: Use OrgVDCNetwork.MergeMetadataWithMetadataValues +func (orgVdcNetwork *OrgVDCNetwork) MergeMetadata(typedValue string, metadata map[string]interface{}) error { + task, err := orgVdcNetwork.MergeMetadataAsync(typedValue, metadata) + if err != nil { + return err + } + return task.WaitTaskCompletion() } -// Deletes metadata (type MetadataStringValue) from the vApp -// TODO: Support all MetadataTypedValue types with this function -func deleteMetadata(client *Client, key string, requestUri string) (Task, error) { - apiEndpoint, _ := url.ParseRequestURI(requestUri) - apiEndpoint.Path += "/metadata/" + key +// AddMetadataEntry adds CatalogItem metadata typedValue and key/value pair provided as input +// and waits for the task to finish. +// Deprecated: Use CatalogItem.AddMetadataEntryWithVisibility instead +func (catalogItem *CatalogItem) AddMetadataEntry(typedValue, key, value string) error { + task, err := catalogItem.AddMetadataEntryAsync(typedValue, key, value) + if err != nil { + return err + } + return task.WaitTaskCompletion() +} - // Return the task - return client.ExecuteTaskRequest(apiEndpoint.String(), http.MethodDelete, - "", "error deleting metadata: %s", nil) +// AddMetadataEntryAsync adds CatalogItem metadata typedValue and key/value pair provided as input +// and returns the task. +// Deprecated: Use CatalogItem.AddMetadataEntryWithVisibilityAsync instead +func (catalogItem *CatalogItem) AddMetadataEntryAsync(typedValue, key, value string) (Task, error) { + return addMetadataDeprecated(catalogItem.client, typedValue, key, value, catalogItem.CatalogItem.HREF) } -// AddMetadata calls private function addMetadata() with vapp.client and vapp.VApp.HREF -// which adds metadata key/value pair provided as input -func (vapp *VApp) AddMetadata(key string, value string) (Task, error) { - return addMetadata(vapp.client, key, value, vapp.VApp.HREF) +// MergeMetadataAsync merges CatalogItem metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// then waits for the task to complete. +// Deprecated: Use CatalogItem.MergeMetadataWithMetadataValuesAsync +func (catalogItem *CatalogItem) MergeMetadataAsync(typedValue string, metadata map[string]interface{}) (Task, error) { + return mergeAllMetadataDeprecated(catalogItem.client, typedValue, metadata, catalogItem.CatalogItem.HREF) +} + +// MergeMetadata merges CatalogItem metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// then waits for the task to complete. +// Deprecated: Use CatalogItem.MergeMetadataWithMetadataValues +func (catalogItem *CatalogItem) MergeMetadata(typedValue string, metadata map[string]interface{}) error { + task, err := catalogItem.MergeMetadataAsync(typedValue, metadata) + if err != nil { + return err + } + return task.WaitTaskCompletion() +} + +// DeleteMetadataEntry deletes CatalogItem metadata depending on key provided as input +// and waits for the task to finish. +// Deprecated: Use CatalogItem.DeleteMetadataEntryWithDomain instead +func (catalogItem *CatalogItem) DeleteMetadataEntry(key string) error { + task, err := catalogItem.DeleteMetadataEntryAsync(key) + if err != nil { + return err + } + + return task.WaitTaskCompletion() +} + +// DeleteMetadataEntryAsync deletes CatalogItem metadata depending on key provided as input +// and returns a task. +// Deprecated: Use CatalogItem.DeleteMetadataEntryWithDomainAsync instead +func (catalogItem *CatalogItem) DeleteMetadataEntryAsync(key string) (Task, error) { + return deleteMetadata(catalogItem.client, catalogItem.CatalogItem.HREF, catalogItem.CatalogItem.Name, key, false) +} + +// DeleteMetadataEntry deletes OrgVDCNetwork metadata depending on key provided as input +// and waits for the task to finish. +// Note: Requires system administrator privileges. +// Deprecated: Use OrgVDCNetwork.DeleteMetadataEntryWithDomain instead +func (orgVdcNetwork *OrgVDCNetwork) DeleteMetadataEntry(key string) error { + task, err := orgVdcNetwork.DeleteMetadataEntryAsync(key) + if err != nil { + return err + } + + return task.WaitTaskCompletion() +} + +// DeleteMetadataEntryAsync deletes OrgVDCNetwork metadata depending on key provided as input +// and returns a task. +// Note: Requires system administrator privileges. +// Deprecated: Use OrgVDCNetwork.DeleteMetadataEntryWithDomainAsync instead +func (orgVdcNetwork *OrgVDCNetwork) DeleteMetadataEntryAsync(key string) (Task, error) { + return deleteMetadata(orgVdcNetwork.client, getAdminURL(orgVdcNetwork.OrgVDCNetwork.HREF), orgVdcNetwork.OrgVDCNetwork.Name, key, false) +} + +// ---------------- +// OpenAPI metadata functions + +// AddMetadataEntry adds OpenApiOrgVdcNetwork metadata typedValue and key/value pair provided as input +// and waits for the task to finish. +// Deprecated: Use OpenApiOrgVdcNetwork.AddMetadataEntryWithVisibility instead +func (openApiOrgVdcNetwork *OpenApiOrgVdcNetwork) AddMetadataEntry(typedValue, key, value string) error { + task, err := addMetadataDeprecated(openApiOrgVdcNetwork.client, typedValue, key, value, fmt.Sprintf("%s/admin/network/%s", openApiOrgVdcNetwork.client.VCDHREF.String(), strings.ReplaceAll(openApiOrgVdcNetwork.OpenApiOrgVdcNetwork.ID, "urn:vcloud:network:", ""))) + if err != nil { + return err + } + return task.WaitTaskCompletion() +} + +// MergeMetadata merges OpenApiOrgVdcNetwork metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// and waits for the task to finish. +// Deprecated: Use OpenApiOrgVdcNetwork.MergeMetadataWithMetadataValues +func (openApiOrgVdcNetwork *OpenApiOrgVdcNetwork) MergeMetadata(typedValue string, metadata map[string]interface{}) error { + task, err := mergeAllMetadataDeprecated(openApiOrgVdcNetwork.client, typedValue, metadata, fmt.Sprintf("%s/admin/network/%s", openApiOrgVdcNetwork.client.VCDHREF.String(), strings.ReplaceAll(openApiOrgVdcNetwork.OpenApiOrgVdcNetwork.ID, "urn:vcloud:network:", ""))) + if err != nil { + return err + } + return task.WaitTaskCompletion() +} + +// DeleteMetadataEntry deletes OpenApiOrgVdcNetwork metadata depending on key provided as input +// and waits for the task to finish. +// Deprecated: Use OpenApiOrgVdcNetwork.DeleteMetadataEntryWithDomain +func (openApiOrgVdcNetwork *OpenApiOrgVdcNetwork) DeleteMetadataEntry(key string) error { + task, err := deleteMetadata(openApiOrgVdcNetwork.client, fmt.Sprintf("%s/admin/network/%s", openApiOrgVdcNetwork.client.VCDHREF.String(), strings.ReplaceAll(openApiOrgVdcNetwork.OpenApiOrgVdcNetwork.ID, "urn:vcloud:network:", "")), openApiOrgVdcNetwork.OpenApiOrgVdcNetwork.Name, key, false) + if err != nil { + return err + } + + return task.WaitTaskCompletion() } -// Adds metadata (type MetadataStringValue) to the vApp -// TODO: Support all MetadataTypedValue types with this function -func addMetadata(client *Client, key string, value string, requestUri string) (Task, error) { +// ---------------- +// Generic private functions + +// addMetadata adds metadata to an entity. +// The function supports passing a typedValue. Use one of the constants defined. +// Constants are types.MetadataStringValue, types.MetadataNumberValue, types.MetadataDateTimeValue and types.MetadataBooleanValue. +// Only tested with types.MetadataStringValue and types.MetadataNumberValue. +// Deprecated +func addMetadataDeprecated(client *Client, typedValue, key, value, requestUri string) (Task, error) { newMetadata := &types.MetadataValue{ Xmlns: types.XMLNamespaceVCloud, Xsi: types.XMLNamespaceXSI, - TypedValue: &types.TypedValue{ - XsiType: "MetadataStringValue", + TypedValue: &types.MetadataTypedValue{ + XsiType: typedValue, Value: value, }, } - apiEndpoint, _ := url.ParseRequestURI(requestUri) + apiEndpoint := urlParseRequestURI(requestUri) apiEndpoint.Path += "/metadata/" + key // Return the task @@ -150,13 +1036,110 @@ func addMetadata(client *Client, key string, value string, requestUri string) (T types.MimeMetaDataValue, "error adding metadata: %s", newMetadata) } -// GetMetadata calls private function getMetadata() with catalogItem.client and catalogItem.CatalogItem.HREF -// which returns a *types.Metadata struct for provided catalog item input. -func (vAppTemplate *VAppTemplate) GetMetadata() (*types.Metadata, error) { - return getMetadata(vAppTemplate.client, vAppTemplate.VAppTemplate.HREF) +// mergeAllMetadataDeprecated merges the metadata key-values provided as parameter with existing entity metadata +// Deprecated +func mergeAllMetadataDeprecated(client *Client, typedValue string, metadata map[string]interface{}, requestUri string) (Task, error) { + var metadataToMerge []*types.MetadataEntry + for key, value := range metadata { + metadataToMerge = append(metadataToMerge, &types.MetadataEntry{ + Xmlns: types.XMLNamespaceVCloud, + Xsi: types.XMLNamespaceXSI, + Key: key, + TypedValue: &types.MetadataTypedValue{ + XsiType: typedValue, + Value: value.(string), + }, + }) + } + + newMetadata := &types.Metadata{ + Xmlns: types.XMLNamespaceVCloud, + Xsi: types.XMLNamespaceXSI, + MetadataEntry: metadataToMerge, + } + + apiEndpoint := urlParseRequestURI(requestUri) + apiEndpoint.Path += "/metadata" + + // Return the task + return client.ExecuteTaskRequest(apiEndpoint.String(), http.MethodPost, + types.MimeMetaData, "error adding metadata: %s", newMetadata) +} + +// ---------------- +// Deprecations + +// Deprecated: use VM.DeleteMetadataEntry. +func (vm *VM) DeleteMetadata(key string) (Task, error) { + return deleteMetadata(vm.client, vm.VM.HREF, vm.VM.Name, key, false) +} + +// Deprecated: use VM.AddMetadataEntry. +func (vm *VM) AddMetadata(key string, value string) (Task, error) { + return addMetadataDeprecated(vm.client, types.MetadataStringValue, key, value, vm.VM.HREF) +} + +// Deprecated: use Vdc.DeleteMetadataEntry. +func (vdc *Vdc) DeleteMetadata(key string) (Vdc, error) { + task, err := deleteMetadata(vdc.client, getAdminURL(vdc.Vdc.HREF), vdc.Vdc.Name, key, false) + if err != nil { + return Vdc{}, err + } + + err = task.WaitTaskCompletion() + if err != nil { + return Vdc{}, err + } + + err = vdc.Refresh() + if err != nil { + return Vdc{}, err + } + + return *vdc, nil +} + +// Deprecated: use Vdc.AddMetadataEntry. +func (vdc *Vdc) AddMetadata(key string, value string) (Vdc, error) { + task, err := addMetadataDeprecated(vdc.client, types.MetadataStringValue, key, value, getAdminURL(vdc.Vdc.HREF)) + if err != nil { + return Vdc{}, err + } + + err = task.WaitTaskCompletion() + if err != nil { + return Vdc{}, err + } + + err = vdc.Refresh() + if err != nil { + return Vdc{}, err + } + + return *vdc, nil +} + +// Deprecated: use Vdc.AddMetadataEntryAsync. +func (vdc *Vdc) AddMetadataAsync(key string, value string) (Task, error) { + return addMetadataDeprecated(vdc.client, types.MetadataStringValue, key, value, getAdminURL(vdc.Vdc.HREF)) +} + +// Deprecated: use Vdc.DeleteMetadataEntryAsync. +func (vdc *Vdc) DeleteMetadataAsync(key string) (Task, error) { + return deleteMetadata(vdc.client, getAdminURL(vdc.Vdc.HREF), vdc.Vdc.Name, key, false) +} + +// Deprecated: use VApp.DeleteMetadataEntry. +func (vapp *VApp) DeleteMetadata(key string) (Task, error) { + return deleteMetadata(vapp.client, vapp.VApp.HREF, vapp.VApp.Name, key, false) +} + +// Deprecated: use VApp.AddMetadataEntry +func (vapp *VApp) AddMetadata(key string, value string) (Task, error) { + return addMetadataDeprecated(vapp.client, types.MetadataStringValue, key, value, vapp.VApp.HREF) } -// AddMetadata adds metadata key/value pair provided as input and returned update VAppTemplate +// Deprecated: use VAppTemplate.AddMetadataEntry. func (vAppTemplate *VAppTemplate) AddMetadata(key string, value string) (*VAppTemplate, error) { task, err := vAppTemplate.AddMetadataAsync(key, value) if err != nil { @@ -175,13 +1158,12 @@ func (vAppTemplate *VAppTemplate) AddMetadata(key string, value string) (*VAppTe return vAppTemplate, nil } -// AddMetadataAsync calls private function addMetadata() with vAppTemplate.client and vAppTemplate.VAppTemplate.HREF -// which adds metadata key/value pair provided as input. +// Deprecated: use VAppTemplate.AddMetadataEntryAsync. func (vAppTemplate *VAppTemplate) AddMetadataAsync(key string, value string) (Task, error) { - return addMetadata(vAppTemplate.client, key, value, vAppTemplate.VAppTemplate.HREF) + return addMetadataDeprecated(vAppTemplate.client, types.MetadataStringValue, key, value, vAppTemplate.VAppTemplate.HREF) } -// DeleteMetadata deletes metadata depending on key provided as input from media item. +// Deprecated: use VAppTemplate.DeleteMetadataEntry. func (vAppTemplate *VAppTemplate) DeleteMetadata(key string) error { task, err := vAppTemplate.DeleteMetadataAsync(key) if err != nil { @@ -195,23 +1177,14 @@ func (vAppTemplate *VAppTemplate) DeleteMetadata(key string) error { return nil } -// DeleteMetadataAsync calls private function deleteMetadata() with vAppTemplate.client and vAppTemplate.VAppTemplate.HREF -// which deletes metadata depending on key provided as input from catalog item. +// Deprecated: use VAppTemplate.DeleteMetadataEntryAsync. func (vAppTemplate *VAppTemplate) DeleteMetadataAsync(key string) (Task, error) { - return deleteMetadata(vAppTemplate.client, key, vAppTemplate.VAppTemplate.HREF) -} - -// GetMetadata calls private function getMetadata() with mediaItem.client and mediaItem.MediaItem.HREF -// which returns a *types.Metadata struct for provided media item input. -// Deprecated: Use MediaRecord.GetMetadata -func (mediaItem *MediaItem) GetMetadata() (*types.Metadata, error) { - return getMetadata(mediaItem.vdc.client, mediaItem.MediaItem.HREF) + return deleteMetadata(vAppTemplate.client, vAppTemplate.VAppTemplate.HREF, vAppTemplate.VAppTemplate.Name, key, false) } -// AddMetadata adds metadata key/value pair provided as input. -// Deprecated: Use MediaRecord.AddMetadata -func (mediaItem *MediaItem) AddMetadata(key string, value string) (*MediaItem, error) { - task, err := mediaItem.AddMetadataAsync(key, value) +// Deprecated: use Media.AddMetadataEntry. +func (media *Media) AddMetadata(key string, value string) (*Media, error) { + task, err := media.AddMetadataAsync(key, value) if err != nil { return nil, err } @@ -220,25 +1193,22 @@ func (mediaItem *MediaItem) AddMetadata(key string, value string) (*MediaItem, e return nil, fmt.Errorf("error completing add metadata for media item task: %s", err) } - err = mediaItem.Refresh() + err = media.Refresh() if err != nil { return nil, fmt.Errorf("error refreshing media item: %s", err) } - return mediaItem, nil + return media, nil } -// AddMetadataAsync calls private function addMetadata() with mediaItem.client and mediaItem.MediaItem.HREF -// which adds metadata key/value pair provided as input. -// Deprecated: Use MediaRecord.AddMetadataAsync -func (mediaItem *MediaItem) AddMetadataAsync(key string, value string) (Task, error) { - return addMetadata(mediaItem.vdc.client, key, value, mediaItem.MediaItem.HREF) +// Deprecated: use Media.AddMetadataEntryAsync. +func (media *Media) AddMetadataAsync(key string, value string) (Task, error) { + return addMetadataDeprecated(media.client, types.MetadataStringValue, key, value, media.Media.HREF) } -// DeleteMetadata deletes metadata depending on key provided as input from media item. -// Deprecated: Use MediaRecord.DeleteMetadata -func (mediaItem *MediaItem) DeleteMetadata(key string) error { - task, err := mediaItem.DeleteMetadataAsync(key) +// Deprecated: use Media.DeleteMetadataEntry. +func (media *Media) DeleteMetadata(key string) error { + task, err := media.DeleteMetadataAsync(key) if err != nil { return err } @@ -250,22 +1220,21 @@ func (mediaItem *MediaItem) DeleteMetadata(key string) error { return nil } -// DeleteMetadataAsync calls private function deleteMetadata() with mediaItem.client and mediaItem.MediaItem.HREF -// which deletes metadata depending on key provided as input from media item. -// Deprecated: Use MediaRecord.DeleteMetadataAsync -func (mediaItem *MediaItem) DeleteMetadataAsync(key string) (Task, error) { - return deleteMetadata(mediaItem.vdc.client, key, mediaItem.MediaItem.HREF) +// Deprecated: use Media.DeleteMetadataEntryAsync. +func (media *Media) DeleteMetadataAsync(key string) (Task, error) { + return deleteMetadata(media.client, media.Media.HREF, media.Media.Name, key, false) } -// GetMetadata calls private function getMetadata() with MediaRecord.client and MediaRecord.MediaRecord.HREF -// which returns a *types.Metadata struct for provided media item input. -func (mediaRecord *MediaRecord) GetMetadata() (*types.Metadata, error) { - return getMetadata(mediaRecord.client, mediaRecord.MediaRecord.HREF) +// GetMetadata returns MediaItem metadata. +// Deprecated: Use MediaRecord.GetMetadata. +func (mediaItem *MediaItem) GetMetadata() (*types.Metadata, error) { + return getMetadata(mediaItem.vdc.client, mediaItem.MediaItem.HREF, mediaItem.MediaItem.Name) } // AddMetadata adds metadata key/value pair provided as input. -func (mediaRecord *MediaRecord) AddMetadata(key string, value string) (*MediaRecord, error) { - task, err := mediaRecord.AddMetadataAsync(key, value) +// Deprecated: Use MediaRecord.AddMetadata. +func (mediaItem *MediaItem) AddMetadata(key string, value string) (*MediaItem, error) { + task, err := mediaItem.AddMetadataAsync(key, value) if err != nil { return nil, err } @@ -274,23 +1243,23 @@ func (mediaRecord *MediaRecord) AddMetadata(key string, value string) (*MediaRec return nil, fmt.Errorf("error completing add metadata for media item task: %s", err) } - err = mediaRecord.Refresh() + err = mediaItem.Refresh() if err != nil { return nil, fmt.Errorf("error refreshing media item: %s", err) } - return mediaRecord, nil + return mediaItem, nil } -// AddMetadataAsync calls private function addMetadata() with MediaRecord.client and MediaRecord.MediaRecord.HREF -// which adds metadata key/value pair provided as input. -func (mediaRecord *MediaRecord) AddMetadataAsync(key string, value string) (Task, error) { - return addMetadata(mediaRecord.client, key, value, mediaRecord.MediaRecord.HREF) +// Deprecated: use MediaItem.AddMetadataEntryAsync. +func (mediaItem *MediaItem) AddMetadataAsync(key string, value string) (Task, error) { + return addMetadataDeprecated(mediaItem.vdc.client, types.MetadataStringValue, key, value, mediaItem.MediaItem.HREF) } // DeleteMetadata deletes metadata depending on key provided as input from media item. -func (mediaRecord *MediaRecord) DeleteMetadata(key string) error { - task, err := mediaRecord.DeleteMetadataAsync(key) +// Deprecated: Use MediaRecord.DeleteMetadata. +func (mediaItem *MediaItem) DeleteMetadata(key string) error { + task, err := mediaItem.DeleteMetadataAsync(key) if err != nil { return err } @@ -302,21 +1271,15 @@ func (mediaRecord *MediaRecord) DeleteMetadata(key string) error { return nil } -// DeleteMetadataAsync calls private function deleteMetadata() with MediaRecord.client and MediaRecord.MediaRecord.HREF -// which deletes metadata depending on key provided as input from media item. -func (mediaRecord *MediaRecord) DeleteMetadataAsync(key string) (Task, error) { - return deleteMetadata(mediaRecord.client, key, mediaRecord.MediaRecord.HREF) -} - -// GetMetadata calls private function getMetadata() with Media.client and Media.Media.HREF -// which returns a *types.Metadata struct for provided media item input. -func (media *Media) GetMetadata() (*types.Metadata, error) { - return getMetadata(media.client, media.Media.HREF) +// DeleteMetadataAsync deletes metadata depending on key provided as input from MediaItem. +// Deprecated: Use MediaRecord.DeleteMetadataAsync. +func (mediaItem *MediaItem) DeleteMetadataAsync(key string) (Task, error) { + return deleteMetadata(mediaItem.vdc.client, mediaItem.MediaItem.HREF, mediaItem.MediaItem.Name, key, false) } -// AddMetadata adds metadata key/value pair provided as input. -func (media *Media) AddMetadata(key string, value string) (*Media, error) { - task, err := media.AddMetadataAsync(key, value) +// Deprecated: use MediaRecord.AddMetadataEntry. +func (mediaRecord *MediaRecord) AddMetadata(key string, value string) (*MediaRecord, error) { + task, err := mediaRecord.AddMetadataAsync(key, value) if err != nil { return nil, err } @@ -325,23 +1288,22 @@ func (media *Media) AddMetadata(key string, value string) (*Media, error) { return nil, fmt.Errorf("error completing add metadata for media item task: %s", err) } - err = media.Refresh() + err = mediaRecord.Refresh() if err != nil { return nil, fmt.Errorf("error refreshing media item: %s", err) } - return media, nil + return mediaRecord, nil } -// AddMetadataAsync calls private function addMetadata() with Media.client and Media.Media.HREF -// which adds metadata key/value pair provided as input. -func (media *Media) AddMetadataAsync(key string, value string) (Task, error) { - return addMetadata(media.client, key, value, media.Media.HREF) +// Deprecated: use MediaRecord.AddMetadataEntryAsync. +func (mediaRecord *MediaRecord) AddMetadataAsync(key string, value string) (Task, error) { + return addMetadataDeprecated(mediaRecord.client, types.MetadataStringValue, key, value, mediaRecord.MediaRecord.HREF) } -// DeleteMetadata deletes metadata depending on key provided as input from media item. -func (media *Media) DeleteMetadata(key string) error { - task, err := media.DeleteMetadataAsync(key) +// Deprecated: use MediaRecord.DeleteMetadataEntry. +func (mediaRecord *MediaRecord) DeleteMetadata(key string) error { + task, err := mediaRecord.DeleteMetadataAsync(key) if err != nil { return err } @@ -353,8 +1315,7 @@ func (media *Media) DeleteMetadata(key string) error { return nil } -// DeleteMetadataAsync calls private function deleteMetadata() with Media.client and Media.Media.HREF -// which deletes metadata depending on key provided as input from media item. -func (media *Media) DeleteMetadataAsync(key string) (Task, error) { - return deleteMetadata(media.client, key, media.Media.HREF) +// Deprecated: use MediaRecord.DeleteMetadataEntryAsync. +func (mediaRecord *MediaRecord) DeleteMetadataAsync(key string) (Task, error) { + return deleteMetadata(mediaRecord.client, mediaRecord.MediaRecord.HREF, mediaRecord.MediaRecord.Name, key, false) } diff --git a/govcd/metadata_openapi.go b/govcd/metadata_openapi.go new file mode 100644 index 000000000..f60c79ea8 --- /dev/null +++ b/govcd/metadata_openapi.go @@ -0,0 +1,298 @@ +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "github.com/vmware/go-vcloud-director/v2/types/v56" + "net/url" + "strings" +) + +// OpenApiMetadataEntry is a wrapper object for types.OpenApiMetadataEntry +type OpenApiMetadataEntry struct { + MetadataEntry *types.OpenApiMetadataEntry + client *Client + Etag string // Allows concurrent operations with metadata + href string // This is the HREF of the given metadata entry + parentEndpoint string // This is the endpoint of the object that has the metadata entries +} + +// --------------------------------------------------------------------------------------------------------------------- +// Specific objects compatible with metadata +// --------------------------------------------------------------------------------------------------------------------- + +// GetMetadata returns all the metadata from a DefinedEntity. +// NOTE: The obtained metadata doesn't have ETags, use GetMetadataById or GetMetadataByKey to obtain a ETag for a specific entry. +func (rde *DefinedEntity) GetMetadata() ([]*OpenApiMetadataEntry, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntities + return getAllOpenApiMetadata(rde.client, endpoint, rde.DefinedEntity.ID, rde.DefinedEntity.Name, "entity", nil) +} + +// GetMetadataByKey returns a unique DefinedEntity metadata entry corresponding to the given domain, namespace and key. +// The domain and namespace are only needed when there's more than one entry with the same key. +// This is a more costly operation than GetMetadataById due to ETags, so use that preferred option whenever possible. +func (rde *DefinedEntity) GetMetadataByKey(domain, namespace, key string) (*OpenApiMetadataEntry, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntities + return getOpenApiMetadataByKey(rde.client, endpoint, rde.DefinedEntity.ID, rde.DefinedEntity.Name, "entity", domain, namespace, key) +} + +// GetMetadataById returns a unique DefinedEntity metadata entry corresponding to the given domain, namespace and key. +// The domain and namespace are only needed when there's more than one entry with the same key. +func (rde *DefinedEntity) GetMetadataById(id string) (*OpenApiMetadataEntry, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntities + return getOpenApiMetadataById(rde.client, endpoint, rde.DefinedEntity.ID, rde.DefinedEntity.Name, "entity", id) +} + +// AddMetadata adds metadata to the receiver DefinedEntity. +func (rde *DefinedEntity) AddMetadata(metadataEntry types.OpenApiMetadataEntry) (*OpenApiMetadataEntry, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntities + return addOpenApiMetadata(rde.client, endpoint, rde.DefinedEntity.ID, metadataEntry) +} + +// --------------------------------------------------------------------------------------------------------------------- +// Metadata Entry methods for OpenAPI metadata +// --------------------------------------------------------------------------------------------------------------------- + +// Update updates the metadata value from the receiver entry. +// Only the value and persistence of the entry can be updated. Re-create the entry in case you want to modify any of the other fields. +func (entry *OpenApiMetadataEntry) Update(value interface{}, persistent bool) error { + if entry.MetadataEntry.ID == "" { + return fmt.Errorf("ID of the receiver metadata entry is empty") + } + + payload := types.OpenApiMetadataEntry{ + ID: entry.MetadataEntry.ID, + IsPersistent: persistent, + IsReadOnly: entry.MetadataEntry.IsReadOnly, + KeyValue: types.OpenApiMetadataKeyValue{ + Domain: entry.MetadataEntry.KeyValue.Domain, + Key: entry.MetadataEntry.KeyValue.Key, + Value: types.OpenApiMetadataTypedValue{ + Value: value, + Type: entry.MetadataEntry.KeyValue.Value.Type, + }, + Namespace: entry.MetadataEntry.KeyValue.Namespace, + }, + } + + apiVersion, err := entry.client.getOpenApiHighestElevatedVersion(entry.parentEndpoint) + if err != nil { + return err + } + + urlRef, err := url.ParseRequestURI(entry.href) + if err != nil { + return err + } + + headers, err := entry.client.OpenApiPutItemAndGetHeaders(apiVersion, urlRef, nil, payload, entry.MetadataEntry, map[string]string{"If-Match": entry.Etag}) + if err != nil { + return err + } + entry.Etag = headers.Get("Etag") + return nil +} + +// Delete deletes the receiver metadata entry. +func (entry *OpenApiMetadataEntry) Delete() error { + if entry.MetadataEntry.ID == "" { + return fmt.Errorf("ID of the receiver metadata entry is empty") + } + + apiVersion, err := entry.client.getOpenApiHighestElevatedVersion(entry.parentEndpoint) + if err != nil { + return err + } + + urlRef, err := url.ParseRequestURI(entry.href) + if err != nil { + return err + } + + err = entry.client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) + if err != nil { + return err + } + + entry.Etag = "" + entry.parentEndpoint = "" + entry.href = "" + entry.MetadataEntry = &types.OpenApiMetadataEntry{} + return nil +} + +// --------------------------------------------------------------------------------------------------------------------- +// OpenAPI Metadata private functions +// --------------------------------------------------------------------------------------------------------------------- + +// getAllOpenApiMetadata is a generic function to retrieve all metadata from any VCD object using its ID and the given OpenAPI endpoint. +// It supports query parameters to input, for example, filtering options. +func getAllOpenApiMetadata(client *Client, endpoint, objectId, objectName, objectType string, queryParameters url.Values) ([]*OpenApiMetadataEntry, error) { + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, fmt.Sprintf("%s/metadata", objectId)) + if err != nil { + return nil, err + } + + allMetadata := []*types.OpenApiMetadataEntry{{}} + err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParameters, &allMetadata, nil) + if err != nil { + return nil, err + } + + var filteredMetadata []*types.OpenApiMetadataEntry + for _, entry := range allMetadata { + _, err = filterSingleOpenApiMetadataEntry(objectType, objectName, entry, client.IgnoredMetadata) + if err != nil { + if strings.Contains(err.Error(), "is being ignored") { + continue + } + return nil, err + } + filteredMetadata = append(filteredMetadata, entry) + } + + // Wrap all type.OpenApiMetadataEntry into OpenApiMetadataEntry types with client + results := make([]*OpenApiMetadataEntry, len(filteredMetadata)) + for i := range filteredMetadata { + results[i] = &OpenApiMetadataEntry{ + MetadataEntry: filteredMetadata[i], + client: client, + href: fmt.Sprintf("%s/%s", urlRef.String(), filteredMetadata[i].ID), + parentEndpoint: endpoint, + } + } + + return results, nil +} + +// getOpenApiMetadataByKey is a generic function to retrieve a unique metadata entry from any VCD object using its domain, namespace and key. +// The domain and namespace are only needed when there's more than one entry with the same key. +func getOpenApiMetadataByKey(client *Client, endpoint, objectId, objectName, objectType string, domain, namespace, key string) (*OpenApiMetadataEntry, error) { + queryParameters := url.Values{} + // As for now, the filter only supports filtering by key + queryParameters.Add("filter", fmt.Sprintf("keyValue.key==%s", key)) + metadata, err := getAllOpenApiMetadata(client, endpoint, objectId, objectName, objectType, queryParameters) + if err != nil { + return nil, err + } + + if len(metadata) == 0 { + return nil, fmt.Errorf("%s could not find the metadata associated to object %s", ErrorEntityNotFound, objectId) + } + + // There's more than one entry with same key, the namespace and domain need to be compared to be able to filter. + if len(metadata) > 1 { + var filteredMetadata []*OpenApiMetadataEntry + for _, entry := range metadata { + if entry.MetadataEntry.KeyValue.Namespace == namespace && entry.MetadataEntry.KeyValue.Domain == domain { + filteredMetadata = append(filteredMetadata, entry) + } + } + if len(filteredMetadata) > 1 { + return nil, fmt.Errorf("found %d metadata entries associated to object %s", len(filteredMetadata), objectId) + } + // Required to retrieve an ETag + return getOpenApiMetadataById(client, endpoint, objectId, objectName, objectType, filteredMetadata[0].MetadataEntry.ID) + } + + // Required to retrieve an ETag + return getOpenApiMetadataById(client, endpoint, objectId, objectName, objectType, metadata[0].MetadataEntry.ID) +} + +// getOpenApiMetadataById is a generic function to retrieve a unique metadata entry from any VCD object using its unique ID. +func getOpenApiMetadataById(client *Client, endpoint, objectId, objectName, objectType, metadataId string) (*OpenApiMetadataEntry, error) { + if metadataId == "" { + return nil, fmt.Errorf("input metadata entry ID is empty") + } + + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, fmt.Sprintf("%s/metadata/%s", objectId, metadataId)) + if err != nil { + return nil, err + } + + response := &OpenApiMetadataEntry{ + MetadataEntry: &types.OpenApiMetadataEntry{}, + client: client, + href: urlRef.String(), + parentEndpoint: endpoint, + } + + headers, err := client.OpenApiGetItemAndHeaders(apiVersion, urlRef, nil, response.MetadataEntry, nil) + if err != nil { + return nil, err + } + + _, err = filterSingleOpenApiMetadataEntry(objectType, objectName, response.MetadataEntry, client.IgnoredMetadata) + if err != nil { + return nil, err + } + + response.Etag = headers.Get("Etag") + return response, nil +} + +// addOpenApiMetadata adds one metadata entry to the VCD object with given ID +func addOpenApiMetadata(client *Client, endpoint, objectId string, metadataEntry types.OpenApiMetadataEntry) (*OpenApiMetadataEntry, error) { + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, fmt.Sprintf("%s/metadata", objectId)) + if err != nil { + return nil, err + } + + response := &OpenApiMetadataEntry{ + client: client, + MetadataEntry: &types.OpenApiMetadataEntry{}, + parentEndpoint: endpoint, + } + headers, err := client.OpenApiPostItemAndGetHeaders(apiVersion, urlRef, nil, metadataEntry, response.MetadataEntry, nil) + if err != nil { + return nil, err + } + response.Etag = headers.Get("Etag") + response.href = fmt.Sprintf("%s/%s", urlRef.String(), response.MetadataEntry.ID) + return response, nil +} + +// --------------------------------------------------------------------------------------------------------------------- +// Ignore OpenAPI Metadata feature +// --------------------------------------------------------------------------------------------------------------------- + +// normaliseOpenApiMetadata transforms OpenAPI metadata into a normalised structure +func normaliseOpenApiMetadata(objectType, name string, metadataEntry *types.OpenApiMetadataEntry) (*normalisedMetadata, error) { + return &normalisedMetadata{ + ObjectType: objectType, + ObjectName: name, + Key: metadataEntry.KeyValue.Key, + Value: fmt.Sprintf("%v", metadataEntry.KeyValue.Value.Value), + }, nil +} + +// filterSingleOpenApiMetadataEntry filters a single OpenAPI metadata entry depending on the contents of the input ignored metadata slice. +func filterSingleOpenApiMetadataEntry(objectType, objectName string, metadataEntry *types.OpenApiMetadataEntry, metadataToIgnore []IgnoredMetadata) (*types.OpenApiMetadataEntry, error) { + normalisedEntry, err := normaliseOpenApiMetadata(objectType, objectName, metadataEntry) + if err != nil { + return nil, err + } + isFiltered := filterSingleGenericMetadataEntry(normalisedEntry, metadataToIgnore) + if isFiltered { + return nil, fmt.Errorf("the metadata entry with key '%s' and value '%v' is being ignored", metadataEntry.KeyValue.Key, metadataEntry.KeyValue.Value.Value) + } + return metadataEntry, nil +} diff --git a/govcd/metadata_openapi_test.go b/govcd/metadata_openapi_test.go new file mode 100644 index 000000000..a8ec95c8c --- /dev/null +++ b/govcd/metadata_openapi_test.go @@ -0,0 +1,395 @@ +//go:build metadata || openapi || rde || functional || ALL + +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" + "regexp" + "strings" +) + +func (vcd *TestVCD) TestRdeMetadata(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + + // This RDE type comes out of the box in VCD + rdeType, err := vcd.client.GetRdeType("vmware", "tkgcluster", "1.0.0") + check.Assert(err, IsNil) + check.Assert(rdeType, NotNil) + + rde, err := rdeType.CreateRde(types.DefinedEntity{ + Name: check.TestName(), + Entity: map[string]interface{}{"foo": "bar"}, // We don't care about schema correctness here + }, nil) + check.Assert(err, IsNil) + check.Assert(rde, NotNil) + + err = rde.Resolve() // State will be RESOLUTION_ERROR, but we don't care. We resolve to be able to delete it later. + check.Assert(err, IsNil) + + // The RDE can't be deleted until rde.Resolve() is called + AddToCleanupListOpenApi(rde.DefinedEntity.ID, check.TestName(), types.OpenApiPathVersion1_0_0+types.OpenApiEndpointRdeEntities+rde.DefinedEntity.ID) + + testOpenApiMetadataCRUDActions(rde, check) + vcd.testOpenApiMetadataIgnore(rde, "entity", rde.DefinedEntity.Name, check) + + err = rde.Delete() + check.Assert(err, IsNil) +} + +// openApiMetadataCompatible allows centralizing and generalizing the tests for OpenAPI metadata compatible resources. +type openApiMetadataCompatible interface { + GetMetadata() ([]*OpenApiMetadataEntry, error) + GetMetadataByKey(domain, namespace, key string) (*OpenApiMetadataEntry, error) + GetMetadataById(id string) (*OpenApiMetadataEntry, error) + AddMetadata(metadataEntry types.OpenApiMetadataEntry) (*OpenApiMetadataEntry, error) +} + +type openApiMetadataTest struct { + Key string + Value interface{} // The type depends on the Type attribute + UpdateValue interface{} + Namespace string + Type string + IsReadOnly bool + IsPersistent bool + Domain string + ExpectErrorOnFirstAddMatchesRegex string +} + +// testOpenApiMetadataCRUDActions performs a complete test of all use cases that metadata in OpenAPI can have, +// for an OpenAPI metadata compatible resource. +func testOpenApiMetadataCRUDActions(resource openApiMetadataCompatible, check *C) { + // Check how much metadata exists + metadata, err := resource.GetMetadata() + check.Assert(err, IsNil) + check.Assert(metadata, NotNil) + existingMetaDataCount := len(metadata) + + var testCases = []openApiMetadataTest{ + { + Key: "stringKey", + Value: "stringValue", + UpdateValue: "stringValueUpdated", + Type: types.OpenApiMetadataStringEntry, + IsReadOnly: false, + Domain: "TENANT", + Namespace: "foo", + }, + { + Key: "numberKey", + Value: "notANumber", + Type: types.OpenApiMetadataNumberEntry, + IsReadOnly: false, + Domain: "TENANT", + Namespace: "foo", + ExpectErrorOnFirstAddMatchesRegex: "notANumber", + }, + { + Key: "numberKey", + Value: float64(1), + UpdateValue: float64(42), + Type: types.OpenApiMetadataNumberEntry, + IsReadOnly: false, + Domain: "TENANT", + Namespace: "foo", + }, + { + Key: "negativeNumberKey", + Value: float64(-1), + UpdateValue: float64(-42), + Type: types.OpenApiMetadataNumberEntry, + IsReadOnly: false, + Domain: "TENANT", + Namespace: "foo", + }, + { + Key: "boolKey", + Value: "notABool", + Type: types.OpenApiMetadataBooleanEntry, + IsReadOnly: false, + Domain: "TENANT", + Namespace: "foo", + ExpectErrorOnFirstAddMatchesRegex: "notABool", + }, + { + Key: "boolKey", + Value: true, + UpdateValue: false, + Type: types.OpenApiMetadataBooleanEntry, + IsReadOnly: false, + Domain: "TENANT", + Namespace: "foo", + }, + { + Key: "providerKey", + Value: "providerValue", + UpdateValue: "providerValueUpdated", + Type: types.OpenApiMetadataStringEntry, + IsReadOnly: false, + Domain: "PROVIDER", + Namespace: "foo", + }, + { + Key: "readOnlyProviderKey", + Value: "readOnlyProviderValue", + Type: types.OpenApiMetadataStringEntry, + IsReadOnly: true, + Domain: "PROVIDER", + Namespace: "foo", + ExpectErrorOnFirstAddMatchesRegex: "VCD_META_CRUD_INVALID_FLAG", + }, + { + Key: "readOnlyTenantKey", + Value: "readOnlyTenantValue", + Type: types.OpenApiMetadataStringEntry, + IsReadOnly: true, + Domain: "TENANT", + Namespace: "foo", + }, + { + Key: "persistentKey", + Value: "persistentValue", + Type: types.OpenApiMetadataStringEntry, + IsReadOnly: false, + IsPersistent: true, + Domain: "TENANT", + Namespace: "foo", + }, + } + + for _, testCase := range testCases { + + var createdEntry *OpenApiMetadataEntry + createdEntry, err = resource.AddMetadata(types.OpenApiMetadataEntry{ + KeyValue: types.OpenApiMetadataKeyValue{ + Domain: testCase.Domain, + Key: testCase.Key, + Namespace: testCase.Namespace, + Value: types.OpenApiMetadataTypedValue{ + Type: testCase.Type, + Value: testCase.Value, + }, + }, + IsPersistent: testCase.IsPersistent, + IsReadOnly: testCase.IsReadOnly, + }) + if testCase.ExpectErrorOnFirstAddMatchesRegex != "" { + p := regexp.MustCompile("(?s)" + testCase.ExpectErrorOnFirstAddMatchesRegex) + check.Assert(p.MatchString(err.Error()), Equals, true) + continue + } + check.Assert(err, IsNil) + check.Assert(createdEntry, NotNil) + check.Assert(createdEntry.href, Not(Equals), "") + check.Assert(createdEntry.Etag, Not(Equals), "") + check.Assert(createdEntry.parentEndpoint, Not(Equals), "") + check.Assert(createdEntry.MetadataEntry, NotNil) + check.Assert(createdEntry.MetadataEntry.ID, Not(Equals), "") + + // Check if metadata was added correctly + metadata, err = resource.GetMetadata() + check.Assert(err, IsNil) + check.Assert(len(metadata), Equals, existingMetaDataCount+1) + found := false + for _, entry := range metadata { + if entry.MetadataEntry.ID == createdEntry.MetadataEntry.ID { + check.Assert(*entry.MetadataEntry, DeepEquals, *createdEntry.MetadataEntry) + found = true + break + } + } + check.Assert(found, Equals, true) + + metadataByKey, err := resource.GetMetadataByKey(createdEntry.MetadataEntry.KeyValue.Domain, createdEntry.MetadataEntry.KeyValue.Namespace, createdEntry.MetadataEntry.KeyValue.Key) + check.Assert(err, IsNil) + check.Assert(metadataByKey, NotNil) + check.Assert(metadataByKey.MetadataEntry, NotNil) + check.Assert(*metadataByKey.MetadataEntry, DeepEquals, *createdEntry.MetadataEntry) + check.Assert(metadataByKey.Etag, Equals, createdEntry.Etag) + check.Assert(metadataByKey.parentEndpoint, Equals, createdEntry.parentEndpoint) + check.Assert(metadataByKey.href, Equals, createdEntry.href) + + metadataById, err := resource.GetMetadataById(metadataByKey.MetadataEntry.ID) + check.Assert(err, IsNil) + check.Assert(metadataById, NotNil) + check.Assert(metadataById.MetadataEntry, NotNil) + check.Assert(*metadataById.MetadataEntry, DeepEquals, *metadataById.MetadataEntry) + check.Assert(metadataById.Etag, Equals, metadataByKey.Etag) + check.Assert(metadataById.parentEndpoint, Equals, metadataByKey.parentEndpoint) + check.Assert(metadataById.href, Equals, metadataByKey.href) + + if testCase.UpdateValue != nil { + oldEtag := metadataById.Etag + err = metadataById.Update(testCase.UpdateValue, !metadataById.MetadataEntry.IsPersistent) + check.Assert(err, IsNil) + check.Assert(metadataById, NotNil) + check.Assert(metadataById.MetadataEntry, NotNil) + // Changed fields + check.Assert(metadataById.MetadataEntry.IsPersistent, Equals, !metadataByKey.MetadataEntry.IsPersistent) + check.Assert(metadataById.MetadataEntry.KeyValue.Value.Value, Equals, testCase.UpdateValue) + // Non-changed fields + check.Assert(metadataById.MetadataEntry.ID, Equals, metadataByKey.MetadataEntry.ID) + check.Assert(metadataById.MetadataEntry.KeyValue.Value.Type, Equals, metadataByKey.MetadataEntry.KeyValue.Value.Type) + check.Assert(metadataById.MetadataEntry.KeyValue.Namespace, Equals, metadataByKey.MetadataEntry.KeyValue.Namespace) + check.Assert(metadataById.MetadataEntry.IsReadOnly, Equals, metadataByKey.MetadataEntry.IsReadOnly) + check.Assert(metadataById.Etag, Not(Equals), oldEtag) // ETag should be refreshed as we did an update + check.Assert(metadataById.parentEndpoint, Equals, metadataByKey.parentEndpoint) + check.Assert(metadataById.href, Equals, metadataByKey.href) + } + + err = metadataById.Delete() + check.Assert(err, IsNil) + check.Assert(*metadataById.MetadataEntry, DeepEquals, types.OpenApiMetadataEntry{}) + check.Assert(metadataById.Etag, Equals, "") + check.Assert(metadataById.href, Equals, "") + check.Assert(metadataById.parentEndpoint, Equals, "") + + // Check if metadata was deleted correctly + deletedMetadata, err := resource.GetMetadataById(metadataByKey.MetadataEntry.ID) + check.Assert(err, NotNil) + check.Assert(deletedMetadata, IsNil) + check.Assert(true, Equals, ContainsNotFound(err)) + } +} + +func (vcd *TestVCD) testOpenApiMetadataIgnore(resource openApiMetadataCompatible, objectType, objectName string, check *C) { + existingMetadata, err := resource.GetMetadata() + check.Assert(err, IsNil) + + _, err = resource.AddMetadata(types.OpenApiMetadataEntry{ + IsPersistent: false, + IsReadOnly: false, + KeyValue: types.OpenApiMetadataKeyValue{ + Domain: "TENANT", + Key: "foo", + Value: types.OpenApiMetadataTypedValue{ + Value: "bar", + Type: types.OpenApiMetadataStringEntry, + }, + Namespace: "", + }, + }) + check.Assert(err, IsNil) + _, err = resource.AddMetadata(types.OpenApiMetadataEntry{ + IsPersistent: false, + IsReadOnly: false, + KeyValue: types.OpenApiMetadataKeyValue{ + Domain: "TENANT", + Key: "not_ignored", + Value: types.OpenApiMetadataTypedValue{ + Value: "bar2", + Type: types.OpenApiMetadataStringEntry, + }, + Namespace: "", + }, + }) + check.Assert(err, IsNil) + + cleanup := func() { + vcd.client.Client.IgnoredMetadata = nil + metadata, err := resource.GetMetadata() + check.Assert(err, IsNil) + for _, entry := range metadata { + itWasAlreadyPresent := false + for _, existingEntry := range existingMetadata { + if existingEntry.MetadataEntry.KeyValue.Namespace == entry.MetadataEntry.KeyValue.Namespace && + existingEntry.MetadataEntry.KeyValue.Key == entry.MetadataEntry.KeyValue.Key && + existingEntry.MetadataEntry.KeyValue.Value.Value == entry.MetadataEntry.KeyValue.Value.Value && + existingEntry.MetadataEntry.KeyValue.Value.Type == entry.MetadataEntry.KeyValue.Value.Type { + itWasAlreadyPresent = true + } + } + if !itWasAlreadyPresent { + toDelete, err := resource.GetMetadataById(entry.MetadataEntry.ID) + check.Assert(err, IsNil) + err = toDelete.Delete() + check.Assert(err, IsNil) + } + } + metadata, err = resource.GetMetadata() + check.Assert(err, IsNil) + check.Assert(len(metadata), Equals, len(existingMetadata)) + } + defer cleanup() + + tests := []struct { + ignoredMetadata []IgnoredMetadata + metadataIsIgnored bool + }{ + { + ignoredMetadata: []IgnoredMetadata{{ObjectType: &objectType, KeyRegex: regexp.MustCompile(`^foo$`)}}, + metadataIsIgnored: true, + }, + { + ignoredMetadata: []IgnoredMetadata{{ObjectType: &objectType, ValueRegex: regexp.MustCompile(`^bar$`)}}, + metadataIsIgnored: true, + }, + { + ignoredMetadata: []IgnoredMetadata{{ObjectType: &objectType, KeyRegex: regexp.MustCompile(`^fizz$`)}}, + metadataIsIgnored: false, + }, + { + ignoredMetadata: []IgnoredMetadata{{ObjectType: &objectType, ValueRegex: regexp.MustCompile(`^buzz$`)}}, + metadataIsIgnored: false, + }, + { + ignoredMetadata: []IgnoredMetadata{{ObjectName: &objectName, KeyRegex: regexp.MustCompile(`^foo$`)}}, + metadataIsIgnored: true, + }, + { + ignoredMetadata: []IgnoredMetadata{{ObjectName: &objectName, ValueRegex: regexp.MustCompile(`^bar$`)}}, + metadataIsIgnored: true, + }, + { + ignoredMetadata: []IgnoredMetadata{{ObjectName: &objectName, KeyRegex: regexp.MustCompile(`^fizz$`)}}, + metadataIsIgnored: false, + }, + { + ignoredMetadata: []IgnoredMetadata{{ObjectName: &objectName, ValueRegex: regexp.MustCompile(`^buzz$`)}}, + metadataIsIgnored: false, + }, + { + ignoredMetadata: []IgnoredMetadata{{ObjectType: &objectType, ObjectName: &objectName, KeyRegex: regexp.MustCompile(`foo`), ValueRegex: regexp.MustCompile(`bar`)}}, + metadataIsIgnored: true, + }, + } + + for _, tt := range tests { + vcd.client.Client.IgnoredMetadata = tt.ignoredMetadata + + // Tests getting a simple metadata entry by its key + singleMetadata, err := resource.GetMetadataByKey("", "", "foo") + if tt.metadataIsIgnored { + check.Assert(err, NotNil) + check.Assert(true, Equals, strings.Contains(err.Error(), "could not find the metadata associated to object")) + } else { + check.Assert(err, IsNil) + check.Assert(singleMetadata, NotNil) + check.Assert(singleMetadata.MetadataEntry.KeyValue.Value.Value, Equals, "bar") + } + + // Retrieve all metadata + allMetadata, err := resource.GetMetadata() + check.Assert(err, IsNil) + check.Assert(allMetadata, NotNil) + if tt.metadataIsIgnored { + // If metadata is ignored, there should be an offset of 1 entry (with key "test") + check.Assert(len(allMetadata), Equals, len(existingMetadata)+1) + for _, entry := range allMetadata { + if tt.metadataIsIgnored { + check.Assert(entry.MetadataEntry.KeyValue.Key, Not(Equals), "foo") + check.Assert(entry.MetadataEntry.KeyValue.Value.Value, Not(Equals), "bar") + } + } + } else { + // If metadata is NOT ignored, there should be an offset of 2 entries (with key "foo" and "test") + check.Assert(len(allMetadata), Equals, len(existingMetadata)+2) + } + } +} diff --git a/govcd/metadata_openapi_unit_test.go b/govcd/metadata_openapi_unit_test.go new file mode 100644 index 000000000..151ec10fd --- /dev/null +++ b/govcd/metadata_openapi_unit_test.go @@ -0,0 +1,109 @@ +//go:build unit || ALL + +/* +* Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "github.com/vmware/go-vcloud-director/v2/types/v56" + "reflect" + "testing" +) + +func Test_normaliseOpenApiMetadata(t *testing.T) { + type args struct { + objectType string + name string + metadataEntry *types.OpenApiMetadataEntry + } + tests := []struct { + name string + args args + want *normalisedMetadata + wantErr bool + }{ + { + name: "number normalised", + args: args{ + objectType: "entity", + name: "foo", + metadataEntry: &types.OpenApiMetadataEntry{ + KeyValue: types.OpenApiMetadataKeyValue{ + Domain: "TENANT", + Key: "key", + Value: types.OpenApiMetadataTypedValue{ + Value: 314159, + Type: types.OpenApiMetadataNumberEntry, + }, + }, + }, + }, + want: &normalisedMetadata{ + ObjectType: "entity", + ObjectName: "foo", + Key: "key", + Value: "314159", + }, + }, + { + name: "string normalised", + args: args{ + objectType: "entity", + name: "foo", + metadataEntry: &types.OpenApiMetadataEntry{ + KeyValue: types.OpenApiMetadataKeyValue{ + Domain: "TENANT", + Key: "key", + Value: types.OpenApiMetadataTypedValue{ + Value: "value", + Type: types.OpenApiMetadataStringEntry, + }, + }, + }, + }, + want: &normalisedMetadata{ + ObjectType: "entity", + ObjectName: "foo", + Key: "key", + Value: "value", + }, + }, + { + name: "bool normalised", + args: args{ + objectType: "entity", + name: "foo", + metadataEntry: &types.OpenApiMetadataEntry{ + KeyValue: types.OpenApiMetadataKeyValue{ + Domain: "TENANT", + Key: "key", + Value: types.OpenApiMetadataTypedValue{ + Value: true, + Type: types.OpenApiMetadataBooleanEntry, + }, + }, + }, + }, + want: &normalisedMetadata{ + ObjectType: "entity", + ObjectName: "foo", + Key: "key", + Value: "true", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := normaliseOpenApiMetadata(tt.args.objectType, tt.args.name, tt.args.metadataEntry) + if (err != nil) != tt.wantErr { + t.Errorf("normaliseOpenApiMetadata() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("normaliseOpenApiMetadata() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/govcd/metadata_test.go b/govcd/metadata_test.go index f2ce2826b..dab4fd09d 100644 --- a/govcd/metadata_test.go +++ b/govcd/metadata_test.go @@ -1,24 +1,24 @@ -// +build vapp vdc metadata functional ALL +//go:build vapp || vdc || metadata || functional || ALL /* - * Copyright 2018 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + * Copyright 2022 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. */ package govcd import ( "fmt" + "strings" "github.com/vmware/go-vcloud-director/v2/types/v56" . "gopkg.in/check.v1" ) -func init() { - testingTags["metadata"] = "metadata_test.go" -} +// TODO: All tests here are deprecated in favor of those present in "metadata_v2_test". Remove this file once go-vcloud-director v3.0 is released. -func (vcd *TestVCD) Test_AddMetadataForVdc(check *C) { +func (vcd *TestVCD) Test_AddAndDeleteMetadataForVdc(check *C) { + vcd.skipIfNotSysAdmin(check) if vcd.config.VCD.Vdc == "" { check.Skip("skipping test because VDC name is empty") } @@ -38,37 +38,18 @@ func (vcd *TestVCD) Test_AddMetadataForVdc(check *C) { check.Assert(len(metadata.MetadataEntry), Equals, 1) check.Assert(metadata.MetadataEntry[0].Key, Equals, "key") check.Assert(metadata.MetadataEntry[0].TypedValue.Value, Equals, "value") -} - -func (vcd *TestVCD) Test_DeleteMetadataForVdc(check *C) { - if vcd.config.VCD.Vdc == "" { - check.Skip("skipping test because VDC name is empty") - } - - fmt.Printf("Running: %s\n", check.TestName()) - - // Add metadata - vdc, err := vcd.vdc.AddMetadata("key", "value") - check.Assert(err, IsNil) - check.Assert(vdc, Not(Equals), Vdc{}) - - AddToCleanupList("key", "vdcMetaData", vcd.org.Org.Name+"|"+vcd.config.VCD.Vdc, check.TestName()) // Remove metadata - vdc, err = vcd.vdc.DeleteMetadata("key2") + vdc, err = vcd.vdc.DeleteMetadata("key") check.Assert(err, IsNil) check.Assert(vdc, Not(Equals), Vdc{}) - metadata, err := vcd.vdc.GetMetadata() + metadata, err = vcd.vdc.GetMetadata() check.Assert(err, IsNil) - for _, k := range metadata.MetadataEntry { - if k.Key == "key2" { - check.Errorf("metadata.MetadataEntry should not contain key: 'key2'") - } - } + check.Assert(len(metadata.MetadataEntry), Equals, 0) } -func (vcd *TestVCD) Test_AddMetadataOnVapp(check *C) { +func (vcd *TestVCD) Test_AddAndDeleteMetadataOnVapp(check *C) { fmt.Printf("Running: %s\n", check.TestName()) if vcd.skipVappTests { @@ -87,41 +68,24 @@ func (vcd *TestVCD) Test_AddMetadataOnVapp(check *C) { check.Assert(len(metadata.MetadataEntry), Equals, 1) check.Assert(metadata.MetadataEntry[0].Key, Equals, "key") check.Assert(metadata.MetadataEntry[0].TypedValue.Value, Equals, "value") -} - -func (vcd *TestVCD) Test_DeleteMetadataOnVapp(check *C) { - fmt.Printf("Running: %s\n", check.TestName()) - - if vcd.skipVappTests { - check.Skip("Skipping test because vApp was not successfully created at setup") - } - // Add metadata - task, err := vcd.vapp.AddMetadata("key2", "value2") - check.Assert(err, IsNil) - err = task.WaitTaskCompletion() - check.Assert(err, IsNil) - check.Assert(task.Task.Status, Equals, "success") // Remove metadata - task, err = vcd.vapp.DeleteMetadata("key2") + task, err = vcd.vapp.DeleteMetadata("key") check.Assert(err, IsNil) err = task.WaitTaskCompletion() check.Assert(err, IsNil) check.Assert(task.Task.Status, Equals, "success") - metadata, err := vcd.vapp.GetMetadata() + + metadata, err = vcd.vapp.GetMetadata() check.Assert(err, IsNil) - for _, k := range metadata.MetadataEntry { - if k.Key == "key2" { - check.Errorf("metadata.MetadataEntry should not contain key: 'key2'") - } - } + check.Assert(len(metadata.MetadataEntry), Equals, 0) } -func (vcd *TestVCD) Test_AddMetadataOnVm(check *C) { +func (vcd *TestVCD) Test_AddAndDeleteMetadataOnVm(check *C) { fmt.Printf("Running: %s\n", check.TestName()) if vcd.skipVappTests { - check.Skip("Skipping test because vapp was not successfully created at setup") + check.Skip("Skipping test because vApp was not successfully created at setup") } // Find VApp @@ -138,6 +102,9 @@ func (vcd *TestVCD) Test_AddMetadataOnVm(check *C) { vm := NewVM(&vcd.client.Client) vm.VM = &vmType + existingMetadata, err := vm.GetMetadata() + check.Assert(err, IsNil) + // Add metadata task, err := vm.AddMetadata("key", "value") check.Assert(err, IsNil) @@ -148,95 +115,27 @@ func (vcd *TestVCD) Test_AddMetadataOnVm(check *C) { // Check if metadata was added correctly metadata, err := vm.GetMetadata() check.Assert(err, IsNil) - check.Assert(len(metadata.MetadataEntry), Equals, 1) - check.Assert(metadata.MetadataEntry[0].Key, Equals, "key") - check.Assert(metadata.MetadataEntry[0].TypedValue.Value, Equals, "value") -} - -func (vcd *TestVCD) Test_DeleteMetadataOnVm(check *C) { - fmt.Printf("Running: %s\n", check.TestName()) - - if vcd.skipVappTests { - check.Skip("Skipping test because vapp was not successfully created at setup") - } - - // Find VApp - if vcd.vapp.VApp == nil { - check.Skip("skipping test because no vApp is found") - } - - vapp := vcd.findFirstVapp() - vmType, vmName := vcd.findFirstVm(vapp) - if vmName == "" { - check.Skip("skipping test because no VM is found") + check.Assert(len(metadata.MetadataEntry), Equals, len(existingMetadata.MetadataEntry)+1) + found := false + for _, entry := range metadata.MetadataEntry { + if entry.Key == "key" && entry.TypedValue.Value == "value" { + found = true + } } - - vm := NewVM(&vcd.client.Client) - vm.VM = &vmType - - // Add metadata - task, err := vm.AddMetadata("key2", "value2") - check.Assert(err, IsNil) - err = task.WaitTaskCompletion() - check.Assert(err, IsNil) - check.Assert(task.Task.Status, Equals, "success") + check.Assert(found, Equals, true) // Remove metadata - task, err = vm.DeleteMetadata("key2") + task, err = vm.DeleteMetadata("key") check.Assert(err, IsNil) err = task.WaitTaskCompletion() check.Assert(err, IsNil) check.Assert(task.Task.Status, Equals, "success") - metadata, err := vm.GetMetadata() - check.Assert(err, IsNil) - for _, k := range metadata.MetadataEntry { - if k.Key == "key2" { - check.Errorf("metadata.MetadataEntry should not contain key: 'key2'") - } - } -} - -func (vcd *TestVCD) Test_DeleteMetadataOnVAppTemplate(check *C) { - fmt.Printf("Running: %s\n", check.TestName()) - cat, err := vcd.org.GetCatalogByName(vcd.config.VCD.Catalog.Name, false) - if err != nil { - check.Skip("Test_DeleteMetadataOnCatalogItem: Catalog not found. Test can't proceed") - return - } - - if vcd.config.VCD.Catalog.CatalogItem == "" { - check.Skip("Test_DeleteMetadataOnCatalogItem: Catalog Item not given. Test can't proceed") - } - - catItem, err := cat.GetCatalogItemByName(vcd.config.VCD.Catalog.CatalogItem, false) - check.Assert(err, IsNil) - check.Assert(catItem, NotNil) - check.Assert(catItem.CatalogItem.Name, Equals, vcd.config.VCD.Catalog.CatalogItem) - - vAppTemplate, err := catItem.GetVAppTemplate() - check.Assert(err, IsNil) - check.Assert(vAppTemplate, NotNil) - check.Assert(vAppTemplate.VAppTemplate.Name, Equals, vcd.config.VCD.Catalog.CatalogItem) - - // Add metadata - _, err = vAppTemplate.AddMetadata("key2", "value2") - check.Assert(err, IsNil) - - // Remove metadata - err = vAppTemplate.DeleteMetadata("key2") - check.Assert(err, IsNil) - - metadata, err := vAppTemplate.GetMetadata() + metadata, err = vm.GetMetadata() check.Assert(err, IsNil) - check.Assert(metadata, NotNil) - for _, k := range metadata.MetadataEntry { - if k.Key == "key2" { - check.Errorf("metadata.MetadataEntry should not contain key: 'key2'") - } - } + check.Assert(len(metadata.MetadataEntry), Equals, len(existingMetadata.MetadataEntry)) } -func (vcd *TestVCD) Test_AddMetadataOnVAppTemplate(check *C) { +func (vcd *TestVCD) Test_AddAndDeleteMetadataOnVAppTemplate(check *C) { fmt.Printf("Running: %s\n", check.TestName()) cat, err := vcd.org.GetCatalogByName(vcd.config.VCD.Catalog.Name, false) if err != nil { @@ -283,16 +182,20 @@ func (vcd *TestVCD) Test_AddMetadataOnVAppTemplate(check *C) { check.Assert(foundEntry.Key, Equals, "key") check.Assert(foundEntry.TypedValue.Value, Equals, "value") + // Remove metadata err = vAppTemplate.DeleteMetadata("key") check.Assert(err, IsNil) + metadata, err = vAppTemplate.GetMetadata() + check.Assert(err, IsNil) + check.Assert(len(metadata.MetadataEntry), Equals, 0) } -func (vcd *TestVCD) Test_DeleteMetadataOnMediaRecord(check *C) { +func (vcd *TestVCD) Test_AddAndDeleteMetadataOnMediaRecord(check *C) { fmt.Printf("Running: %s\n", check.TestName()) - //prepare mediaRecord item + //prepare media item skipWhenMediaPathMissing(vcd, check) - itemName := "TestDeleteMediaMetaData" + itemName := "TestAddMediaMetaDataEntry" org, err := vcd.client.GetAdminOrgByName(vcd.org.Org.Name) check.Assert(err, IsNil) @@ -301,6 +204,7 @@ func (vcd *TestVCD) Test_DeleteMetadataOnMediaRecord(check *C) { catalog, err := org.GetCatalogByName(vcd.config.VCD.Catalog.Name, false) check.Assert(err, IsNil) check.Assert(catalog, NotNil) + check.Assert(catalog.Catalog.Name, Equals, vcd.config.VCD.Catalog.Name) uploadTask, err := catalog.UploadMediaImage(itemName, "upload from test", vcd.config.Media.MediaPath, 1024) check.Assert(err, IsNil) @@ -308,7 +212,7 @@ func (vcd *TestVCD) Test_DeleteMetadataOnMediaRecord(check *C) { err = uploadTask.WaitTaskCompletion() check.Assert(err, IsNil) - AddToCleanupList(itemName, "mediaCatalogImage", vcd.org.Org.Name+"|"+vcd.config.VCD.Catalog.Name, "Test_DeleteMetadataOnMediaRecord") + AddToCleanupList(itemName, "mediaCatalogImage", vcd.org.Org.Name+"|"+vcd.config.VCD.Catalog.Name, check.TestName()) err = vcd.org.Refresh() check.Assert(err, IsNil) @@ -319,24 +223,138 @@ func (vcd *TestVCD) Test_DeleteMetadataOnMediaRecord(check *C) { check.Assert(mediaRecord.MediaRecord.Name, Equals, itemName) // Add metadata - _, err = mediaRecord.AddMetadata("key2", "value2") + _, err = mediaRecord.AddMetadata("key", "value") check.Assert(err, IsNil) + // Check if metadata was added correctly + metadata, err := mediaRecord.GetMetadata() + check.Assert(err, IsNil) + check.Assert(metadata, NotNil) + check.Assert(len(metadata.MetadataEntry), Equals, 1) + check.Assert(metadata.MetadataEntry[0].Key, Equals, "key") + check.Assert(metadata.MetadataEntry[0].TypedValue.Value, Equals, "value") + // Remove metadata - err = mediaRecord.DeleteMetadata("key2") + err = mediaRecord.DeleteMetadata("key") + check.Assert(err, IsNil) + metadata, err = mediaRecord.GetMetadata() check.Assert(err, IsNil) + check.Assert(len(metadata.MetadataEntry), Equals, 0) - metadata, err := mediaRecord.GetMetadata() + // Remove catalog item so far other tests don't fail + task, err := mediaRecord.Delete() check.Assert(err, IsNil) - check.Assert(metadata, NotNil) - for _, k := range metadata.MetadataEntry { - if k.Key == "key2" { - check.Errorf("metadata.MetadataEntry should not contain key: 'key2'") + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) +} + +func (vcd *TestVCD) Test_MetadataOnAdminCatalogCRUD(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + + var catalogName string = check.TestName() + + org, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(org, NotNil) + + catalog, err := vcd.org.CreateCatalog(catalogName, catalogName) + check.Assert(err, IsNil) + check.Assert(catalog, NotNil) + AddToCleanupList(catalogName, "catalog", org.AdminOrg.Name, catalogName) + + adminCatalog, err := org.GetAdminCatalogByName(catalogName, true) + check.Assert(err, IsNil) + check.Assert(adminCatalog, NotNil) + + testMetadataCRUDActionsDeprecated(adminCatalog, check, func() { + metadata, err := catalog.GetMetadata() + check.Assert(err, IsNil) + check.Assert(metadata, NotNil) + check.Assert(len(metadata.MetadataEntry), Equals, 1) + var foundEntry *types.MetadataEntry + for _, entry := range metadata.MetadataEntry { + if entry.Key == "key" { + foundEntry = entry + } } + check.Assert(foundEntry, NotNil) + check.Assert(foundEntry.Key, Equals, "key") + check.Assert(foundEntry.TypedValue.Value, Equals, "value") + }) + err = catalog.Delete(true, true) + check.Assert(err, IsNil) +} + +func (vcd *TestVCD) Test_MetadataEntryForVdcCRUD(check *C) { + vcd.skipIfNotSysAdmin(check) + if vcd.config.VCD.Vdc == "" { + check.Skip("skipping test because VDC name is empty") + } + + fmt.Printf("Running: %s\n", check.TestName()) + + testMetadataCRUDActionsDeprecated(vcd.vdc, check, nil) +} + +func (vcd *TestVCD) Test_MetadataEntryOnVappCRUD(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + + if vcd.skipVappTests { + check.Skip("Skipping test because vApp was not successfully created at setup") + } + testMetadataCRUDActionsDeprecated(vcd.vapp, check, nil) +} + +func (vcd *TestVCD) Test_MetadataEntryOnVmCRUD(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + + if vcd.skipVappTests { + check.Skip("Skipping test because vapp was not successfully created at setup") + } + + // Find VApp + if vcd.vapp.VApp == nil { + check.Skip("skipping test because no vApp is found") + } + + vapp := vcd.findFirstVapp() + vmType, vmName := vcd.findFirstVm(vapp) + if vmName == "" { + check.Skip("skipping test because no VM is found") + } + + vm := NewVM(&vcd.client.Client) + vm.VM = &vmType + + testMetadataCRUDActionsDeprecated(vm, check, nil) +} + +func (vcd *TestVCD) Test_MetadataEntryOnVAppTemplateCRUD(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + cat, err := vcd.org.GetCatalogByName(vcd.config.VCD.Catalog.Name, false) + if err != nil { + check.Skip("Test_DeleteMetadataOnCatalogItem: Catalog not found. Test can't proceed") + return } + + if vcd.config.VCD.Catalog.CatalogItem == "" { + check.Skip("Test_DeleteMetadataOnCatalogItem: Catalog Item not given. Test can't proceed") + } + + catItem, err := cat.GetCatalogItemByName(vcd.config.VCD.Catalog.CatalogItem, false) + check.Assert(err, IsNil) + check.Assert(catItem, NotNil) + check.Assert(catItem.CatalogItem.Name, Equals, vcd.config.VCD.Catalog.CatalogItem) + + vAppTemplate, err := catItem.GetVAppTemplate() + check.Assert(err, IsNil) + check.Assert(vAppTemplate, NotNil) + check.Assert(vAppTemplate.VAppTemplate.Name, Equals, vcd.config.VCD.Catalog.CatalogItem) + + testMetadataCRUDActionsDeprecated(&vAppTemplate, check, nil) } -func (vcd *TestVCD) Test_AddMetadataOnMediaRecord(check *C) { +func (vcd *TestVCD) Test_MetadataEntryOnMediaRecordCRUD(check *C) { fmt.Printf("Running: %s\n", check.TestName()) //prepare media item @@ -358,7 +376,7 @@ func (vcd *TestVCD) Test_AddMetadataOnMediaRecord(check *C) { err = uploadTask.WaitTaskCompletion() check.Assert(err, IsNil) - AddToCleanupList(itemName, "mediaCatalogImage", vcd.org.Org.Name+"|"+vcd.config.VCD.Catalog.Name, "Test_AddMetadataOnMediaRecord") + AddToCleanupList(itemName, "mediaCatalogImage", vcd.org.Org.Name+"|"+vcd.config.VCD.Catalog.Name, check.TestName()) err = vcd.org.Refresh() check.Assert(err, IsNil) @@ -368,15 +386,276 @@ func (vcd *TestVCD) Test_AddMetadataOnMediaRecord(check *C) { check.Assert(mediaRecord, NotNil) check.Assert(mediaRecord.MediaRecord.Name, Equals, itemName) + testMetadataCRUDActionsDeprecated(mediaRecord, check, nil) + task, err := mediaRecord.Delete() + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) +} + +func (vcd *TestVCD) Test_MetadataOnAdminOrgCRUD(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + adminOrg, err := vcd.client.GetAdminOrgById(vcd.org.Org.ID) + if err != nil { + check.Skip("Test_AddMetadataOnAdminOrg: Organization (admin) not found. Test can't proceed") + return + } + org, err := vcd.client.GetOrgById(vcd.org.Org.ID) + if err != nil { + check.Skip("Test_AddMetadataOnAdminOrg: Organization not found. Test can't proceed") + return + } + + testMetadataCRUDActionsDeprecated(adminOrg, check, func() { + metadata, err := org.GetMetadata() + check.Assert(err, IsNil) + check.Assert(metadata, NotNil) + check.Assert(len(metadata.MetadataEntry), Equals, 1) + var foundEntry *types.MetadataEntry + for _, entry := range metadata.MetadataEntry { + if entry.Key == "key" { + foundEntry = entry + } + } + check.Assert(foundEntry, NotNil) + check.Assert(foundEntry.Key, Equals, "key") + check.Assert(foundEntry.TypedValue.Value, Equals, "value") + }) + +} + +func (vcd *TestVCD) Test_MetadataOnIndependentDiskCRUD(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + // Create disk + diskCreateParamsDisk := &types.Disk{ + Name: TestCreateDisk, + SizeMb: 11, + Description: TestCreateDisk, + } + + diskCreateParams := &types.DiskCreateParams{ + Disk: diskCreateParamsDisk, + } + + task, err := vcd.vdc.CreateDisk(diskCreateParams) + check.Assert(err, IsNil) + + diskHREF := task.Task.Owner.HREF + PrependToCleanupList(diskHREF, "disk", "", check.TestName()) + + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) + + disk, err := vcd.vdc.GetDiskByHref(diskHREF) + check.Assert(err, IsNil) + + testMetadataCRUDActionsDeprecated(disk, check, nil) + + task, err = disk.Delete() + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) +} + +func (vcd *TestVCD) Test_MetadataOnVdcNetworkCRUD(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + net, err := vcd.vdc.GetOrgVdcNetworkByName(vcd.config.VCD.Network.Net1, false) + if err != nil { + check.Skip(fmt.Sprintf("%s: Network %s not found. Test can't proceed", check.TestName(), vcd.config.VCD.Network.Net1)) + return + } + + testMetadataCRUDActionsDeprecated(net, check, nil) +} + +func (vcd *TestVCD) Test_MetadataOnCatalogItemCRUD(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + catalog, err := vcd.org.GetCatalogByName(vcd.config.VCD.Catalog.Name, false) + if err != nil { + check.Skip(fmt.Sprintf("%s: Catalog %s not found. Test can't proceed", check.TestName(), vcd.config.VCD.Catalog.Name)) + return + } + + catalogItem, err := catalog.GetCatalogItemByName(vcd.config.VCD.Catalog.CatalogItem, false) + if err != nil { + check.Skip(fmt.Sprintf("%s: Catalog item %s not found. Test can't proceed", check.TestName(), vcd.config.VCD.Catalog.CatalogItem)) + return + } + + testMetadataCRUDActionsDeprecated(catalogItem, check, nil) +} + +func (vcd *TestVCD) Test_MetadataOnProviderVdcCRUD(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + vcd.skipIfNotSysAdmin(check) + providerVdc, err := vcd.client.GetProviderVdcByName(vcd.config.VCD.NsxtProviderVdc.Name) + if err != nil { + check.Skip(fmt.Sprintf("%s: Provider VDC %s not found. Test can't proceed", check.TestName(), vcd.config.VCD.NsxtProviderVdc.Name)) + return + } + + testMetadataCRUDActionsDeprecated(providerVdc, check, nil) +} + +func (vcd *TestVCD) Test_MetadataOnOpenApiOrgVdcNetworkCRUD(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + net, err := vcd.vdc.GetOpenApiOrgVdcNetworkByName(vcd.config.VCD.Network.Net1) + if err != nil { + check.Skip(fmt.Sprintf("%s: Network %s not found. Test can't proceed", check.TestName(), vcd.config.VCD.Network.Net1)) + return + } + + testMetadataCRUDActionsDeprecated(net, check, nil) +} + +func (vcd *TestVCD) Test_MetadataByHrefCRUD(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + vcd.skipIfNotSysAdmin(check) + storageProfileRef, err := vcd.vdc.FindStorageProfileReference(vcd.config.VCD.StorageProfile.SP1) + if err != nil { + check.Skip(fmt.Sprintf("%s: Storage Profile %s not found. Test can't proceed", check.TestName(), vcd.config.VCD.StorageProfile.SP1)) + return + } + + storageProfileAdminHref := strings.ReplaceAll(storageProfileRef.HREF, "api", "api/admin") + + // Check how much metadata exists + metadata, err := vcd.client.GetMetadataByHref(storageProfileAdminHref) + check.Assert(err, IsNil) + check.Assert(metadata, NotNil) + existingMetaDataCount := len(metadata.MetadataEntry) + // Add metadata - _, err = mediaRecord.AddMetadata("key", "value") + err = vcd.client.AddMetadataEntryByHref(storageProfileAdminHref, types.MetadataStringValue, "key", "value") check.Assert(err, IsNil) // Check if metadata was added correctly - metadata, err := mediaRecord.GetMetadata() + metadata, err = vcd.client.GetMetadataByHref(storageProfileAdminHref) check.Assert(err, IsNil) check.Assert(metadata, NotNil) - check.Assert(len(metadata.MetadataEntry), Equals, 1) - check.Assert(metadata.MetadataEntry[0].Key, Equals, "key") - check.Assert(metadata.MetadataEntry[0].TypedValue.Value, Equals, "value") + check.Assert(len(metadata.MetadataEntry), Equals, existingMetaDataCount+1) + var foundEntry *types.MetadataEntry + for _, entry := range metadata.MetadataEntry { + if entry.Key == "key" { + foundEntry = entry + } + } + check.Assert(foundEntry, NotNil) + check.Assert(foundEntry.Key, Equals, "key") + check.Assert(foundEntry.TypedValue.Value, Equals, "value") + + // Check the same without admin privileges + metadata, err = vcd.client.GetMetadataByHref(storageProfileRef.HREF) + check.Assert(err, IsNil) + check.Assert(metadata, NotNil) + check.Assert(len(metadata.MetadataEntry), Equals, existingMetaDataCount+1) + for _, entry := range metadata.MetadataEntry { + if entry.Key == "key" { + foundEntry = entry + } + } + check.Assert(foundEntry, NotNil) + check.Assert(foundEntry.Key, Equals, "key") + check.Assert(foundEntry.TypedValue.Value, Equals, "value") + + // Merge updated metadata with a new entry + err = vcd.client.MergeMetadataByHref(storageProfileAdminHref, types.MetadataStringValue, map[string]interface{}{ + "key": "valueUpdated", + "key2": "value2", + }) + check.Assert(err, IsNil) + + // Check that the first key was updated and the second, created + metadata, err = vcd.client.GetMetadataByHref(storageProfileRef.HREF) + check.Assert(err, IsNil) + check.Assert(metadata, NotNil) + check.Assert(len(metadata.MetadataEntry), Equals, existingMetaDataCount+2) + for _, entry := range metadata.MetadataEntry { + switch entry.Key { + case "key": + check.Assert(entry.TypedValue.Value, Equals, "valueUpdated") + case "key2": + check.Assert(entry.TypedValue.Value, Equals, "value2") + } + } + + // Delete the metadata + err = vcd.client.DeleteMetadataEntryByHref(storageProfileAdminHref, "key") + check.Assert(err, IsNil) + err = vcd.client.DeleteMetadataEntryByHref(storageProfileAdminHref, "key2") + check.Assert(err, IsNil) + // Check if metadata was deleted correctly + metadata, err = vcd.client.GetMetadataByHref(storageProfileAdminHref) + check.Assert(err, IsNil) + check.Assert(metadata, NotNil) + check.Assert(len(metadata.MetadataEntry), Equals, 0) +} + +type metadataCompatibleDeprecated interface { + GetMetadata() (*types.Metadata, error) + AddMetadataEntry(typedValue, key, value string) error + MergeMetadata(typedValue string, metadata map[string]interface{}) error + DeleteMetadataEntry(key string) error +} + +func testMetadataCRUDActionsDeprecated(resource metadataCompatibleDeprecated, check *C, extraCheck func()) { + // Check how much metadata exists + metadata, err := resource.GetMetadata() + check.Assert(err, IsNil) + check.Assert(metadata, NotNil) + existingMetaDataCount := len(metadata.MetadataEntry) + + // Add metadata + err = resource.AddMetadataEntry(types.MetadataStringValue, "key", "value") + check.Assert(err, IsNil) + + // Check if metadata was added correctly + metadata, err = resource.GetMetadata() + check.Assert(err, IsNil) + check.Assert(metadata, NotNil) + check.Assert(len(metadata.MetadataEntry), Equals, existingMetaDataCount+1) + var foundEntry *types.MetadataEntry + for _, entry := range metadata.MetadataEntry { + if entry.Key == "key" { + foundEntry = entry + } + } + check.Assert(foundEntry, NotNil) + check.Assert(foundEntry.Key, Equals, "key") + check.Assert(foundEntry.TypedValue.Value, Equals, "value") + + if extraCheck != nil { + extraCheck() + } + + // Merge updated metadata with a new entry + err = resource.MergeMetadata(types.MetadataStringValue, map[string]interface{}{ + "key": "valueUpdated", + "key2": "value2", + }) + check.Assert(err, IsNil) + + // Check that the first key was updated and the second, created + metadata, err = resource.GetMetadata() + check.Assert(err, IsNil) + check.Assert(metadata, NotNil) + check.Assert(len(metadata.MetadataEntry), Equals, existingMetaDataCount+2) + for _, entry := range metadata.MetadataEntry { + switch entry.Key { + case "key": + check.Assert(entry.TypedValue.Value, Equals, "valueUpdated") + case "key2": + check.Assert(entry.TypedValue.Value, Equals, "value2") + } + } + + err = resource.DeleteMetadataEntry("key") + check.Assert(err, IsNil) + err = resource.DeleteMetadataEntry("key2") + check.Assert(err, IsNil) + // Check if metadata was deleted correctly + metadata, err = resource.GetMetadata() + check.Assert(err, IsNil) + check.Assert(metadata, NotNil) + check.Assert(len(metadata.MetadataEntry), Equals, existingMetaDataCount) } diff --git a/govcd/metadata_v2.go b/govcd/metadata_v2.go new file mode 100644 index 000000000..7cc766de0 --- /dev/null +++ b/govcd/metadata_v2.go @@ -0,0 +1,1084 @@ +/* + * Copyright 2022 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "github.com/vmware/go-vcloud-director/v2/types/v56" + "github.com/vmware/go-vcloud-director/v2/util" + "net/http" + "regexp" + "strings" +) + +// NOTE: This "v2" is not v2 in terms of API versioning, it's just a way to separate the functions that handle +// metadata in a complete way (v2, this file) and the deprecated functions that were incomplete (v1, they lacked +// "visibility" and "domain" handling). +// +// The idea is that once a new major version of go-vcloud-director is released, one can just remove "v1" file and perform +// a minor refactoring of the code here (probably renaming functions). Also, the code in "v2" is organized differently, +// as this is classified using "CRUD blocks" (meaning that all Create functions are together, same for Read... etc), +// which makes the code more readable. + +// ------------------------------------------------------------------------------------------------ +// GET metadata by key +// ------------------------------------------------------------------------------------------------ + +// GetMetadataByKeyAndHref returns metadata from the given resource reference, corresponding to the given key and domain. +func (vcdClient *VCDClient) GetMetadataByKeyAndHref(href, key string, isSystem bool) (*types.MetadataValue, error) { + return getMetadataByKey(&vcdClient.Client, href, "", key, isSystem) +} + +// GetMetadataByKey returns VM metadata corresponding to the given key and domain. +func (vm *VM) GetMetadataByKey(key string, isSystem bool) (*types.MetadataValue, error) { + return getMetadataByKey(vm.client, vm.VM.HREF, vm.VM.Name, key, isSystem) +} + +// GetMetadataByKey returns VDC metadata corresponding to the given key and domain. +func (vdc *Vdc) GetMetadataByKey(key string, isSystem bool) (*types.MetadataValue, error) { + return getMetadataByKey(vdc.client, vdc.Vdc.HREF, vdc.Vdc.Name, key, isSystem) +} + +// GetMetadataByKey returns AdminVdc metadata corresponding to the given key and domain. +func (adminVdc *AdminVdc) GetMetadataByKey(key string, isSystem bool) (*types.MetadataValue, error) { + return getMetadataByKey(adminVdc.client, adminVdc.AdminVdc.HREF, adminVdc.AdminVdc.Name, key, isSystem) +} + +// GetMetadataByKey returns ProviderVdc metadata corresponding to the given key and domain. +// Note: Requires system administrator privileges. +func (providerVdc *ProviderVdc) GetMetadataByKey(key string, isSystem bool) (*types.MetadataValue, error) { + return getMetadataByKey(providerVdc.client, providerVdc.ProviderVdc.HREF, providerVdc.ProviderVdc.Name, key, isSystem) +} + +// GetMetadataByKey returns VApp metadata corresponding to the given key and domain. +func (vapp *VApp) GetMetadataByKey(key string, isSystem bool) (*types.MetadataValue, error) { + return getMetadataByKey(vapp.client, vapp.VApp.HREF, vapp.VApp.Name, key, isSystem) +} + +// GetMetadataByKey returns VAppTemplate metadata corresponding to the given key and domain. +func (vAppTemplate *VAppTemplate) GetMetadataByKey(key string, isSystem bool) (*types.MetadataValue, error) { + return getMetadataByKey(vAppTemplate.client, vAppTemplate.VAppTemplate.HREF, vAppTemplate.VAppTemplate.Name, key, isSystem) +} + +// GetMetadataByKey returns MediaRecord metadata corresponding to the given key and domain. +func (mediaRecord *MediaRecord) GetMetadataByKey(key string, isSystem bool) (*types.MetadataValue, error) { + return getMetadataByKey(mediaRecord.client, mediaRecord.MediaRecord.HREF, mediaRecord.MediaRecord.Name, key, isSystem) +} + +// GetMetadataByKey returns Media metadata corresponding to the given key and domain. +func (media *Media) GetMetadataByKey(key string, isSystem bool) (*types.MetadataValue, error) { + return getMetadataByKey(media.client, media.Media.HREF, media.Media.Name, key, isSystem) +} + +// GetMetadataByKey returns Catalog metadata corresponding to the given key and domain. +func (catalog *Catalog) GetMetadataByKey(key string, isSystem bool) (*types.MetadataValue, error) { + return getMetadataByKey(catalog.client, catalog.Catalog.HREF, catalog.Catalog.Name, key, isSystem) +} + +// GetMetadataByKey returns AdminCatalog metadata corresponding to the given key and domain. +func (adminCatalog *AdminCatalog) GetMetadataByKey(key string, isSystem bool) (*types.MetadataValue, error) { + return getMetadataByKey(adminCatalog.client, adminCatalog.AdminCatalog.HREF, adminCatalog.AdminCatalog.Name, key, isSystem) +} + +// GetMetadataByKey returns the Org metadata corresponding to the given key and domain. +func (org *Org) GetMetadataByKey(key string, isSystem bool) (*types.MetadataValue, error) { + return getMetadataByKey(org.client, org.Org.HREF, org.Org.Name, key, isSystem) +} + +// GetMetadataByKey returns the AdminOrg metadata corresponding to the given key and domain. +// Note: Requires system administrator privileges. +func (adminOrg *AdminOrg) GetMetadataByKey(key string, isSystem bool) (*types.MetadataValue, error) { + return getMetadataByKey(adminOrg.client, adminOrg.AdminOrg.HREF, adminOrg.AdminOrg.Name, key, isSystem) +} + +// GetMetadataByKey returns the metadata corresponding to the given key and domain. +func (disk *Disk) GetMetadataByKey(key string, isSystem bool) (*types.MetadataValue, error) { + return getMetadataByKey(disk.client, disk.Disk.HREF, disk.Disk.Name, key, isSystem) +} + +// GetMetadataByKey returns OrgVDCNetwork metadata corresponding to the given key and domain. +func (orgVdcNetwork *OrgVDCNetwork) GetMetadataByKey(key string, isSystem bool) (*types.MetadataValue, error) { + return getMetadataByKey(orgVdcNetwork.client, orgVdcNetwork.OrgVDCNetwork.HREF, orgVdcNetwork.OrgVDCNetwork.Name, key, isSystem) +} + +// GetMetadataByKey returns CatalogItem metadata corresponding to the given key and domain. +func (catalogItem *CatalogItem) GetMetadataByKey(key string, isSystem bool) (*types.MetadataValue, error) { + return getMetadataByKey(catalogItem.client, catalogItem.CatalogItem.HREF, catalogItem.CatalogItem.Name, key, isSystem) +} + +// GetMetadataByKey returns OpenApiOrgVdcNetwork metadata corresponding to the given key and domain. +// NOTE: This function cannot retrieve metadata if the network belongs to a VDC Group. +// TODO: This function is currently using XML API underneath as OpenAPI metadata is still not supported. +func (openApiOrgVdcNetwork *OpenApiOrgVdcNetwork) GetMetadataByKey(key string, isSystem bool) (*types.MetadataValue, error) { + href := fmt.Sprintf("%s/network/%s", openApiOrgVdcNetwork.client.VCDHREF.String(), extractUuid(openApiOrgVdcNetwork.OpenApiOrgVdcNetwork.ID)) + return getMetadataByKey(openApiOrgVdcNetwork.client, href, openApiOrgVdcNetwork.OpenApiOrgVdcNetwork.Name, key, isSystem) +} + +// ------------------------------------------------------------------------------------------------ +// GET all metadata +// ------------------------------------------------------------------------------------------------ + +// GetMetadataByHref returns metadata from the given resource reference. +func (vcdClient *VCDClient) GetMetadataByHref(href string) (*types.Metadata, error) { + return getMetadata(&vcdClient.Client, href, "") +} + +// GetMetadata returns VM metadata. +func (vm *VM) GetMetadata() (*types.Metadata, error) { + return getMetadata(vm.client, vm.VM.HREF, vm.VM.Name) +} + +// GetMetadata returns VDC metadata. +func (vdc *Vdc) GetMetadata() (*types.Metadata, error) { + return getMetadata(vdc.client, vdc.Vdc.HREF, vdc.Vdc.Name) +} + +// GetMetadata returns AdminVdc metadata. +func (adminVdc *AdminVdc) GetMetadata() (*types.Metadata, error) { + return getMetadata(adminVdc.client, adminVdc.AdminVdc.HREF, adminVdc.AdminVdc.Name) +} + +// GetMetadata returns ProviderVdc metadata. +// Note: Requires system administrator privileges. +func (providerVdc *ProviderVdc) GetMetadata() (*types.Metadata, error) { + return getMetadata(providerVdc.client, providerVdc.ProviderVdc.HREF, providerVdc.ProviderVdc.Name) +} + +// GetMetadata returns VApp metadata. +func (vapp *VApp) GetMetadata() (*types.Metadata, error) { + return getMetadata(vapp.client, vapp.VApp.HREF, vapp.VApp.Name) +} + +// GetMetadata returns VAppTemplate metadata. +func (vAppTemplate *VAppTemplate) GetMetadata() (*types.Metadata, error) { + return getMetadata(vAppTemplate.client, vAppTemplate.VAppTemplate.HREF, vAppTemplate.VAppTemplate.Name) +} + +// GetMetadata returns MediaRecord metadata. +func (mediaRecord *MediaRecord) GetMetadata() (*types.Metadata, error) { + return getMetadata(mediaRecord.client, mediaRecord.MediaRecord.HREF, mediaRecord.MediaRecord.Name) +} + +// GetMetadata returns Media metadata. +func (media *Media) GetMetadata() (*types.Metadata, error) { + return getMetadata(media.client, media.Media.HREF, media.Media.Name) +} + +// GetMetadata returns Catalog metadata. +func (catalog *Catalog) GetMetadata() (*types.Metadata, error) { + return getMetadata(catalog.client, catalog.Catalog.HREF, catalog.Catalog.Name) +} + +// GetMetadata returns AdminCatalog metadata. +func (adminCatalog *AdminCatalog) GetMetadata() (*types.Metadata, error) { + return getMetadata(adminCatalog.client, adminCatalog.AdminCatalog.HREF, adminCatalog.AdminCatalog.Name) +} + +// GetMetadata returns the Org metadata of the corresponding organization seen as administrator +func (org *Org) GetMetadata() (*types.Metadata, error) { + return getMetadata(org.client, org.Org.HREF, org.Org.Name) +} + +// GetMetadata returns the AdminOrg metadata of the corresponding organization seen as administrator +func (adminOrg *AdminOrg) GetMetadata() (*types.Metadata, error) { + return getMetadata(adminOrg.client, adminOrg.AdminOrg.HREF, adminOrg.AdminOrg.Name) +} + +// GetMetadata returns the metadata of the corresponding independent disk +func (disk *Disk) GetMetadata() (*types.Metadata, error) { + return getMetadata(disk.client, disk.Disk.HREF, disk.Disk.Name) +} + +// GetMetadata returns OrgVDCNetwork metadata. +func (orgVdcNetwork *OrgVDCNetwork) GetMetadata() (*types.Metadata, error) { + return getMetadata(orgVdcNetwork.client, orgVdcNetwork.OrgVDCNetwork.HREF, orgVdcNetwork.OrgVDCNetwork.Name) +} + +// GetMetadata returns CatalogItem metadata. +func (catalogItem *CatalogItem) GetMetadata() (*types.Metadata, error) { + return getMetadata(catalogItem.client, catalogItem.CatalogItem.HREF, catalogItem.CatalogItem.Name) +} + +// GetMetadata returns OpenApiOrgVdcNetwork metadata. +// NOTE: This function cannot retrieve metadata if the network belongs to a VDC Group. +// TODO: This function is currently using XML API underneath as OpenAPI metadata is still not supported. +func (openApiOrgVdcNetwork *OpenApiOrgVdcNetwork) GetMetadata() (*types.Metadata, error) { + href := fmt.Sprintf("%s/network/%s", openApiOrgVdcNetwork.client.VCDHREF.String(), extractUuid(openApiOrgVdcNetwork.OpenApiOrgVdcNetwork.ID)) + return getMetadata(openApiOrgVdcNetwork.client, href, openApiOrgVdcNetwork.OpenApiOrgVdcNetwork.Name) +} + +// ------------------------------------------------------------------------------------------------ +// ADD metadata async +// ------------------------------------------------------------------------------------------------ + +// AddMetadataEntryWithVisibilityByHrefAsync adds metadata to the given resource reference with the given key, value, type and visibility +// and returns the task. +func (vcdClient *VCDClient) AddMetadataEntryWithVisibilityByHrefAsync(href, key, value, typedValue, visibility string, isSystem bool) (Task, error) { + return addMetadata(&vcdClient.Client, href, "", key, value, typedValue, visibility, isSystem) +} + +// AddMetadataEntryWithVisibilityAsync adds metadata to the given VM with the given key, value, type and visibility +// // and returns the task. +func (vm *VM) AddMetadataEntryWithVisibilityAsync(key, value, typedValue, visibility string, isSystem bool) (Task, error) { + return addMetadata(vm.client, vm.VM.HREF, vm.VM.Name, key, value, typedValue, visibility, isSystem) +} + +// AddMetadataEntryWithVisibilityAsync adds metadata to the given AdminVdc with the given key, value, type and visibility +// and returns the task. +func (adminVdc *AdminVdc) AddMetadataEntryWithVisibilityAsync(key, value, typedValue, visibility string, isSystem bool) (Task, error) { + return addMetadata(adminVdc.client, adminVdc.AdminVdc.HREF, adminVdc.AdminVdc.Name, key, value, typedValue, visibility, isSystem) +} + +// AddMetadataEntryWithVisibilityAsync adds metadata to the given ProviderVdc with the given key, value, type and visibility +// and returns the task. +// Note: Requires system administrator privileges. +func (providerVdc *ProviderVdc) AddMetadataEntryWithVisibilityAsync(key, value, typedValue, visibility string, isSystem bool) (Task, error) { + return addMetadata(providerVdc.client, providerVdc.ProviderVdc.HREF, providerVdc.ProviderVdc.Name, key, value, typedValue, visibility, isSystem) +} + +// AddMetadataEntryWithVisibilityAsync adds metadata to the given VApp with the given key, value, type and visibility +// and returns the task. +func (vapp *VApp) AddMetadataEntryWithVisibilityAsync(key, value, typedValue, visibility string, isSystem bool) (Task, error) { + return addMetadata(vapp.client, vapp.VApp.HREF, vapp.VApp.Name, key, value, typedValue, visibility, isSystem) +} + +// AddMetadataEntryWithVisibilityAsync adds metadata to the given VAppTemplate with the given key, value, type and visibility +// and returns the task. +func (vAppTemplate *VAppTemplate) AddMetadataEntryWithVisibilityAsync(key, value, typedValue, visibility string, isSystem bool) (Task, error) { + return addMetadata(vAppTemplate.client, vAppTemplate.VAppTemplate.HREF, vAppTemplate.VAppTemplate.Name, key, value, typedValue, visibility, isSystem) +} + +// AddMetadataEntryWithVisibilityAsync adds metadata to the given MediaRecord with the given key, value, type and visibility +// and returns the task. +func (mediaRecord *MediaRecord) AddMetadataEntryWithVisibilityAsync(key, value, typedValue, visibility string, isSystem bool) (Task, error) { + return addMetadata(mediaRecord.client, mediaRecord.MediaRecord.HREF, mediaRecord.MediaRecord.Name, key, value, typedValue, visibility, isSystem) +} + +// AddMetadataEntryWithVisibilityAsync adds metadata to the given Media with the given key, value, type and visibility +// and returns the task. +func (media *Media) AddMetadataEntryWithVisibilityAsync(key, value, typedValue, visibility string, isSystem bool) (Task, error) { + return addMetadata(media.client, media.Media.HREF, media.Media.Name, key, value, typedValue, visibility, isSystem) +} + +// AddMetadataEntryWithVisibilityAsync adds metadata to the given AdminCatalog with the given key, value, type and visibility +// and returns the task. +func (adminCatalog *AdminCatalog) AddMetadataEntryWithVisibilityAsync(key, value, typedValue, visibility string, isSystem bool) (Task, error) { + return addMetadata(adminCatalog.client, adminCatalog.AdminCatalog.HREF, adminCatalog.AdminCatalog.Name, key, value, typedValue, visibility, isSystem) +} + +// AddMetadataEntryWithVisibilityAsync adds metadata to the given AdminOrg with the given key, value, type and visibility +// and returns the task. +func (adminOrg *AdminOrg) AddMetadataEntryWithVisibilityAsync(key, value, typedValue, visibility string, isSystem bool) (Task, error) { + return addMetadata(adminOrg.client, adminOrg.AdminOrg.HREF, adminOrg.AdminOrg.Name, key, value, typedValue, visibility, isSystem) +} + +// AddMetadataEntryWithVisibilityAsync adds metadata to the given Disk with the given key, value, type and visibility +// and returns the task. +func (disk *Disk) AddMetadataEntryWithVisibilityAsync(key, value, typedValue, visibility string, isSystem bool) (Task, error) { + return addMetadata(disk.client, disk.Disk.HREF, disk.Disk.Name, key, value, typedValue, visibility, isSystem) +} + +// AddMetadataEntryWithVisibilityAsync adds metadata to the given OrgVDCNetwork with the given key, value, type and visibility +// and returns the task. +// Note: Requires system administrator privileges. +func (orgVdcNetwork *OrgVDCNetwork) AddMetadataEntryWithVisibilityAsync(key, value, typedValue, visibility string, isSystem bool) (Task, error) { + return addMetadata(orgVdcNetwork.client, getAdminURL(orgVdcNetwork.OrgVDCNetwork.HREF), orgVdcNetwork.OrgVDCNetwork.Name, key, value, typedValue, visibility, isSystem) +} + +// AddMetadataEntryWithVisibilityAsync adds metadata to the given Catalog Item with the given key, value, type and visibility +// and returns the task. +func (catalogItem *CatalogItem) AddMetadataEntryWithVisibilityAsync(key, value, typedValue, visibility string, isSystem bool) (Task, error) { + return addMetadata(catalogItem.client, catalogItem.CatalogItem.HREF, catalogItem.CatalogItem.Name, key, value, typedValue, visibility, isSystem) +} + +// ------------------------------------------------------------------------------------------------ +// ADD metadata +// ------------------------------------------------------------------------------------------------ + +// AddMetadataEntryWithVisibilityByHref adds metadata to the given resource reference with the given key, value, type and visibility +// and waits for completion. +func (vcdClient *VCDClient) AddMetadataEntryWithVisibilityByHref(href, key, value, typedValue, visibility string, isSystem bool) error { + task, err := vcdClient.AddMetadataEntryWithVisibilityByHrefAsync(href, key, value, typedValue, visibility, isSystem) + if err != nil { + return err + } + return task.WaitTaskCompletion() +} + +// AddMetadataEntryWithVisibility adds metadata to the receiver VM and waits for the task to finish. +func (vm *VM) AddMetadataEntryWithVisibility(key, value, typedValue, visibility string, isSystem bool) error { + return addMetadataAndWait(vm.client, vm.VM.HREF, vm.VM.Name, key, value, typedValue, visibility, isSystem) +} + +// AddMetadataEntryWithVisibility adds metadata to the receiver AdminVdc and waits for the task to finish. +func (adminVdc *AdminVdc) AddMetadataEntryWithVisibility(key, value, typedValue, visibility string, isSystem bool) error { + return addMetadataAndWait(adminVdc.client, adminVdc.AdminVdc.HREF, adminVdc.AdminVdc.Name, key, value, typedValue, visibility, isSystem) +} + +// AddMetadataEntryWithVisibility adds metadata to the receiver ProviderVdc and waits for the task to finish. +// Note: Requires system administrator privileges. +func (providerVdc *ProviderVdc) AddMetadataEntryWithVisibility(key, value, typedValue, visibility string, isSystem bool) error { + return addMetadataAndWait(providerVdc.client, providerVdc.ProviderVdc.HREF, providerVdc.ProviderVdc.Name, key, value, typedValue, visibility, isSystem) +} + +// AddMetadataEntryWithVisibility adds metadata to the receiver VApp and waits for the task to finish. +func (vapp *VApp) AddMetadataEntryWithVisibility(key, value, typedValue, visibility string, isSystem bool) error { + return addMetadataAndWait(vapp.client, vapp.VApp.HREF, vapp.VApp.Name, key, value, typedValue, visibility, isSystem) +} + +// AddMetadataEntryWithVisibility adds metadata to the receiver VAppTemplate and waits for the task to finish. +func (vAppTemplate *VAppTemplate) AddMetadataEntryWithVisibility(key, value, typedValue, visibility string, isSystem bool) error { + return addMetadataAndWait(vAppTemplate.client, vAppTemplate.VAppTemplate.HREF, vAppTemplate.VAppTemplate.Name, key, value, typedValue, visibility, isSystem) +} + +// AddMetadataEntryWithVisibility adds metadata to the receiver MediaRecord and waits for the task to finish. +func (mediaRecord *MediaRecord) AddMetadataEntryWithVisibility(key, value, typedValue, visibility string, isSystem bool) error { + return addMetadataAndWait(mediaRecord.client, mediaRecord.MediaRecord.HREF, mediaRecord.MediaRecord.Name, key, value, typedValue, visibility, isSystem) +} + +// AddMetadataEntryWithVisibility adds metadata to the receiver Media and waits for the task to finish. +func (media *Media) AddMetadataEntryWithVisibility(key, value, typedValue, visibility string, isSystem bool) error { + return addMetadataAndWait(media.client, media.Media.HREF, media.Media.Name, key, value, typedValue, visibility, isSystem) +} + +// AddMetadataEntryWithVisibility adds metadata to the receiver AdminCatalog and waits for the task to finish. +func (adminCatalog *AdminCatalog) AddMetadataEntryWithVisibility(key, value, typedValue, visibility string, isSystem bool) error { + return addMetadataAndWait(adminCatalog.client, adminCatalog.AdminCatalog.HREF, adminCatalog.AdminCatalog.Name, key, value, typedValue, visibility, isSystem) +} + +// AddMetadataEntryWithVisibility adds metadata to the receiver AdminOrg and waits for the task to finish. +func (adminOrg *AdminOrg) AddMetadataEntryWithVisibility(key, value, typedValue, visibility string, isSystem bool) error { + return addMetadataAndWait(adminOrg.client, adminOrg.AdminOrg.HREF, adminOrg.AdminOrg.Name, key, value, typedValue, visibility, isSystem) +} + +// AddMetadataEntryWithVisibility adds metadata to the receiver Disk and waits for the task to finish. +func (disk *Disk) AddMetadataEntryWithVisibility(key, value, typedValue, visibility string, isSystem bool) error { + return addMetadataAndWait(disk.client, disk.Disk.HREF, disk.Disk.Name, key, value, typedValue, visibility, isSystem) +} + +// AddMetadataEntryWithVisibility adds metadata to the receiver OrgVDCNetwork and waits for the task to finish. +// Note: Requires system administrator privileges. +func (orgVdcNetwork *OrgVDCNetwork) AddMetadataEntryWithVisibility(key, value, typedValue, visibility string, isSystem bool) error { + return addMetadataAndWait(orgVdcNetwork.client, getAdminURL(orgVdcNetwork.OrgVDCNetwork.HREF), orgVdcNetwork.OrgVDCNetwork.Name, key, value, typedValue, visibility, isSystem) +} + +// AddMetadataEntryWithVisibility adds metadata to the receiver CatalogItem and waits for the task to finish. +func (catalogItem *CatalogItem) AddMetadataEntryWithVisibility(key, value, typedValue, visibility string, isSystem bool) error { + return addMetadataAndWait(catalogItem.client, catalogItem.CatalogItem.HREF, catalogItem.CatalogItem.Name, key, value, typedValue, visibility, isSystem) +} + +// AddMetadataEntryWithVisibility adds metadata to the receiver OpenApiOrgVdcNetwork and waits for the task to finish. +// Note: It doesn't add metadata to networks that belong to a VDC Group. +// TODO: This function is currently using XML API underneath as OpenAPI metadata is still not supported. +func (openApiOrgVdcNetwork *OpenApiOrgVdcNetwork) AddMetadataEntryWithVisibility(key, value, typedValue, visibility string, isSystem bool) error { + href := fmt.Sprintf("%s/admin/network/%s", openApiOrgVdcNetwork.client.VCDHREF.String(), extractUuid(openApiOrgVdcNetwork.OpenApiOrgVdcNetwork.ID)) + task, err := addMetadata(openApiOrgVdcNetwork.client, href, openApiOrgVdcNetwork.OpenApiOrgVdcNetwork.Name, key, value, typedValue, visibility, isSystem) + if err != nil { + return err + } + return task.WaitTaskCompletion() +} + +// ------------------------------------------------------------------------------------------------ +// MERGE metadata async +// ------------------------------------------------------------------------------------------------ + +// MergeMetadataWithVisibilityByHrefAsync updates the metadata entries present in the referenced entity and creates the ones not present, then +// returns the task. +func (vcdClient *VCDClient) MergeMetadataWithVisibilityByHrefAsync(href string, metadata map[string]types.MetadataValue) (Task, error) { + return mergeAllMetadata(&vcdClient.Client, href, "", metadata) +} + +// MergeMetadataWithMetadataValuesAsync merges VM metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// then returns the task. +func (vm *VM) MergeMetadataWithMetadataValuesAsync(metadata map[string]types.MetadataValue) (Task, error) { + return mergeAllMetadata(vm.client, vm.VM.HREF, vm.VM.Name, metadata) +} + +// MergeMetadataWithMetadataValuesAsync merges AdminVdc metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// then waits for the task to complete. +func (adminVdc *AdminVdc) MergeMetadataWithMetadataValuesAsync(metadata map[string]types.MetadataValue) (Task, error) { + return mergeAllMetadata(adminVdc.client, adminVdc.AdminVdc.HREF, adminVdc.AdminVdc.Name, metadata) +} + +// MergeMetadataWithMetadataValuesAsync merges Provider VDC metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// then waits for the task to complete. +// Note: Requires system administrator privileges. +func (providerVdc *ProviderVdc) MergeMetadataWithMetadataValuesAsync(metadata map[string]types.MetadataValue) (Task, error) { + return mergeAllMetadata(providerVdc.client, providerVdc.ProviderVdc.HREF, providerVdc.ProviderVdc.Name, metadata) +} + +// MergeMetadataWithMetadataValuesAsync merges VApp metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// then waits for the task to complete. +func (vapp *VApp) MergeMetadataWithMetadataValuesAsync(metadata map[string]types.MetadataValue) (Task, error) { + return mergeAllMetadata(vapp.client, vapp.VApp.HREF, vapp.VApp.Name, metadata) +} + +// MergeMetadataWithMetadataValuesAsync merges VAppTemplate metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// then waits for the task to complete. +func (vAppTemplate *VAppTemplate) MergeMetadataWithMetadataValuesAsync(metadata map[string]types.MetadataValue) (Task, error) { + return mergeAllMetadata(vAppTemplate.client, vAppTemplate.VAppTemplate.HREF, vAppTemplate.VAppTemplate.Name, metadata) +} + +// MergeMetadataWithMetadataValuesAsync merges MediaRecord metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// then waits for the task to complete. +func (mediaRecord *MediaRecord) MergeMetadataWithMetadataValuesAsync(metadata map[string]types.MetadataValue) (Task, error) { + return mergeAllMetadata(mediaRecord.client, mediaRecord.MediaRecord.HREF, mediaRecord.MediaRecord.Name, metadata) +} + +// MergeMetadataWithMetadataValuesAsync merges Media metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// then waits for the task to complete. +func (media *Media) MergeMetadataWithMetadataValuesAsync(metadata map[string]types.MetadataValue) (Task, error) { + return mergeAllMetadata(media.client, media.Media.HREF, media.Media.Name, metadata) +} + +// MergeMetadataWithMetadataValuesAsync merges AdminCatalog metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// then waits for the task to complete. +func (adminCatalog *AdminCatalog) MergeMetadataWithMetadataValuesAsync(metadata map[string]types.MetadataValue) (Task, error) { + return mergeAllMetadata(adminCatalog.client, adminCatalog.AdminCatalog.HREF, adminCatalog.AdminCatalog.Name, metadata) +} + +// MergeMetadataWithMetadataValuesAsync merges AdminOrg metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// then waits for the task to complete. +func (adminOrg *AdminOrg) MergeMetadataWithMetadataValuesAsync(metadata map[string]types.MetadataValue) (Task, error) { + return mergeAllMetadata(adminOrg.client, adminOrg.AdminOrg.HREF, adminOrg.AdminOrg.Name, metadata) +} + +// MergeMetadataWithMetadataValuesAsync merges Disk metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// then waits for the task to complete. +func (disk *Disk) MergeMetadataWithMetadataValuesAsync(metadata map[string]types.MetadataValue) (Task, error) { + return mergeAllMetadata(disk.client, disk.Disk.HREF, disk.Disk.Name, metadata) +} + +// MergeMetadataWithMetadataValuesAsync merges OrgVDCNetwork metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// then waits for the task to complete. +// Note: Requires system administrator privileges. +func (orgVdcNetwork *OrgVDCNetwork) MergeMetadataWithMetadataValuesAsync(metadata map[string]types.MetadataValue) (Task, error) { + return mergeAllMetadata(orgVdcNetwork.client, getAdminURL(orgVdcNetwork.OrgVDCNetwork.HREF), orgVdcNetwork.OrgVDCNetwork.Name, metadata) +} + +// MergeMetadataWithMetadataValuesAsync merges CatalogItem metadata provided as a key-value map of type `typedValue` with the already present in VCD, +// then waits for the task to complete. +func (catalogItem *CatalogItem) MergeMetadataWithMetadataValuesAsync(metadata map[string]types.MetadataValue) (Task, error) { + return mergeAllMetadata(catalogItem.client, catalogItem.CatalogItem.HREF, catalogItem.CatalogItem.Name, metadata) +} + +// ------------------------------------------------------------------------------------------------ +// MERGE metadata +// ------------------------------------------------------------------------------------------------ + +// MergeMetadataWithMetadataValues updates the metadata values that are already present in the receiver VM and creates the ones not present. +// The input metadata map has a "metadata key"->"metadata value" relation. +// This function waits until merge finishes. +func (vm *VM) MergeMetadataWithMetadataValues(metadata map[string]types.MetadataValue) error { + return mergeMetadataAndWait(vm.client, vm.VM.HREF, vm.VM.Name, metadata) +} + +// MergeMetadataWithMetadataValues updates the metadata values that are already present in the receiver AdminVdc and creates the ones not present. +// The input metadata map has a "metadata key"->"metadata value" relation. +// This function waits until merge finishes. +func (adminVdc *AdminVdc) MergeMetadataWithMetadataValues(metadata map[string]types.MetadataValue) error { + return mergeMetadataAndWait(adminVdc.client, adminVdc.AdminVdc.HREF, adminVdc.AdminVdc.Name, metadata) +} + +// MergeMetadataWithMetadataValues updates the metadata values that are already present in the receiver ProviderVdc and creates the ones not present. +// The input metadata map has a "metadata key"->"metadata value" relation. +// This function waits until merge finishes. +// Note: Requires system administrator privileges. +func (providerVdc *ProviderVdc) MergeMetadataWithMetadataValues(metadata map[string]types.MetadataValue) error { + return mergeMetadataAndWait(providerVdc.client, providerVdc.ProviderVdc.HREF, providerVdc.ProviderVdc.Name, metadata) +} + +// MergeMetadataWithMetadataValues updates the metadata values that are already present in the receiver VApp and creates the ones not present. +// The input metadata map has a "metadata key"->"metadata value" relation. +// This function waits until merge finishes. +func (vApp *VApp) MergeMetadataWithMetadataValues(metadata map[string]types.MetadataValue) error { + return mergeMetadataAndWait(vApp.client, vApp.VApp.HREF, vApp.VApp.Name, metadata) +} + +// MergeMetadataWithMetadataValues updates the metadata values that are already present in the receiver VAppTemplate and creates the ones not present. +// The input metadata map has a "metadata key"->"metadata value" relation. +// This function waits until merge finishes. +func (vAppTemplate *VAppTemplate) MergeMetadataWithMetadataValues(metadata map[string]types.MetadataValue) error { + return mergeMetadataAndWait(vAppTemplate.client, vAppTemplate.VAppTemplate.HREF, vAppTemplate.VAppTemplate.Name, metadata) +} + +// MergeMetadataWithMetadataValues updates the metadata values that are already present in the receiver MediaRecord and creates the ones not present. +// The input metadata map has a "metadata key"->"metadata value" relation. +// This function waits until merge finishes. +func (mediaRecord *MediaRecord) MergeMetadataWithMetadataValues(metadata map[string]types.MetadataValue) error { + return mergeMetadataAndWait(mediaRecord.client, mediaRecord.MediaRecord.HREF, mediaRecord.MediaRecord.Name, metadata) +} + +// MergeMetadataWithMetadataValues updates the metadata values that are already present in the receiver Media and creates the ones not present. +// The input metadata map has a "metadata key"->"metadata value" relation. +// This function waits until merge finishes. +func (media *Media) MergeMetadataWithMetadataValues(metadata map[string]types.MetadataValue) error { + return mergeMetadataAndWait(media.client, media.Media.HREF, media.Media.Name, metadata) +} + +// MergeMetadataWithMetadataValues updates the metadata values that are already present in the receiver AdminCatalog and creates the ones not present. +// The input metadata map has a "metadata key"->"metadata value" relation. +// This function waits until merge finishes. +func (adminCatalog *AdminCatalog) MergeMetadataWithMetadataValues(metadata map[string]types.MetadataValue) error { + return mergeMetadataAndWait(adminCatalog.client, adminCatalog.AdminCatalog.HREF, adminCatalog.AdminCatalog.Name, metadata) +} + +// MergeMetadataWithMetadataValues updates the metadata values that are already present in the receiver AdminOrg and creates the ones not present. +// The input metadata map has a "metadata key"->"metadata value" relation. +// This function waits until merge finishes. +func (adminOrg *AdminOrg) MergeMetadataWithMetadataValues(metadata map[string]types.MetadataValue) error { + return mergeMetadataAndWait(adminOrg.client, adminOrg.AdminOrg.HREF, adminOrg.AdminOrg.Name, metadata) +} + +// MergeMetadataWithMetadataValues updates the metadata values that are already present in the receiver Disk and creates the ones not present. +// The input metadata map has a "metadata key"->"metadata value" relation. +// This function waits until merge finishes. +func (disk *Disk) MergeMetadataWithMetadataValues(metadata map[string]types.MetadataValue) error { + return mergeMetadataAndWait(disk.client, disk.Disk.HREF, disk.Disk.Name, metadata) +} + +// MergeMetadataWithMetadataValues updates the metadata values that are already present in the receiver OrgVDCNetwork and creates the ones not present. +// The input metadata map has a "metadata key"->"metadata value" relation. +// This function waits until merge finishes. +// Note: Requires system administrator privileges. +func (orgVdcNetwork *OrgVDCNetwork) MergeMetadataWithMetadataValues(metadata map[string]types.MetadataValue) error { + return mergeMetadataAndWait(orgVdcNetwork.client, getAdminURL(orgVdcNetwork.OrgVDCNetwork.HREF), orgVdcNetwork.OrgVDCNetwork.Name, metadata) +} + +// MergeMetadataWithMetadataValues updates the metadata values that are already present in the receiver CatalogItem and creates the ones not present. +// The input metadata map has a "metadata key"->"metadata value" relation. +// This function waits until merge finishes. +func (catalogItem *CatalogItem) MergeMetadataWithMetadataValues(metadata map[string]types.MetadataValue) error { + return mergeMetadataAndWait(catalogItem.client, catalogItem.CatalogItem.HREF, catalogItem.CatalogItem.Name, metadata) +} + +// MergeMetadataWithMetadataValues updates the metadata values that are already present in the receiver OpenApiOrgVdcNetwork and creates the ones not present. +// The input metadata map has a "metadata key"->"metadata value" relation. +// This function waits until merge finishes. +// Note: It doesn't merge metadata to networks that belong to a VDC Group. +// TODO: This function is currently using XML API underneath as OpenAPI metadata is still not supported. +func (openApiOrgVdcNetwork *OpenApiOrgVdcNetwork) MergeMetadataWithMetadataValues(metadata map[string]types.MetadataValue) error { + href := fmt.Sprintf("%s/admin/network/%s", openApiOrgVdcNetwork.client.VCDHREF.String(), extractUuid(openApiOrgVdcNetwork.OpenApiOrgVdcNetwork.ID)) + task, err := mergeAllMetadata(openApiOrgVdcNetwork.client, href, openApiOrgVdcNetwork.OpenApiOrgVdcNetwork.Name, metadata) + if err != nil { + return err + } + return task.WaitTaskCompletion() +} + +// ------------------------------------------------------------------------------------------------ +// DELETE metadata async +// ------------------------------------------------------------------------------------------------ + +// DeleteMetadataEntryWithDomainByHrefAsync deletes metadata from the given resource reference, depending on key provided as input +// and returns a task. +func (vcdClient *VCDClient) DeleteMetadataEntryWithDomainByHrefAsync(href, key string, isSystem bool) (Task, error) { + return deleteMetadata(&vcdClient.Client, href, "", key, isSystem) +} + +// DeleteMetadataEntryWithDomainAsync deletes VM metadata associated to the input key and returns the task. +func (vm *VM) DeleteMetadataEntryWithDomainAsync(key string, isSystem bool) (Task, error) { + return deleteMetadata(vm.client, vm.VM.HREF, vm.VM.Name, key, isSystem) +} + +// DeleteMetadataEntryWithDomainAsync deletes AdminVdc metadata associated to the input key and returns the task. +func (adminVdc *AdminVdc) DeleteMetadataEntryWithDomainAsync(key string, isSystem bool) (Task, error) { + return deleteMetadata(adminVdc.client, adminVdc.AdminVdc.HREF, adminVdc.AdminVdc.Name, key, isSystem) +} + +// DeleteMetadataEntryWithDomainAsync deletes ProviderVdc metadata associated to the input key and returns the task. +// Note: Requires system administrator privileges. +func (providerVdc *ProviderVdc) DeleteMetadataEntryWithDomainAsync(key string, isSystem bool) (Task, error) { + return deleteMetadata(providerVdc.client, providerVdc.ProviderVdc.HREF, providerVdc.ProviderVdc.Name, key, isSystem) +} + +// DeleteMetadataEntryWithDomainAsync deletes VApp metadata associated to the input key and returns the task. +func (vapp *VApp) DeleteMetadataEntryWithDomainAsync(key string, isSystem bool) (Task, error) { + return deleteMetadata(vapp.client, vapp.VApp.HREF, vapp.VApp.Name, key, isSystem) +} + +// DeleteMetadataEntryWithDomainAsync deletes VAppTemplate metadata associated to the input key and returns the task. +func (vAppTemplate *VAppTemplate) DeleteMetadataEntryWithDomainAsync(key string, isSystem bool) (Task, error) { + return deleteMetadata(vAppTemplate.client, vAppTemplate.VAppTemplate.HREF, vAppTemplate.VAppTemplate.Name, key, isSystem) +} + +// DeleteMetadataEntryWithDomainAsync deletes MediaRecord metadata associated to the input key and returns the task. +func (mediaRecord *MediaRecord) DeleteMetadataEntryWithDomainAsync(key string, isSystem bool) (Task, error) { + return deleteMetadata(mediaRecord.client, mediaRecord.MediaRecord.HREF, mediaRecord.MediaRecord.Name, key, isSystem) +} + +// DeleteMetadataEntryWithDomainAsync deletes Media metadata associated to the input key and returns the task. +func (media *Media) DeleteMetadataEntryWithDomainAsync(key string, isSystem bool) (Task, error) { + return deleteMetadata(media.client, media.Media.HREF, media.Media.Name, key, isSystem) +} + +// DeleteMetadataEntryWithDomainAsync deletes AdminCatalog metadata associated to the input key and returns the task. +func (adminCatalog *AdminCatalog) DeleteMetadataEntryWithDomainAsync(key string, isSystem bool) (Task, error) { + return deleteMetadata(adminCatalog.client, adminCatalog.AdminCatalog.HREF, adminCatalog.AdminCatalog.Name, key, isSystem) +} + +// DeleteMetadataEntryWithDomainAsync deletes AdminOrg metadata associated to the input key and returns the task. +func (adminOrg *AdminOrg) DeleteMetadataEntryWithDomainAsync(key string, isSystem bool) (Task, error) { + return deleteMetadata(adminOrg.client, adminOrg.AdminOrg.HREF, adminOrg.AdminOrg.Name, key, isSystem) +} + +// DeleteMetadataEntryWithDomainAsync deletes Disk metadata associated to the input key and returns the task. +func (disk *Disk) DeleteMetadataEntryWithDomainAsync(key string, isSystem bool) (Task, error) { + return deleteMetadata(disk.client, disk.Disk.HREF, disk.Disk.Name, key, isSystem) +} + +// DeleteMetadataEntryWithDomainAsync deletes OrgVDCNetwork metadata associated to the input key and returns the task. +// Note: Requires system administrator privileges. +func (orgVdcNetwork *OrgVDCNetwork) DeleteMetadataEntryWithDomainAsync(key string, isSystem bool) (Task, error) { + return deleteMetadata(orgVdcNetwork.client, getAdminURL(orgVdcNetwork.OrgVDCNetwork.HREF), orgVdcNetwork.OrgVDCNetwork.Name, key, isSystem) +} + +// DeleteMetadataEntryWithDomainAsync deletes CatalogItem metadata associated to the input key and returns the task. +func (catalogItem *CatalogItem) DeleteMetadataEntryWithDomainAsync(key string, isSystem bool) (Task, error) { + return deleteMetadata(catalogItem.client, catalogItem.CatalogItem.HREF, catalogItem.CatalogItem.Name, key, isSystem) +} + +// ------------------------------------------------------------------------------------------------ +// DELETE metadata +// ------------------------------------------------------------------------------------------------ + +// DeleteMetadataEntryWithDomainByHref deletes metadata from the given resource reference, depending on key provided as input +// and waits for the task to finish. +func (vcdClient *VCDClient) DeleteMetadataEntryWithDomainByHref(href, key string, isSystem bool) error { + task, err := vcdClient.DeleteMetadataEntryWithDomainByHrefAsync(href, key, isSystem) + if err != nil { + return err + } + return task.WaitTaskCompletion() +} + +// DeleteMetadataEntryWithDomain deletes VM metadata associated to the input key and waits for the task to finish. +func (vm *VM) DeleteMetadataEntryWithDomain(key string, isSystem bool) error { + return deleteMetadataAndWait(vm.client, vm.VM.HREF, vm.VM.Name, key, isSystem) +} + +// DeleteMetadataEntryWithDomain deletes AdminVdc metadata associated to the input key and waits for the task to finish. +// Note: Requires system administrator privileges. +func (adminVdc *AdminVdc) DeleteMetadataEntryWithDomain(key string, isSystem bool) error { + return deleteMetadataAndWait(adminVdc.client, getAdminURL(adminVdc.AdminVdc.HREF), adminVdc.AdminVdc.Name, key, isSystem) +} + +// DeleteMetadataEntryWithDomain deletes ProviderVdc metadata associated to the input key and waits for the task to finish. +// Note: Requires system administrator privileges. +func (providerVdc *ProviderVdc) DeleteMetadataEntryWithDomain(key string, isSystem bool) error { + return deleteMetadataAndWait(providerVdc.client, providerVdc.ProviderVdc.HREF, providerVdc.ProviderVdc.Name, key, isSystem) +} + +// DeleteMetadataEntryWithDomain deletes VApp metadata associated to the input key and waits for the task to finish. +func (vApp *VApp) DeleteMetadataEntryWithDomain(key string, isSystem bool) error { + return deleteMetadataAndWait(vApp.client, vApp.VApp.HREF, vApp.VApp.Name, key, isSystem) +} + +// DeleteMetadataEntryWithDomain deletes VAppTemplate metadata associated to the input key and waits for the task to finish. +func (vAppTemplate *VAppTemplate) DeleteMetadataEntryWithDomain(key string, isSystem bool) error { + return deleteMetadataAndWait(vAppTemplate.client, vAppTemplate.VAppTemplate.HREF, vAppTemplate.VAppTemplate.Name, key, isSystem) +} + +// DeleteMetadataEntryWithDomain deletes MediaRecord metadata associated to the input key and waits for the task to finish. +func (mediaRecord *MediaRecord) DeleteMetadataEntryWithDomain(key string, isSystem bool) error { + return deleteMetadataAndWait(mediaRecord.client, mediaRecord.MediaRecord.HREF, mediaRecord.MediaRecord.Name, key, isSystem) +} + +// DeleteMetadataEntryWithDomain deletes Media metadata associated to the input key and waits for the task to finish. +func (media *Media) DeleteMetadataEntryWithDomain(key string, isSystem bool) error { + return deleteMetadataAndWait(media.client, media.Media.HREF, media.Media.Name, key, isSystem) +} + +// DeleteMetadataEntryWithDomain deletes AdminCatalog metadata associated to the input key and waits for the task to finish. +func (adminCatalog *AdminCatalog) DeleteMetadataEntryWithDomain(key string, isSystem bool) error { + return deleteMetadataAndWait(adminCatalog.client, adminCatalog.AdminCatalog.HREF, adminCatalog.AdminCatalog.Name, key, isSystem) +} + +// DeleteMetadataEntryWithDomain deletes AdminOrg metadata associated to the input key and waits for the task to finish. +func (adminOrg *AdminOrg) DeleteMetadataEntryWithDomain(key string, isSystem bool) error { + return deleteMetadataAndWait(adminOrg.client, adminOrg.AdminOrg.HREF, adminOrg.AdminOrg.Name, key, isSystem) +} + +// DeleteMetadataEntryWithDomain deletes Disk metadata associated to the input key and waits for the task to finish. +func (disk *Disk) DeleteMetadataEntryWithDomain(key string, isSystem bool) error { + return deleteMetadataAndWait(disk.client, disk.Disk.HREF, disk.Disk.Name, key, isSystem) +} + +// DeleteMetadataEntryWithDomain deletes OrgVDCNetwork metadata associated to the input key and waits for the task to finish. +// Note: Requires system administrator privileges. +func (orgVdcNetwork *OrgVDCNetwork) DeleteMetadataEntryWithDomain(key string, isSystem bool) error { + return deleteMetadataAndWait(orgVdcNetwork.client, getAdminURL(orgVdcNetwork.OrgVDCNetwork.HREF), orgVdcNetwork.OrgVDCNetwork.Name, key, isSystem) +} + +// DeleteMetadataEntryWithDomain deletes CatalogItem metadata associated to the input key and waits for the task to finish. +func (catalogItem *CatalogItem) DeleteMetadataEntryWithDomain(key string, isSystem bool) error { + return deleteMetadataAndWait(catalogItem.client, catalogItem.CatalogItem.HREF, catalogItem.CatalogItem.Name, key, isSystem) +} + +// DeleteMetadataEntryWithDomain deletes OpenApiOrgVdcNetwork metadata associated to the input key and waits for the task to finish. +// Note: It doesn't delete metadata from networks that belong to a VDC Group. +// TODO: This function is currently using XML API underneath as OpenAPI metadata is still not supported. +func (openApiOrgVdcNetwork *OpenApiOrgVdcNetwork) DeleteMetadataEntryWithDomain(key string, isSystem bool) error { + href := fmt.Sprintf("%s/admin/network/%s", openApiOrgVdcNetwork.client.VCDHREF.String(), extractUuid(openApiOrgVdcNetwork.OpenApiOrgVdcNetwork.ID)) + task, err := deleteMetadata(openApiOrgVdcNetwork.client, href, openApiOrgVdcNetwork.OpenApiOrgVdcNetwork.Name, key, isSystem) + if err != nil { + return err + } + return task.WaitTaskCompletion() +} + +// ------------------------------------------------------------------------------------------------ +// Ignored metadata set/unset +// ------------------------------------------------------------------------------------------------ + +// SetMetadataToIgnore allows to update the metadata to be ignored in all metadata API calls with +// the given input. It returns the old IgnoredMetadata configuration from the client +func (vcdClient *VCDClient) SetMetadataToIgnore(ignoredMetadata []IgnoredMetadata) []IgnoredMetadata { + result := vcdClient.Client.IgnoredMetadata + vcdClient.Client.IgnoredMetadata = ignoredMetadata + return result +} + +// ------------------------------------------------------------------------------------------------ +// Generic private functions +// ------------------------------------------------------------------------------------------------ + +// getMetadata is a generic function to retrieve metadata from VCD +func getMetadataByKey(client *Client, requestUri, name, key string, isSystem bool) (*types.MetadataValue, error) { + metadata := &types.MetadataValue{} + href := requestUri + "/metadata/" + + if isSystem { + href += "SYSTEM/" + } + + _, err := client.ExecuteRequest(href+key, http.MethodGet, types.MimeMetaData, "error retrieving metadata by key "+key+": %s", nil, metadata) + if err != nil { + return nil, err + } + return filterSingleXmlMetadataEntry(key, requestUri, name, metadata, client.IgnoredMetadata) +} + +// getMetadata is a generic function to retrieve metadata from VCD +func getMetadata(client *Client, requestUri, name string) (*types.Metadata, error) { + metadata := &types.Metadata{} + + _, err := client.ExecuteRequest(requestUri+"/metadata/", http.MethodGet, types.MimeMetaData, "error retrieving metadata: %s", nil, metadata) + if err != nil { + return nil, err + } + return filterXmlMetadata(metadata, requestUri, name, client.IgnoredMetadata) +} + +// addMetadata adds metadata to an entity. +// If the metadata entry is of the SYSTEM domain (isSystem=true), one can set different types of Visibility: +// types.MetadataReadOnlyVisibility, types.MetadataHiddenVisibility but NOT types.MetadataReadWriteVisibility. +// If the metadata entry is of the GENERAL domain (isSystem=false), visibility is always types.MetadataReadWriteVisibility. +// In terms of typedValues, that must be one of: +// types.MetadataStringValue, types.MetadataNumberValue, types.MetadataDateTimeValue and types.MetadataBooleanValue. +func addMetadata(client *Client, requestUri, name, key, value, typedValue, visibility string, isSystem bool) (Task, error) { + apiEndpoint := urlParseRequestURI(requestUri) + newMetadata := &types.MetadataValue{ + Xmlns: types.XMLNamespaceVCloud, + Xsi: types.XMLNamespaceXSI, + TypedValue: &types.MetadataTypedValue{ + XsiType: typedValue, + Value: value, + }, + Domain: &types.MetadataDomainTag{ + Visibility: visibility, + Domain: "SYSTEM", + }, + } + + if isSystem { + apiEndpoint.Path += "/metadata/SYSTEM/" + key + } else { + apiEndpoint.Path += "/metadata/" + key + newMetadata.Domain.Domain = "GENERAL" + if visibility != types.MetadataReadWriteVisibility { + newMetadata.Domain.Visibility = types.MetadataReadWriteVisibility + } + } + + _, err := filterSingleXmlMetadataEntry(key, requestUri, name, newMetadata, client.IgnoredMetadata) + if err != nil { + return Task{}, err + } + + domain := newMetadata.Domain.Visibility + task, err := client.ExecuteTaskRequest(apiEndpoint.String(), http.MethodPut, types.MimeMetaDataValue, "error adding metadata: %s", newMetadata) + + // Workaround for ugly error returned by VCD: "API Error: 500: [ ] visibility" + if err != nil && strings.HasSuffix(err.Error(), "visibility") { + err = fmt.Errorf("error adding metadata with key %s: visibility cannot be %s when domain is %s: %s", key, visibility, domain, err) + } + return task, err +} + +// addMetadataAndWait adds metadata to an entity and waits for the task completion. +// The function supports passing a value that requires a typed value that must be one of: +// types.MetadataStringValue, types.MetadataNumberValue, types.MetadataDateTimeValue and types.MetadataBooleanValue. +// Visibility also needs to be one of: types.MetadataReadOnlyVisibility, types.MetadataHiddenVisibility or types.MetadataReadWriteVisibility +func addMetadataAndWait(client *Client, requestUri, name, key, value, typedValue, visibility string, isSystem bool) error { + task, err := addMetadata(client, requestUri, name, key, value, typedValue, visibility, isSystem) + if err != nil { + return err + } + + return task.WaitTaskCompletion() +} + +// mergeAllMetadata updates the metadata values that are already present in VCD and creates the ones not present. +// The input metadata map has a "metadata key"->"metadata value" relation. +// If the operation is successful, it returns the created task. +func mergeAllMetadata(client *Client, requestUri, name string, metadata map[string]types.MetadataValue) (Task, error) { + var metadataToMerge []*types.MetadataEntry + for key, value := range metadata { + metadataToMerge = append(metadataToMerge, &types.MetadataEntry{ + Xmlns: types.XMLNamespaceVCloud, + Xsi: types.XMLNamespaceXSI, + Key: key, + TypedValue: value.TypedValue, + Domain: value.Domain, + }) + } + + newMetadata := &types.Metadata{ + Xmlns: types.XMLNamespaceVCloud, + Xsi: types.XMLNamespaceXSI, + MetadataEntry: metadataToMerge, + } + + apiEndpoint := urlParseRequestURI(requestUri) + apiEndpoint.Path += "/metadata" + + filteredMetadata, err := filterXmlMetadata(newMetadata, requestUri, name, client.IgnoredMetadata) + if err != nil { + return Task{}, err + } + if len(filteredMetadata.MetadataEntry) == 0 { + return Task{}, fmt.Errorf("after filtering metadata, there is no metadata to merge") + } + + return client.ExecuteTaskRequest(apiEndpoint.String(), http.MethodPost, types.MimeMetaData, "error merging metadata: %s", filteredMetadata) +} + +// mergeAllMetadata updates the metadata values that are already present in VCD and creates the ones not present. +// The input metadata map has a "metadata key"->"metadata value" relation. +// This function waits until merge finishes. +func mergeMetadataAndWait(client *Client, requestUri, name string, metadata map[string]types.MetadataValue) error { + task, err := mergeAllMetadata(client, requestUri, name, metadata) + if err != nil { + return err + } + + return task.WaitTaskCompletion() +} + +// deleteMetadata deletes metadata associated to the input key from an entity referenced by its URI, then returns the +// task. +func deleteMetadata(client *Client, requestUri, name, key string, isSystem bool) (Task, error) { + apiEndpoint := urlParseRequestURI(requestUri) + if isSystem { + apiEndpoint.Path += "/metadata/SYSTEM/" + key + } else { + apiEndpoint.Path += "/metadata/" + key + } + + err := filterMetadataToDelete(client, key, requestUri, name, isSystem, client.IgnoredMetadata) + if err != nil { + return Task{}, err + } + + return client.ExecuteTaskRequest(apiEndpoint.String(), http.MethodDelete, "", "error deleting metadata: %s", nil) +} + +// deleteMetadata deletes metadata associated to the input key from an entity referenced by its URI. +func deleteMetadataAndWait(client *Client, requestUri, name, key string, isSystem bool) error { + task, err := deleteMetadata(client, requestUri, name, key, isSystem) + if err != nil { + return err + } + + return task.WaitTaskCompletion() +} + +// IgnoredMetadata is a structure that defines the metadata entries that should be ignored by the VCD Client. +// The filtering works in such a way that all the non-nil pointers in an instance of this struct are evaluated with +// a logical AND. +// For example, ignoredMetadata.ObjectType = "org", ignoredMetadata.ObjectName = "foo" will ignore all metadata from +// Organizations whose name is "foo", with any key and any value. +// Note: This struct is only used by metadata_v2.go methods. +// Note 2: Filtering by ObjectName is not possible in the "ByHref" methods from VCDClient. +type IgnoredMetadata struct { + ObjectType *string // Type of the object that has the metadata as defined in the API documentation https://developer.vmware.com/apis/1601/vmware-cloud-director, for example "catalog", "disk", "org"... + ObjectName *string // Name of the object + KeyRegex *regexp.Regexp // A regular expression to filter out metadata keys + ValueRegex *regexp.Regexp // A regular expression to filter out metadata values +} + +func (im IgnoredMetadata) String() string { + objectType := "" + if im.ObjectType != nil { + objectType = *im.ObjectType + } + + objectName := "" + if im.ObjectName != nil { + objectName = *im.ObjectName + } + + return fmt.Sprintf("IgnoredMetadata(ObjectType=%v, ObjectName=%v, KeyRegex=%v, ValueRegex=%v)", objectType, objectName, im.KeyRegex, im.ValueRegex) +} + +// normalisedMetadata is an auxiliary type that allows to transform XML and OpenAPI metadata into a common structure +// for operations that are executed the same way in both flavors. +type normalisedMetadata struct { + ObjectType string + ObjectName string + Key string + Value string +} + +// normaliseXmlMetadata transforms XML metadata into a normalised structure +func normaliseXmlMetadata(key, href, objectName string, metadataEntry *types.MetadataValue) (*normalisedMetadata, error) { + objectType, err := getMetadataObjectTypeFromHref(href) + if err != nil { + return nil, err + } + + return &normalisedMetadata{ + ObjectType: objectType, + ObjectName: objectName, + Key: key, + Value: metadataEntry.TypedValue.Value, + }, nil +} + +// filterXmlMetadata filters all metadata entries, given a slice of metadata that needs to be ignored. It doesn't +// alter the input metadata, but returns a copy of the filtered metadata. +func filterXmlMetadata(allMetadata *types.Metadata, href, objectName string, metadataToIgnore []IgnoredMetadata) (*types.Metadata, error) { + if len(metadataToIgnore) == 0 { + return allMetadata, nil + } + + result := &types.Metadata{ + XMLName: allMetadata.XMLName, + Xmlns: allMetadata.Xmlns, + HREF: allMetadata.HREF, + Type: allMetadata.Type, + Xsi: allMetadata.Xsi, + Link: allMetadata.Link, + MetadataEntry: nil, + } + + var filteredMetadata []*types.MetadataEntry + for _, originalEntry := range allMetadata.MetadataEntry { + _, err := filterSingleXmlMetadataEntry(originalEntry.Key, href, objectName, &types.MetadataValue{Domain: originalEntry.Domain, TypedValue: originalEntry.TypedValue}, metadataToIgnore) + if err != nil { + if strings.Contains(err.Error(), "ignored") { + continue + } + return nil, err + } + filteredMetadata = append(filteredMetadata, originalEntry) + } + result.MetadataEntry = filteredMetadata + return result, nil +} + +func filterSingleXmlMetadataEntry(key, href, objectName string, metadataEntry *types.MetadataValue, metadataToIgnore []IgnoredMetadata) (*types.MetadataValue, error) { + normalisedEntry, err := normaliseXmlMetadata(key, href, objectName, metadataEntry) + if err != nil { + return nil, err + } + isFiltered := filterSingleGenericMetadataEntry(normalisedEntry, metadataToIgnore) + if isFiltered { + return nil, fmt.Errorf("the metadata entry with key '%s' and value '%v' is being ignored", key, metadataEntry.TypedValue.Value) + } + return metadataEntry, nil +} + +// filterSingleGenericMetadataEntry filters a single metadata entry given a slice of metadata that needs to be ignored. It doesn't +// alter the input metadata, but returns a bool that indicates whether the entry should be ignored or not. +func filterSingleGenericMetadataEntry(normalisedMetadataEntry *normalisedMetadata, metadataToIgnore []IgnoredMetadata) bool { + if len(metadataToIgnore) == 0 { + return false + } + + for _, entryToIgnore := range metadataToIgnore { + if entryToIgnore.ObjectType == nil && entryToIgnore.ObjectName == nil && entryToIgnore.KeyRegex == nil && entryToIgnore.ValueRegex == nil { + continue + } + util.Logger.Printf("[DEBUG] Comparing metadata with key '%s' with ignored metadata filter '%s'", normalisedMetadataEntry.Key, entryToIgnore) + // We apply an optimistic approach here to simplify the conditions, so the metadata entry will always be ignored unless the filters + // tell otherwise, that is, if they are nil (not all of them as per the condition above), if they're empty or if they don't match. + // All the filtering options (type, name, keyRegex and valueRegex) must compute to true for the metadata to be ignored. + if (entryToIgnore.ObjectType == nil || strings.TrimSpace(*entryToIgnore.ObjectType) == "" || *entryToIgnore.ObjectType == normalisedMetadataEntry.ObjectType) && + (entryToIgnore.ObjectName == nil || strings.TrimSpace(*entryToIgnore.ObjectName) == "" || strings.TrimSpace(normalisedMetadataEntry.ObjectName) == "" || *entryToIgnore.ObjectName == normalisedMetadataEntry.ObjectName) && + (entryToIgnore.KeyRegex == nil || entryToIgnore.KeyRegex.MatchString(normalisedMetadataEntry.Key)) && + (entryToIgnore.ValueRegex == nil || entryToIgnore.ValueRegex.MatchString(normalisedMetadataEntry.Value)) { + util.Logger.Printf("[DEBUG] the metadata entry with key '%s' and value '%v' is being ignored", normalisedMetadataEntry.ObjectType, normalisedMetadataEntry.Value) + return true + } + } + return false +} + +// filterMetadataToDelete filters a metadata entry that is going to be deleted, given a slice of metadata that needs to be ignored. +func filterMetadataToDelete(client *Client, key, href, objectName string, isSystem bool, metadataToIgnore []IgnoredMetadata) error { + if len(metadataToIgnore) == 0 { + return nil + } + + objectType, err := getMetadataObjectTypeFromHref(href) + if err != nil { + return err + } + for _, entryToIgnore := range metadataToIgnore { + if entryToIgnore.ObjectType == nil && entryToIgnore.ObjectName == nil && entryToIgnore.KeyRegex == nil && entryToIgnore.ValueRegex == nil { + continue + } + + if (entryToIgnore.ObjectType == nil || strings.TrimSpace(*entryToIgnore.ObjectType) == "" || *entryToIgnore.ObjectType == objectType) && + (entryToIgnore.ObjectName == nil || strings.TrimSpace(*entryToIgnore.ObjectName) == "" || strings.TrimSpace(objectName) == "" || *entryToIgnore.ObjectName == objectName) && + (entryToIgnore.KeyRegex == nil || entryToIgnore.KeyRegex.MatchString(key)) { + + // Entering here means that it is a good candidate to be ignored, but we need to know the metadata value + // as we may be filtering by value + ignore := true + if entryToIgnore.ValueRegex != nil { + metadataEntry, err := getMetadataByKey(client, href, objectName, key, isSystem) + if err != nil { + return err + } + if !entryToIgnore.ValueRegex.MatchString(metadataEntry.TypedValue.Value) { + ignore = false + } + } + + if ignore { + util.Logger.Printf("[DEBUG] can't delete metadata entry %s as it is ignored", key) + return fmt.Errorf("can't delete metadata entry %s as it is ignored", key) + } + return nil + } + } + return nil + +} + +// getMetadataObjectTypeFromHref returns the type of the object referenced by the input HREF. +// For example, "https://atl1-vcd-static-130-117.eng.vmware.com/api/admin/org/11582a00-16bb-4916-a42f-2d5e453ccf36" +// will return "org". +func getMetadataObjectTypeFromHref(href string) (string, error) { + splitHref := strings.Split(href, "/") + if len(splitHref) < 2 { + return "", fmt.Errorf("could not find any object type in the provided HREF '%s'", href) + } + return splitHref[len(splitHref)-2], nil +} diff --git a/govcd/metadata_v2_test.go b/govcd/metadata_v2_test.go new file mode 100644 index 000000000..03992a80a --- /dev/null +++ b/govcd/metadata_v2_test.go @@ -0,0 +1,684 @@ +//go:build (vapp || vdc || metadata || functional || ALL) && !skipLong + +/* + * Copyright 2022 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" + "regexp" + "strings" +) + +func init() { + testingTags["metadata"] = "metadata_v2_test.go" +} + +func (vcd *TestVCD) TestVmMetadata(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + + if vcd.skipVappTests { + check.Skip("Skipping test because vApp was not successfully created at setup") + } + + if vcd.vapp.VApp == nil { + check.Skip("skipping test because no vApp is found in configuration") + } + + vApp := vcd.findFirstVapp() + vmType, vmName := vcd.findFirstVm(vApp) + if vmName == "" { + check.Skip("skipping test because no VM is found") + } + + vm := NewVM(&vcd.client.Client) + vm.VM = &vmType + + vcd.testMetadataCRUDActions(vm, check, nil) + vcd.testMetadataIgnore(vm, "vApp", vm.VM.Name, check) +} + +func (vcd *TestVCD) TestAdminVdcMetadata(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + vcd.skipIfNotSysAdmin(check) + if vcd.config.VCD.Nsxt.Vdc == "" { + check.Skip("skipping test because VDC name is empty") + } + + org, err := vcd.client.GetAdminOrgByName(vcd.org.Org.Name) + check.Assert(err, IsNil) + check.Assert(org, NotNil) + + adminVdc, err := org.GetAdminVDCByName(vcd.config.VCD.Nsxt.Vdc, false) + check.Assert(err, IsNil) + check.Assert(adminVdc, NotNil) + + vcd.testMetadataCRUDActions(adminVdc, check, func(testCase metadataTest) { + testVdcMetadata(vcd, check, testCase) + }) + vcd.testMetadataIgnore(adminVdc, "vdc", adminVdc.AdminVdc.Name, check) +} + +func testVdcMetadata(vcd *TestVCD, check *C, testCase metadataTest) { + org, err := vcd.client.GetAdminOrgByName(vcd.org.Org.Name) + check.Assert(err, IsNil) + check.Assert(org, NotNil) + + vdc, err := org.GetVDCByName(vcd.config.VCD.Nsxt.Vdc, false) + check.Assert(err, IsNil) + check.Assert(vdc, NotNil) + check.Assert(vdc.Vdc.Name, Equals, vcd.config.VCD.Nsxt.Vdc) + + metadata, err := vdc.GetMetadata() + check.Assert(err, IsNil) + assertMetadata(check, metadata, testCase, 1) +} + +func (vcd *TestVCD) TestProviderVdcMetadata(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + vcd.skipIfNotSysAdmin(check) + providerVdc, err := vcd.client.GetProviderVdcByName(vcd.config.VCD.NsxtProviderVdc.Name) + if err != nil { + check.Skip(fmt.Sprintf("%s: Provider VDC %s not found. Test can't proceed", check.TestName(), vcd.config.VCD.NsxtProviderVdc.Name)) + return + } + vcd.testMetadataCRUDActions(providerVdc, check, nil) + vcd.testMetadataIgnore(providerVdc, "providervdc", providerVdc.ProviderVdc.Name, check) +} + +func (vcd *TestVCD) TestVAppMetadata(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + if vcd.skipVappTests { + check.Skip("Skipping test because vApp was not successfully created at setup") + } + vcd.testMetadataCRUDActions(vcd.vapp, check, nil) + vcd.testMetadataIgnore(vcd.vapp, "vApp", vcd.vapp.VApp.Name, check) +} + +func (vcd *TestVCD) TestVAppTemplateMetadata(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + vAppTemplate, err := vcd.nsxtVdc.GetVAppTemplateByName(vcd.config.VCD.Catalog.NsxtCatalogItem) + if err != nil { + check.Skip("Skipping test because vApp Template was not found. Test can't proceed") + return + } + check.Assert(vAppTemplate, NotNil) + check.Assert(vAppTemplate.VAppTemplate.Name, Equals, vcd.config.VCD.Catalog.NsxtCatalogItem) + + vcd.testMetadataCRUDActions(vAppTemplate, check, nil) + vcd.testMetadataIgnore(vAppTemplate, "vAppTemplate", vAppTemplate.VAppTemplate.Name, check) +} + +func (vcd *TestVCD) TestMediaRecordMetadata(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + + skipWhenMediaPathMissing(vcd, check) + + org, err := vcd.client.GetAdminOrgByName(vcd.org.Org.Name) + check.Assert(err, IsNil) + check.Assert(org, NotNil) + + catalog, err := org.GetCatalogByName(vcd.config.VCD.Catalog.Name, false) + check.Assert(err, IsNil) + check.Assert(catalog, NotNil) + check.Assert(catalog.Catalog.Name, Equals, vcd.config.VCD.Catalog.Name) + + uploadTask, err := catalog.UploadMediaImage(check.TestName(), check.TestName(), vcd.config.Media.MediaPath, 1024) + check.Assert(err, IsNil) + check.Assert(uploadTask, NotNil) + err = uploadTask.WaitTaskCompletion() + check.Assert(err, IsNil) + // cleanup uploaded media so that other tests don't fail + defer func() { + media, err := catalog.GetMediaByName(check.TestName(), true) + check.Assert(err, IsNil) + check.Assert(media, NotNil) + + deleteTask, err := media.Delete() + check.Assert(err, IsNil) + check.Assert(deleteTask, NotNil) + err = deleteTask.WaitTaskCompletion() + check.Assert(err, IsNil) + }() + + AddToCleanupList(check.TestName(), "mediaCatalogImage", vcd.org.Org.Name+"|"+vcd.config.VCD.Catalog.Name, "Test_AddMetadataOnMediaRecord") + + err = vcd.org.Refresh() + check.Assert(err, IsNil) + + mediaRecord, err := catalog.QueryMedia(check.TestName()) + check.Assert(err, IsNil) + check.Assert(mediaRecord, NotNil) + check.Assert(mediaRecord.MediaRecord.Name, Equals, check.TestName()) + + vcd.testMetadataCRUDActions(mediaRecord, check, nil) + vcd.testMetadataIgnore(mediaRecord, "media", mediaRecord.MediaRecord.Name, check) +} + +func (vcd *TestVCD) TestMediaMetadata(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + + skipWhenMediaPathMissing(vcd, check) + + org, err := vcd.client.GetAdminOrgByName(vcd.org.Org.Name) + check.Assert(err, IsNil) + check.Assert(org, NotNil) + + catalog, err := org.GetCatalogByName(vcd.config.VCD.Catalog.Name, false) + check.Assert(err, IsNil) + check.Assert(catalog, NotNil) + check.Assert(catalog.Catalog.Name, Equals, vcd.config.VCD.Catalog.Name) + + media, err := catalog.GetMediaByName(vcd.config.Media.Media, false) + check.Assert(err, IsNil) + + vcd.testMetadataCRUDActions(media, check, nil) + vcd.testMetadataIgnore(media, "media", media.Media.Name, check) +} + +func (vcd *TestVCD) TestAdminCatalogMetadata(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + + org, err := vcd.client.GetAdminOrgByName(vcd.org.Org.Name) + check.Assert(err, IsNil) + check.Assert(org, NotNil) + + adminCatalog, err := org.GetAdminCatalogByName(vcd.config.VCD.Catalog.NsxtBackedCatalogName, false) + check.Assert(err, IsNil) + check.Assert(adminCatalog, NotNil) + check.Assert(adminCatalog.AdminCatalog.Name, Equals, vcd.config.VCD.Catalog.NsxtBackedCatalogName) + + vcd.testMetadataCRUDActions(adminCatalog, check, func(testCase metadataTest) { + testCatalogMetadata(vcd, check, testCase) + }) + vcd.testMetadataIgnore(adminCatalog, "catalog", adminCatalog.AdminCatalog.Name, check) +} + +func testCatalogMetadata(vcd *TestVCD, check *C, testCase metadataTest) { + org, err := vcd.client.GetAdminOrgByName(vcd.org.Org.Name) + check.Assert(err, IsNil) + check.Assert(org, NotNil) + + catalog, err := org.GetCatalogByName(vcd.config.VCD.Catalog.NsxtBackedCatalogName, false) + check.Assert(err, IsNil) + check.Assert(catalog, NotNil) + check.Assert(catalog.Catalog.Name, Equals, vcd.config.VCD.Catalog.NsxtBackedCatalogName) + + metadata, err := catalog.GetMetadata() + check.Assert(err, IsNil) + assertMetadata(check, metadata, testCase, 1) +} + +func (vcd *TestVCD) TestAdminOrgMetadata(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.org.Org.Name) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + + vcd.testMetadataCRUDActions(adminOrg, check, func(testCase metadataTest) { + testOrgMetadata(vcd, check, testCase) + }) + vcd.testMetadataIgnore(adminOrg, "org", adminOrg.AdminOrg.Name, check) +} + +func testOrgMetadata(vcd *TestVCD, check *C, testCase metadataTest) { + org, err := vcd.client.GetOrgByName(vcd.org.Org.Name) + check.Assert(err, IsNil) + check.Assert(org, NotNil) + + metadata, err := org.GetMetadata() + check.Assert(err, IsNil) + assertMetadata(check, metadata, testCase, 1) +} + +func (vcd *TestVCD) TestDiskMetadata(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + + diskCreateParams := &types.DiskCreateParams{ + Disk: &types.Disk{ + Name: TestCreateDisk, + SizeMb: 11, + Description: TestCreateDisk, + }, + } + + task, err := vcd.vdc.CreateDisk(diskCreateParams) + check.Assert(err, IsNil) + + diskHREF := task.Task.Owner.HREF + PrependToCleanupList(diskHREF, "disk", "", check.TestName()) + + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) + + disk, err := vcd.vdc.GetDiskByHref(diskHREF) + check.Assert(err, IsNil) + + vcd.testMetadataCRUDActions(disk, check, nil) + vcd.testMetadataIgnore(disk, "disk", disk.Disk.Name, check) + + task, err = disk.Delete() + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) +} + +func (vcd *TestVCD) TestOrgVDCNetworkMetadata(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + net, err := vcd.vdc.GetOrgVdcNetworkByName(vcd.config.VCD.Network.Net1, false) + if err != nil { + check.Skip(fmt.Sprintf("network %s not found. Test can't proceed", vcd.config.VCD.Network.Net1)) + return + } + vcd.testMetadataCRUDActions(net, check, nil) + vcd.testMetadataIgnore(net, "network", net.OrgVDCNetwork.Name, check) +} + +func (vcd *TestVCD) TestCatalogItemMetadata(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + catalog, err := vcd.org.GetCatalogByName(vcd.config.VCD.Catalog.Name, false) + if err != nil { + check.Skip(fmt.Sprintf("Catalog %s not found. Test can't proceed", vcd.config.VCD.Catalog.Name)) + return + } + + catalogItem, err := catalog.GetCatalogItemByName(vcd.config.VCD.Catalog.CatalogItem, false) + if err != nil { + check.Skip(fmt.Sprintf("Catalog item %s not found. Test can't proceed", vcd.config.VCD.Catalog.CatalogItem)) + return + } + + vcd.testMetadataCRUDActions(catalogItem, check, nil) + vcd.testMetadataIgnore(catalogItem, "catalogItem", catalogItem.CatalogItem.Name, check) +} + +func (vcd *TestVCD) testMetadataIgnore(resource metadataCompatible, objectType, objectName string, check *C) { + existingMetadata, err := resource.GetMetadata() + check.Assert(err, IsNil) + + err = resource.AddMetadataEntryWithVisibility("foo", "bar", types.MetadataStringValue, types.MetadataReadWriteVisibility, false) + check.Assert(err, IsNil) + + // Add a new entry that won't be filtered out + err = resource.AddMetadataEntryWithVisibility("not_ignored", "bar2", types.MetadataStringValue, types.MetadataReadWriteVisibility, false) + check.Assert(err, IsNil) + + cleanup := func() { + vcd.client.Client.IgnoredMetadata = nil + metadata, err := resource.GetMetadata() + check.Assert(err, IsNil) + for _, entry := range metadata.MetadataEntry { + itWasAlreadyPresent := false + for _, existingEntry := range existingMetadata.MetadataEntry { + if existingEntry.Key == entry.Key && existingEntry.TypedValue.Value == entry.TypedValue.Value && + existingEntry.Type == entry.Type { + itWasAlreadyPresent = true + } + } + if !itWasAlreadyPresent { + err = resource.DeleteMetadataEntryWithDomain(entry.Key, entry.Domain != nil && entry.Domain.Domain == "SYSTEM") + check.Assert(err, IsNil) + } + } + metadata, err = resource.GetMetadata() + check.Assert(err, IsNil) + check.Assert(metadata, NotNil) + check.Assert(len(metadata.MetadataEntry), Equals, len(existingMetadata.MetadataEntry)) + } + defer cleanup() + + tests := []struct { + ignoredMetadata []IgnoredMetadata + metadataIsIgnored bool + }{ + { + ignoredMetadata: []IgnoredMetadata{{ObjectType: &objectType, KeyRegex: regexp.MustCompile(`^foo$`)}}, + metadataIsIgnored: true, + }, + { + ignoredMetadata: []IgnoredMetadata{{ObjectType: &objectType, ValueRegex: regexp.MustCompile(`^bar$`)}}, + metadataIsIgnored: true, + }, + { + ignoredMetadata: []IgnoredMetadata{{ObjectType: &objectType, KeyRegex: regexp.MustCompile(`^fizz$`)}}, + metadataIsIgnored: false, + }, + { + ignoredMetadata: []IgnoredMetadata{{ObjectType: &objectType, ValueRegex: regexp.MustCompile(`^buzz$`)}}, + metadataIsIgnored: false, + }, + { + ignoredMetadata: []IgnoredMetadata{{ObjectName: &objectName, KeyRegex: regexp.MustCompile(`^foo$`)}}, + metadataIsIgnored: true, + }, + { + ignoredMetadata: []IgnoredMetadata{{ObjectName: &objectName, ValueRegex: regexp.MustCompile(`^bar$`)}}, + metadataIsIgnored: true, + }, + { + ignoredMetadata: []IgnoredMetadata{{ObjectName: &objectName, KeyRegex: regexp.MustCompile(`^fizz$`)}}, + metadataIsIgnored: false, + }, + { + ignoredMetadata: []IgnoredMetadata{{ObjectName: &objectName, ValueRegex: regexp.MustCompile(`^buzz$`)}}, + metadataIsIgnored: false, + }, + { + ignoredMetadata: []IgnoredMetadata{{ObjectType: &objectType, ObjectName: &objectName, KeyRegex: regexp.MustCompile(`foo`), ValueRegex: regexp.MustCompile(`bar`)}}, + metadataIsIgnored: true, + }, + } + + // Tests that the ignored metadata setter works as expected + vcd.client.Client.IgnoredMetadata = []IgnoredMetadata{{ObjectType: &objectType, ValueRegex: regexp.MustCompile(`dummy`)}} + previousIgnoredMetadata := vcd.client.SetMetadataToIgnore(nil) + check.Assert(vcd.client.Client.IgnoredMetadata, IsNil) + check.Assert(len(previousIgnoredMetadata) > 0, Equals, true) + previousIgnoredMetadata = vcd.client.SetMetadataToIgnore(previousIgnoredMetadata) + check.Assert(previousIgnoredMetadata, IsNil) + check.Assert(len(vcd.client.Client.IgnoredMetadata) > 0, Equals, true) + + for _, tt := range tests { + vcd.client.Client.IgnoredMetadata = tt.ignoredMetadata + + // Tests getting a simple metadata entry by its key + singleMetadata, err := resource.GetMetadataByKey("foo", false) + if tt.metadataIsIgnored { + check.Assert(err, NotNil) + check.Assert(true, Equals, strings.Contains(err.Error(), "ignored")) + } else { + check.Assert(err, IsNil) + check.Assert(singleMetadata, NotNil) + check.Assert(singleMetadata.TypedValue.Value, Equals, "bar") + } + + // Retrieve all metadata + allMetadata, err := resource.GetMetadata() + check.Assert(err, IsNil) + check.Assert(allMetadata, NotNil) + if tt.metadataIsIgnored { + // If metadata is ignored, there should be an offset of 1 entry (with key "test") + check.Assert(len(allMetadata.MetadataEntry), Equals, len(existingMetadata.MetadataEntry)+1) + for _, entry := range allMetadata.MetadataEntry { + if tt.metadataIsIgnored { + check.Assert(entry.Key, Not(Equals), "foo") + check.Assert(entry.TypedValue.Value, Not(Equals), "bar") + } + } + } else { + // If metadata is NOT ignored, there should be an offset of 2 entries (with key "foo" and "test") + check.Assert(len(allMetadata.MetadataEntry), Equals, len(existingMetadata.MetadataEntry)+2) + } + } + + // Tries to delete a metadata entry that is ignored, it should hence fail + err = resource.DeleteMetadataEntryWithDomain("foo", false) + check.Assert(err, NotNil) + check.Assert(true, Equals, strings.Contains(err.Error(), "ignored")) + + // Tries to merge metadata that is filtered out, hence it should fail + err = resource.MergeMetadataWithMetadataValues(map[string]types.MetadataValue{ + "foo": { + TypedValue: &types.MetadataTypedValue{ + XsiType: types.MetadataStringValue, + Value: "bar3", + }, + }, + }) + check.Assert(err, NotNil) + check.Assert(true, Equals, strings.Contains(err.Error(), "after filtering metadata, there is no metadata to merge")) + + // Tries to merge metadata, one entry is filtered out, another is not + err = resource.MergeMetadataWithMetadataValues(map[string]types.MetadataValue{ + "foo": { + TypedValue: &types.MetadataTypedValue{ + XsiType: types.MetadataStringValue, + Value: "bar3", + }, + }, + "not_ignored": { + TypedValue: &types.MetadataTypedValue{ + XsiType: types.MetadataStringValue, + Value: "bar", + }, + }, + }) + check.Assert(err, IsNil) +} + +// metadataCompatible allows centralizing and generalizing the tests for metadata compatible resources. +type metadataCompatible interface { + GetMetadata() (*types.Metadata, error) + GetMetadataByKey(key string, isSystem bool) (*types.MetadataValue, error) + AddMetadataEntryWithVisibility(key, value, typedValue, visibility string, isSystem bool) error + MergeMetadataWithMetadataValues(metadata map[string]types.MetadataValue) error + DeleteMetadataEntryWithDomain(key string, isSystem bool) error +} + +type metadataTest struct { + Key string + Value string + UpdatedValue string + Type string + Visibility string + IsSystem bool + ExpectErrorOnFirstAdd bool +} + +// testMetadataCRUDActions performs a complete test of all use cases that metadata can have, for a metadata compatible resource. +// The function parameter extraReadStep performs an extra read step that can be passed as a function. Useful to perform a test +// on "admin+not admin" resource combinations, where the "not admin" only has a GetMetadata function. +// For example, AdminOrg and Org, where Org only has GetMetadata. +func (vcd *TestVCD) testMetadataCRUDActions(resource metadataCompatible, check *C, extraReadStep func(testCase metadataTest)) { + // Check how much metadata exists + metadata, err := resource.GetMetadata() + check.Assert(err, IsNil) + check.Assert(metadata, NotNil) + existingMetaDataCount := len(metadata.MetadataEntry) + + var testCases = []metadataTest{ + { + Key: "stringKey", + Value: "stringValue", + UpdatedValue: "stringValueUpdated", + Type: types.MetadataStringValue, + Visibility: types.MetadataReadWriteVisibility, + IsSystem: false, + ExpectErrorOnFirstAdd: false, + }, + { + Key: "numberKey", + Value: "notANumber", + Type: types.MetadataNumberValue, + Visibility: types.MetadataReadWriteVisibility, + IsSystem: false, + ExpectErrorOnFirstAdd: true, + }, + { + Key: "numberKey", + Value: "1", + UpdatedValue: "99", + Type: types.MetadataNumberValue, + Visibility: types.MetadataReadWriteVisibility, + IsSystem: false, + ExpectErrorOnFirstAdd: false, + }, + { + Key: "boolKey", + Value: "notABool", + Type: types.MetadataBooleanValue, + Visibility: types.MetadataReadWriteVisibility, + IsSystem: false, + ExpectErrorOnFirstAdd: true, + }, + { + Key: "boolKey", + Value: "true", + UpdatedValue: "false", + Type: types.MetadataBooleanValue, + Visibility: types.MetadataReadWriteVisibility, + IsSystem: false, + ExpectErrorOnFirstAdd: false, + }, + { + Key: "dateKey", + Value: "notADate", + Type: types.MetadataDateTimeValue, + Visibility: types.MetadataReadWriteVisibility, + IsSystem: false, + ExpectErrorOnFirstAdd: true, + }, + { + Key: "dateKey", + Value: "2022-10-05T13:44:00.000Z", + UpdatedValue: "2022-12-05T13:44:00.000Z", + Type: types.MetadataDateTimeValue, + Visibility: types.MetadataReadWriteVisibility, + IsSystem: false, + ExpectErrorOnFirstAdd: false, + }, + { + Key: "hidden", + Value: "hiddenValue", + UpdatedValue: "hiddenValueUpdated", + Type: types.MetadataStringValue, + Visibility: types.MetadataHiddenVisibility, + IsSystem: true, + ExpectErrorOnFirstAdd: false, + }, + { + Key: "readOnly", + Value: "readOnlyValue", + UpdatedValue: "readOnlyValueUpdated", + Type: types.MetadataStringValue, + Visibility: types.MetadataReadOnlyVisibility, + IsSystem: true, + ExpectErrorOnFirstAdd: false, + }, + { + Key: "readWriteKey", + Value: "butPlacedInSystem", + Type: types.MetadataStringValue, + Visibility: types.MetadataReadWriteVisibility, + IsSystem: true, + ExpectErrorOnFirstAdd: true, // types.MetadataReadWriteVisibility can't have isSystem=true + }, + } + + for _, testCase := range testCases { + + // The SYSTEM domain can only be set by a system administrator. + // If this test runs as org user, we skip the cases containing 'IsSystem' constraints + if !vcd.client.Client.IsSysAdmin && testCase.IsSystem { + continue + } + + err = resource.AddMetadataEntryWithVisibility(testCase.Key, testCase.Value, testCase.Type, testCase.Visibility, testCase.IsSystem) + if testCase.ExpectErrorOnFirstAdd { + check.Assert(err, NotNil) + continue + } + check.Assert(err, IsNil) + + // Check if metadata was added correctly + metadata, err = resource.GetMetadata() + check.Assert(err, IsNil) + assertMetadata(check, metadata, testCase, existingMetaDataCount+1) + + metadataValue, err := resource.GetMetadataByKey(testCase.Key, testCase.IsSystem) + check.Assert(err, IsNil) + check.Assert(metadataValue.TypedValue.Value, Equals, testCase.Value) + check.Assert(metadataValue.TypedValue.XsiType, Equals, testCase.Type) + + // Perform an extra read step that can be passed as a function. Useful to perform a test + // on resources that only have a GetMetadata function. For example, AdminOrg and Org, where Org only has GetMetadata. + if extraReadStep != nil { + extraReadStep(testCase) + } + + domain := "GENERAL" + if testCase.IsSystem { + domain = "SYSTEM" + } + // Merge updated metadata with a new entry + err = resource.MergeMetadataWithMetadataValues(map[string]types.MetadataValue{ + "mergedKey": { + TypedValue: &types.MetadataTypedValue{ + Value: "mergedValue", + XsiType: types.MetadataStringValue, + }, + }, + testCase.Key: { + Domain: &types.MetadataDomainTag{ + Visibility: testCase.Visibility, + Domain: domain, + }, + TypedValue: &types.MetadataTypedValue{ + Value: testCase.UpdatedValue, + XsiType: testCase.Type, + }, + }, + }) + check.Assert(err, IsNil) + + // Check that the first key was updated and the second, created + metadata, err = resource.GetMetadata() + check.Assert(err, IsNil) + check.Assert(metadata, NotNil) + check.Assert(len(metadata.MetadataEntry), Equals, existingMetaDataCount+2) + for _, entry := range metadata.MetadataEntry { + switch entry.Key { + case "mergedKey": + check.Assert(entry.TypedValue.Value, Equals, "mergedValue") + case testCase.Key: + check.Assert(entry.TypedValue.Value, Equals, testCase.UpdatedValue) + } + } + + err = resource.DeleteMetadataEntryWithDomain("mergedKey", false) + check.Assert(err, IsNil) + err = resource.DeleteMetadataEntryWithDomain(testCase.Key, testCase.IsSystem) + check.Assert(err, IsNil) + + // Check if metadata was deleted correctly + metadata, err = resource.GetMetadata() + check.Assert(err, IsNil) + check.Assert(metadata, NotNil) + check.Assert(len(metadata.MetadataEntry), Equals, existingMetaDataCount) + } +} + +// assertMetadata performs a common set of assertions on the given metadata +func assertMetadata(check *C, given *types.Metadata, expected metadataTest, expectedMetadataEntries int) { + check.Assert(given, NotNil) + check.Assert(len(given.MetadataEntry), Equals, expectedMetadataEntries) + var foundEntry *types.MetadataEntry + for _, entry := range given.MetadataEntry { + if entry.Key == expected.Key { + foundEntry = entry + } + } + check.Assert(foundEntry, NotNil) + check.Assert(foundEntry.Key, Equals, expected.Key) + check.Assert(foundEntry.TypedValue.Value, Equals, expected.Value) + check.Assert(foundEntry.TypedValue.XsiType, Equals, expected.Type) + if expected.IsSystem { + // If it's on SYSTEM domain, VCD should return the Domain subtype always populated + check.Assert(foundEntry.Domain, NotNil) + check.Assert(foundEntry.Domain.Domain, Equals, "SYSTEM") + check.Assert(foundEntry.Domain.Visibility, Equals, expected.Visibility) + } else { + if expected.Visibility == types.MetadataReadWriteVisibility { + // If it's on GENERAL domain, and the entry is Read/Write, VCD doesn't return the Domain subtype. + check.Assert(foundEntry.Domain, IsNil) + } else { + check.Assert(foundEntry.Domain.Domain, Equals, "GENERAL") + check.Assert(foundEntry.Domain.Visibility, Equals, expected.Visibility) + } + } +} diff --git a/govcd/metadata_v2_unit_test.go b/govcd/metadata_v2_unit_test.go new file mode 100644 index 000000000..17a2636fa --- /dev/null +++ b/govcd/metadata_v2_unit_test.go @@ -0,0 +1,121 @@ +//go:build unit || ALL + +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "github.com/vmware/go-vcloud-director/v2/types/v56" + "reflect" + "testing" +) + +func Test_normaliseXmlMetadata(t *testing.T) { + type args struct { + key string + href string + objectName string + metadataEntry *types.MetadataValue + } + tests := []struct { + name string + args args + want *normalisedMetadata + wantErr bool + }{ + { + name: "string normalised", + args: args{ + key: "key", + objectName: "foo", + href: "/admin/catalog/67e119b7-083b-349e-8dfd-6cf0c19b83cf", + metadataEntry: &types.MetadataValue{ + TypedValue: &types.MetadataTypedValue{ + XsiType: types.MetadataStringValue, + Value: "value", + }, + }, + }, + want: &normalisedMetadata{ + ObjectType: "catalog", + ObjectName: "foo", + Key: "key", + Value: "value", + }, + }, + { + name: "bool normalised", + args: args{ + key: "key", + objectName: "foo", + href: "/admin/catalog/67e119b7-083b-349e-8dfd-6cf0c19b83cf", + metadataEntry: &types.MetadataValue{ + TypedValue: &types.MetadataTypedValue{ + XsiType: types.MetadataBooleanValue, + Value: "true", + }, + }, + }, + want: &normalisedMetadata{ + ObjectType: "catalog", + ObjectName: "foo", + Key: "key", + Value: "true", + }, + }, + { + name: "number normalised", + args: args{ + key: "key", + objectName: "foo", + href: "/admin/catalog/67e119b7-083b-349e-8dfd-6cf0c19b83cf", + metadataEntry: &types.MetadataValue{ + TypedValue: &types.MetadataTypedValue{ + XsiType: types.MetadataNumberValue, + Value: "314159", + }, + }, + }, + want: &normalisedMetadata{ + ObjectType: "catalog", + ObjectName: "foo", + Key: "key", + Value: "314159", + }, + }, + { + name: "date normalised", + args: args{ + key: "key", + objectName: "foo", + href: "/admin/catalog/67e119b7-083b-349e-8dfd-6cf0c19b83cf", + metadataEntry: &types.MetadataValue{ + TypedValue: &types.MetadataTypedValue{ + XsiType: types.MetadataDateTimeValue, + Value: "2023-11-16T09:56:00.000Z", + }, + }, + }, + want: &normalisedMetadata{ + ObjectType: "catalog", + ObjectName: "foo", + Key: "key", + Value: "2023-11-16T09:56:00.000Z", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := normaliseXmlMetadata(tt.args.key, tt.args.href, tt.args.objectName, tt.args.metadataEntry) + if (err != nil) != tt.wantErr { + t.Errorf("normaliseXmlMetadata() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("normaliseXmlMetadata() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/govcd/monitor.go b/govcd/monitor.go index c20a02688..75f72fe7c 100644 --- a/govcd/monitor.go +++ b/govcd/monitor.go @@ -163,7 +163,7 @@ func prettyTask(task *types.Task) string { } // Returns an Edge Gateway service configuration structure as JSON -//func prettyEdgeGatewayServiceConfiguration(conf types.EdgeGatewayServiceConfiguration) string { +// func prettyEdgeGatewayServiceConfiguration(conf types.EdgeGatewayServiceConfiguration) string { func prettyEdgeGateway(egw types.EdgeGateway) string { result := "" byteBuf, err := json.MarshalIndent(egw, " ", " ") @@ -308,6 +308,20 @@ func SimpleShowTask(task *types.Task, howManyTimes int, elapsed time.Duration, f simpleOutTask("screen", task, howManyTimes, elapsed, first, last) } +func MinimalShowTask(task *types.Task, howManyTimes int, elapsed time.Duration, first, last bool) { + marker := "." + if task.Status == "success" { + marker = "+" + } + if task.Status == "error" { + marker = "-" + } + fmt.Print(marker) + if last { + fmt.Println() + } +} + func SimpleLogTask(task *types.Task, howManyTimes int, elapsed time.Duration, first, last bool) { simpleOutTask("log", task, howManyTimes, elapsed, first, last) } diff --git a/govcd/multi_site.go b/govcd/multi_site.go new file mode 100644 index 000000000..8132f7471 --- /dev/null +++ b/govcd/multi_site.go @@ -0,0 +1,390 @@ +package govcd + +import ( + "encoding/xml" + "fmt" + "github.com/vmware/go-vcloud-director/v2/types/v56" + "net/http" + "net/url" + "os" + "path" + "time" +) + +/* +This file contains methods to retrieve, set, and delete associations between VCD entities. + +The associations come in two flavors: + +1. Site association: will let one VCD use the other site entities as its own +2. Org associations: + 2a. Associates an organization with another on the same VCD + 2b. Associates an organization with another in a different VCD (requires a site association) + +*/ + +// ----------------------------------------------------------------------------------------------------------------- +// Site read operations +// ----------------------------------------------------------------------------------------------------------------- + +// GetSite retrieves the data for the current site (VCD) +func (client Client) GetSite() (*types.Site, error) { + href, err := url.JoinPath(client.VCDHREF.String(), "site") + if err != nil { + return nil, fmt.Errorf("error setting the URL path for site: %s", err) + } + var site types.Site + _, err = client.ExecuteRequest(href, http.MethodGet, "application/*+xml", + "error retrieving site: %s", nil, &site) + if err != nil { + return nil, err + } + + return &site, nil +} + +// ----------------------------------------------------------------------------------------------------------------- +// Site association read operations +// ----------------------------------------------------------------------------------------------------------------- + +// QueryAllSiteAssociations retrieves all site associations for the current site +func (client Client) QueryAllSiteAssociations(params, notEncodedParams map[string]string) ([]*types.QueryResultSiteAssociationRecord, error) { + if !client.IsSysAdmin { + return nil, fmt.Errorf("system administrator privileges are needed to handle site associations") + } + + result, err := client.cumulativeQuery(types.QtSiteAssociation, params, notEncodedParams) + if err != nil { + return nil, err + } + + return result.Results.SiteAssociationRecord, nil +} + +// GetSiteAssociationData retrieves the structured data needed to start an association with another site +// This is useful when we have control of both sites from the same client +func (client Client) GetSiteAssociationData() (*types.SiteAssociationMember, error) { + href, err := url.JoinPath(client.VCDHREF.String(), "site", "associations", "localAssociationData") + if err != nil { + return nil, fmt.Errorf("error setting the URL path for localAssociationData: %s", err) + } + var associationData types.SiteAssociationMember + _, err = client.ExecuteRequest(href, http.MethodGet, types.MimeSiteAssociation, + "error retrieving site associations: %s", nil, &associationData) + if err != nil { + return nil, err + } + + return &associationData, nil +} + +// GetSiteRawAssociationData retrieves the raw (XML) data needed to start an association with another site +// This is useful when we want to save this data to a file for future use +func (client Client) GetSiteRawAssociationData() ([]byte, error) { + href, err := url.JoinPath(client.VCDHREF.String(), "site", "associations", "localAssociationData") + if err != nil { + return nil, fmt.Errorf("error setting the URL path for site/associations/localAssociationData: %s", err) + } + return client.RetrieveRemoteDocument(href) +} + +// GetSiteAssociations retrieves all current site associations +// If no associations are available, it returns an empty slice with no error +func (client Client) GetSiteAssociations() ([]*types.SiteAssociationMember, error) { + + href, err := url.JoinPath(client.VCDHREF.String(), "site", "associations") + if err != nil { + return nil, fmt.Errorf("error setting the URL path for site/associations: %s", err) + } + var associations types.SiteAssociations + _, err = client.ExecuteRequest(href, http.MethodGet, types.MimeSiteAssociation, + "error retrieving site associations: %s", nil, &associations) + if err != nil { + return nil, err + } + + return associations.SiteAssociations, nil +} + +// GetSiteAssociationBySiteId retrieves a single site association by the ID of the associated site +// Note that there could be only one association between two sites +func (client Client) GetSiteAssociationBySiteId(siteId string) (*types.SiteAssociationMember, error) { + associations, err := client.GetSiteAssociations() + if err != nil { + return nil, fmt.Errorf("error retrieving associations for current site: %s", err) + } + + for _, a := range associations { + if equalIds(siteId, a.SiteID, "") { + return a, nil + } + } + return nil, fmt.Errorf("no association found for site ID %s", siteId) +} + +// CheckSiteAssociation polls the state of a given site association until it becomes active, or a timeout is reached. +// Note: this method should be called only after both sides have performed the data association upload. +func (client Client) CheckSiteAssociation(siteId string, timeout time.Duration) (string, time.Duration, error) { + startTime := time.Now() + + foundStatus := "" + elapsed := time.Since(startTime) + for elapsed < timeout { + time.Sleep(time.Second) + elapsed = time.Since(startTime) + siteAssociation, err := client.GetSiteAssociationBySiteId(siteId) + if err != nil { + return foundStatus, elapsed, fmt.Errorf("error getting site association by ID '%s': %s", siteId, err) + } + foundStatus = siteAssociation.Status + if foundStatus == string(types.StatusActive) { + return foundStatus, elapsed, nil + } + } + return foundStatus, elapsed, fmt.Errorf("site association '%s' not ACTIVE within the given timeout of %s: found status: '%s'", siteId, timeout, foundStatus) +} + +// ----------------------------------------------------------------------------------------------------------------- +// Site association modifying operations +// ----------------------------------------------------------------------------------------------------------------- + +// SetSiteAssociationAsync sets a new site association without waiting for completion +func (client Client) SetSiteAssociationAsync(associationData types.SiteAssociationMember) (Task, error) { + href, err := url.JoinPath(client.VCDHREF.String(), "site", "associations") + if err != nil { + return Task{}, fmt.Errorf("error setting the URL path for site/associations: %s", err) + } + associationData.Xmlns = types.XMLNamespaceVCloud + task, err := client.ExecuteTaskRequest(href, http.MethodPost, "application/*+xml", + "error setting site association: %s", &associationData) + if err != nil { + return Task{}, err + } + + return task, nil +} + +// SetSiteAssociation sets a new site association, waiting for completion +func (client Client) SetSiteAssociation(associationData types.SiteAssociationMember) error { + task, err := client.SetSiteAssociationAsync(associationData) + if err != nil { + return err + } + return task.WaitTaskCompletion() +} + +// RemoveSiteAssociationAsync removes a site association without waiting for completion +func (client Client) RemoveSiteAssociationAsync(associationHref string) (Task, error) { + task, err := client.ExecuteTaskRequest(associationHref, http.MethodDelete, "", + "error removing site association: %s", nil) + if err != nil { + return Task{}, err + } + + return task, nil +} + +// RemoveSiteAssociation removes a site association, waiting for completion +func (client Client) RemoveSiteAssociation(associationHref string) error { + task, err := client.RemoveSiteAssociationAsync(associationHref) + if err != nil { + return err + } + return task.WaitTaskCompletion() +} + +// ----------------------------------------------------------------------------------------------------------------- +// Org association read operations +// ----------------------------------------------------------------------------------------------------------------- + +// QueryAllOrgAssociations retrieve all site associations with optional search parameters +func (client Client) QueryAllOrgAssociations(params, notEncodedParams map[string]string) ([]*types.QueryResultOrgAssociationRecord, error) { + if !client.IsSysAdmin { + return nil, fmt.Errorf("system administrator privileges are needed to handle Org associations") + } + + result, err := client.cumulativeQuery(types.QtOrgAssociation, params, notEncodedParams) + if err != nil { + return nil, err + } + + return result.Results.OrgAssociationRecord, nil +} + +// GetOrgAssociations retrieves all associations available for the given Org +func (org AdminOrg) GetOrgAssociations() ([]*types.OrgAssociationMember, error) { + href, err := org.getAssociationLink(false) + if err != nil { + return nil, fmt.Errorf("error retrieving association URL: %s", err) + } + var associations types.OrgAssociations + _, err = org.client.ExecuteRequest(href, http.MethodGet, types.MimeOrgAssociation, + "error retrieving org associations: %s", nil, &associations) + if err != nil { + return nil, err + } + + return associations.OrgAssociations, nil +} + +// GetOrgAssociationByOrgId retrieves a single Org association by the ID of the associated Org +// Note that there could be only one association between two organization +func (org AdminOrg) GetOrgAssociationByOrgId(orgId string) (*types.OrgAssociationMember, error) { + associations, err := org.GetOrgAssociations() + if err != nil { + return nil, fmt.Errorf("error retrieving associations for org '%s': %s", org.AdminOrg.Name, err) + } + + for _, a := range associations { + if equalIds(orgId, a.OrgID, "") { + return a, nil + } + } + return nil, fmt.Errorf("no association found for Org ID %s", orgId) +} + +// GetOrgAssociationData retrieves the structured data needed to start an association with another Org +// This is useful when we have control of both Orgs from the same client +func (org AdminOrg) GetOrgAssociationData() (*types.OrgAssociationMember, error) { + href, err := org.getAssociationLink(true) + if err != nil { + return nil, fmt.Errorf("error retrieving association URL: %s", err) + } + var associationData types.OrgAssociationMember + _, err = org.client.ExecuteRequest(href, http.MethodGet, types.MimeOrgAssociation, + "error retrieving org association data: %s", nil, &associationData) + if err != nil { + return nil, err + } + + return &associationData, nil +} + +// GetOrgRawAssociationData retrieves the raw (XML) data needed to start an association with another Org +// This is useful when we want to save this data to a file for future use +func (org AdminOrg) GetOrgRawAssociationData() ([]byte, error) { + href, err := org.getAssociationLink(true) + if err != nil { + return nil, fmt.Errorf("error retrieving association URL: %s", err) + } + return org.client.RetrieveRemoteDocument(href) +} + +// CheckOrgAssociation polls the state of a given Org association until it becomes active, or a timeout is reached. +// Note: this method should be called only after both sides have performed the data association upload. +func (org AdminOrg) CheckOrgAssociation(orgId string, timeout time.Duration) (string, time.Duration, error) { + startTime := time.Now() + + foundStatus := "" + elapsed := time.Since(startTime) + for elapsed < timeout { + time.Sleep(time.Second) + elapsed = time.Since(startTime) + orgAssociation, err := org.GetOrgAssociationByOrgId(orgId) + if err != nil { + return foundStatus, elapsed, fmt.Errorf("error getting org association by ID '%s': %s", orgId, err) + } + foundStatus = orgAssociation.Status + if foundStatus == string(types.StatusActive) { + return foundStatus, elapsed, nil + } + } + return foundStatus, elapsed, fmt.Errorf("org association '%s' not ACTIVE within the given timeout of %s: found status: '%s'", orgId, timeout, foundStatus) +} + +// ----------------------------------------------------------------------------------------------------------------- +// Org association modifying operations +// ----------------------------------------------------------------------------------------------------------------- + +// SetOrgAssociationAsync sets a new Org association without waiting for completion +func (org *AdminOrg) SetOrgAssociationAsync(associationData types.OrgAssociationMember) (Task, error) { + href, err := org.getAssociationLink(false) + if err != nil { + return Task{}, fmt.Errorf("error retrieving association URL: %s", err) + } + associationData.Xmlns = types.XMLNamespaceVCloud + task, err := org.client.ExecuteTaskRequest(href, http.MethodPost, "application/*+xml", + "error setting org association: %s", &associationData) + if err != nil { + return Task{}, err + } + + return task, nil +} + +// SetOrgAssociation sets a new Org association, waiting for completion +func (org *AdminOrg) SetOrgAssociation(associationData types.OrgAssociationMember) error { + task, err := org.SetOrgAssociationAsync(associationData) + if err != nil { + return err + } + return task.WaitTaskCompletion() +} + +// RemoveOrgAssociationAsync removes an Org association without waiting for completion +func (org *AdminOrg) RemoveOrgAssociationAsync(associationHref string) (Task, error) { + task, err := org.client.ExecuteTaskRequest(associationHref, http.MethodDelete, "", + "error removing org association: %s", nil) + if err != nil { + return Task{}, err + } + + return task, nil +} + +// RemoveOrgAssociation removes an Org association, waiting for completion +func (org *AdminOrg) RemoveOrgAssociation(associationHref string) error { + task, err := org.RemoveOrgAssociationAsync(associationHref) + if err != nil { + return err + } + return task.WaitTaskCompletion() +} + +// ----------------------------------------------------------------------------------------------------------------- +// Miscellaneous +// ----------------------------------------------------------------------------------------------------------------- + +// getAssociationLink retrieves the URL needed to run associations operations with an Org. +// If the 'localData' parameter is true, it returns the URL needed to download the association +// data needed to create a new association +func (org AdminOrg) getAssociationLink(localData bool) (string, error) { + href := getUrlFromLink(org.AdminOrg.Link, "down", types.MimeOrgAssociation) + if href == "" { + return "", fmt.Errorf("no HREF found to get Org association data for Org '%s'", org.AdminOrg.Name) + } + + if localData { + var err error + href, err = url.JoinPath(href, "localAssociationData") + if err != nil { + return "", err + } + } + return href, nil +} + +// ReadXmlDataFromFile reads the contents of a file and attempts decoding an expected data type +// Examples: +// orgSettingData, err := ReadXmlDataFromFile[types.OrgAssociationMember]("./data/org1-association-data.xml") +// siteSettingData, err := ReadXmlDataFromFile[types.SiteAssociationMember]("./data/site1-association-data.xml") +func ReadXmlDataFromFile[dataType any](fileName string) (*dataType, error) { + contents, err := os.ReadFile(path.Clean(fileName)) + if err != nil { + return nil, fmt.Errorf("error reading file '%s': %s", fileName, err) + } + return RawDataToStructuredXml[dataType](contents) +} + +// RawDataToStructuredXml reads an input byte stream and attempts decoding an expected data type +// Examples: +// orgSettingData, err := RawDataToStructuredXml[types.OrgAssociationMember](data) +// siteSettingData, err := RawDataToStructuredXml[types.SiteAssociationMember](data) +func RawDataToStructuredXml[dataType any](rawData []byte) (*dataType, error) { + var localData dataType + err := xml.Unmarshal(rawData, &localData) + if err != nil { + return nil, fmt.Errorf("error decoding data: %s", err) + } + return &localData, nil +} diff --git a/govcd/multi_site_test.go b/govcd/multi_site_test.go new file mode 100644 index 000000000..feedda9f6 --- /dev/null +++ b/govcd/multi_site_test.go @@ -0,0 +1,369 @@ +//go:build system || functional || ALL + +/* + * Copyright 2024 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" + "os" + "strings" + "time" +) + +// #nosec G101 -- These credentials are fake for testing purposes +const ( + secondVcdUrl = "VCD_URL2" + secondVcdUser = "VCD_USER2" + secondVcdPassword = "VCD_PASSWORD2" + secondVcdSysOrg = "VCD_SYSORG2" + secondVcdOrg2 = "VCD_ORG2" + secondVcdOrgUser2 = "VCD_ORGUSER2" + secondVcdOrgUserPassword2 = "VCD_ORGUSER_PASSWORD2" +) + +func getClientConnectionFromEnv() (*VCDClient, error) { + vcdUrl := os.Getenv(secondVcdUrl) + user := os.Getenv(secondVcdUser) + password := os.Getenv(secondVcdPassword) + orgName := os.Getenv(secondVcdSysOrg) + if !strings.HasSuffix(vcdUrl, "/api") { + return nil, fmt.Errorf("the VCD URL must terminate with '/api'") + } + var missing []string + if vcdUrl == "" { + missing = append(missing, secondVcdUrl) + } + if user == "" { + missing = append(missing, secondVcdUser) + } + if password == "" { + missing = append(missing, secondVcdPassword) + } + if orgName == "" { + missing = append(missing, secondVcdSysOrg) + } + if len(missing) > 0 { + return nil, fmt.Errorf("missing environment variables for connection: %v", missing) + } + + return newUserConnection(vcdUrl, user, password, orgName, true) +} + +/* + Test_SiteAssociations will test the associations between two sites + To run this test, make a shell script like the one below, filling the variables + in addition to the VCD defined in govcd_test_config.yaml + +$ cat connection.sh +export VCD_URL2=https://some-vcd-url.com/api +export VCD_USER2=administrator +export VCD_PASSWORD2='myPassword' +export VCD_SYSORG2=System +export VCD_ORG2=orgname2 +export VCD_ORGUSER2=org-admin-name +export VCD_ORGUSER_PASSWORD2='myOrgAdminPassword' + +$ source connection.sh +$ go test -tags functional -check.f Test_SiteAssociations -vcd-skip-vapp-creation -check.vv -timeout 0 +*/ +func (vcd *TestVCD) Test_SiteAssociations(check *C) { + + if !vcd.client.Client.IsSysAdmin { + check.Skip(fmt.Sprintf("test %s requires system administrator privileges\n", check.TestName())) + } + if !vcd.client.Client.APIClientVersionIs(">= 37.0") { + check.Skip(fmt.Sprintf("Minimum API version required for this test is 37.0. Found: %s", vcd.client.Client.APIVersion)) + } + + firstVcdClient := vcd.client + secondVcdClient, err := getClientConnectionFromEnv() + if err != nil { + check.Skip(fmt.Sprintf("this test requires connection from a second VCD, identified by environment variables %s %s %s %s: %s", + secondVcdUrl, secondVcdUser, secondVcdPassword, secondVcdSysOrg, err)) + } + + // The second VCD must be different from the first one + check.Assert(os.Getenv(secondVcdUrl), Not(Equals), firstVcdClient.Client.VCDHREF.String()) + + version1, _, err := firstVcdClient.Client.GetVcdVersion() + check.Assert(err, IsNil) + version2, _, err := secondVcdClient.Client.GetVcdVersion() + check.Assert(err, IsNil) + + // Both VCDs must have the same version + check.Assert(version1, Equals, version2) + + // STEP 1 Get the site association data from both VCDs + firstVcdStructuredAssociationData, err := firstVcdClient.Client.GetSiteAssociationData() + check.Assert(err, IsNil) + firstVcdRawAssociationData, err := firstVcdClient.Client.GetSiteRawAssociationData() + check.Assert(err, IsNil) + secondVcdStructuredAssociationData, err := secondVcdClient.Client.GetSiteAssociationData() + check.Assert(err, IsNil) + secondVcdRawAssociationData, err := secondVcdClient.Client.GetSiteRawAssociationData() + check.Assert(err, IsNil) + + // Check that the raw data is equivalent to the structured data + firstConvertedAssociationData, err := RawDataToStructuredXml[types.SiteAssociationMember](firstVcdRawAssociationData) + check.Assert(err, IsNil) + check.Assert(firstConvertedAssociationData.SiteID, Equals, firstVcdStructuredAssociationData.SiteID) + check.Assert(firstConvertedAssociationData.PublicKey, Equals, firstVcdStructuredAssociationData.PublicKey) + check.Assert(firstConvertedAssociationData.RestEndpointCertificate, Equals, firstVcdStructuredAssociationData.RestEndpointCertificate) + secondConvertedAssociationData, err := RawDataToStructuredXml[types.SiteAssociationMember](secondVcdRawAssociationData) + check.Assert(err, IsNil) + check.Assert(secondConvertedAssociationData.SiteID, Equals, secondVcdStructuredAssociationData.SiteID) + check.Assert(secondConvertedAssociationData.PublicKey, Equals, secondVcdStructuredAssociationData.PublicKey) + check.Assert(secondConvertedAssociationData.RestEndpointCertificate, Equals, secondVcdStructuredAssociationData.RestEndpointCertificate) + + // STEP 2 Get the list of current site associations from both sites for further comparison + orgs1before, err := firstVcdClient.GetAllOrgs(nil, false) + check.Assert(err, IsNil) + orgs2before, err := secondVcdClient.GetAllOrgs(nil, false) + check.Assert(err, IsNil) + + associations1before, err := firstVcdClient.Client.GetSiteAssociations() + check.Assert(err, IsNil) + associations2before, err := secondVcdClient.Client.GetSiteAssociations() + check.Assert(err, IsNil) + + // STEP 3 Set the associations in both VCDs + err = firstVcdClient.Client.SetSiteAssociation(*secondVcdStructuredAssociationData) + check.Assert(err, IsNil) + err = secondVcdClient.Client.SetSiteAssociation(*firstVcdStructuredAssociationData) + check.Assert(err, IsNil) + // Note: there is no call to AddToCleanupList, because we can't defer that action to a temporary client in a separate VCD + + // STEP 4 get the list of associations and organizations + associations1, err := firstVcdClient.Client.GetSiteAssociations() + check.Assert(err, IsNil) + check.Assert(len(associations1), Equals, len(associations1before)+1) + associations2, err := secondVcdClient.Client.GetSiteAssociations() + check.Assert(err, IsNil) + check.Assert(len(associations2), Equals, len(associations2before)+1) + + // STEP 5 retrieve the specific associations that we have just created (used for removal) + association1, err := firstVcdClient.Client.GetSiteAssociationBySiteId(secondVcdStructuredAssociationData.SiteID) + check.Assert(err, IsNil) + association2, err := secondVcdClient.Client.GetSiteAssociationBySiteId(firstVcdStructuredAssociationData.SiteID) + check.Assert(err, IsNil) + + // STEP 6 trigger the site association removal (at the end of tests) + defer func() { + fmt.Println("removing site association 1") + err = firstVcdClient.Client.RemoveSiteAssociation(association1.Href) + check.Assert(err, IsNil) + fmt.Println("removing site association 2") + err = secondVcdClient.Client.RemoveSiteAssociation(association2.Href) + check.Assert(err, IsNil) + }() + + // STEP 7 Check that the association is complete + status1, elapsed1, err := firstVcdClient.Client.CheckSiteAssociation(secondVcdStructuredAssociationData.SiteID, 120*time.Second) + check.Assert(err, IsNil) + fmt.Printf("site #1: status: %s - elapsed: %s\n", status1, elapsed1) + status2, elapsed2, err := secondVcdClient.Client.CheckSiteAssociation(firstVcdStructuredAssociationData.SiteID, 120*time.Second) + check.Assert(err, IsNil) + fmt.Printf("site #2: status: %s - elapsed: %s\n", status2, elapsed2) + + // STEP 8 check number of organizations + orgs1after, err := firstVcdClient.GetAllOrgs(nil, true) + check.Assert(err, IsNil) + orgs2after, err := secondVcdClient.GetAllOrgs(nil, true) + check.Assert(err, IsNil) + fmt.Printf("site #1 - Number of orgs before associations: %d - after association: %d\n", len(orgs1before), len(orgs1after)) + fmt.Printf("site #2 - Number of orgs before associations: %d - after association: %d\n", len(orgs2before), len(orgs2after)) + check.Assert(len(orgs1after), Equals, len(orgs1before)+len(orgs2before)) + check.Assert(len(orgs2after), Equals, len(orgs1before)+len(orgs2before)) + + // STEP 9 get organization association data from both sides + // NOTE: org association from different sites can only happen AFTER the two VCDs have been associated at site level + + if len(vcd.config.Tenants) == 0 { + fmt.Println("no tenant user defined for this VCD") + return + } + localUser, err := newUserConnection( + firstVcdClient.Client.VCDHREF.String(), + vcd.config.Tenants[0].User, + vcd.config.Tenants[0].Password, + vcd.config.Tenants[0].SysOrg, true) + check.Assert(err, IsNil) + localOrg, err := localUser.GetAdminOrgByName(vcd.config.Tenants[0].SysOrg) + fmt.Printf("Using Org user '%s@%s' (site 1 %s)\n", vcd.config.Tenants[0].User, vcd.config.Tenants[0].SysOrg, firstVcdClient.Client.VCDHREF.String()) + check.Assert(err, IsNil) + + fmt.Println("org1 (site1) connected") + remoteOrgName := os.Getenv(secondVcdOrg2) + remoteOrgUserName := os.Getenv(secondVcdOrgUser2) + remoteOrgPassword := os.Getenv(secondVcdOrgUserPassword2) + if remoteOrgName == "" || remoteOrgPassword == "" || remoteOrgUserName == "" { + fmt.Printf("one or more of [%s %s %s] was not defined\n", secondVcdOrg2, secondVcdOrgUser2, secondVcdOrgUserPassword2) + return + } + _, err = secondVcdClient.GetOrgByName(remoteOrgName) + if err != nil { + fmt.Printf("Error retrieving Org '%s' in site %s\n", remoteOrgName, secondVcdClient.Client.VCDHREF.String()) + } + check.Assert(err, IsNil) + fmt.Printf("Using Org user '%s@%s' (site 2 %s)\n", remoteOrgUserName, remoteOrgName, secondVcdClient.Client.VCDHREF.String()) + remoteUser, err := newUserConnection( + secondVcdClient.Client.VCDHREF.String(), + remoteOrgUserName, + remoteOrgPassword, + remoteOrgName, true) + check.Assert(err, IsNil) + remoteOrg, err := remoteUser.GetAdminOrgByName(remoteOrgName) + check.Assert(err, IsNil) + fmt.Println("org2 (site2) connected") + + orgAssociationData1, err := localOrg.GetOrgAssociationData() + check.Assert(err, IsNil) + orgAssociationData2, err := remoteOrg.GetOrgAssociationData() + check.Assert(err, IsNil) + + // STEP 10: set org association between two VCDs + err = localOrg.SetOrgAssociation(*orgAssociationData2) + check.Assert(err, IsNil) + err = remoteOrg.SetOrgAssociation(*orgAssociationData1) + check.Assert(err, IsNil) + + // STEP 12: retrieve the specific associations that we have just created (used for removal) + orgAssociation1, err := localOrg.GetOrgAssociationByOrgId(orgAssociationData2.OrgID) + check.Assert(err, IsNil) + orgAssociation2, err := remoteOrg.GetOrgAssociationByOrgId(orgAssociationData1.OrgID) + check.Assert(err, IsNil) + + // STEP 11: trigger association removal (at the end of the test: it will happen before the removal of site association) + defer func() { + fmt.Println("removing org association 1") + err = localOrg.RemoveOrgAssociation(orgAssociation1.Href) + check.Assert(err, IsNil) + fmt.Println("removing org association 2") + err = remoteOrg.RemoveOrgAssociation(orgAssociation2.Href) + check.Assert(err, IsNil) + }() + + // STEP 12: check org association connection + status1, elapsed1, err = localOrg.CheckOrgAssociation(orgAssociationData2.OrgID, 120*time.Second) + check.Assert(err, IsNil) + fmt.Printf("org #1 '%s' (from site 1): status: %s - elapsed: %s\n", localOrg.AdminOrg.Name, status1, elapsed1) + status2, elapsed2, err = remoteOrg.CheckOrgAssociation(orgAssociationData1.OrgID, 120*time.Second) + check.Assert(err, IsNil) + fmt.Printf("org #2 '%s' (from site 2): status: %s - elapsed: %s\n", remoteOrg.AdminOrg.Name, status2, elapsed2) + + // STEP 13: deferred org and site removal will happen here +} + +func (vcd *TestVCD) Test_OrgAssociations(check *C) { + + // Note: this test runs regardless of `VCD_TEST_ORG_USER` state, as it uses explicit Org user connections + // to perform its operations + + if len(vcd.config.Tenants) < 2 { + check.Skip(fmt.Sprintf("not enough tenant structures defined in configuration. Two are requited. %d were found", len(vcd.config.Tenants))) + } + // Make sure that the tenants structure is populated + for _, tenant := range vcd.config.Tenants { + if tenant.User == "" || tenant.SysOrg == "" || tenant.Password == "" { + check.Skip("One or more components in tenant structure are empty.") + return + } + } + + firstOrgName := vcd.config.Tenants[0].SysOrg + secondOrgName := vcd.config.Tenants[1].SysOrg + + // STEP 0: define two Org user connections + firstVcdClient, err := newUserConnection(vcd.client.Client.VCDHREF.String(), + vcd.config.Tenants[0].User, + vcd.config.Tenants[0].Password, + firstOrgName, true) + check.Assert(err, IsNil) + secondVcdClient, err := newUserConnection(vcd.client.Client.VCDHREF.String(), + vcd.config.Tenants[1].User, + vcd.config.Tenants[1].Password, + secondOrgName, true) + check.Assert(err, IsNil) + fmt.Printf("Using user '%s@%s'\n", vcd.config.Tenants[0].User, firstOrgName) + fmt.Printf("Using user '%s@%s'\n", vcd.config.Tenants[1].User, secondOrgName) + + // STEP 1: get organization association data from both sides, using their own Org users + var firstOrg *AdminOrg + var secondOrg *AdminOrg + firstOrg, err = firstVcdClient.GetAdminOrgByName(firstOrgName) + check.Assert(err, IsNil) + secondOrg, err = secondVcdClient.GetAdminOrgByName(secondOrgName) + check.Assert(err, IsNil) + + orgAssociationData1, err := firstOrg.GetOrgAssociationData() + check.Assert(err, IsNil) + rawOrgAssociationData1, err := firstOrg.GetOrgRawAssociationData() + check.Assert(err, IsNil) + orgAssociationData2, err := secondOrg.GetOrgAssociationData() + check.Assert(err, IsNil) + + // Check that the raw data is the same as the structured data + rawOrgAssociationData2, err := secondOrg.GetOrgRawAssociationData() + check.Assert(err, IsNil) + convertedAssociationData1, err := RawDataToStructuredXml[types.OrgAssociationMember](rawOrgAssociationData1) + check.Assert(err, IsNil) + check.Assert(orgAssociationData1.OrgID, Equals, convertedAssociationData1.OrgID) + check.Assert(orgAssociationData1.OrgPublicKey, Equals, convertedAssociationData1.OrgPublicKey) + convertedAssociationData2, err := RawDataToStructuredXml[types.OrgAssociationMember](rawOrgAssociationData2) + check.Assert(err, IsNil) + check.Assert(orgAssociationData2.OrgID, Equals, convertedAssociationData2.OrgID) + check.Assert(orgAssociationData2.OrgPublicKey, Equals, convertedAssociationData2.OrgPublicKey) + + // Check number of networks for future comparison + networks1Before, err := firstOrg.GetAllOpenApiOrgVdcNetworks(nil, false) + check.Assert(err, IsNil) + networks2Before, err := secondOrg.GetAllOpenApiOrgVdcNetworks(nil, false) + check.Assert(err, IsNil) + + // STEP 2: set org associations within the same VCD + err = firstOrg.SetOrgAssociation(*orgAssociationData2) + check.Assert(err, IsNil) + err = secondOrg.SetOrgAssociation(*orgAssociationData1) + check.Assert(err, IsNil) + + // STEP 3: check association connection + status1, elapsed1, err := firstOrg.CheckOrgAssociation(orgAssociationData2.OrgID, 120*time.Second) + check.Assert(err, IsNil) + fmt.Printf("org #1 (same site): status: %s - elapsed: %s\n", status1, elapsed1) + status2, elapsed2, err := secondOrg.CheckOrgAssociation(orgAssociationData1.OrgID, 120*time.Second) + check.Assert(err, IsNil) + fmt.Printf("org #2 (same site): status: %s - elapsed: %s\n", status2, elapsed2) + + // STEP 4 retrieve the specific associations that we have just created (used for removal) + orgAssociation1, err := firstOrg.GetOrgAssociationByOrgId(orgAssociationData2.OrgID) + check.Assert(err, IsNil) + orgAssociation2, err := secondOrg.GetOrgAssociationByOrgId(orgAssociationData1.OrgID) + check.Assert(err, IsNil) + + // STEP 5: trigger association removal + defer func() { + check.Assert(err, IsNil) + err = firstOrg.RemoveOrgAssociation(orgAssociation1.Href) + check.Assert(err, IsNil) + err = secondOrg.RemoveOrgAssociation(orgAssociation2.Href) + check.Assert(err, IsNil) + }() + + // STEP 6: check number of networks after association + networks1After, err := firstOrg.GetAllOpenApiOrgVdcNetworks(nil, true) + check.Assert(err, IsNil) + networks2After, err := secondOrg.GetAllOpenApiOrgVdcNetworks(nil, true) + check.Assert(err, IsNil) + + fmt.Printf("org #1 - Networks before associations: %d - after association: %d\n", len(networks1Before), len(networks1After)) + fmt.Printf("org #2 - Networks before associations: %d - after association: %d\n", len(networks2Before), len(networks2After)) + check.Assert(len(networks1After), Equals, len(networks1Before)+len(networks2Before)) + check.Assert(len(networks2After), Equals, len(networks1Before)+len(networks2Before)) + + // STEP 7: deferred associations removal happens here +} diff --git a/govcd/network_pool.go b/govcd/network_pool.go new file mode 100644 index 000000000..212303c91 --- /dev/null +++ b/govcd/network_pool.go @@ -0,0 +1,448 @@ +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "github.com/vmware/go-vcloud-director/v2/types/v56" + "net/url" + "strings" +) + +type NetworkPool struct { + NetworkPool *types.NetworkPool + vcdClient *VCDClient +} + +var backingUseErrorMessages = map[types.BackingUseConstraint]string{ + types.BackingUseExplicit: "no element named %s found", + types.BackingUseWhenOnlyOne: "no single element found for this backing", + types.BackingUseFirstAvailable: "no elements found for this backing", +} + +// GetOpenApiUrl retrieves the full URL of a network pool +func (np *NetworkPool) GetOpenApiUrl() (string, error) { + response, err := url.JoinPath(np.vcdClient.sessionHREF.String(), "admin", "extension", "networkPool", np.NetworkPool.Id) + if err != nil { + return "", err + } + return response, nil +} + +// GetNetworkPoolSummaries retrieves the list of all available network pools +func (vcdClient *VCDClient) GetNetworkPoolSummaries(queryParameters url.Values) ([]*types.NetworkPool, error) { + client := vcdClient.Client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNetworkPoolSummaries + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + typeResponse := []*types.NetworkPool{{}} + err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParameters, &typeResponse, nil) + if err != nil { + return nil, err + } + + return typeResponse, nil +} + +// GetNetworkPoolById retrieves Network Pool with a given ID +func (vcdClient *VCDClient) GetNetworkPoolById(id string) (*NetworkPool, error) { + if id == "" { + return nil, fmt.Errorf("network pool lookup requires ID") + } + + client := vcdClient.Client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNetworkPools + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, id) + if err != nil { + return nil, err + } + + response := &NetworkPool{ + vcdClient: vcdClient, + NetworkPool: &types.NetworkPool{}, + } + + err = client.OpenApiGetItem(apiVersion, urlRef, nil, response.NetworkPool, nil) + if err != nil { + return nil, err + } + + return response, nil +} + +// GetNetworkPoolByName retrieves a network pool with a given name +// Note. It will return an error if multiple network pools exist with the same name +func (vcdClient *VCDClient) GetNetworkPoolByName(name string) (*NetworkPool, error) { + if name == "" { + return nil, fmt.Errorf("network pool lookup requires name") + } + + queryParameters := url.Values{} + queryParameters.Add("filter", "name=="+name) + + filteredNetworkPools, err := vcdClient.GetNetworkPoolSummaries(queryParameters) + if err != nil { + return nil, fmt.Errorf("error getting network pools: %s", err) + } + + if len(filteredNetworkPools) == 0 { + return nil, fmt.Errorf("no network pool found with name '%s' - %s", name, ErrorEntityNotFound) + } + + if len(filteredNetworkPools) > 1 { + return nil, fmt.Errorf("more than one network pool found with name '%s'", name) + } + + return vcdClient.GetNetworkPoolById(filteredNetworkPools[0].Id) +} + +// CreateNetworkPool creates a network pool using the given configuration +// It can create any type of network pool +func (vcdClient *VCDClient) CreateNetworkPool(config *types.NetworkPool) (*NetworkPool, error) { + client := vcdClient.Client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNetworkPools + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + result := &NetworkPool{ + NetworkPool: &types.NetworkPool{}, + vcdClient: vcdClient, + } + + err = client.OpenApiPostItem(apiVersion, urlRef, nil, config, result.NetworkPool, nil) + if err != nil { + return nil, err + } + + return result, nil +} + +// Update will change all changeable network pool items +func (np *NetworkPool) Update() error { + if np == nil || np.NetworkPool == nil || np.NetworkPool.Id == "" { + return fmt.Errorf("network pool must have ID") + } + if np.vcdClient == nil || np.vcdClient.Client.APIVersion == "" { + return fmt.Errorf("network pool '%s': no client found", np.NetworkPool.Name) + } + + client := np.vcdClient.Client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNetworkPools + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, np.NetworkPool.Id) + if err != nil { + return err + } + + err = client.OpenApiPutItem(apiVersion, urlRef, nil, np.NetworkPool, np.NetworkPool, nil) + if err != nil { + return err + } + + if err != nil { + return fmt.Errorf("error updating network pool '%s': %s", np.NetworkPool.Name, err) + } + + return nil +} + +// Delete removes a network pool +func (np *NetworkPool) Delete() error { + if np == nil || np.NetworkPool == nil || np.NetworkPool.Id == "" { + return fmt.Errorf("network pool must have ID") + } + if np.vcdClient == nil || np.vcdClient.Client.APIVersion == "" { + return fmt.Errorf("network pool '%s': no client found", np.NetworkPool.Name) + } + + client := np.vcdClient.Client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNetworkPools + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, np.NetworkPool.Id) + if err != nil { + return err + } + + err = client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) + if err != nil { + return err + } + + if err != nil { + return fmt.Errorf("error deleting network pool '%s': %s", np.NetworkPool.Name, err) + } + + return nil +} + +func backingErrorMessage(constraint types.BackingUseConstraint, name string) string { + errorMessage := fmt.Sprintf("[constraint: %s] %s", constraint, backingUseErrorMessages[constraint]) + if strings.Contains(errorMessage, "%s") { + return fmt.Sprintf(errorMessage, name) + } + return errorMessage +} + +type getElementFunc[B any] func(*B) string +type validElementFunc[B any] func(*B) bool + +// chooseBackingElement will select a backing element from a list, using the given constraint +// * constraint is the type of choice we are looking for +// * wantedName is the name of the element we want. If we use a constraint other than types.BackingUseExplicit, it can be empty +// * elements is the list of backing elements we want to choose from +// * getEl is a function that, given an element, returns its name +// * validateEl is an optional function that tells whether a given element is valid or not. If missing, we assume all elements are valid +func chooseBackingElement[B any](constraint types.BackingUseConstraint, wantedName string, elements []*B, getEl getElementFunc[B], validateEl validElementFunc[B]) (*B, error) { + var searchedElement *B + if validateEl == nil { + validateEl = func(*B) bool { return true } + } + numberOfValidElements := 0 + // We need to pre-calculate the number of valid elements, to use it when constraint == BackingUseWhenOnlyOne + for _, element := range elements { + if validateEl(element) { + numberOfValidElements++ + } + } + // availableElements will contain the list of available elements, to be used in error messages + var availableElements []string + for _, element := range elements { + elementName := getEl(element) + if !validateEl(element) { + continue + } + availableElements = append(availableElements, elementName) + + switch constraint { + case types.BackingUseExplicit: + // When asking for a specific element explicitly, we return it only if the name matches the request) + if wantedName == elementName { + searchedElement = element + } + case types.BackingUseWhenOnlyOne: + // With BackingUseWhenOnlyOne, we return the element only if there is a single *valid* element in the list + if wantedName == "" && numberOfValidElements == 1 { + searchedElement = element + } + case types.BackingUseFirstAvailable: + // This is the most permissive constraint: we get the first available element + if wantedName == "" { + searchedElement = element + } + } + if searchedElement != nil { + break + } + } + // If no item was retrieved, we build an error message appropriate for the current constraint, and add + // the list of available elements to it + if searchedElement == nil { + return nil, fmt.Errorf(backingErrorMessage(constraint, wantedName)+" - available elements: %v", availableElements) + } + + // When we reach this point, we have found what was requested, and return the element + return searchedElement, nil +} + +// CreateNetworkPoolGeneve creates a network pool of GENEVE type +// The function retrieves the given NSX-T manager and corresponding transport zone names +// If the transport zone name is empty, the first available will be used +func (vcdClient *VCDClient) CreateNetworkPoolGeneve(name, description, nsxtManagerName, transportZoneName string, constraint types.BackingUseConstraint) (*NetworkPool, error) { + managers, err := vcdClient.QueryNsxtManagerByName(nsxtManagerName) + if err != nil { + return nil, err + } + + if len(managers) == 0 { + return nil, fmt.Errorf("no manager '%s' found", nsxtManagerName) + } + if len(managers) > 1 { + return nil, fmt.Errorf("more than one manager '%s' found", nsxtManagerName) + } + manager := managers[0] + + managerId := "urn:vcloud:nsxtmanager:" + extractUuid(managers[0].HREF) + transportZones, err := vcdClient.GetAllNsxtTransportZones(managerId, nil) + if err != nil { + return nil, fmt.Errorf("error retrieving transport zones for manager '%s': %s", manager.Name, err) + } + transportZone, err := chooseBackingElement[types.TransportZone]( + constraint, + transportZoneName, + transportZones, + func(tz *types.TransportZone) string { return tz.Name }, + func(tz *types.TransportZone) bool { return !tz.AlreadyImported }, + ) + + if err != nil { + return nil, err + } + if transportZone.AlreadyImported { + return nil, fmt.Errorf("transport zone '%s' is already imported", transportZone.Name) + } + + // Note: in this type of network pool, the managing owner is the NSX-T manager + managingOwner := types.OpenApiReference{ + Name: manager.Name, + ID: managerId, + } + var config = &types.NetworkPool{ + Name: name, + Description: description, + PoolType: types.NetworkPoolGeneveType, + ManagingOwnerRef: managingOwner, + Backing: types.NetworkPoolBacking{ + TransportZoneRef: types.OpenApiReference{ + ID: transportZone.Id, + Name: transportZone.Name, + }, + ProviderRef: managingOwner, + }, + } + return vcdClient.CreateNetworkPool(config) +} + +// CreateNetworkPoolPortGroup creates a network pool of PORTGROUP_BACKED type +// The function retrieves the given vCenter and corresponding port group names +// If the port group name is empty, the first available will be used +func (vcdClient *VCDClient) CreateNetworkPoolPortGroup(name, description, vCenterName string, portgroupNames []string, constraint types.BackingUseConstraint) (*NetworkPool, error) { + vCenter, err := vcdClient.GetVCenterByName(vCenterName) + if err != nil { + return nil, fmt.Errorf("error retrieving vCenter '%s': %s", vCenterName, err) + } + var params = make(url.Values) + params.Set("filter", "virtualCenter.id=="+vCenter.VSphereVCenter.VcId) + portgroups, err := vcdClient.GetAllVcenterImportableDvpgs(params) + if err != nil { + return nil, fmt.Errorf("error retrieving portgroups for vCenter '%s': %s", vCenterName, err) + } + + var chosenPortgroups []*VcenterImportableDvpg + var chosenReferences []types.OpenApiReference + for _, portgroupName := range portgroupNames { + portGroup, err := chooseBackingElement[VcenterImportableDvpg]( + constraint, + portgroupName, + portgroups, + func(v *VcenterImportableDvpg) string { + return v.VcenterImportableDvpg.BackingRef.Name + }, + nil, + ) + + if err != nil { + return nil, err + } + chosenPortgroups = append(chosenPortgroups, portGroup) + chosenReferences = append(chosenReferences, types.OpenApiReference{ + Name: portGroup.VcenterImportableDvpg.BackingRef.Name, + ID: portGroup.VcenterImportableDvpg.BackingRef.ID, + }) + } + + if len(chosenPortgroups) == 0 { + return nil, fmt.Errorf("no suitable portgroups found for names %v", portgroupNames) + } + if len(chosenPortgroups) > 1 { + if !chosenPortgroups[0].UsableWith(chosenPortgroups...) { + return nil, fmt.Errorf("portgroups %v should all belong to the same host", portgroupNames) + } + } + + // Note: in this type of network pool, the managing owner is the vCenter + managingOwner := types.OpenApiReference{ + Name: vCenter.VSphereVCenter.Name, + ID: vCenter.VSphereVCenter.VcId, + } + config := types.NetworkPool{ + Name: name, + Description: description, + PoolType: types.NetworkPoolPortGroupType, + ManagingOwnerRef: managingOwner, + Backing: types.NetworkPoolBacking{ + PortGroupRefs: chosenReferences, + ProviderRef: managingOwner, + }, + } + return vcdClient.CreateNetworkPool(&config) +} + +// CreateNetworkPoolVlan creates a network pool of VLAN type +// The function retrieves the given vCenter and corresponding distributed switch names +// If the distributed switch name is empty, the first available will be used +func (vcdClient *VCDClient) CreateNetworkPoolVlan(name, description, vCenterName, dsName string, ranges []types.VlanIdRange, constraint types.BackingUseConstraint) (*NetworkPool, error) { + vCenter, err := vcdClient.GetVCenterByName(vCenterName) + if err != nil { + return nil, fmt.Errorf("error retrieving vCenter '%s': %s", vCenterName, err) + } + + dswitches, err := vcdClient.GetAllVcenterDistributedSwitches(vCenter.VSphereVCenter.VcId, nil) + if err != nil { + return nil, fmt.Errorf("error retrieving distributed switches for vCenter '%s': %s", vCenterName, err) + } + + dswitch, err := chooseBackingElement[types.VcenterDistributedSwitch]( + constraint, + dsName, + dswitches, + func(t *types.VcenterDistributedSwitch) string { return t.BackingRef.Name }, + nil, + ) + if err != nil { + return nil, err + } + + // Note: in this type of network pool, the managing owner is the vCenter + managingOwner := types.OpenApiReference{ + Name: vCenter.VSphereVCenter.Name, + ID: vCenter.VSphereVCenter.VcId, + } + config := types.NetworkPool{ + Name: name, + Description: description, + PoolType: types.NetworkPoolVlanType, + ManagingOwnerRef: managingOwner, + Backing: types.NetworkPoolBacking{ + VlanIdRanges: types.VlanIdRanges{ + Values: ranges, + }, + VdsRefs: []types.OpenApiReference{ + { + Name: dswitch.BackingRef.Name, + ID: dswitch.BackingRef.ID, + }, + }, + ProviderRef: managingOwner, + }, + } + return vcdClient.CreateNetworkPool(&config) +} diff --git a/govcd/network_pool_test.go b/govcd/network_pool_test.go new file mode 100644 index 000000000..c272422a5 --- /dev/null +++ b/govcd/network_pool_test.go @@ -0,0 +1,373 @@ +//go:build providervdc || functional || ALL + +package govcd + +import ( + "fmt" + "github.com/kr/pretty" + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" + "net/url" +) + +func (vcd *TestVCD) Test_GetNetworkPools(check *C) { + + if vcd.skipAdminTests { + check.Skip("this test requires system administrator privileges") + } + knownNetworkPoolName := vcd.config.VCD.NsxtProviderVdc.NetworkPool + networkPools, err := vcd.client.GetNetworkPoolSummaries(nil) + check.Assert(err, IsNil) + check.Assert(len(networkPools) > 0, Equals, true) + + checkNetworkPoolName := false + foundNetworkPool := false + if knownNetworkPoolName != "" { + checkNetworkPoolName = true + } + + for i, nps := range networkPools { + if nps.Name == knownNetworkPoolName { + foundNetworkPool = true + } + networkPoolById, err := vcd.client.GetNetworkPoolById(nps.Id) + check.Assert(err, IsNil) + check.Assert(networkPoolById, NotNil) + check.Assert(networkPoolById.NetworkPool.Id, Equals, nps.Id) + check.Assert(networkPoolById.NetworkPool.Name, Equals, nps.Name) + + networkPoolByName, err := vcd.client.GetNetworkPoolByName(nps.Name) + check.Assert(err, IsNil) + check.Assert(networkPoolByName, NotNil) + check.Assert(networkPoolByName.NetworkPool.Id, Equals, nps.Id) + check.Assert(networkPoolByName.NetworkPool.Name, Equals, nps.Name) + if testVerbose { + fmt.Printf("%d, %# v\n", i, pretty.Formatter(networkPoolByName.NetworkPool)) + } + } + if checkNetworkPoolName { + check.Assert(foundNetworkPool, Equals, true) + } +} + +// Test_CreateNetworkPoolGeneve shows the creation of a "GENEVE" network pool +// using first the low-level method, then using the shortcut methods, +// and finally using the shortcut method without explicit transport zone +func (vcd *TestVCD) Test_CreateNetworkPoolGeneve(check *C) { + if vcd.skipAdminTests { + check.Skip("this test requires system administrator privileges") + } + if vcd.config.VCD.Nsxt.Manager == "" { + check.Skip("no manager name is available") + } + networkPoolName := check.TestName() + + managers, err := vcd.client.QueryNsxtManagerByName(vcd.config.VCD.Nsxt.Manager) + check.Assert(err, IsNil) + check.Assert(len(managers), Equals, 1) + + manager := managers[0] + managerId := "urn:vcloud:nsxtmanager:" + extractUuid(manager.HREF) + + transportZones, err := vcd.client.GetAllNsxtTransportZones(managerId, nil) + check.Assert(err, IsNil) + if len(transportZones) == 0 { + check.Skip("no available transport zones found") + } + var importableTransportZones []*types.TransportZone + + for _, tz := range transportZones { + if !tz.AlreadyImported { + importableTransportZones = append(importableTransportZones, tz) + } + } + if len(importableTransportZones) == 0 { + check.Skip("no unimported transport zone found") + } + + for _, transportZone := range importableTransportZones { + config := types.NetworkPool{ + Name: networkPoolName, + Description: "test network pool geneve", + PoolType: types.NetworkPoolGeneveType, + ManagingOwnerRef: types.OpenApiReference{ + Name: manager.Name, + ID: managerId, + }, + Backing: types.NetworkPoolBacking{ + TransportZoneRef: types.OpenApiReference{ + Name: transportZone.Name, + ID: transportZone.Id, + }, + ProviderRef: types.OpenApiReference{ + Name: manager.Name, + ID: managerId, + }, + }, + } + runTestCreateNetworkPool("geneve-full-config-("+transportZone.Name+")", func() (*NetworkPool, error) { + return vcd.client.CreateNetworkPool(&config) + }, func(_ *NetworkPool) { + tzs, err := vcd.client.GetAllNsxtTransportZones(managerId, nil) + check.Assert(err, IsNil) + for _, tz := range tzs { + if tz.Name == transportZone.Name { + check.Assert(tz.AlreadyImported, Equals, true) + } + } + }, + check) + + runTestCreateNetworkPool("geneve-names-("+transportZone.Name+")", func() (*NetworkPool, error) { + return vcd.client.CreateNetworkPoolGeneve(networkPoolName, "test network pool geneve", manager.Name, transportZone.Name, types.BackingUseExplicit) + }, nil, check) + } + if len(importableTransportZones) == 1 { + // When no transport zone name is provided and there is only one TZ, we ask for that (unnamed) only one to be used + runTestCreateNetworkPool("geneve-names-no-tz-name-only-element", func() (*NetworkPool, error) { + return vcd.client.CreateNetworkPoolGeneve(networkPoolName, "test network pool geneve", manager.Name, "", types.BackingUseWhenOnlyOne) + }, nil, check) + } + // When no transport zone name is provided, the first one available will be used + runTestCreateNetworkPool("geneve-names-no-tz-name-first-element", func() (*NetworkPool, error) { + return vcd.client.CreateNetworkPoolGeneve(networkPoolName, "test network pool geneve", manager.Name, "", types.BackingUseFirstAvailable) + }, nil, check) +} + +// Test_CreateNetworkPoolPortgroup shows the creation of a "PORTGROUP_BACKED" network pool +// using first the low-level method, then using the shortcut methods, +// and finally using the shortcut method without explicit port group +func (vcd *TestVCD) Test_CreateNetworkPoolPortgroup(check *C) { + if vcd.skipAdminTests { + check.Skip("this test requires system administrator privileges") + } + if vcd.config.VCD.VimServer == "" { + check.Skip("no vCenter found in configuration") + } + + vCenter, err := vcd.client.GetVCenterByName(vcd.config.VCD.VimServer) + check.Assert(err, IsNil) + + networkPoolName := check.TestName() + + var params = make(url.Values) + params.Set("filter", "virtualCenter.id=="+vCenter.VSphereVCenter.VcId) + portgroups, err := vcd.client.GetAllVcenterImportableDvpgs(params) + check.Assert(err, IsNil) + check.Assert(len(portgroups) > 0, Equals, true) + + var sameHost []*VcenterImportableDvpg + for _, pg := range portgroups { + + for _, other := range portgroups { + if len(sameHost) > 0 { + break + } + if other.VcenterImportableDvpg.BackingRef.ID == pg.VcenterImportableDvpg.BackingRef.ID { + continue + } + if other.Parent().ID == pg.Parent().ID { + sameHost = append(sameHost, pg) + sameHost = append(sameHost, other) + break + } + } + config := types.NetworkPool{ + Name: networkPoolName, + Description: "test network pool port group", + PoolType: types.NetworkPoolPortGroupType, + ManagingOwnerRef: types.OpenApiReference{ + Name: vCenter.VSphereVCenter.Name, + ID: vCenter.VSphereVCenter.VcId, + }, + Backing: types.NetworkPoolBacking{ + PortGroupRefs: []types.OpenApiReference{ + { + ID: pg.VcenterImportableDvpg.BackingRef.ID, + Name: pg.VcenterImportableDvpg.BackingRef.Name, + }, + }, + ProviderRef: types.OpenApiReference{ + Name: vCenter.VSphereVCenter.Name, + ID: vCenter.VSphereVCenter.VcId, + }, + }, + } + + runTestCreateNetworkPool("port-group-full-config-("+pg.VcenterImportableDvpg.BackingRef.Name+")", func() (*NetworkPool, error) { + return vcd.client.CreateNetworkPool(&config) + }, nil, check) + runTestCreateNetworkPool("port-group-names-("+pg.VcenterImportableDvpg.BackingRef.Name+")", func() (*NetworkPool, error) { + return vcd.client.CreateNetworkPoolPortGroup(networkPoolName, "test network pool port group", vCenter.VSphereVCenter.Name, + []string{pg.VcenterImportableDvpg.BackingRef.Name}, types.BackingUseExplicit) + }, nil, check) + } + if len(sameHost) == 2 { + names := []string{ + sameHost[0].VcenterImportableDvpg.BackingRef.Name, + sameHost[1].VcenterImportableDvpg.BackingRef.Name, + } + runTestCreateNetworkPool("port-group-multi-names", func() (*NetworkPool, error) { + return vcd.client.CreateNetworkPoolPortGroup(networkPoolName, "test network pool port group", + vCenter.VSphereVCenter.Name, names, types.BackingUseExplicit) + }, nil, check) + } + if len(portgroups) == 1 { + // When no port group name is provided, and only one is available, we ask for that (unnamed) one to be used + runTestCreateNetworkPool("port-group-names-no-pg-name-only-element", func() (*NetworkPool, error) { + return vcd.client.CreateNetworkPoolPortGroup(networkPoolName, "test network pool port group", vCenter.VSphereVCenter.Name, []string{""}, types.BackingUseWhenOnlyOne) + }, nil, check) + } + // When no port group name is provided, the first one available will be used + runTestCreateNetworkPool("port-group-names-no-pg-name-first-element", func() (*NetworkPool, error) { + return vcd.client.CreateNetworkPoolPortGroup(networkPoolName, "test network pool port group", vCenter.VSphereVCenter.Name, []string{""}, types.BackingUseFirstAvailable) + }, nil, check) +} + +// Test_CreateNetworkPoolVlan shows the creation of a "VLAN" network pool +// using first the low-level method, then using the shortcut methods, +// and finally using the shortcut method without explicit distributed switch +func (vcd *TestVCD) Test_CreateNetworkPoolVlan(check *C) { + if vcd.skipAdminTests { + check.Skip("this test requires system administrator privileges") + } + if vcd.config.VCD.VimServer == "" { + check.Skip("no vCenter found in configuration") + } + + vCenter, err := vcd.client.GetVCenterByName(vcd.config.VCD.VimServer) + check.Assert(err, IsNil) + + networkPoolName := check.TestName() + + switches, err := vcd.client.GetAllVcenterDistributedSwitches(vCenter.VSphereVCenter.VcId, nil) + check.Assert(err, IsNil) + if len(switches) == 0 { + check.Skip("no available distributed found in vCenter") + } + // range ID for network pools + ranges := []types.VlanIdRange{ + {StartId: 1, EndId: 100}, + {StartId: 201, EndId: 300}, + } + // updateWithRanges updates the network pool + updateWithRanges := func(pool *NetworkPool) { + check.Assert(len(pool.NetworkPool.Backing.VlanIdRanges.Values), Equals, 2) + pool.NetworkPool.Backing.VlanIdRanges.Values = []types.VlanIdRange{{StartId: 1001, EndId: 2000}} + + updatedName := pool.NetworkPool.Name + "-changed" + updatedDescription := pool.NetworkPool.Description + " - changed" + pool.NetworkPool.Name = updatedName + pool.NetworkPool.Description = updatedDescription + err = pool.Update() + check.Assert(err, IsNil) + retrievedNetworkPool, err := pool.vcdClient.GetNetworkPoolById(pool.NetworkPool.Id) + check.Assert(err, IsNil) + check.Assert(retrievedNetworkPool, NotNil) + check.Assert(retrievedNetworkPool.NetworkPool.Id, Equals, pool.NetworkPool.Id) + check.Assert(retrievedNetworkPool.NetworkPool.Name, Equals, updatedName) + check.Assert(retrievedNetworkPool.NetworkPool.Description, Equals, updatedDescription) + + err = pool.Update() + check.Assert(err, IsNil) + newPool, err := vcd.client.GetNetworkPoolById(pool.NetworkPool.Id) + check.Assert(err, IsNil) + check.Assert(len(newPool.NetworkPool.Backing.VlanIdRanges.Values), Equals, 1) + } + for _, sw := range switches { + config := types.NetworkPool{ + Name: networkPoolName, + Description: "test network pool VLAN", + PoolType: types.NetworkPoolVlanType, + ManagingOwnerRef: types.OpenApiReference{ + Name: vCenter.VSphereVCenter.Name, + ID: vCenter.VSphereVCenter.VcId, + }, + Backing: types.NetworkPoolBacking{ + VlanIdRanges: types.VlanIdRanges{ + Values: ranges, + }, + VdsRefs: []types.OpenApiReference{ + { + Name: sw.BackingRef.Name, + ID: sw.BackingRef.ID, + }, + }, + ProviderRef: types.OpenApiReference{ + Name: vCenter.VSphereVCenter.Name, + ID: vCenter.VSphereVCenter.VcId, + }, + }, + } + + runTestCreateNetworkPool("vlan-full-config-("+sw.BackingRef.Name+")", + func() (*NetworkPool, error) { + return vcd.client.CreateNetworkPool(&config) + }, + updateWithRanges, + check) + + runTestCreateNetworkPool("vlan-names-("+sw.BackingRef.Name+")", + func() (*NetworkPool, error) { + return vcd.client.CreateNetworkPoolVlan(networkPoolName, "test network pool VLAN", vCenter.VSphereVCenter.Name, sw.BackingRef.Name, ranges, types.BackingUseExplicit) + }, + updateWithRanges, + check) + } + if len(switches) == 1 { + // When no switch name is provided, and only one is available, we ask to use that (unnamed) one + runTestCreateNetworkPool("vlan-names-no-sw-name-only-element", func() (*NetworkPool, error) { + return vcd.client.CreateNetworkPoolVlan(networkPoolName, "test network pool VLAN", vCenter.VSphereVCenter.Name, "", ranges, types.BackingUseWhenOnlyOne) + }, + updateWithRanges, + check) + } + // When no switch name is provided, the first one available will be used + runTestCreateNetworkPool("vlan-names-no-sw-name-first-element", func() (*NetworkPool, error) { + return vcd.client.CreateNetworkPoolVlan(networkPoolName, "test network pool VLAN", vCenter.VSphereVCenter.Name, "", ranges, types.BackingUseFirstAvailable) + }, + updateWithRanges, + check) +} + +// runTestCreateNetworkPool runs a generic test for network pool creation, using `creationFunc` for creating the object +// and `postCreation` to run updates or other management actions +func runTestCreateNetworkPool(label string, creationFunc func() (*NetworkPool, error), postCreation func(pool *NetworkPool), check *C) { + fmt.Printf("[test create network pool] %s\n", label) + + networkPool, err := creationFunc() + check.Assert(err, IsNil) + defer func() { + if networkPool != nil { + _ = networkPool.Delete() + } + }() + check.Assert(networkPool, NotNil) + networkPoolName := networkPool.NetworkPool.Name + if postCreation != nil { + postCreation(networkPool) + // Refresh the network pool + networkPool, err = networkPool.vcdClient.GetNetworkPoolById(networkPool.NetworkPool.Id) + check.Assert(err, IsNil) + } + + // if no update was run through the postCreation + if networkPool.NetworkPool.Name == networkPoolName { + updatedName := networkPool.NetworkPool.Name + "-update" + updatedDescription := networkPool.NetworkPool.Description + " - update" + networkPool.NetworkPool.Name = updatedName + networkPool.NetworkPool.Description = updatedDescription + err = networkPool.Update() + check.Assert(err, IsNil) + retrievedNetworkPool, err := networkPool.vcdClient.GetNetworkPoolById(networkPool.NetworkPool.Id) + check.Assert(err, IsNil) + check.Assert(retrievedNetworkPool, NotNil) + check.Assert(retrievedNetworkPool.NetworkPool.Id, Equals, networkPool.NetworkPool.Id) + check.Assert(retrievedNetworkPool.NetworkPool.Name, Equals, updatedName) + check.Assert(retrievedNetworkPool.NetworkPool.Description, Equals, updatedDescription) + } + + err = networkPool.Delete() + check.Assert(err, IsNil) + networkPool = nil +} diff --git a/govcd/nsxt_alb_clouds.go b/govcd/nsxt_alb_clouds.go new file mode 100644 index 000000000..bd6e8c43e --- /dev/null +++ b/govcd/nsxt_alb_clouds.go @@ -0,0 +1,188 @@ +/* + * Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "errors" + "fmt" + "net/url" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +// NsxtAlbCloud helps to use the virtual infrastructure provided by NSX Advanced Load Balancer, register NSX-T Cloud +// instances with VMware Cloud Director by consuming NsxtAlbImportableCloud. +type NsxtAlbCloud struct { + NsxtAlbCloud *types.NsxtAlbCloud + vcdClient *VCDClient +} + +// GetAllAlbClouds returns all configured NSX-T ALB Clouds +func (vcdClient *VCDClient) GetAllAlbClouds(queryParameters url.Values) ([]*NsxtAlbCloud, error) { + client := vcdClient.Client + if !client.IsSysAdmin { + return nil, errors.New("handling NSX-T ALB Clouds require System user") + } + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbCloud + apiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + typeResponses := []*types.NsxtAlbCloud{{}} + err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParameters, &typeResponses, nil) + if err != nil { + return nil, err + } + + // Wrap all typeResponses into NsxtAlbCloud types with client + wrappedResponses := make([]*NsxtAlbCloud, len(typeResponses)) + for sliceIndex := range typeResponses { + wrappedResponses[sliceIndex] = &NsxtAlbCloud{ + NsxtAlbCloud: typeResponses[sliceIndex], + vcdClient: vcdClient, + } + } + + return wrappedResponses, nil +} + +// GetAlbCloudByName returns NSX-T ALB Cloud by name +func (vcdClient *VCDClient) GetAlbCloudByName(name string) (*NsxtAlbCloud, error) { + queryParameters := copyOrNewUrlValues(nil) + queryParameters.Add("filter", "name=="+name) + + albClouds, err := vcdClient.GetAllAlbClouds(queryParameters) + if err != nil { + return nil, fmt.Errorf("error reading NSX-T ALB Cloud with Name '%s': %s", name, err) + } + + if len(albClouds) == 0 { + return nil, fmt.Errorf("%s could not find NSX-T ALB Cloud with Name '%s'", ErrorEntityNotFound, name) + } + + if len(albClouds) > 1 { + return nil, fmt.Errorf("found more than 1 NSX-T ALB Cloud with Name '%s'", name) + } + + return albClouds[0], nil +} + +// GetAlbCloudById returns NSX-T ALB Cloud by ID +// +// Note. This function uses server side filtering instead of directly querying endpoint with specified ID because such +// endpoint does not exist +func (vcdClient *VCDClient) GetAlbCloudById(id string) (*NsxtAlbCloud, error) { + + queryParameters := copyOrNewUrlValues(nil) + queryParameters.Add("filter", "id=="+id) + + albCloud, err := vcdClient.GetAllAlbClouds(queryParameters) + if err != nil { + return nil, fmt.Errorf("error reading NSX-T ALB Cloud with ID '%s': %s", id, err) + } + + if len(albCloud) == 0 { + return nil, fmt.Errorf("%s could not find NSX-T ALB Cloud by ID '%s'", ErrorEntityNotFound, id) + } + + return albCloud[0], nil +} + +// CreateAlbCloud creates NSX-T ALB Cloud +func (vcdClient *VCDClient) CreateAlbCloud(albCloudConfig *types.NsxtAlbCloud) (*NsxtAlbCloud, error) { + client := vcdClient.Client + if !client.IsSysAdmin { + return nil, errors.New("handling NSX-T ALB Clouds require System user") + } + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbCloud + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + returnObject := &NsxtAlbCloud{ + NsxtAlbCloud: &types.NsxtAlbCloud{}, + vcdClient: vcdClient, + } + + err = client.OpenApiPostItem(minimumApiVersion, urlRef, nil, albCloudConfig, returnObject.NsxtAlbCloud, nil) + if err != nil { + return nil, fmt.Errorf("error creating NSX-T ALB Cloud: %s", err) + } + + return returnObject, nil +} + +// Update is not supported in VCD 10.3 and older therefore this function remains commented +// +// Update updates existing NSX-T ALB Cloud with new supplied albCloudConfig configuration +//func (nsxtAlbCloud *NsxtAlbCloud) Update(albCloudConfig *types.NsxtAlbCloud) (*NsxtAlbCloud, error) { +// client := nsxtAlbCloud.vcdClient.Client +// endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbCloud +// minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) +// if err != nil { +// return nil, err +// } +// +// if albCloudConfig.ID == "" { +// return nil, fmt.Errorf("cannot update NSX-T ALB Cloud without ID") +// } +// +// urlRef, err := client.OpenApiBuildEndpoint(endpoint, albCloudConfig.ID) +// if err != nil { +// return nil, err +// } +// +// responseAlbCloud := &NsxtAlbCloud{ +// NsxtAlbCloud: &types.NsxtAlbCloud{}, +// vcdClient: nsxtAlbCloud.vcdClient, +// } +// +// err = client.OpenApiPutItem(minimumApiVersion, urlRef, nil, albCloudConfig, responseAlbCloud.NsxtAlbCloud, nil) +// if err != nil { +// return nil, fmt.Errorf("error updating NSX-T ALB Cloud: %s", err) +// } +// +// return responseAlbCloud, nil +//} + +// Delete removes NSX-T ALB Cloud configuration +func (nsxtAlbCloud *NsxtAlbCloud) Delete() error { + client := nsxtAlbCloud.vcdClient.Client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbCloud + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return err + } + + if nsxtAlbCloud.NsxtAlbCloud.ID == "" { + return fmt.Errorf("cannot delete NSX-T ALB Cloud without ID") + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, nsxtAlbCloud.NsxtAlbCloud.ID) + if err != nil { + return err + } + + err = client.OpenApiDeleteItem(minimumApiVersion, urlRef, nil, nil) + if err != nil { + return fmt.Errorf("error deleting NSX-T ALB Cloud: %s", err) + } + + return nil +} diff --git a/govcd/nsxt_alb_clouds_test.go b/govcd/nsxt_alb_clouds_test.go new file mode 100644 index 000000000..8711c7567 --- /dev/null +++ b/govcd/nsxt_alb_clouds_test.go @@ -0,0 +1,120 @@ +//go:build nsxt || alb || functional || ALL + +package govcd + +import ( + "fmt" + "net/url" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_AlbClouds(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + skipNoNsxtAlbConfiguration(vcd, check) + + albController := spawnAlbController(vcd, check) + check.Assert(albController, NotNil) + + importableCloud, err := albController.GetAlbImportableCloudByName(vcd.config.VCD.Nsxt.NsxtAlbImportableCloud) + check.Assert(err, IsNil) + + albCloudConfig := &types.NsxtAlbCloud{ + Name: check.TestName(), + Description: "alb-cloud-description", + LoadBalancerCloudBacking: types.NsxtAlbCloudBacking{ + BackingId: importableCloud.NsxtAlbImportableCloud.ID, + BackingType: types.NsxtAlbCloudBackingTypeNsxtAlb, + LoadBalancerControllerRef: types.OpenApiReference{ + ID: albController.NsxtAlbController.ID, + }, + }, + NetworkPoolRef: &types.OpenApiReference{ + ID: importableCloud.NsxtAlbImportableCloud.NetworkPoolRef.ID, + }, + } + + createdAlbCloud, err := vcd.client.CreateAlbCloud(albCloudConfig) + check.Assert(err, IsNil) + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbCloud + createdAlbCloud.NsxtAlbCloud.ID + AddToCleanupListOpenApi(createdAlbCloud.NsxtAlbCloud.Name, check.TestName(), openApiEndpoint) + + // Get all clouds and ensure the needed one is found + allClouds, err := vcd.client.GetAllAlbClouds(nil) + check.Assert(err, IsNil) + var foundCreatedCloud bool + for cloudIndex := range allClouds { + if allClouds[cloudIndex].NsxtAlbCloud.ID == createdAlbCloud.NsxtAlbCloud.ID { + foundCreatedCloud = true + break + } + } + check.Assert(foundCreatedCloud, Equals, true) + + // Filter lookup by name + filter := url.Values{} + filter.Add("filter", "name=="+createdAlbCloud.NsxtAlbCloud.Name) + allCloudsFiltered, err := vcd.client.GetAllAlbClouds(filter) + check.Assert(err, IsNil) + check.Assert(len(allCloudsFiltered), Equals, 1) + check.Assert(allCloudsFiltered[0].NsxtAlbCloud.ID, Equals, createdAlbCloud.NsxtAlbCloud.ID) + + // Get by Name + albCloudByName, err := vcd.client.GetAlbCloudByName(createdAlbCloud.NsxtAlbCloud.Name) + check.Assert(err, IsNil) + check.Assert(albCloudByName.NsxtAlbCloud.Name, Equals, createdAlbCloud.NsxtAlbCloud.Name) + + // Get by ID + albCloudById, err := vcd.client.GetAlbCloudById(createdAlbCloud.NsxtAlbCloud.ID) + check.Assert(err, IsNil) + check.Assert(albCloudById.NsxtAlbCloud.Name, Equals, createdAlbCloud.NsxtAlbCloud.Name) + + // Cleanup + err = createdAlbCloud.Delete() + check.Assert(err, IsNil) + + _, err = vcd.client.GetAlbCloudByName(createdAlbCloud.NsxtAlbCloud.Name) + check.Assert(ContainsNotFound(err), Equals, true) + + err = albController.Delete() + check.Assert(err, IsNil) +} + +// spawnAlbControllerAndCloud is a helper function to spawn NSX-T ALB Controller and Cloud +// It automatically adds these artefacts to clean up list +func spawnAlbControllerAndCloud(vcd *TestVCD, check *C) (*NsxtAlbController, *NsxtAlbCloud) { + skipNoNsxtAlbConfiguration(vcd, check) + + albController := spawnAlbController(vcd, check) + check.Assert(albController, NotNil) + + importableCloud, err := albController.GetAlbImportableCloudByName(vcd.config.VCD.Nsxt.NsxtAlbImportableCloud) + check.Assert(err, IsNil) + + albCloudConfig := &types.NsxtAlbCloud{ + Name: check.TestName(), + Description: "alb-cloud-description", + LoadBalancerCloudBacking: types.NsxtAlbCloudBacking{ + BackingId: importableCloud.NsxtAlbImportableCloud.ID, + //BackingType: types.NsxtAlbCloudBackingTypeNsxtAlb, + LoadBalancerControllerRef: types.OpenApiReference{ + ID: albController.NsxtAlbController.ID, + }, + }, + NetworkPoolRef: &types.OpenApiReference{ + ID: importableCloud.NsxtAlbImportableCloud.NetworkPoolRef.ID, + }, + } + + createdAlbCloud, err := vcd.client.CreateAlbCloud(albCloudConfig) + check.Assert(err, IsNil) + + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbCloud + createdAlbCloud.NsxtAlbCloud.ID + PrependToCleanupListOpenApi(createdAlbCloud.NsxtAlbCloud.Name, check.TestName(), openApiEndpoint) + + return albController, createdAlbCloud +} diff --git a/govcd/nsxt_alb_controllers.go b/govcd/nsxt_alb_controllers.go new file mode 100644 index 000000000..e15b3f922 --- /dev/null +++ b/govcd/nsxt_alb_controllers.go @@ -0,0 +1,224 @@ +/* + * Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "errors" + "fmt" + "net/url" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +// NsxtAlbController helps to integrate VMware Cloud Director with NSX-T Advanced Load Balancer deployment. +// Controller instances are registered with VMware Cloud Director instance. Controller instances serve as a central +// control plane for the load-balancing services provided by NSX-T Advanced Load Balancer. +// To configure an NSX-T ALB one needs to supply AVI Controller endpoint, credentials and license to be used. +type NsxtAlbController struct { + NsxtAlbController *types.NsxtAlbController + vcdClient *VCDClient +} + +// GetAllAlbControllers returns all configured NSX-T ALB Controllers +func (vcdClient *VCDClient) GetAllAlbControllers(queryParameters url.Values) ([]*NsxtAlbController, error) { + client := vcdClient.Client + if !client.IsSysAdmin { + return nil, errors.New("reading NSX-T ALB Controllers require System user") + } + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbController + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + typeResponses := []*types.NsxtAlbController{{}} + err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParameters, &typeResponses, nil) + if err != nil { + return nil, err + } + + // Wrap all typeResponses into NsxtAlbController types with client + wrappedResponses := make([]*NsxtAlbController, len(typeResponses)) + for sliceIndex := range typeResponses { + wrappedResponses[sliceIndex] = &NsxtAlbController{ + NsxtAlbController: typeResponses[sliceIndex], + vcdClient: vcdClient, + } + } + + return wrappedResponses, nil +} + +// GetAlbControllerByName returns NSX-T ALB Controller by Name +func (vcdClient *VCDClient) GetAlbControllerByName(name string) (*NsxtAlbController, error) { + queryParameters := copyOrNewUrlValues(nil) + queryParameters.Add("filter", "name=="+name) + + controllers, err := vcdClient.GetAllAlbControllers(queryParameters) + if err != nil { + return nil, fmt.Errorf("error reading ALB Controller with Name '%s': %s", name, err) + } + + if len(controllers) == 0 { + return nil, fmt.Errorf("%s: could not find ALB Controller with Name '%s'", ErrorEntityNotFound, name) + } + + if len(controllers) > 1 { + return nil, fmt.Errorf("found more than 1 ALB Controller with Name '%s'", name) + } + + return controllers[0], nil +} + +// GetAlbControllerById returns NSX-T ALB Controller by ID +func (vcdClient *VCDClient) GetAlbControllerById(id string) (*NsxtAlbController, error) { + client := vcdClient.Client + if !client.IsSysAdmin { + return nil, errors.New("reading NSX-T ALB Controllers require System user") + } + + if id == "" { + return nil, fmt.Errorf("ID is required to lookup NSX-T ALB Controller by ID") + } + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbController + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, id) + if err != nil { + return nil, err + } + + typeResponse := &types.NsxtAlbController{} + err = client.OpenApiGetItem(apiVersion, urlRef, nil, &typeResponse, nil) + if err != nil { + return nil, err + } + + wrappedResponse := &NsxtAlbController{ + NsxtAlbController: typeResponse, + vcdClient: vcdClient, + } + + return wrappedResponse, nil +} + +// GetAlbControllerByUrl returns configured ALB Controller by URL +// +// Note. Filtering is performed on client side. +func (vcdClient *VCDClient) GetAlbControllerByUrl(url string) (*NsxtAlbController, error) { + // Ideally this function could filter on VCD side, but API does not support filtering on URL + controllers, err := vcdClient.GetAllAlbControllers(nil) + if err != nil { + return nil, fmt.Errorf("error reading ALB Controller with Url '%s': %s", url, err) + } + + // Search for controllers + filteredControllers := make([]*NsxtAlbController, 0) + for _, controller := range controllers { + if controller.NsxtAlbController.Url == url { + filteredControllers = append(filteredControllers, controller) + } + } + + return oneOrError("url", url, filteredControllers) +} + +// CreateNsxtAlbController creates controller with supplied albControllerConfig configuration +func (vcdClient *VCDClient) CreateNsxtAlbController(albControllerConfig *types.NsxtAlbController) (*NsxtAlbController, error) { + client := vcdClient.Client + if !client.IsSysAdmin { + return nil, errors.New("handling NSX-T ALB Controllers require System user") + } + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbController + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + returnObject := &NsxtAlbController{ + NsxtAlbController: &types.NsxtAlbController{}, + vcdClient: vcdClient, + } + + err = client.OpenApiPostItem(apiVersion, urlRef, nil, albControllerConfig, returnObject.NsxtAlbController, nil) + if err != nil { + return nil, fmt.Errorf("error creating NSX-T ALB Controller: %s", err) + } + + return returnObject, nil +} + +// Update updates existing NSX-T ALB Controller with new supplied albControllerConfig configuration +func (nsxtAlbController *NsxtAlbController) Update(albControllerConfig *types.NsxtAlbController) (*NsxtAlbController, error) { + client := nsxtAlbController.vcdClient.Client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbController + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + if albControllerConfig.ID == "" { + return nil, fmt.Errorf("cannot update NSX-T ALB Controller without ID") + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, albControllerConfig.ID) + if err != nil { + return nil, err + } + + responseAlbController := &NsxtAlbController{ + NsxtAlbController: &types.NsxtAlbController{}, + vcdClient: nsxtAlbController.vcdClient, + } + + err = client.OpenApiPutItem(apiVersion, urlRef, nil, albControllerConfig, responseAlbController.NsxtAlbController, nil) + if err != nil { + return nil, fmt.Errorf("error updating NSX-T ALB Controller: %s", err) + } + + return responseAlbController, nil +} + +// Delete deletes existing NSX-T ALB Controller +func (nsxtAlbController *NsxtAlbController) Delete() error { + client := nsxtAlbController.vcdClient.Client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbController + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return err + } + + if nsxtAlbController.NsxtAlbController.ID == "" { + return fmt.Errorf("cannot delete NSX-T ALB Controller without ID") + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, nsxtAlbController.NsxtAlbController.ID) + if err != nil { + return err + } + + err = client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) + if err != nil { + return fmt.Errorf("error deleting NSX-T ALB Controller: %s", err) + } + + return nil +} diff --git a/govcd/nsxt_alb_controllers_test.go b/govcd/nsxt_alb_controllers_test.go new file mode 100644 index 000000000..266aa51f2 --- /dev/null +++ b/govcd/nsxt_alb_controllers_test.go @@ -0,0 +1,110 @@ +//go:build nsxt || alb || functional || ALL + +package govcd + +import ( + "fmt" + "net/url" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + + . "gopkg.in/check.v1" +) + +// Test_NsxtAlbController tests out NSX-T ALB Controller capabilities +func (vcd *TestVCD) Test_NsxtAlbController(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + skipNoNsxtAlbConfiguration(vcd, check) + + newController := spawnAlbController(vcd, check) + + // Get by Url + controllerByUrl, err := vcd.client.GetAlbControllerByUrl(newController.NsxtAlbController.Url) + check.Assert(err, IsNil) + + // Get by Name + controllerByName, err := vcd.client.GetAlbControllerByName(controllerByUrl.NsxtAlbController.Name) + check.Assert(err, IsNil) + check.Assert(controllerByName.NsxtAlbController.ID, Equals, controllerByUrl.NsxtAlbController.ID) + + // Get by ID + controllerById, err := vcd.client.GetAlbControllerById(controllerByUrl.NsxtAlbController.ID) + check.Assert(err, IsNil) + check.Assert(controllerById.NsxtAlbController.ID, Equals, controllerByName.NsxtAlbController.ID) + + // Get all Controllers and expect to find at least the known one + allControllers, err := vcd.client.GetAllAlbControllers(nil) + check.Assert(err, IsNil) + check.Assert(len(allControllers) > 0, Equals, true) + var foundController bool + for controllerIndex := range allControllers { + if allControllers[controllerIndex].NsxtAlbController.ID == controllerByUrl.NsxtAlbController.ID { + foundController = true + } + } + check.Assert(foundController, Equals, true) + + // Check filtering for GetAllAlbControllers works + filter := url.Values{} + filter.Add("filter", "name=="+controllerByUrl.NsxtAlbController.Name) + filteredControllers, err := vcd.client.GetAllAlbControllers(nil) + check.Assert(err, IsNil) + check.Assert(len(filteredControllers), Equals, 1) + check.Assert(filteredControllers[0].NsxtAlbController.ID, Equals, controllerByUrl.NsxtAlbController.ID) + + // Test update of ALB controller + updateControllerDef := &types.NsxtAlbController{ + ID: controllerByUrl.NsxtAlbController.ID, + Name: controllerByUrl.NsxtAlbController.Name + "-update", + Description: "Description set", + Url: vcd.config.VCD.Nsxt.NsxtAlbControllerUrl, + Username: vcd.config.VCD.Nsxt.NsxtAlbControllerUser, + Password: vcd.config.VCD.Nsxt.NsxtAlbControllerPassword, + LicenseType: "BASIC", // Not used since v37.0 + } + updatedController, err := controllerByUrl.Update(updateControllerDef) + check.Assert(err, IsNil) + check.Assert(updatedController.NsxtAlbController.Name, Equals, updateControllerDef.Name) + check.Assert(updatedController.NsxtAlbController.Description, Equals, updateControllerDef.Description) + check.Assert(updatedController.NsxtAlbController.Url, Equals, updateControllerDef.Url) + check.Assert(updatedController.NsxtAlbController.Username, Equals, updateControllerDef.Username) + if vcd.client.Client.APIVCDMaxVersionIs("< 37.0") { + check.Assert(updatedController.NsxtAlbController.LicenseType, Equals, updateControllerDef.LicenseType) + } + + // Revert settings to original ones + _, err = controllerByUrl.Update(controllerByUrl.NsxtAlbController) + check.Assert(err, IsNil) + + // Remove and make sure it is not found + err = updatedController.Delete() + check.Assert(err, IsNil) + + // Try to find controller and expect an + _, err = vcd.client.GetAlbControllerByName(controllerByUrl.NsxtAlbController.Name) + check.Assert(ContainsNotFound(err), Equals, true) +} + +// spawnAlbController is a helper function to spawn NSX-T ALB Controller instance from defined config +func spawnAlbController(vcd *TestVCD, check *C) *NsxtAlbController { + skipNoNsxtAlbConfiguration(vcd, check) + + newControllerDef := &types.NsxtAlbController{ + Name: check.TestName(), + Url: vcd.config.VCD.Nsxt.NsxtAlbControllerUrl, + Username: vcd.config.VCD.Nsxt.NsxtAlbControllerUser, + Password: vcd.config.VCD.Nsxt.NsxtAlbControllerPassword, + LicenseType: "ENTERPRISE", // Not used since v37.0 + } + + newController, err := vcd.client.CreateNsxtAlbController(newControllerDef) + check.Assert(err, IsNil) + check.Assert(newController.NsxtAlbController.ID, Not(Equals), "") + + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbController + newController.NsxtAlbController.ID + AddToCleanupListOpenApi(newController.NsxtAlbController.Name, check.TestName(), openApiEndpoint) + + return newController +} diff --git a/govcd/nsxt_alb_importable_clouds.go b/govcd/nsxt_alb_importable_clouds.go new file mode 100644 index 000000000..d239526d6 --- /dev/null +++ b/govcd/nsxt_alb_importable_clouds.go @@ -0,0 +1,121 @@ +/* + * Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "errors" + "fmt" + "net/url" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +// NsxtAlbImportableCloud allows user to list importable NSX-T ALB Clouds. Each importable cloud can only be imported +// once by using NsxtAlbCloud construct. It has a flag AlreadyImported which hints if it is already consumed or not. +type NsxtAlbImportableCloud struct { + NsxtAlbImportableCloud *types.NsxtAlbImportableCloud + vcdClient *VCDClient +} + +// GetAllAlbImportableClouds returns importable NSX-T ALB Clouds. +// parentAlbControllerUrn (ID in URN format of a parent ALB Controller) is mandatory +func (vcdClient *VCDClient) GetAllAlbImportableClouds(parentAlbControllerUrn string, queryParameters url.Values) ([]*NsxtAlbImportableCloud, error) { + client := vcdClient.Client + if parentAlbControllerUrn == "" { + return nil, fmt.Errorf("parent ALB Controller ID is required") + } + if !client.IsSysAdmin { + return nil, errors.New("handling NSX-T ALB Importable Clouds require System user") + } + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbImportableClouds + apiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + queryParams := copyOrNewUrlValues(queryParameters) + queryParams = queryParameterFilterAnd(fmt.Sprintf("_context==%s", parentAlbControllerUrn), queryParams) + typeResponses := []*types.NsxtAlbImportableCloud{{}} + err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParams, &typeResponses, nil) + if err != nil { + return nil, err + } + + wrappedResponses := make([]*NsxtAlbImportableCloud, len(typeResponses)) + for sliceIndex := range typeResponses { + wrappedResponses[sliceIndex] = &NsxtAlbImportableCloud{ + NsxtAlbImportableCloud: typeResponses[sliceIndex], + vcdClient: vcdClient, + } + } + + return wrappedResponses, nil +} + +// GetAlbImportableCloudByName returns importable NSX-T ALB Clouds. +func (vcdClient *VCDClient) GetAlbImportableCloudByName(parentAlbControllerUrn, name string) (*NsxtAlbImportableCloud, error) { + albImportableClouds, err := vcdClient.GetAllAlbImportableClouds(parentAlbControllerUrn, nil) + if err != nil { + return nil, fmt.Errorf("error finding NSX-T ALB Importable Cloud by Name '%s': %s", name, err) + } + + // Filtering by Name is not supported by API therefore it must be filtered on client side + var foundResult bool + var foundAlbImportableCloud *NsxtAlbImportableCloud + for i, value := range albImportableClouds { + if albImportableClouds[i].NsxtAlbImportableCloud.DisplayName == name { + foundResult = true + foundAlbImportableCloud = value + break + } + } + + if !foundResult { + return nil, fmt.Errorf("%s: could not find NSX-T ALB Importable Cloud by Name %s", ErrorEntityNotFound, name) + } + + return foundAlbImportableCloud, nil +} + +// GetAlbImportableCloudById returns importable NSX-T ALB Clouds. +// Note. ID filtering is performed on client side +func (vcdClient *VCDClient) GetAlbImportableCloudById(parentAlbControllerUrn, id string) (*NsxtAlbImportableCloud, error) { + albImportableClouds, err := vcdClient.GetAllAlbImportableClouds(parentAlbControllerUrn, nil) + if err != nil { + return nil, fmt.Errorf("error finding NSX-T ALB Importable Cloud by ID '%s': %s", id, err) + } + + // Filtering by ID is not supported by API therefore it must be filtered on client side + var foundResult bool + var foundAlbImportableCloud *NsxtAlbImportableCloud + for i, value := range albImportableClouds { + if albImportableClouds[i].NsxtAlbImportableCloud.ID == id { + foundResult = true + foundAlbImportableCloud = value + } + } + + if !foundResult { + return nil, fmt.Errorf("%s: could not find NSX-T ALB Importable Cloud by ID %s", ErrorEntityNotFound, id) + } + + return foundAlbImportableCloud, nil +} + +// GetAllAlbImportableClouds is attached to NsxtAlbController type for a convenient parent/child relationship +func (nsxtAlbController *NsxtAlbController) GetAllAlbImportableClouds(queryParameters url.Values) ([]*NsxtAlbImportableCloud, error) { + return nsxtAlbController.vcdClient.GetAllAlbImportableClouds(nsxtAlbController.NsxtAlbController.ID, queryParameters) +} + +// GetAlbImportableCloudByName is attached to NsxtAlbController type for a convenient parent/child relationship +func (nsxtAlbController *NsxtAlbController) GetAlbImportableCloudByName(name string) (*NsxtAlbImportableCloud, error) { + return nsxtAlbController.vcdClient.GetAlbImportableCloudByName(nsxtAlbController.NsxtAlbController.ID, name) +} diff --git a/govcd/nsxt_alb_importable_clouds_test.go b/govcd/nsxt_alb_importable_clouds_test.go new file mode 100644 index 000000000..f132d0e6e --- /dev/null +++ b/govcd/nsxt_alb_importable_clouds_test.go @@ -0,0 +1,37 @@ +//go:build nsxt || alb || functional || ALL + +package govcd + +import ( + "fmt" + + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_GetAllAlbImportableClouds(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + skipNoNsxtAlbConfiguration(vcd, check) + + albController := spawnAlbController(vcd, check) + + // Test client function with explicit ALB Controller ID requirement + clientImportableClouds, err := vcd.client.GetAllAlbImportableClouds(albController.NsxtAlbController.ID, nil) + check.Assert(err, IsNil) + check.Assert(len(clientImportableClouds) > 0, Equals, true) + + // Test functions attached directly to NsxtAlbController + controllerImportableClouds, err := albController.GetAllAlbImportableClouds(nil) + check.Assert(err, IsNil) + check.Assert(len(controllerImportableClouds) > 0, Equals, true) + + controllerImportableCloudByName, err := albController.GetAlbImportableCloudByName(vcd.config.VCD.Nsxt.NsxtAlbImportableCloud) + check.Assert(err, IsNil) + check.Assert(controllerImportableCloudByName, NotNil) + check.Assert(controllerImportableCloudByName.NsxtAlbImportableCloud.ID, Equals, controllerImportableClouds[0].NsxtAlbImportableCloud.ID) + + // Cleanup + err = albController.Delete() + check.Assert(err, IsNil) +} diff --git a/govcd/nsxt_alb_importable_service_engine_groups.go b/govcd/nsxt_alb_importable_service_engine_groups.go new file mode 100644 index 000000000..75e79f7c5 --- /dev/null +++ b/govcd/nsxt_alb_importable_service_engine_groups.go @@ -0,0 +1,202 @@ +/* + * Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "errors" + "fmt" + "net/url" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +// NsxtAlbImportableServiceEngineGroups provides capability to list all Importable Service Engine Groups available in +// ALB Controller so that they can be consumed by NsxtAlbServiceEngineGroup +// +// Note. The API does not return Importable Service Engine Group once it is consumed. +type NsxtAlbImportableServiceEngineGroups struct { + NsxtAlbImportableServiceEngineGroups *types.NsxtAlbImportableServiceEngineGroups + vcdClient *VCDClient +} + +// GetAllAlbImportableServiceEngineGroups lists all Importable Service Engine Groups available in ALB Controller +func (vcdClient *VCDClient) GetAllAlbImportableServiceEngineGroups(parentAlbCloudUrn string, queryParameters url.Values) ([]*NsxtAlbImportableServiceEngineGroups, error) { + client := vcdClient.Client + if parentAlbCloudUrn == "" { + return nil, fmt.Errorf("parentAlbCloudUrn is required") + } + if !client.IsSysAdmin { + return nil, errors.New("handling NSX-T ALB Importable Service Engine Groups requires System user") + } + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbImportableServiceEngineGroups + apiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + queryParams := copyOrNewUrlValues(queryParameters) + queryParams = queryParameterFilterAnd(fmt.Sprintf("_context==%s", parentAlbCloudUrn), queryParams) + typeResponses := []*types.NsxtAlbImportableServiceEngineGroups{{}} + err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParams, &typeResponses, nil) + if err != nil { + return nil, err + } + + wrappedResponses := make([]*NsxtAlbImportableServiceEngineGroups, len(typeResponses)) + for sliceIndex := range typeResponses { + wrappedResponses[sliceIndex] = &NsxtAlbImportableServiceEngineGroups{ + NsxtAlbImportableServiceEngineGroups: typeResponses[sliceIndex], + vcdClient: vcdClient, + } + } + + return wrappedResponses, nil +} + +// GetAlbImportableServiceEngineGroupByName returns importable NSX-T ALB Clouds. +func (vcdClient *VCDClient) GetAlbImportableServiceEngineGroupByName(parentAlbCloudUrn, name string) (*NsxtAlbImportableServiceEngineGroups, error) { + albClouds, err := vcdClient.GetAllAlbImportableServiceEngineGroups(parentAlbCloudUrn, nil) + if err != nil { + return nil, fmt.Errorf("error finding NSX-T ALB Importable Service Engine Group by Name '%s': %s", name, err) + } + + // Filtering by Name is not supported by API therefore it must be filtered on client side + var foundResult bool + var foundAlbCloud *NsxtAlbImportableServiceEngineGroups + for i, value := range albClouds { + if albClouds[i].NsxtAlbImportableServiceEngineGroups.DisplayName == name { + foundResult = true + foundAlbCloud = value + break + } + } + + if !foundResult { + return nil, fmt.Errorf("%s: could not find NSX-T ALB Importable Service Engine Group by Name %s", ErrorEntityNotFound, name) + } + + return foundAlbCloud, nil +} + +// GetAlbImportableServiceEngineGroupById +// Note. ID filtering is performed on client side +func (vcdClient *VCDClient) GetAlbImportableServiceEngineGroupById(parentAlbCloudUrn, id string) (*NsxtAlbImportableServiceEngineGroups, error) { + albClouds, err := vcdClient.GetAllAlbImportableServiceEngineGroups(parentAlbCloudUrn, nil) + if err != nil { + return nil, fmt.Errorf("error finding NSX-T ALB Importable Service Engine Group by ID '%s': %s", id, err) + } + + // Filtering by ID is not supported by API therefore it must be filtered on client side + var foundResult bool + var foundImportableSEGroups *NsxtAlbImportableServiceEngineGroups + for i, value := range albClouds { + if albClouds[i].NsxtAlbImportableServiceEngineGroups.ID == id { + foundResult = true + foundImportableSEGroups = value + } + } + + if !foundResult { + return nil, fmt.Errorf("%s: could not find NSX-T ALB Importable Service Engine Group by ID %s", ErrorEntityNotFound, id) + } + + return foundImportableSEGroups, nil +} + +// GetAllAlbImportableServiceEngineGroups lists all Importable Service Engine Groups available in ALB Controller +func (nsxtAlbCloud *NsxtAlbCloud) GetAllAlbImportableServiceEngineGroups(parentAlbCloudUrn string, queryParameters url.Values) ([]*NsxtAlbImportableServiceEngineGroups, error) { + client := nsxtAlbCloud.vcdClient.Client + if parentAlbCloudUrn == "" { + return nil, fmt.Errorf("parentAlbCloudUrn is required") + } + if !client.IsSysAdmin { + return nil, errors.New("handling NSX-T ALB Importable Service Engine Groups requires System user") + } + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbImportableServiceEngineGroups + apiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + queryParams := copyOrNewUrlValues(queryParameters) + queryParams = queryParameterFilterAnd(fmt.Sprintf("_context==%s", parentAlbCloudUrn), queryParams) + typeResponses := []*types.NsxtAlbImportableServiceEngineGroups{{}} + err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParams, &typeResponses, nil) + if err != nil { + return nil, err + } + + wrappedResponses := make([]*NsxtAlbImportableServiceEngineGroups, len(typeResponses)) + for sliceIndex := range typeResponses { + wrappedResponses[sliceIndex] = &NsxtAlbImportableServiceEngineGroups{ + NsxtAlbImportableServiceEngineGroups: typeResponses[sliceIndex], + vcdClient: nsxtAlbCloud.vcdClient, + } + } + + return wrappedResponses, nil +} + +// GetAlbImportableServiceEngineGroupByName returns importable NSX-T ALB Clouds. +func (nsxtAlbCloud *NsxtAlbCloud) GetAlbImportableServiceEngineGroupByName(parentAlbCloudUrn, name string) (*NsxtAlbImportableServiceEngineGroups, error) { + albClouds, err := nsxtAlbCloud.vcdClient.GetAllAlbImportableServiceEngineGroups(parentAlbCloudUrn, nil) + if err != nil { + return nil, fmt.Errorf("error finding NSX-T ALB Importable Service Engine Group by Name '%s': %s", name, err) + } + + // Filtering by ID is not supported by API therefore it must be filtered on client side + var foundResult bool + var foundAlbCloud *NsxtAlbImportableServiceEngineGroups + for i, value := range albClouds { + if albClouds[i].NsxtAlbImportableServiceEngineGroups.DisplayName == name { + foundResult = true + foundAlbCloud = value + break + } + } + + if !foundResult { + return nil, fmt.Errorf("%s: could not find NSX-T ALB Importable Service Engine Group by Name %s", ErrorEntityNotFound, name) + } + + return foundAlbCloud, nil +} + +// GetAlbImportableServiceEngineGroupById +// Note. ID filtering is performed on client side +func (nsxtAlbCloud *NsxtAlbCloud) GetAlbImportableServiceEngineGroupById(parentAlbCloudUrn, id string) (*NsxtAlbImportableServiceEngineGroups, error) { + albClouds, err := nsxtAlbCloud.vcdClient.GetAllAlbImportableServiceEngineGroups(parentAlbCloudUrn, nil) + if err != nil { + return nil, fmt.Errorf("error finding NSX-T ALB Importable Service Engine Group by ID '%s': %s", id, err) + } + + // Filtering by ID is not supported by API therefore it must be filtered on client side + var foundResult bool + var foundImportableSEGroups *NsxtAlbImportableServiceEngineGroups + for i, value := range albClouds { + if albClouds[i].NsxtAlbImportableServiceEngineGroups.ID == id { + foundResult = true + foundImportableSEGroups = value + } + } + + if !foundResult { + return nil, fmt.Errorf("%s: could not find NSX-T ALB Importable Service Engine Group by ID %s", ErrorEntityNotFound, id) + } + + return foundImportableSEGroups, nil +} diff --git a/govcd/nsxt_alb_importable_service_engine_groups_test.go b/govcd/nsxt_alb_importable_service_engine_groups_test.go new file mode 100644 index 000000000..5dea76df1 --- /dev/null +++ b/govcd/nsxt_alb_importable_service_engine_groups_test.go @@ -0,0 +1,49 @@ +//go:build nsxt || alb || functional || ALL + +package govcd + +import ( + "fmt" + + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_GetAllAlbImportableServiceEngineGroups(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + albController, createdAlbCloud := spawnAlbControllerAndCloud(vcd, check) + + importableSeGroups, err := vcd.client.GetAllAlbImportableServiceEngineGroups(createdAlbCloud.NsxtAlbCloud.ID, nil) + check.Assert(err, IsNil) + check.Assert(len(importableSeGroups) > 0, Equals, true) + check.Assert(importableSeGroups[0].NsxtAlbImportableServiceEngineGroups.ID != "", Equals, true) + check.Assert(importableSeGroups[0].NsxtAlbImportableServiceEngineGroups.DisplayName != "", Equals, true) + check.Assert(importableSeGroups[0].NsxtAlbImportableServiceEngineGroups.HaMode != "", Equals, true) + + // Get By Name + impSeGrpByName, err := vcd.client.GetAlbImportableServiceEngineGroupByName(createdAlbCloud.NsxtAlbCloud.ID, importableSeGroups[0].NsxtAlbImportableServiceEngineGroups.DisplayName) + check.Assert(err, IsNil) + // Get By ID + impSeGrpById, err := vcd.client.GetAlbImportableServiceEngineGroupById(createdAlbCloud.NsxtAlbCloud.ID, importableSeGroups[0].NsxtAlbImportableServiceEngineGroups.ID) + check.Assert(err, IsNil) + + // Get By Name on parent Cloud + cldImpSeGrpByName, err := createdAlbCloud.GetAlbImportableServiceEngineGroupByName(createdAlbCloud.NsxtAlbCloud.ID, importableSeGroups[0].NsxtAlbImportableServiceEngineGroups.DisplayName) + check.Assert(err, IsNil) + // Get By ID on parent Cloud + cldImpSeGrpById, err := createdAlbCloud.GetAlbImportableServiceEngineGroupById(createdAlbCloud.NsxtAlbCloud.ID, importableSeGroups[0].NsxtAlbImportableServiceEngineGroups.ID) + check.Assert(err, IsNil) + + check.Assert(impSeGrpByName.NsxtAlbImportableServiceEngineGroups, DeepEquals, importableSeGroups[0].NsxtAlbImportableServiceEngineGroups) + check.Assert(impSeGrpByName.NsxtAlbImportableServiceEngineGroups, DeepEquals, impSeGrpById.NsxtAlbImportableServiceEngineGroups) + check.Assert(impSeGrpByName.NsxtAlbImportableServiceEngineGroups, DeepEquals, cldImpSeGrpByName.NsxtAlbImportableServiceEngineGroups) + check.Assert(impSeGrpByName.NsxtAlbImportableServiceEngineGroups, DeepEquals, cldImpSeGrpById.NsxtAlbImportableServiceEngineGroups) + + // Cleanup + err = createdAlbCloud.Delete() + check.Assert(err, IsNil) + + err = albController.Delete() + check.Assert(err, IsNil) +} diff --git a/govcd/nsxt_alb_pool.go b/govcd/nsxt_alb_pool.go new file mode 100644 index 000000000..e592e07db --- /dev/null +++ b/govcd/nsxt_alb_pool.go @@ -0,0 +1,215 @@ +/* + * Copyright 2021 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" +) + +// NsxtAlbPool defines configuration of a single NSX-T ALB Pool. Pools maintain the list of servers assigned to them and +// perform health monitoring, load balancing, persistence. A pool may only be used or referenced by only one virtual +// service at a time. +type NsxtAlbPool struct { + NsxtAlbPool *types.NsxtAlbPool + vcdClient *VCDClient +} + +// GetAllAlbPoolSummaries retrieves partial information for type `NsxtAlbPool`, but it is the only way to retrieve all ALB +// pools for Edge Gateway +func (vcdClient *VCDClient) GetAllAlbPoolSummaries(edgeGatewayId string, queryParameters url.Values) ([]*NsxtAlbPool, error) { + client := vcdClient.Client + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbPoolSummaries + apiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, edgeGatewayId)) + if err != nil { + return nil, err + } + + typeResponses := []*types.NsxtAlbPool{{}} + err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParameters, &typeResponses, nil) + if err != nil { + return nil, err + } + + // Wrap all typeResponses into NsxtAlbPool types with client + wrappedResponses := make([]*NsxtAlbPool, len(typeResponses)) + for sliceIndex := range typeResponses { + wrappedResponses[sliceIndex] = &NsxtAlbPool{ + NsxtAlbPool: typeResponses[sliceIndex], + vcdClient: vcdClient, + } + } + + return wrappedResponses, nil +} + +// GetAllAlbPools uses GetAllAlbPoolSummaries behind the scenes and the fetches complete data for all ALB Pools. This +// has performance penalty because each ALB Pool is fetched individually. +func (vcdClient *VCDClient) GetAllAlbPools(edgeGatewayId string, queryParameters url.Values) ([]*NsxtAlbPool, error) { + allAlbPoolSummaries, err := vcdClient.GetAllAlbPoolSummaries(edgeGatewayId, queryParameters) + if err != nil { + return nil, fmt.Errorf("error retrieving all ALB Pool summaries: %s", err) + } + + // Loop over all Summaries and retrieve complete information + allAlbPools := make([]*NsxtAlbPool, len(allAlbPoolSummaries)) + for index := range allAlbPoolSummaries { + + allAlbPools[index], err = vcdClient.GetAlbPoolById(allAlbPoolSummaries[index].NsxtAlbPool.ID) + if err != nil { + return nil, fmt.Errorf("error retrieving complete ALB Pool: %s", err) + } + + } + + return allAlbPools, nil +} + +// GetAlbPoolByName fetches ALB Pool By Name +func (vcdClient *VCDClient) GetAlbPoolByName(edgeGatewayId string, name string) (*NsxtAlbPool, error) { + queryParameters := copyOrNewUrlValues(nil) + queryParameters.Add("filter", "name=="+name) + + allAlbPools, err := vcdClient.GetAllAlbPools(edgeGatewayId, queryParameters) + if err != nil { + return nil, fmt.Errorf("error retrieving ALB Pool with Name '%s': %s", name, err) + } + + if len(allAlbPools) == 0 { + return nil, fmt.Errorf("%s: could not find ALB Pool with Name '%s'", ErrorEntityNotFound, name) + } + + if len(allAlbPools) > 1 { + return nil, fmt.Errorf("found more than 1 ALB Pool with Name '%s'", name) + } + + return allAlbPools[0], nil +} + +// GetAlbPoolById fetches ALB Pool By Id +func (vcdClient *VCDClient) GetAlbPoolById(id string) (*NsxtAlbPool, error) { + client := vcdClient.Client + + if id == "" { + return nil, fmt.Errorf("ID is required to lookup NSX-T ALB Pool by ID") + } + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbPools + apiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, id) + if err != nil { + return nil, err + } + + typeResponse := &types.NsxtAlbPool{} + err = client.OpenApiGetItem(apiVersion, urlRef, nil, &typeResponse, nil) + if err != nil { + return nil, err + } + + wrappedResponse := &NsxtAlbPool{ + NsxtAlbPool: typeResponse, + vcdClient: vcdClient, + } + + return wrappedResponse, nil +} + +// CreateNsxtAlbPool creates NSX-T ALB Pool based on supplied configuration +func (vcdClient *VCDClient) CreateNsxtAlbPool(albPoolConfig *types.NsxtAlbPool) (*NsxtAlbPool, error) { + client := vcdClient.Client + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbPools + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + returnObject := &NsxtAlbPool{ + NsxtAlbPool: &types.NsxtAlbPool{}, + vcdClient: vcdClient, + } + + err = client.OpenApiPostItem(minimumApiVersion, urlRef, nil, albPoolConfig, returnObject.NsxtAlbPool, nil) + if err != nil { + return nil, fmt.Errorf("error creating NSX-T ALB Pool: %s", err) + } + + return returnObject, nil +} + +// Update updates NSX-T ALB Pool based on supplied configuration +func (nsxtAlbPool *NsxtAlbPool) Update(albPoolConfig *types.NsxtAlbPool) (*NsxtAlbPool, error) { + client := nsxtAlbPool.vcdClient.Client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbPools + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + if albPoolConfig.ID == "" { + return nil, fmt.Errorf("cannot update NSX-T ALB Pool without ID") + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, albPoolConfig.ID) + if err != nil { + return nil, err + } + + responseAlbController := &NsxtAlbPool{ + NsxtAlbPool: &types.NsxtAlbPool{}, + vcdClient: nsxtAlbPool.vcdClient, + } + + err = client.OpenApiPutItem(minimumApiVersion, urlRef, nil, albPoolConfig, responseAlbController.NsxtAlbPool, nil) + if err != nil { + return nil, fmt.Errorf("error updating NSX-T ALB Pool: %s", err) + } + + return responseAlbController, nil +} + +// Delete deletes NSX-T ALB Pool +func (nsxtAlbPool *NsxtAlbPool) Delete() error { + client := nsxtAlbPool.vcdClient.Client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbPools + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return err + } + + if nsxtAlbPool.NsxtAlbPool.ID == "" { + return fmt.Errorf("cannot delete NSX-T ALB Pool without ID") + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, nsxtAlbPool.NsxtAlbPool.ID) + if err != nil { + return err + } + + err = client.OpenApiDeleteItem(minimumApiVersion, urlRef, nil, nil) + if err != nil { + return fmt.Errorf("error deleting NSX-T ALB Pool: %s", err) + } + + return nil +} diff --git a/govcd/nsxt_alb_pool_test.go b/govcd/nsxt_alb_pool_test.go new file mode 100644 index 000000000..eecd11d05 --- /dev/null +++ b/govcd/nsxt_alb_pool_test.go @@ -0,0 +1,323 @@ +//go:build nsxt || alb || functional || ALL + +package govcd + +import ( + "fmt" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_AlbPool(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + skipNoNsxtAlbConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointAlbEdgeGateway) + + // Setup prerequisite components + controller, cloud, seGroup, edge, assignment := setupAlbPoolPrerequisites(check, vcd) + + // Setup Org user and connection + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + orgUserVcdClient, orgUser, err := newOrgUserConnection(adminOrg, "alb-pool-testing", "CHANGE-ME", vcd.config.Provider.Url, true) + check.Assert(err, IsNil) + + // defer prerequisite teardown + defer func() { tearDownAlbPoolPrerequisites(check, assignment, edge, seGroup, cloud, controller) }() + + // Run tests with System user + testMinimalPoolConfig(check, edge, vcd, vcd.client) + testAdvancedPoolConfig(check, edge, vcd, vcd.client) + testPoolWithCertNoPrivateKey(check, vcd, edge.EdgeGateway.ID, vcd.client) + testPoolWithCertAndPrivateKey(check, vcd, edge.EdgeGateway.ID, vcd.client) + + // Run tests with Org admin user + testMinimalPoolConfig(check, edge, vcd, orgUserVcdClient) + testAdvancedPoolConfig(check, edge, vcd, orgUserVcdClient) + testPoolWithCertNoPrivateKey(check, vcd, edge.EdgeGateway.ID, orgUserVcdClient) + testPoolWithCertAndPrivateKey(check, vcd, edge.EdgeGateway.ID, orgUserVcdClient) + + // Cleanup Org user + err = orgUser.Delete(true) + check.Assert(err, IsNil) +} + +func testMinimalPoolConfig(check *C, edge *NsxtEdgeGateway, vcd *TestVCD, client *VCDClient) { + poolConfigMinimal := &types.NsxtAlbPool{ + Name: check.TestName() + "Minimal", + GatewayRef: types.OpenApiReference{ID: edge.EdgeGateway.ID}, + } + + poolConfigMinimalUpdated := &types.NsxtAlbPool{ + Name: poolConfigMinimal.Name + "-updated", + GatewayRef: types.OpenApiReference{ID: edge.EdgeGateway.ID}, + } + + testAlbPoolConfig(check, vcd, "Minimal", poolConfigMinimal, poolConfigMinimalUpdated, client) +} + +func testAdvancedPoolConfig(check *C, edge *NsxtEdgeGateway, vcd *TestVCD, client *VCDClient) { + poolConfigAdvanced := &types.NsxtAlbPool{ + Name: check.TestName() + "-Advanced", + GatewayRef: types.OpenApiReference{ID: edge.EdgeGateway.ID}, + Algorithm: "FEWEST_SERVERS", + DefaultPort: addrOf(8443), + GracefulTimeoutPeriod: addrOf(1), + PassiveMonitoringEnabled: addrOf(true), + HealthMonitors: nil, + Members: []types.NsxtAlbPoolMember{ + { + Enabled: true, + IpAddress: "1.1.1.1", + Port: 8400, + Ratio: addrOf(2), + }, + { + Enabled: false, + IpAddress: "1.1.1.2", + }, + { + Enabled: true, + IpAddress: "1.1.1.3", + }, + }, + PersistenceProfile: &types.NsxtAlbPoolPersistenceProfile{ + Name: "PersistenceProfile1", + Type: "CLIENT_IP", + Value: "", + }, + } + + poolConfigAdvancedUpdated := &types.NsxtAlbPool{ + Name: poolConfigAdvanced.Name + "-Updated", + GatewayRef: types.OpenApiReference{ID: edge.EdgeGateway.ID}, + Enabled: addrOf(false), + Algorithm: "LEAST_LOAD", + GracefulTimeoutPeriod: addrOf(0), + PassiveMonitoringEnabled: addrOf(false), + HealthMonitors: nil, + Members: []types.NsxtAlbPoolMember{ + { + Enabled: true, + IpAddress: "1.1.1.1", + Port: 8300, + Ratio: addrOf(3), + }, + { + Enabled: true, + IpAddress: "1.1.1.2", + }, + }, + PersistenceProfile: nil, + } + + testAlbPoolConfig(check, vcd, "Advanced", poolConfigAdvanced, poolConfigAdvancedUpdated, client) +} + +func testPoolWithCertNoPrivateKey(check *C, vcd *TestVCD, edgeGatewayId string, client *VCDClient) { + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.org.Org.Name) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + + certificateConfigWithoutPrivateKey := &types.CertificateLibraryItem{ + Alias: check.TestName(), + Certificate: certificate, + } + openApiEndpoint, err := getEndpointByVersion(&vcd.client.Client) + check.Assert(err, IsNil) + createdCertificate, err := adminOrg.AddCertificateToLibrary(certificateConfigWithoutPrivateKey) + check.Assert(err, IsNil) + PrependToCleanupListOpenApi(createdCertificate.CertificateLibrary.Alias, check.TestName(), openApiEndpoint+createdCertificate.CertificateLibrary.Id) + + poolConfigWithCert := &types.NsxtAlbPool{ + Name: check.TestName() + "-complicated", + GatewayRef: types.OpenApiReference{ID: edgeGatewayId}, + Algorithm: "FASTEST_RESPONSE", + CaCertificateRefs: []types.OpenApiReference{types.OpenApiReference{ID: createdCertificate.CertificateLibrary.Id}}, + CommonNameCheckEnabled: addrOf(true), + DomainNames: []string{"one", "two", "three"}, + DefaultPort: addrOf(1211), + SslEnabled: addrOf(true), + } + + testAlbPoolConfig(check, vcd, "CertificateWithNoPrivateKey", poolConfigWithCert, nil, client) + + err = createdCertificate.Delete() + check.Assert(err, IsNil) +} + +func testPoolWithCertAndPrivateKey(check *C, vcd *TestVCD, edgeGatewayId string, client *VCDClient) { + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.org.Org.Name) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + + certificateConfigWithoutPrivateKey := &types.CertificateLibraryItem{ + Alias: check.TestName(), + Certificate: certificate, + PrivateKey: privateKey, + PrivateKeyPassphrase: "test", + } + + openApiEndpoint, err := getEndpointByVersion(&vcd.client.Client) + check.Assert(err, IsNil) + createdCertificate, err := adminOrg.AddCertificateToLibrary(certificateConfigWithoutPrivateKey) + check.Assert(err, IsNil) + PrependToCleanupListOpenApi(createdCertificate.CertificateLibrary.Alias, check.TestName(), openApiEndpoint+createdCertificate.CertificateLibrary.Id) + + poolConfigWithCertAndKey := &types.NsxtAlbPool{ + Name: check.TestName() + "-complicated", + GatewayRef: types.OpenApiReference{ID: edgeGatewayId}, + + Algorithm: "FASTEST_RESPONSE", + CaCertificateRefs: []types.OpenApiReference{types.OpenApiReference{ID: createdCertificate.CertificateLibrary.Id}}, + DefaultPort: addrOf(1211), + SslEnabled: addrOf(true), + } + + testAlbPoolConfig(check, vcd, "CertificateWithPrivateKey", poolConfigWithCertAndKey, nil, client) + + err = createdCertificate.Delete() + check.Assert(err, IsNil) +} + +func testAlbPoolConfig(check *C, vcd *TestVCD, name string, setupConfig *types.NsxtAlbPool, updateConfig *types.NsxtAlbPool, client *VCDClient) { + fmt.Printf("# Running ALB Pool test with config %s ('System' user: %t) ", name, client.Client.IsSysAdmin) + + edge, err := vcd.nsxtVdc.GetNsxtEdgeGatewayByName(vcd.config.VCD.Nsxt.EdgeGateway) + check.Assert(err, IsNil) + + createdPool, err := client.CreateNsxtAlbPool(setupConfig) + check.Assert(err, IsNil) + check.Assert(createdPool, NotNil) + check.Assert(createdPool.NsxtAlbPool, NotNil) + + // Verify mandatory fields + check.Assert(createdPool.NsxtAlbPool.ID, NotNil) + check.Assert(createdPool.NsxtAlbPool.Name, NotNil) + check.Assert(createdPool.NsxtAlbPool.GatewayRef.ID, NotNil) + + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbPools + createdPool.NsxtAlbPool.ID + PrependToCleanupListOpenApi(createdPool.NsxtAlbPool.Name, check.TestName(), openApiEndpoint) + + // Get By ID + poolById, err := client.GetAlbPoolById(createdPool.NsxtAlbPool.ID) + check.Assert(err, IsNil) + check.Assert(poolById.NsxtAlbPool.ID, Equals, createdPool.NsxtAlbPool.ID) + check.Assert(poolById, NotNil) + check.Assert(poolById.NsxtAlbPool, NotNil) + + // Get By Name + poolByName, err := client.GetAlbPoolByName(edge.EdgeGateway.ID, createdPool.NsxtAlbPool.Name) + check.Assert(err, IsNil) + check.Assert(poolByName.NsxtAlbPool.ID, Equals, createdPool.NsxtAlbPool.ID) + check.Assert(poolByName, NotNil) + check.Assert(poolByName.NsxtAlbPool, NotNil) + + // Get All Pool summaries + allPoolSummaries, err := client.GetAllAlbPoolSummaries(edge.EdgeGateway.ID, nil) + check.Assert(err, IsNil) + check.Assert(len(allPoolSummaries) > 0, Equals, true) + + // Get All Pools + allPools, err := client.GetAllAlbPools(edge.EdgeGateway.ID, nil) + check.Assert(err, IsNil) + check.Assert(len(allPools) > 0, Equals, true) + + check.Assert(len(allPoolSummaries), Equals, len(allPools)) + + // Attempt an update if config is provided + if updateConfig != nil { + updateConfig.ID = createdPool.NsxtAlbPool.ID + updatedPool, err := createdPool.Update(updateConfig) + check.Assert(err, IsNil) + check.Assert(createdPool.NsxtAlbPool.ID, Equals, updatedPool.NsxtAlbPool.ID) + check.Assert(updatedPool.NsxtAlbPool.Name, NotNil) + check.Assert(updatedPool.NsxtAlbPool.GatewayRef.ID, NotNil) + check.Assert(updatedPool, NotNil) + check.Assert(updatedPool.NsxtAlbPool, NotNil) + } + + err = createdPool.Delete() + check.Assert(err, IsNil) + fmt.Printf("Done.\n") +} + +func setupAlbPoolPrerequisites(check *C, vcd *TestVCD) (*NsxtAlbController, *NsxtAlbCloud, *NsxtAlbServiceEngineGroup, *NsxtEdgeGateway, *NsxtAlbServiceEngineGroupAssignment) { + controller, cloud, seGroup := spawnAlbControllerCloudServiceEngineGroup(vcd, check, "SHARED") + edge, err := vcd.nsxtVdc.GetNsxtEdgeGatewayByName(vcd.config.VCD.Nsxt.EdgeGateway) + check.Assert(err, IsNil) + + // Enable ALB on Edge Gateway with default ServiceNetworkDefinition + albSettingsConfig := &types.NsxtAlbConfig{ + Enabled: true, + } + + // Field is only available when using API version v37.0 onwards + if vcd.client.Client.APIVCDMaxVersionIs(">= 37.0") { + albSettingsConfig.SupportedFeatureSet = "PREMIUM" + } + + // Enable IPv6 service network definition (VCD 10.4.0+) + if vcd.client.Client.APIVCDMaxVersionIs(">= 37.0") { + printVerbose("# Enabling IPv6 service network definition (VCD 10.4.0+)\n") + albSettingsConfig.ServiceNetworkDefinition = "192.168.255.125/25" + albSettingsConfig.Ipv6ServiceNetworkDefinition = "2001:0db8:85a3:0000:0000:8a2e:0370:7334/120" + } + + // Enable Transparent mode on VCD >= 10.4.1 + if vcd.client.Client.APIVCDMaxVersionIs(">= 37.1") { + printVerbose("# Enabling Transparent mode on Edge Gateway (VCD 10.4.1+)\n") + albSettingsConfig.TransparentModeEnabled = addrOf(true) + } + + enabledSettings, err := edge.UpdateAlbSettings(albSettingsConfig) + if err != nil { + fmt.Printf("# error occured while enabling ALB on Edge Gateway. Cleaning up Service Engine Group, ALB Cloud and ALB Controller: %s", err) + err2 := seGroup.Delete() + if err2 != nil { + fmt.Printf("# got error while cleaning up Service Engine Group: %s", err) + } + err2 = cloud.Delete() + if err2 != nil { + fmt.Printf("# got error while cleaning up ALB Cloud: %s", err) + } + err2 = controller.Delete() + if err2 != nil { + fmt.Printf("# got error while cleaning up ALB Controller: %s", err) + } + } + check.Assert(err, IsNil) + check.Assert(enabledSettings.Enabled, Equals, true) + PrependToCleanupList(check.TestName()+"-ALB-settings", "OpenApiEntityAlbSettingsDisable", edge.EdgeGateway.Name, check.TestName()) + + serviceEngineGroupAssignmentConfig := &types.NsxtAlbServiceEngineGroupAssignment{ + GatewayRef: &types.OpenApiReference{ID: edge.EdgeGateway.ID}, + ServiceEngineGroupRef: &types.OpenApiReference{ID: seGroup.NsxtAlbServiceEngineGroup.ID}, + MaxVirtualServices: addrOf(89), + MinVirtualServices: addrOf(20), + } + + assignment, err := vcd.client.CreateAlbServiceEngineGroupAssignment(serviceEngineGroupAssignmentConfig) + check.Assert(err, IsNil) + check.Assert(assignment.NsxtAlbServiceEngineGroupAssignment.ID, Not(Equals), "") + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbServiceEngineGroupAssignments + assignment.NsxtAlbServiceEngineGroupAssignment.ID + PrependToCleanupListOpenApi(assignment.NsxtAlbServiceEngineGroupAssignment.ServiceEngineGroupRef.Name, check.TestName(), openApiEndpoint) + return controller, cloud, seGroup, edge, assignment +} + +func tearDownAlbPoolPrerequisites(check *C, assignment *NsxtAlbServiceEngineGroupAssignment, edge *NsxtEdgeGateway, seGroup *NsxtAlbServiceEngineGroup, cloud *NsxtAlbCloud, controller *NsxtAlbController) { + err := assignment.Delete() + check.Assert(err, IsNil) + err = edge.DisableAlb() + check.Assert(err, IsNil) + err = seGroup.Delete() + check.Assert(err, IsNil) + err = cloud.Delete() + check.Assert(err, IsNil) + err = controller.Delete() + check.Assert(err, IsNil) +} diff --git a/govcd/nsxt_alb_service_engine_group_assignment.go b/govcd/nsxt_alb_service_engine_group_assignment.go new file mode 100644 index 000000000..f9b84a535 --- /dev/null +++ b/govcd/nsxt_alb_service_engine_group_assignment.go @@ -0,0 +1,213 @@ +/* + * Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "errors" + "fmt" + "net/url" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +// NsxtAlbServiceEngineGroupAssignment handles Service Engine Group Assignment to NSX-T Edge Gateways +type NsxtAlbServiceEngineGroupAssignment struct { + NsxtAlbServiceEngineGroupAssignment *types.NsxtAlbServiceEngineGroupAssignment + vcdClient *VCDClient +} + +func (vcdClient *VCDClient) GetAllAlbServiceEngineGroupAssignments(queryParameters url.Values) ([]*NsxtAlbServiceEngineGroupAssignment, error) { + client := vcdClient.Client + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbServiceEngineGroupAssignments + apiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + typeResponses := []*types.NsxtAlbServiceEngineGroupAssignment{{}} + err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParameters, &typeResponses, nil) + if err != nil { + return nil, err + } + + wrappedResponses := make([]*NsxtAlbServiceEngineGroupAssignment, len(typeResponses)) + for sliceIndex := range typeResponses { + wrappedResponses[sliceIndex] = &NsxtAlbServiceEngineGroupAssignment{ + NsxtAlbServiceEngineGroupAssignment: typeResponses[sliceIndex], + vcdClient: vcdClient, + } + } + + return wrappedResponses, nil +} + +func (vcdClient *VCDClient) GetAlbServiceEngineGroupAssignmentById(id string) (*NsxtAlbServiceEngineGroupAssignment, error) { + client := vcdClient.Client + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbServiceEngineGroupAssignments + apiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, id) + if err != nil { + return nil, err + } + + typeResponse := &types.NsxtAlbServiceEngineGroupAssignment{} + + err = client.OpenApiGetItem(apiVersion, urlRef, nil, &typeResponse, nil) + if err != nil { + return nil, err + } + + wrappedResponse := &NsxtAlbServiceEngineGroupAssignment{ + NsxtAlbServiceEngineGroupAssignment: typeResponse, + vcdClient: vcdClient, + } + + return wrappedResponse, nil +} + +func (vcdClient *VCDClient) GetAlbServiceEngineGroupAssignmentByName(name string) (*NsxtAlbServiceEngineGroupAssignment, error) { + // Filtering by Service Engine Group name is not supported on API therefore filtering is done locally + allServiceEngineGroupAssignments, err := vcdClient.GetAllAlbServiceEngineGroupAssignments(nil) + if err != nil { + return nil, err + } + + var foundGroup *NsxtAlbServiceEngineGroupAssignment + + for _, serviceEngineGroupAssignment := range allServiceEngineGroupAssignments { + if serviceEngineGroupAssignment.NsxtAlbServiceEngineGroupAssignment.ServiceEngineGroupRef.Name == name { + foundGroup = serviceEngineGroupAssignment + } + } + + if foundGroup == nil { + return nil, ErrorEntityNotFound + } + + return foundGroup, nil +} + +// GetFilteredAlbServiceEngineGroupAssignmentByName will get all ALB Service Engine Group assignments based on filters +// provided in queryParameters additionally will filter by name locally because VCD does not support server side +// filtering by name. +func (vcdClient *VCDClient) GetFilteredAlbServiceEngineGroupAssignmentByName(name string, queryParameters url.Values) (*NsxtAlbServiceEngineGroupAssignment, error) { + // Filtering by Service Engine Group name is not supported on API therefore filtering is done locally + allServiceEngineGroupAssignments, err := vcdClient.GetAllAlbServiceEngineGroupAssignments(queryParameters) + if err != nil { + return nil, err + } + + var foundGroup *NsxtAlbServiceEngineGroupAssignment + + for _, serviceEngineGroupAssignment := range allServiceEngineGroupAssignments { + if serviceEngineGroupAssignment.NsxtAlbServiceEngineGroupAssignment.ServiceEngineGroupRef.Name == name { + foundGroup = serviceEngineGroupAssignment + } + } + + if foundGroup == nil { + return nil, ErrorEntityNotFound + } + + return foundGroup, nil +} + +func (vcdClient *VCDClient) CreateAlbServiceEngineGroupAssignment(assignmentConfig *types.NsxtAlbServiceEngineGroupAssignment) (*NsxtAlbServiceEngineGroupAssignment, error) { + client := vcdClient.Client + if !client.IsSysAdmin { + return nil, errors.New("handling NSX-T ALB Service Engine Group Assignment require System user") + } + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbServiceEngineGroupAssignments + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + returnObject := &NsxtAlbServiceEngineGroupAssignment{ + NsxtAlbServiceEngineGroupAssignment: &types.NsxtAlbServiceEngineGroupAssignment{}, + vcdClient: vcdClient, + } + + err = client.OpenApiPostItem(minimumApiVersion, urlRef, nil, assignmentConfig, returnObject.NsxtAlbServiceEngineGroupAssignment, nil) + if err != nil { + return nil, fmt.Errorf("error creating NSX-T ALB Service Engine Group Assignment: %s", err) + } + + return returnObject, nil +} + +// Update updates existing ALB Service Engine Group Assignment with new supplied assignmentConfig configuration +func (nsxtEdgeAlbServiceEngineGroup *NsxtAlbServiceEngineGroupAssignment) Update(assignmentConfig *types.NsxtAlbServiceEngineGroupAssignment) (*NsxtAlbServiceEngineGroupAssignment, error) { + client := nsxtEdgeAlbServiceEngineGroup.vcdClient.Client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbServiceEngineGroupAssignments + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + if assignmentConfig.ID == "" { + return nil, fmt.Errorf("cannot update NSX-T ALB Service Engine Group Assignment without ID") + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, assignmentConfig.ID) + if err != nil { + return nil, err + } + + responseAlbController := &NsxtAlbServiceEngineGroupAssignment{ + NsxtAlbServiceEngineGroupAssignment: &types.NsxtAlbServiceEngineGroupAssignment{}, + vcdClient: nsxtEdgeAlbServiceEngineGroup.vcdClient, + } + + err = client.OpenApiPutItem(minimumApiVersion, urlRef, nil, assignmentConfig, responseAlbController.NsxtAlbServiceEngineGroupAssignment, nil) + if err != nil { + return nil, fmt.Errorf("error updating NSX-T ALB Service Engine Group Assignment: %s", err) + } + + return responseAlbController, nil +} + +// Delete deletes NSX-T ALB Service Engine Group Assignment +func (nsxtEdgeAlbServiceEngineGroup *NsxtAlbServiceEngineGroupAssignment) Delete() error { + client := nsxtEdgeAlbServiceEngineGroup.vcdClient.Client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbServiceEngineGroupAssignments + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return err + } + + if nsxtEdgeAlbServiceEngineGroup.NsxtAlbServiceEngineGroupAssignment.ID == "" { + return fmt.Errorf("cannot delete NSX-T ALB Service Engine Group Assignment without ID") + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, nsxtEdgeAlbServiceEngineGroup.NsxtAlbServiceEngineGroupAssignment.ID) + if err != nil { + return err + } + + err = client.OpenApiDeleteItem(minimumApiVersion, urlRef, nil, nil) + if err != nil { + return fmt.Errorf("error deleting NSX-T ALB Service Engine Group Assignment: %s", err) + } + + return nil +} diff --git a/govcd/nsxt_alb_service_engine_group_assignment_test.go b/govcd/nsxt_alb_service_engine_group_assignment_test.go new file mode 100644 index 000000000..6c0486ca1 --- /dev/null +++ b/govcd/nsxt_alb_service_engine_group_assignment_test.go @@ -0,0 +1,169 @@ +//go:build nsxt || alb || functional || ALL + +package govcd + +import ( + "fmt" + "net/url" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_GetAllEdgeAlbServiceEngineGroupAssignmentsDedicated(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + skipNoNsxtAlbConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointAlbEdgeGateway) + + controller, cloud, seGroup := spawnAlbControllerCloudServiceEngineGroup(vcd, check, "DEDICATED") + edge, err := vcd.nsxtVdc.GetNsxtEdgeGatewayByName(vcd.config.VCD.Nsxt.EdgeGateway) + check.Assert(err, IsNil) + + // Enable ALB on Edge Gateway with default ServiceNetworkDefinition + albSettingsConfig := &types.NsxtAlbConfig{ + Enabled: true, + } + enabledSettings, err := edge.UpdateAlbSettings(albSettingsConfig) + check.Assert(err, IsNil) + check.Assert(enabledSettings.Enabled, Equals, true) + PrependToCleanupList("OpenApiEntityAlbSettingsDisable", "OpenApiEntityAlbSettingsDisable", edge.EdgeGateway.Name, check.TestName()) + + serviceEngineGroupAssignmentConfig := &types.NsxtAlbServiceEngineGroupAssignment{ + GatewayRef: &types.OpenApiReference{ID: edge.EdgeGateway.ID}, + ServiceEngineGroupRef: &types.OpenApiReference{ID: seGroup.NsxtAlbServiceEngineGroup.ID}, + } + assignment, err := vcd.client.CreateAlbServiceEngineGroupAssignment(serviceEngineGroupAssignmentConfig) + check.Assert(err, IsNil) + check.Assert(assignment.NsxtAlbServiceEngineGroupAssignment.ID, Not(Equals), "") + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbServiceEngineGroupAssignments + assignment.NsxtAlbServiceEngineGroupAssignment.ID + PrependToCleanupListOpenApi(assignment.NsxtAlbServiceEngineGroupAssignment.ServiceEngineGroupRef.Name, check.TestName(), openApiEndpoint) + + // Get By ID + assignmentById, err := vcd.client.GetAlbServiceEngineGroupAssignmentById(assignment.NsxtAlbServiceEngineGroupAssignment.ID) + check.Assert(err, IsNil) + check.Assert(assignmentById.NsxtAlbServiceEngineGroupAssignment, DeepEquals, assignment.NsxtAlbServiceEngineGroupAssignment) + + // Get By Name + assignmentByName, err := vcd.client.GetAlbServiceEngineGroupAssignmentByName(assignment.NsxtAlbServiceEngineGroupAssignment.ServiceEngineGroupRef.Name) + check.Assert(err, IsNil) + check.Assert(assignmentByName.NsxtAlbServiceEngineGroupAssignment, DeepEquals, assignment.NsxtAlbServiceEngineGroupAssignment) + + // Filtered by name and Edge Gateway ID + queryParams := url.Values{} + queryParams.Add("filter", fmt.Sprintf("gatewayRef.id==%s", edge.EdgeGateway.ID)) + filteredAssignmentByName, err := vcd.client.GetFilteredAlbServiceEngineGroupAssignmentByName(assignment.NsxtAlbServiceEngineGroupAssignment.ServiceEngineGroupRef.Name, queryParams) + check.Assert(err, IsNil) + check.Assert(filteredAssignmentByName.NsxtAlbServiceEngineGroupAssignment, DeepEquals, filteredAssignmentByName.NsxtAlbServiceEngineGroupAssignment) + + // Get all + allAssignments, err := vcd.client.GetAllAlbServiceEngineGroupAssignments(nil) + check.Assert(err, IsNil) + var foundAssignment bool + for i := range allAssignments { + if allAssignments[i].NsxtAlbServiceEngineGroupAssignment.ID == assignment.NsxtAlbServiceEngineGroupAssignment.ID { + foundAssignment = true + } + } + check.Assert(foundAssignment, Equals, true) + + assignment.NsxtAlbServiceEngineGroupAssignment.MaxVirtualServices = addrOf(50) + assignment.NsxtAlbServiceEngineGroupAssignment.MinVirtualServices = addrOf(30) + // Expect an error because "DEDICATED" service engine group does not support specifying virtual services + updatedAssignment, err := assignment.Update(assignment.NsxtAlbServiceEngineGroupAssignment) + check.Assert(err, NotNil) + check.Assert(updatedAssignment, IsNil) + + // Perform immediate cleanups + err = assignment.Delete() + check.Assert(err, IsNil) + err = edge.DisableAlb() + check.Assert(err, IsNil) + err = seGroup.Delete() + check.Assert(err, IsNil) + err = cloud.Delete() + check.Assert(err, IsNil) + err = controller.Delete() + check.Assert(err, IsNil) +} + +func (vcd *TestVCD) Test_GetAllEdgeAlbServiceEngineGroupAssignmentsShared(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + skipNoNsxtAlbConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointAlbEdgeGateway) + + controller, cloud, seGroup := spawnAlbControllerCloudServiceEngineGroup(vcd, check, "SHARED") + edge, err := vcd.nsxtVdc.GetNsxtEdgeGatewayByName(vcd.config.VCD.Nsxt.EdgeGateway) + check.Assert(err, IsNil) + + // Enable ALB on Edge Gateway with default ServiceNetworkDefinition + albSettingsConfig := &types.NsxtAlbConfig{ + Enabled: true, + } + enabledSettings, err := edge.UpdateAlbSettings(albSettingsConfig) + check.Assert(err, IsNil) + check.Assert(enabledSettings.Enabled, Equals, true) + PrependToCleanupList(check.TestName()+"-ALB-settings", "OpenApiEntityAlbSettingsDisable", edge.EdgeGateway.Name, check.TestName()) + + serviceEngineGroupAssignmentConfig := &types.NsxtAlbServiceEngineGroupAssignment{ + GatewayRef: &types.OpenApiReference{ID: edge.EdgeGateway.ID}, + ServiceEngineGroupRef: &types.OpenApiReference{ID: seGroup.NsxtAlbServiceEngineGroup.ID}, + MaxVirtualServices: addrOf(89), + MinVirtualServices: addrOf(20), + } + assignment, err := vcd.client.CreateAlbServiceEngineGroupAssignment(serviceEngineGroupAssignmentConfig) + check.Assert(err, IsNil) + check.Assert(assignment.NsxtAlbServiceEngineGroupAssignment.ID, Not(Equals), "") + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbServiceEngineGroupAssignments + assignment.NsxtAlbServiceEngineGroupAssignment.ID + PrependToCleanupListOpenApi(assignment.NsxtAlbServiceEngineGroupAssignment.ServiceEngineGroupRef.Name, check.TestName(), openApiEndpoint) + + // Get By ID + assignmentById, err := vcd.client.GetAlbServiceEngineGroupAssignmentById(assignment.NsxtAlbServiceEngineGroupAssignment.ID) + check.Assert(err, IsNil) + check.Assert(assignmentById.NsxtAlbServiceEngineGroupAssignment, DeepEquals, assignment.NsxtAlbServiceEngineGroupAssignment) + + // Get By Name + assignmentByName, err := vcd.client.GetAlbServiceEngineGroupAssignmentByName(assignment.NsxtAlbServiceEngineGroupAssignment.ServiceEngineGroupRef.Name) + check.Assert(err, IsNil) + check.Assert(assignmentByName.NsxtAlbServiceEngineGroupAssignment, DeepEquals, assignment.NsxtAlbServiceEngineGroupAssignment) + + // Filtered by name and Edge Gateway ID + queryParams := url.Values{} + queryParams.Add("filter", fmt.Sprintf("gatewayRef.id==%s", edge.EdgeGateway.ID)) + filteredAssignmentByName, err := vcd.client.GetFilteredAlbServiceEngineGroupAssignmentByName(assignment.NsxtAlbServiceEngineGroupAssignment.ServiceEngineGroupRef.Name, queryParams) + check.Assert(err, IsNil) + check.Assert(filteredAssignmentByName.NsxtAlbServiceEngineGroupAssignment, DeepEquals, filteredAssignmentByName.NsxtAlbServiceEngineGroupAssignment) + + // Get all + allAssignments, err := vcd.client.GetAllAlbServiceEngineGroupAssignments(nil) + check.Assert(err, IsNil) + var foundAssignment bool + for i := range allAssignments { + if allAssignments[i].NsxtAlbServiceEngineGroupAssignment.ID == assignment.NsxtAlbServiceEngineGroupAssignment.ID { + foundAssignment = true + } + } + check.Assert(foundAssignment, Equals, true) + + assignment.NsxtAlbServiceEngineGroupAssignment.MaxVirtualServices = addrOf(50) + assignment.NsxtAlbServiceEngineGroupAssignment.MinVirtualServices = addrOf(30) + + updatedAssignment, err := assignment.Update(assignment.NsxtAlbServiceEngineGroupAssignment) + check.Assert(err, IsNil) + check.Assert(updatedAssignment, NotNil) + + // Perform immediate cleanups + err = assignment.Delete() + check.Assert(err, IsNil) + err = edge.DisableAlb() + check.Assert(err, IsNil) + err = seGroup.Delete() + check.Assert(err, IsNil) + err = cloud.Delete() + check.Assert(err, IsNil) + err = controller.Delete() + check.Assert(err, IsNil) +} diff --git a/govcd/nsxt_alb_service_engine_groups.go b/govcd/nsxt_alb_service_engine_groups.go new file mode 100644 index 000000000..f80fab476 --- /dev/null +++ b/govcd/nsxt_alb_service_engine_groups.go @@ -0,0 +1,256 @@ +/* + * Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "errors" + "fmt" + "net/url" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +// NsxtAlbServiceEngineGroup provides virtual service management capabilities for tenants. This entity can be created +// by referencing a backing importable service engine group - NsxtAlbImportableServiceEngineGroups. +// +// A service engine group is an isolation domain that also defines shared service engine properties, such as size, +// network access, and failover. Resources in a service engine group can be used for different virtual services, +// depending on your tenant needs. These resources cannot be shared between different service engine groups. +type NsxtAlbServiceEngineGroup struct { + NsxtAlbServiceEngineGroup *types.NsxtAlbServiceEngineGroup + vcdClient *VCDClient +} + +// GetAllAlbServiceEngineGroups retrieves NSX-T ALB Service Engines with possible filters +// +// Context is not mandatory for this resource. Supported contexts are: +// * Gateway ID (_context==gatewayId) - returns all Load Balancer Service Engine Groups that are accessible to the +// gateway. +// * Assignable Gateway ID (_context=gatewayId;_context==assignable) returns all Load Balancer Service Engine Groups +// that are assignable to the gateway. This filters out any Load Balancer Service Engine groups that are already +// assigned to the gateway or assigned to another gateway if the reservation type is 'DEDICATED’. +func (vcdClient *VCDClient) GetAllAlbServiceEngineGroups(context string, queryParameters url.Values) ([]*NsxtAlbServiceEngineGroup, error) { + client := vcdClient.Client + + if !client.IsSysAdmin { + return nil, errors.New("handling NSX-T ALB Service Engine Groups require System user") + } + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbServiceEngineGroups + 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) + if context != "" { + queryParams = queryParameterFilterAnd(fmt.Sprintf("_context==%s", context), queryParams) + } + typeResponses := []*types.NsxtAlbServiceEngineGroup{{}} + + err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParams, &typeResponses, nil) + if err != nil { + return nil, err + } + + wrappedResponses := make([]*NsxtAlbServiceEngineGroup, len(typeResponses)) + for sliceIndex := range typeResponses { + wrappedResponses[sliceIndex] = &NsxtAlbServiceEngineGroup{ + NsxtAlbServiceEngineGroup: typeResponses[sliceIndex], + vcdClient: vcdClient, + } + } + + return wrappedResponses, nil +} + +// GetAlbServiceEngineGroupByName returns NSX-T ALB Service Engine by Name +// Context is not mandatory for this resource. Supported contexts are: +// * Gateway ID (_context==gatewayId) - returns all Load Balancer Service Engine Groups that are accessible to the +// gateway. +// * Assignable Gateway ID (_context=gatewayId;_context==assignable) returns all Load Balancer Service Engine Groups +// that are assignable to the gateway. This filters out any Load Balancer Service Engine groups that are already +// assigned to the gateway or assigned to another gateway if the reservation type is 'DEDICATED’. +func (vcdClient *VCDClient) GetAlbServiceEngineGroupByName(optionalContext, name string) (*NsxtAlbServiceEngineGroup, error) { + queryParams := copyOrNewUrlValues(nil) + if optionalContext != "" { + queryParams = queryParameterFilterAnd(fmt.Sprintf("_context==%s", optionalContext), queryParams) + } + queryParams.Add("filter", fmt.Sprintf("name==%s", name)) + + albSeGroups, err := vcdClient.GetAllAlbServiceEngineGroups("", queryParams) + if err != nil { + return nil, fmt.Errorf("error retrieving NSX-T ALB Service Engine Group By Name '%s': %s", name, err) + } + + if len(albSeGroups) == 0 { + return nil, fmt.Errorf("%s", ErrorEntityNotFound) + } + + if len(albSeGroups) > 1 { + return nil, fmt.Errorf("more than 1 NSX-T ALB Service Engine Group with Name '%s' found", name) + } + + return albSeGroups[0], nil +} + +// GetAlbServiceEngineGroupById returns importable NSX-T ALB Cloud by ID +func (vcdClient *VCDClient) GetAlbServiceEngineGroupById(id string) (*NsxtAlbServiceEngineGroup, error) { + client := vcdClient.Client + + if !client.IsSysAdmin { + return nil, errors.New("handling NSX-T ALB Service Engine Groups require System user") + } + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbServiceEngineGroups + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, id) + if err != nil { + return nil, err + } + + typeResponse := &types.NsxtAlbServiceEngineGroup{} + + err = client.OpenApiGetItem(apiVersion, urlRef, nil, &typeResponse, nil) + if err != nil { + return nil, err + } + + wrappedResponse := &NsxtAlbServiceEngineGroup{ + NsxtAlbServiceEngineGroup: typeResponse, + vcdClient: vcdClient, + } + + return wrappedResponse, nil +} + +func (vcdClient *VCDClient) CreateNsxtAlbServiceEngineGroup(albServiceEngineGroup *types.NsxtAlbServiceEngineGroup) (*NsxtAlbServiceEngineGroup, error) { + client := vcdClient.Client + if !client.IsSysAdmin { + return nil, errors.New("handling NSX-T ALB Service Engine Groups require System user") + } + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbServiceEngineGroups + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + returnObject := &NsxtAlbServiceEngineGroup{ + NsxtAlbServiceEngineGroup: &types.NsxtAlbServiceEngineGroup{}, + vcdClient: vcdClient, + } + + err = client.OpenApiPostItem(apiVersion, urlRef, nil, albServiceEngineGroup, returnObject.NsxtAlbServiceEngineGroup, nil) + if err != nil { + return nil, fmt.Errorf("error creating NSX-T ALB Service Engine Group: %s", err) + } + + return returnObject, nil +} + +// Update updates existing ALB Controller with new supplied albControllerConfig configuration +func (nsxtAlbServiceEngineGroup *NsxtAlbServiceEngineGroup) Update(albSEGroupConfig *types.NsxtAlbServiceEngineGroup) (*NsxtAlbServiceEngineGroup, error) { + client := nsxtAlbServiceEngineGroup.vcdClient.Client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbServiceEngineGroups + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + if albSEGroupConfig.ID == "" { + return nil, fmt.Errorf("cannot update NSX-T ALB Service Engine Group without ID") + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, albSEGroupConfig.ID) + if err != nil { + return nil, err + } + + responseAlbController := &NsxtAlbServiceEngineGroup{ + NsxtAlbServiceEngineGroup: &types.NsxtAlbServiceEngineGroup{}, + vcdClient: nsxtAlbServiceEngineGroup.vcdClient, + } + + err = client.OpenApiPutItem(apiVersion, urlRef, nil, albSEGroupConfig, responseAlbController.NsxtAlbServiceEngineGroup, nil) + if err != nil { + return nil, fmt.Errorf("error updating NSX-T ALB Service Engine Group: %s", err) + } + + return responseAlbController, nil +} + +// Delete deletes NSX-T ALB Service Engine Group configuration +func (nsxtAlbServiceEngineGroup *NsxtAlbServiceEngineGroup) Delete() error { + client := nsxtAlbServiceEngineGroup.vcdClient.Client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbServiceEngineGroups + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return err + } + + if nsxtAlbServiceEngineGroup.NsxtAlbServiceEngineGroup.ID == "" { + return fmt.Errorf("cannot delete NSX-T ALB Service Engine Group without ID") + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, nsxtAlbServiceEngineGroup.NsxtAlbServiceEngineGroup.ID) + if err != nil { + return err + } + + err = client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) + if err != nil { + return fmt.Errorf("error deleting NSX-T ALB Service Engine Group: %s", err) + } + + return nil +} + +// Sync syncs a specified Load Balancer Service Engine Group. It requests the HA mode and the maximum number of +// supported Virtual Services for this Service Engine Group from the Load Balancer, and updates vCD's local record of +// these properties. +func (nsxtAlbServiceEngineGroup *NsxtAlbServiceEngineGroup) Sync() error { + client := nsxtAlbServiceEngineGroup.vcdClient.Client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbServiceEngineGroups + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return err + } + + if nsxtAlbServiceEngineGroup.NsxtAlbServiceEngineGroup.ID == "" { + return fmt.Errorf("cannot sync NSX-T ALB Service Engine Group without ID") + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, nsxtAlbServiceEngineGroup.NsxtAlbServiceEngineGroup.ID, "/sync") + if err != nil { + return err + } + + task, err := client.OpenApiPostItemAsync(apiVersion, urlRef, nil, nil) + if err != nil { + return fmt.Errorf("error syncing NSX-T ALB Service Engine Group: %s", err) + } + + err = task.WaitTaskCompletion() + if err != nil { + return fmt.Errorf("sync task for NSX-T ALB Service Engine Group failed: %s", err) + } + + return nil +} diff --git a/govcd/nsxt_alb_service_engine_groups_test.go b/govcd/nsxt_alb_service_engine_groups_test.go new file mode 100644 index 000000000..97b677629 --- /dev/null +++ b/govcd/nsxt_alb_service_engine_groups_test.go @@ -0,0 +1,132 @@ +//go:build nsxt || alb || functional || ALL + +package govcd + +import ( + "fmt" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_GetAllAlbServiceEngineGroups(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + + controller, createdAlbCloud := spawnAlbControllerAndCloud(vcd, check) + + importableSeGroups, err := vcd.client.GetAllAlbImportableServiceEngineGroups(createdAlbCloud.NsxtAlbCloud.ID, nil) + check.Assert(err, IsNil) + check.Assert(len(importableSeGroups) > 0, Equals, true) + + albSeGroup := &types.NsxtAlbServiceEngineGroup{ + Name: check.TestName() + "SE-group", + Description: "Service Engine Group created by " + check.TestName(), + ReservationType: "DEDICATED", + ServiceEngineGroupBacking: types.ServiceEngineGroupBacking{ + BackingId: importableSeGroups[0].NsxtAlbImportableServiceEngineGroups.ID, + LoadBalancerCloudRef: &types.OpenApiReference{ + ID: createdAlbCloud.NsxtAlbCloud.ID, + }, + }, + } + + // Field is only available when using API version v37.0 onwards + if vcd.client.Client.APIVCDMaxVersionIs(">= 37.0") { + albSeGroup.SupportedFeatureSet = "PREMIUM" + } + + createdSeGroup, err := vcd.client.CreateNsxtAlbServiceEngineGroup(albSeGroup) + check.Assert(err, IsNil) + + check.Assert(createdSeGroup.NsxtAlbServiceEngineGroup.ID != "", Equals, true) + check.Assert(createdSeGroup.NsxtAlbServiceEngineGroup.Name, Equals, albSeGroup.Name) + check.Assert(createdSeGroup.NsxtAlbServiceEngineGroup.Description, Equals, albSeGroup.Description) + check.Assert(createdSeGroup.NsxtAlbServiceEngineGroup.ReservationType, Equals, albSeGroup.ReservationType) + // Field is only populated in responses when using API version v37.0 onwards + if vcd.client.Client.APIVCDMaxVersionIs(">= 37.0") { + check.Assert(createdSeGroup.NsxtAlbServiceEngineGroup.SupportedFeatureSet, Equals, albSeGroup.SupportedFeatureSet) + } + + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbServiceEngineGroups + createdSeGroup.NsxtAlbServiceEngineGroup.ID + AddToCleanupListOpenApi(createdSeGroup.NsxtAlbServiceEngineGroup.Name, check.TestName(), openApiEndpoint) + + // Sync + err = createdSeGroup.Sync() + check.Assert(err, IsNil) + + // Find by Name + seGroupByName, err := vcd.client.GetAlbServiceEngineGroupByName("", createdSeGroup.NsxtAlbServiceEngineGroup.Name) + check.Assert(err, IsNil) + check.Assert(seGroupByName, NotNil) + + // Find by ID + seGroupById, err := vcd.client.GetAlbServiceEngineGroupById(createdSeGroup.NsxtAlbServiceEngineGroup.ID) + check.Assert(err, IsNil) + check.Assert(seGroupById, NotNil) + + check.Assert(seGroupByName.NsxtAlbServiceEngineGroup.ID, Equals, createdSeGroup.NsxtAlbServiceEngineGroup.ID) + check.Assert(seGroupById.NsxtAlbServiceEngineGroup.ID, Equals, createdSeGroup.NsxtAlbServiceEngineGroup.ID) + + // Test update + createdSeGroup.NsxtAlbServiceEngineGroup.Name = createdSeGroup.NsxtAlbServiceEngineGroup.Name + "updated" + // Field is only available when using API version v37.0 onwards + if vcd.client.Client.APIVCDMaxVersionIs(">= 37.0") { + albSeGroup.SupportedFeatureSet = "STANDARD" + } + updatedSeGroup, err := createdSeGroup.Update(createdSeGroup.NsxtAlbServiceEngineGroup) + check.Assert(err, IsNil) + + // SupportedFeatureSet is a field only available since v37.0, in that case we ignore it in the following DeepEquals + if vcd.client.Client.APIVCDMaxVersionIs("< 37.0") { + updatedSeGroup.NsxtAlbServiceEngineGroup.SupportedFeatureSet = createdSeGroup.NsxtAlbServiceEngineGroup.SupportedFeatureSet + } + check.Assert(updatedSeGroup.NsxtAlbServiceEngineGroup, DeepEquals, createdSeGroup.NsxtAlbServiceEngineGroup) + + // Cleanup + err = createdSeGroup.Delete() + check.Assert(err, IsNil) + + err = createdAlbCloud.Delete() + check.Assert(err, IsNil) + + err = controller.Delete() + check.Assert(err, IsNil) +} + +// spawnAlbControllerCloudServiceEngineGroup is a helper function to spawn NSX-T ALB Controller, ALB Cloud, and ALB +// Service Engine Group +func spawnAlbControllerCloudServiceEngineGroup(vcd *TestVCD, check *C, seGroupReservationType string) (*NsxtAlbController, *NsxtAlbCloud, *NsxtAlbServiceEngineGroup) { + skipNoNsxtAlbConfiguration(vcd, check) + + albController, createdAlbCloud := spawnAlbControllerAndCloud(vcd, check) + + importableSeGroup, err := vcd.client.GetAlbImportableServiceEngineGroupByName(createdAlbCloud.NsxtAlbCloud.ID, vcd.config.VCD.Nsxt.NsxtAlbServiceEngineGroup) + check.Assert(err, IsNil) + + albSeGroup := &types.NsxtAlbServiceEngineGroup{ + Name: check.TestName() + "SE-group", + Description: "Service Engine Group created by " + check.TestName(), + ReservationType: seGroupReservationType, + ServiceEngineGroupBacking: types.ServiceEngineGroupBacking{ + BackingId: importableSeGroup.NsxtAlbImportableServiceEngineGroups.ID, + LoadBalancerCloudRef: &types.OpenApiReference{ + ID: createdAlbCloud.NsxtAlbCloud.ID, + }, + }, + } + + // Field is only available when using API version v37.0 onwards + if vcd.client.Client.APIVCDMaxVersionIs(">= 37.0") { + albSeGroup.SupportedFeatureSet = "PREMIUM" + } + + createdSeGroup, err := vcd.client.CreateNsxtAlbServiceEngineGroup(albSeGroup) + check.Assert(err, IsNil) + + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbServiceEngineGroups + createdSeGroup.NsxtAlbServiceEngineGroup.ID + PrependToCleanupListOpenApi(createdSeGroup.NsxtAlbServiceEngineGroup.Name, check.TestName(), openApiEndpoint) + + return albController, createdAlbCloud, createdSeGroup +} diff --git a/govcd/nsxt_alb_settings.go b/govcd/nsxt_alb_settings.go new file mode 100644 index 000000000..b763f9edd --- /dev/null +++ b/govcd/nsxt_alb_settings.go @@ -0,0 +1,66 @@ +package govcd + +import ( + "fmt" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +// GetAlbSettings retrieves NSX-T ALB settings for a particular Edge Gateway +func (egw *NsxtEdgeGateway) GetAlbSettings() (*types.NsxtAlbConfig, error) { + client := egw.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbEdgeGateway + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, egw.EdgeGateway.ID)) + if err != nil { + return nil, err + } + + typeResponse := &types.NsxtAlbConfig{} + err = client.OpenApiGetItem(apiVersion, urlRef, nil, &typeResponse, nil) + if err != nil { + return nil, err + } + + return typeResponse, nil +} + +// UpdateAlbSettings updates NSX-T ALB settings for a particular Edge Gateway +func (egw *NsxtEdgeGateway) UpdateAlbSettings(config *types.NsxtAlbConfig) (*types.NsxtAlbConfig, error) { + client := egw.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbEdgeGateway + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, egw.EdgeGateway.ID)) + if err != nil { + return nil, err + } + + typeResponse := &types.NsxtAlbConfig{} + err = client.OpenApiPutItem(apiVersion, urlRef, nil, config, typeResponse, nil) + if err != nil { + return nil, err + } + + return typeResponse, nil +} + +// DisableAlb is a shortcut wrapping UpdateAlbSettings which disables ALB configuration +func (egw *NsxtEdgeGateway) DisableAlb() error { + config := &types.NsxtAlbConfig{ + Enabled: false, + } + _, err := egw.UpdateAlbSettings(config) + if err != nil { + return fmt.Errorf("error disabling NSX-T ALB: %s", err) + } + + return nil +} diff --git a/govcd/nsxt_alb_settings_test.go b/govcd/nsxt_alb_settings_test.go new file mode 100644 index 000000000..f2ac3642e --- /dev/null +++ b/govcd/nsxt_alb_settings_test.go @@ -0,0 +1,109 @@ +//go:build nsxt || alb || functional || ALL + +package govcd + +import ( + "fmt" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_GetAlbSettings(check *C) { + skipNoNsxtAlbConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointAlbEdgeGateway) + + edge, err := vcd.nsxtVdc.GetNsxtEdgeGatewayByName(vcd.config.VCD.Nsxt.EdgeGateway) + check.Assert(err, IsNil) + + albSettings, err := edge.GetAlbSettings() + check.Assert(err, IsNil) + check.Assert(albSettings, NotNil) + check.Assert(albSettings.Enabled, Equals, false) +} + +func (vcd *TestVCD) Test_UpdateAlbSettings(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + skipNoNsxtAlbConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointAlbEdgeGateway) + + controller, cloud, seGroup := spawnAlbControllerCloudServiceEngineGroup(vcd, check, "DEDICATED") + edge, err := vcd.nsxtVdc.GetNsxtEdgeGatewayByName(vcd.config.VCD.Nsxt.EdgeGateway) + check.Assert(err, IsNil) + + // Enable ALB on Edge Gateway with default ServiceNetworkDefinition + albSettingsConfig := &types.NsxtAlbConfig{ + Enabled: true, + } + + // Field is only available when using API version v37.0 onwards + if vcd.client.Client.APIVCDMaxVersionIs(">= 37.0") { + albSettingsConfig.SupportedFeatureSet = "STANDARD" + } + + enabledSettings, err := edge.UpdateAlbSettings(albSettingsConfig) + check.Assert(err, IsNil) + check.Assert(enabledSettings.Enabled, Equals, true) + check.Assert(enabledSettings.ServiceNetworkDefinition, Equals, "192.168.255.1/25") + // Field is only available when using API version v37.0 onwards + if vcd.client.Client.APIVCDMaxVersionIs(">= 37.0") { + check.Assert(enabledSettings.SupportedFeatureSet, Equals, "STANDARD") + } + PrependToCleanupList("", "OpenApiEntityAlbSettingsDisable", edge.EdgeGateway.Name, check.TestName()) + + // Disable ALB on Edge Gateway + albSettingsConfig.Enabled = false + disabledSettings, err := edge.UpdateAlbSettings(albSettingsConfig) + check.Assert(err, IsNil) + check.Assert(disabledSettings.Enabled, Equals, false) + + // Enable ALB on Edge Gateway with custom ServiceNetworkDefinition + albSettingsConfig.Enabled = true + albSettingsConfig.ServiceNetworkDefinition = "93.93.11.1/25" + enabledSettingsCustomServiceDefinition, err := edge.UpdateAlbSettings(albSettingsConfig) + check.Assert(err, IsNil) + check.Assert(enabledSettingsCustomServiceDefinition.Enabled, Equals, true) + check.Assert(enabledSettingsCustomServiceDefinition.ServiceNetworkDefinition, Equals, "93.93.11.1/25") + + // Disable ALB on Edge Gateway + err = edge.DisableAlb() + check.Assert(err, IsNil) + + // Enable IPv6 service network definition (VCD 10.4.0+) + if vcd.client.Client.APIVCDMaxVersionIs(">= 37.0") { + printVerbose("Enabling IPv6 service network definition for VCD 10.4.0+\n") + albSettingsConfig.Ipv6ServiceNetworkDefinition = "2001:0db8:85a3:0000:0000:8a2e:0370:7334/120" + enabledSettingsIpv6ServiceDefinition, err := edge.UpdateAlbSettings(albSettingsConfig) + check.Assert(err, IsNil) + check.Assert(enabledSettingsIpv6ServiceDefinition.Ipv6ServiceNetworkDefinition, Equals, "2001:0db8:85a3:0000:0000:8a2e:0370:7334/120") + err = edge.DisableAlb() + check.Assert(err, IsNil) + } + + // Enable Transparent mode (VCD 10.4.1+) + if vcd.client.Client.APIVCDMaxVersionIs(">= 37.1") { + printVerbose("Enabling Transparent mode for VCD 10.4.1+\n") + albSettingsConfig.TransparentModeEnabled = addrOf(true) + enabledSettingsTransparentMode, err := edge.UpdateAlbSettings(albSettingsConfig) + check.Assert(err, IsNil) + check.Assert(*enabledSettingsTransparentMode.TransparentModeEnabled, Equals, true) + err = edge.DisableAlb() + check.Assert(err, IsNil) + } + + albSettings, err := edge.GetAlbSettings() + check.Assert(err, IsNil) + check.Assert(albSettings, NotNil) + check.Assert(albSettings.Enabled, Equals, false) + + // Remove objects + err = seGroup.Delete() + check.Assert(err, IsNil) + err = cloud.Delete() + check.Assert(err, IsNil) + err = controller.Delete() + check.Assert(err, IsNil) +} diff --git a/govcd/nsxt_alb_virtual_service.go b/govcd/nsxt_alb_virtual_service.go new file mode 100644 index 000000000..75d131390 --- /dev/null +++ b/govcd/nsxt_alb_virtual_service.go @@ -0,0 +1,214 @@ +/* + * Copyright 2021 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" +) + +// NsxtAlbVirtualService combines Load Balancer Pools with Service Engine Groups and exposes a virtual service on +// defined VIP (virtual IP address) while optionally allowing to use encrypted traffic +type NsxtAlbVirtualService struct { + NsxtAlbVirtualService *types.NsxtAlbVirtualService + vcdClient *VCDClient +} + +// GetAllAlbVirtualServiceSummaries returns a limited subset of NsxtAlbVirtualService values, but does it in single +// query. To fetch complete information for ALB Virtual Services one can use GetAllAlbVirtualServices(), but it is slower +// as it has to retrieve Virtual Services one by one. +func (vcdClient *VCDClient) GetAllAlbVirtualServiceSummaries(edgeGatewayId string, queryParameters url.Values) ([]*NsxtAlbVirtualService, error) { + client := vcdClient.Client + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbVirtualServiceSummaries + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, edgeGatewayId)) + if err != nil { + return nil, err + } + + typeResponses := []*types.NsxtAlbVirtualService{{}} + err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParameters, &typeResponses, nil) + if err != nil { + return nil, err + } + + // Wrap all typeResponses into NsxtAlbPool types with client + wrappedResponses := make([]*NsxtAlbVirtualService, len(typeResponses)) + for sliceIndex := range typeResponses { + wrappedResponses[sliceIndex] = &NsxtAlbVirtualService{ + NsxtAlbVirtualService: typeResponses[sliceIndex], + vcdClient: vcdClient, + } + } + + return wrappedResponses, nil +} + +// GetAllAlbVirtualServices fetches ALB Virtual Services by at first listing all Virtual Services summaries and then +// fetching complete structure one by one +func (vcdClient *VCDClient) GetAllAlbVirtualServices(edgeGatewayId string, queryParameters url.Values) ([]*NsxtAlbVirtualService, error) { + allAlbVirtualServiceSummaries, err := vcdClient.GetAllAlbVirtualServiceSummaries(edgeGatewayId, queryParameters) + if err != nil { + return nil, fmt.Errorf("error retrieving all ALB Virtual Service summaries: %s", err) + } + + // Loop over all Summaries and retrieve complete information + allAlbVirtualServices := make([]*NsxtAlbVirtualService, len(allAlbVirtualServiceSummaries)) + for index := range allAlbVirtualServiceSummaries { + allAlbVirtualServices[index], err = vcdClient.GetAlbVirtualServiceById(allAlbVirtualServiceSummaries[index].NsxtAlbVirtualService.ID) + if err != nil { + return nil, fmt.Errorf("error retrieving complete ALB Virtual Service: %s", err) + } + + } + + return allAlbVirtualServices, nil +} + +// GetAlbVirtualServiceByName fetches ALB Virtual Service By Name +func (vcdClient *VCDClient) GetAlbVirtualServiceByName(edgeGatewayId string, name string) (*NsxtAlbVirtualService, error) { + queryParameters := copyOrNewUrlValues(nil) + queryParameters.Add("filter", "name=="+name) + + allAlbVirtualServices, err := vcdClient.GetAllAlbVirtualServices(edgeGatewayId, queryParameters) + if err != nil { + return nil, fmt.Errorf("error reading ALB Virtual Service with Name '%s': %s", name, err) + } + + if len(allAlbVirtualServices) == 0 { + return nil, fmt.Errorf("%s: could not find ALB Virtual Service with Name '%s'", ErrorEntityNotFound, name) + } + + if len(allAlbVirtualServices) > 1 { + return nil, fmt.Errorf("found more than 1 ALB Virtual Service with Name '%s'", name) + } + + return allAlbVirtualServices[0], nil +} + +// GetAlbVirtualServiceById fetches ALB Virtual Service By ID +func (vcdClient *VCDClient) GetAlbVirtualServiceById(id string) (*NsxtAlbVirtualService, error) { + client := vcdClient.Client + + if id == "" { + return nil, fmt.Errorf("ID is required to lookup NSX-T ALB Virtual Service by ID") + } + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbVirtualServices + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, id) + if err != nil { + return nil, err + } + + typeResponse := &types.NsxtAlbVirtualService{} + err = client.OpenApiGetItem(apiVersion, urlRef, nil, &typeResponse, nil) + if err != nil { + return nil, err + } + + wrappedResponse := &NsxtAlbVirtualService{ + NsxtAlbVirtualService: typeResponse, + vcdClient: vcdClient, + } + + return wrappedResponse, nil +} + +// CreateNsxtAlbVirtualService creates NSX-T ALB Virtual Service based on supplied configuration +func (vcdClient *VCDClient) CreateNsxtAlbVirtualService(albVirtualServiceConfig *types.NsxtAlbVirtualService) (*NsxtAlbVirtualService, error) { + client := vcdClient.Client + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbVirtualServices + minimumApiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + returnObject := &NsxtAlbVirtualService{ + NsxtAlbVirtualService: &types.NsxtAlbVirtualService{}, + vcdClient: vcdClient, + } + + err = client.OpenApiPostItem(minimumApiVersion, urlRef, nil, albVirtualServiceConfig, returnObject.NsxtAlbVirtualService, nil) + if err != nil { + return nil, fmt.Errorf("error creating NSX-T ALB Virtual Service: %s", err) + } + + return returnObject, nil +} + +// Update updates NSX-T ALB Virtual Service based on supplied configuration +func (nsxtAlbVirtualService *NsxtAlbVirtualService) Update(albVirtualServiceConfig *types.NsxtAlbVirtualService) (*NsxtAlbVirtualService, error) { + client := nsxtAlbVirtualService.vcdClient.Client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbVirtualServices + minimumApiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + if albVirtualServiceConfig.ID == "" { + return nil, fmt.Errorf("cannot update NSX-T ALB Virtual Service without ID") + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, albVirtualServiceConfig.ID) + if err != nil { + return nil, err + } + + responseAlbController := &NsxtAlbVirtualService{ + NsxtAlbVirtualService: &types.NsxtAlbVirtualService{}, + vcdClient: nsxtAlbVirtualService.vcdClient, + } + + err = client.OpenApiPutItem(minimumApiVersion, urlRef, nil, albVirtualServiceConfig, responseAlbController.NsxtAlbVirtualService, nil) + if err != nil { + return nil, fmt.Errorf("error updating NSX-T ALB Virtual Service: %s", err) + } + + return responseAlbController, nil +} + +// Delete deletes NSX-T ALB Virtual Service +func (nsxtAlbVirtualService *NsxtAlbVirtualService) Delete() error { + client := nsxtAlbVirtualService.vcdClient.Client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbVirtualServices + minimumApiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return err + } + + if nsxtAlbVirtualService.NsxtAlbVirtualService.ID == "" { + return fmt.Errorf("cannot delete NSX-T ALB Virtual Service without ID") + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, nsxtAlbVirtualService.NsxtAlbVirtualService.ID) + if err != nil { + return err + } + + err = client.OpenApiDeleteItem(minimumApiVersion, urlRef, nil, nil) + if err != nil { + return fmt.Errorf("error deleting NSX-T ALB Virtual Service: %s", err) + } + + return nil +} diff --git a/govcd/nsxt_alb_virtual_service_test.go b/govcd/nsxt_alb_virtual_service_test.go new file mode 100644 index 000000000..5a1d65a2e --- /dev/null +++ b/govcd/nsxt_alb_virtual_service_test.go @@ -0,0 +1,573 @@ +//go:build nsxt || alb || functional || ALL + +package govcd + +import ( + "fmt" + "time" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_AlbVirtualService(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + skipNoNsxtAlbConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointAlbEdgeGateway) + + // Setup prerequisite components + controller, cloud, seGroup, edge, seGroupAssignment, albPool := setupAlbVirtualServicePrerequisites(check, vcd) + + // Setup Org user and connection + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + orgUserVcdClient, orgUser, err := newOrgUserConnection(adminOrg, "alb-virtual-service-testing", "CHANGE-ME", vcd.config.Provider.Url, true) + check.Assert(err, IsNil) + + printVerbose("# Running tests as Sysadmin user\n") + // Run tests with System user + testMinimalVirtualServiceConfigHTTP(check, edge, albPool, seGroup, vcd, vcd.client) + testVirtualServiceConfigWithCertHTTPS(check, edge, albPool, seGroup, vcd, vcd.client) + testMinimalVirtualServiceConfigL4(check, edge, albPool, seGroup, vcd, vcd.client) + testMinimalVirtualServiceConfigL4TLS(check, edge, albPool, seGroup, vcd, vcd.client) + if vcd.client.Client.APIVCDMaxVersionIs(">= 37.0") { + printVerbose("# Running 10.4.0+ IPv6 Virtual Service test as Sysadmin user\n") + testVirtualServiceConfigHTTPIPv6(check, edge, albPool, seGroup, vcd, vcd.client) + } + + printVerbose("# Running tests as Org user\n") + // Run tests with Org admin user + testMinimalVirtualServiceConfigHTTP(check, edge, albPool, seGroup, vcd, orgUserVcdClient) + testVirtualServiceConfigWithCertHTTPS(check, edge, albPool, seGroup, vcd, orgUserVcdClient) + testMinimalVirtualServiceConfigL4(check, edge, albPool, seGroup, vcd, orgUserVcdClient) + testMinimalVirtualServiceConfigL4TLS(check, edge, albPool, seGroup, vcd, orgUserVcdClient) + if vcd.client.Client.APIVCDMaxVersionIs(">= 37.0") { + printVerbose("# Running 10.4.0+ IPv6 Virtual Service test as Org user\n") + testVirtualServiceConfigHTTPIPv6(check, edge, albPool, seGroup, vcd, orgUserVcdClient) + } + + // Test 10.4.1 Transparent mode on VCD >= 10.4.1 + if vcd.client.Client.APIVCDMaxVersionIs(">= 37.1") { + printVerbose("# Running 10.4.1+ tests as Sysadmin user\n") + + printVerbose("## Creating ALB Pool with Member Group (VCD 10.4.1+) as Sysadmin\n") + ipSet, poolWithMemberGroup := setupAlbPoolFirewallGroupMembers(check, vcd, edge) + + testMinimalVirtualServiceConfigHTTPTransparent(check, edge, poolWithMemberGroup, seGroup, vcd, vcd.client, true) + testMinimalVirtualServiceConfigHTTPTransparent(check, edge, poolWithMemberGroup, seGroup, vcd, vcd.client, false) + + printVerbose("# Running 10.4.1+ tests as Org user\n") + + printVerbose("## Creating ALB Pool with Member Group (VCD 10.4.1+) as Org user\n") + testMinimalVirtualServiceConfigHTTPTransparent(check, edge, poolWithMemberGroup, seGroup, vcd, orgUserVcdClient, true) + testMinimalVirtualServiceConfigHTTPTransparent(check, edge, poolWithMemberGroup, seGroup, vcd, orgUserVcdClient, false) + + // cleanup ipset and pool membership + err = poolWithMemberGroup.Delete() + check.Assert(err, IsNil) + + err = retryOnError(ipSet.Delete, 5, 1*time.Second) + check.Assert(err, IsNil) + } + + // teardown prerequisites + tearDownAlbVirtualServicePrerequisites(check, albPool, seGroupAssignment, edge, seGroup, cloud, controller) + + // cleanup Org user + err = orgUser.Delete(true) + check.Assert(err, IsNil) +} + +func testMinimalVirtualServiceConfigHTTP(check *C, edge *NsxtEdgeGateway, pool *NsxtAlbPool, seGroup *NsxtAlbServiceEngineGroup, vcd *TestVCD, client *VCDClient) { + virtualServiceConfig := &types.NsxtAlbVirtualService{ + Name: check.TestName(), + Enabled: addrOf(true), + ApplicationProfile: types.NsxtAlbVirtualServiceApplicationProfile{ + SystemDefined: true, + Type: "HTTP", + }, + GatewayRef: types.OpenApiReference{ID: edge.EdgeGateway.ID}, + LoadBalancerPoolRef: types.OpenApiReference{ID: pool.NsxtAlbPool.ID}, + ServiceEngineGroupRef: types.OpenApiReference{ID: seGroup.NsxtAlbServiceEngineGroup.ID}, + ServicePorts: []types.NsxtAlbVirtualServicePort{ + { + PortStart: addrOf(80), + }, + }, + VirtualIpAddress: edge.EdgeGateway.EdgeGatewayUplinks[0].Subnets.Values[0].PrimaryIP, + } + + virtualServiceConfigUpdated := &types.NsxtAlbVirtualService{ + Name: check.TestName(), + Description: "Updated", + Enabled: addrOf(true), + ApplicationProfile: types.NsxtAlbVirtualServiceApplicationProfile{ + SystemDefined: true, + Type: "HTTP", + }, + GatewayRef: types.OpenApiReference{ID: edge.EdgeGateway.ID}, + LoadBalancerPoolRef: types.OpenApiReference{ID: pool.NsxtAlbPool.ID}, + ServiceEngineGroupRef: types.OpenApiReference{ID: seGroup.NsxtAlbServiceEngineGroup.ID}, + ServicePorts: []types.NsxtAlbVirtualServicePort{ + { + PortStart: addrOf(443), + PortEnd: addrOf(449), + SslEnabled: addrOf(false), + }, + { + PortStart: addrOf(2000), + PortEnd: addrOf(2010), + SslEnabled: addrOf(false), + }, + }, + // Use Primary IP of Edge Gateway as virtual service IP + VirtualIpAddress: edge.EdgeGateway.EdgeGatewayUplinks[0].Subnets.Values[0].PrimaryIP, + //HealthStatus: "", + //HealthMessage: "", + //DetailedHealthMessage: "", + } + + testAlbVirtualServiceConfig(check, vcd, "MinimalHTTP", virtualServiceConfig, virtualServiceConfigUpdated, client) +} + +func testVirtualServiceConfigHTTPIPv6(check *C, edge *NsxtEdgeGateway, pool *NsxtAlbPool, seGroup *NsxtAlbServiceEngineGroup, vcd *TestVCD, client *VCDClient) { + // Enable SLAAC Profile - this is a property of Edge Gateway - it will be removed with Edge + // Gateway itself upon cleanup + _, err := edge.UpdateSlaacProfile(&types.NsxtEdgeGatewaySlaacProfile{Enabled: true, Mode: "SLAAC"}) + check.Assert(err, IsNil) + defer func() { + _, err := edge.UpdateSlaacProfile(&types.NsxtEdgeGatewaySlaacProfile{Enabled: false, Mode: "DISABLED"}) + check.Assert(err, IsNil) + }() + + virtualServiceConfig := &types.NsxtAlbVirtualService{ + Name: check.TestName(), + Enabled: addrOf(true), + ApplicationProfile: types.NsxtAlbVirtualServiceApplicationProfile{ + SystemDefined: true, + Type: "HTTP", + }, + GatewayRef: types.OpenApiReference{ID: edge.EdgeGateway.ID}, + LoadBalancerPoolRef: types.OpenApiReference{ID: pool.NsxtAlbPool.ID}, + ServiceEngineGroupRef: types.OpenApiReference{ID: seGroup.NsxtAlbServiceEngineGroup.ID}, + ServicePorts: []types.NsxtAlbVirtualServicePort{ + { + PortStart: addrOf(80), + }, + }, + VirtualIpAddress: edge.EdgeGateway.EdgeGatewayUplinks[0].Subnets.Values[0].PrimaryIP, + IPv6VirtualIpAddress: "2002:0:0:1234:abcd:ffff:c0a8:103", + } + + virtualServiceConfigUpdated := &types.NsxtAlbVirtualService{ + Name: check.TestName(), + Description: "Updated", + Enabled: addrOf(true), + ApplicationProfile: types.NsxtAlbVirtualServiceApplicationProfile{ + SystemDefined: true, + Type: "HTTP", + }, + GatewayRef: types.OpenApiReference{ID: edge.EdgeGateway.ID}, + LoadBalancerPoolRef: types.OpenApiReference{ID: pool.NsxtAlbPool.ID}, + ServiceEngineGroupRef: types.OpenApiReference{ID: seGroup.NsxtAlbServiceEngineGroup.ID}, + ServicePorts: []types.NsxtAlbVirtualServicePort{ + { + PortStart: addrOf(443), + PortEnd: addrOf(449), + SslEnabled: addrOf(false), + }, + { + PortStart: addrOf(2000), + PortEnd: addrOf(2010), + SslEnabled: addrOf(false), + }, + }, + // Use Primary IP of Edge Gateway as virtual service IP + VirtualIpAddress: edge.EdgeGateway.EdgeGatewayUplinks[0].Subnets.Values[0].PrimaryIP, + IPv6VirtualIpAddress: "2002:0:0:1234:abcd:ffff:c0a8:103", + //HealthStatus: "", + //HealthMessage: "", + //DetailedHealthMessage: "", + } + + testAlbVirtualServiceConfig(check, vcd, "IPv6", virtualServiceConfig, virtualServiceConfigUpdated, client) +} + +func testMinimalVirtualServiceConfigHTTPTransparent(check *C, edge *NsxtEdgeGateway, poolWithMemberGroup *NsxtAlbPool, seGroup *NsxtAlbServiceEngineGroup, vcd *TestVCD, client *VCDClient, trueOnCreate bool) { + createTransparentMode := trueOnCreate + updateTransparentMode := !createTransparentMode + + virtualServiceConfig := &types.NsxtAlbVirtualService{ + Name: check.TestName(), + Enabled: addrOf(true), + TransparentModeEnabled: &createTransparentMode, + ApplicationProfile: types.NsxtAlbVirtualServiceApplicationProfile{ + SystemDefined: true, + Type: "HTTP", + }, + GatewayRef: types.OpenApiReference{ID: edge.EdgeGateway.ID}, + LoadBalancerPoolRef: types.OpenApiReference{ID: poolWithMemberGroup.NsxtAlbPool.ID}, + ServiceEngineGroupRef: types.OpenApiReference{ID: seGroup.NsxtAlbServiceEngineGroup.ID}, + ServicePorts: []types.NsxtAlbVirtualServicePort{ + { + PortStart: addrOf(80), + }, + }, + VirtualIpAddress: edge.EdgeGateway.EdgeGatewayUplinks[0].Subnets.Values[0].PrimaryIP, + } + + virtualServiceConfigUpdated := &types.NsxtAlbVirtualService{ + Name: check.TestName(), + Description: "Updated", + Enabled: addrOf(true), + TransparentModeEnabled: &updateTransparentMode, + ApplicationProfile: types.NsxtAlbVirtualServiceApplicationProfile{ + SystemDefined: true, + Type: "HTTP", + }, + GatewayRef: types.OpenApiReference{ID: edge.EdgeGateway.ID}, + LoadBalancerPoolRef: types.OpenApiReference{ID: poolWithMemberGroup.NsxtAlbPool.ID}, + ServiceEngineGroupRef: types.OpenApiReference{ID: seGroup.NsxtAlbServiceEngineGroup.ID}, + ServicePorts: []types.NsxtAlbVirtualServicePort{ + { + PortStart: addrOf(443), + PortEnd: addrOf(449), + SslEnabled: addrOf(false), + }, + { + PortStart: addrOf(2000), + PortEnd: addrOf(2010), + SslEnabled: addrOf(false), + }, + }, + // Use Primary IP of Edge Gateway as virtual service IP + VirtualIpAddress: edge.EdgeGateway.EdgeGatewayUplinks[0].Subnets.Values[0].PrimaryIP, + //HealthStatus: "", + //HealthMessage: "", + //DetailedHealthMessage: "", + } + + testAlbVirtualServiceConfig(check, vcd, fmt.Sprintf("MinimalHTTPWithTransparentModeOnCreate%t", createTransparentMode), virtualServiceConfig, virtualServiceConfigUpdated, client) +} + +func testMinimalVirtualServiceConfigL4(check *C, edge *NsxtEdgeGateway, pool *NsxtAlbPool, seGroup *NsxtAlbServiceEngineGroup, vcd *TestVCD, client *VCDClient) { + virtualServiceConfig := &types.NsxtAlbVirtualService{ + Name: check.TestName(), + Enabled: addrOf(true), + ApplicationProfile: types.NsxtAlbVirtualServiceApplicationProfile{ + SystemDefined: true, + Type: "L4", + }, + GatewayRef: types.OpenApiReference{ID: edge.EdgeGateway.ID}, + LoadBalancerPoolRef: types.OpenApiReference{ID: pool.NsxtAlbPool.ID}, + ServiceEngineGroupRef: types.OpenApiReference{ID: seGroup.NsxtAlbServiceEngineGroup.ID}, + ServicePorts: []types.NsxtAlbVirtualServicePort{ + { + PortStart: addrOf(80), + }, + }, + VirtualIpAddress: edge.EdgeGateway.EdgeGatewayUplinks[0].Subnets.Values[0].PrimaryIP, + } + + virtualServiceConfigUpdated := &types.NsxtAlbVirtualService{ + Name: check.TestName(), + Description: "Updated", + Enabled: addrOf(true), + ApplicationProfile: types.NsxtAlbVirtualServiceApplicationProfile{ + SystemDefined: true, + Type: "L4", + }, + GatewayRef: types.OpenApiReference{ID: edge.EdgeGateway.ID}, + LoadBalancerPoolRef: types.OpenApiReference{ID: pool.NsxtAlbPool.ID}, + ServiceEngineGroupRef: types.OpenApiReference{ID: seGroup.NsxtAlbServiceEngineGroup.ID}, + ServicePorts: []types.NsxtAlbVirtualServicePort{ + { + PortStart: addrOf(443), + TcpUdpProfile: &types.NsxtAlbVirtualServicePortTcpUdpProfile{ + SystemDefined: true, + Type: "TCP_PROXY", + }, + }, + { + PortStart: addrOf(8443), + PortEnd: addrOf(8445), + TcpUdpProfile: &types.NsxtAlbVirtualServicePortTcpUdpProfile{ + SystemDefined: true, + Type: "TCP_FAST_PATH", + }, + }, + { + PortStart: addrOf(9000), + TcpUdpProfile: &types.NsxtAlbVirtualServicePortTcpUdpProfile{ + SystemDefined: true, + Type: "UDP_FAST_PATH", + }, + }, + { + PortStart: addrOf(10000), + }, + }, + // Use Primary IP of Edge Gateway as virtual service IP + VirtualIpAddress: edge.EdgeGateway.EdgeGatewayUplinks[0].Subnets.Values[0].PrimaryIP, + } + + testAlbVirtualServiceConfig(check, vcd, "L4", virtualServiceConfig, virtualServiceConfigUpdated, client) +} + +func testMinimalVirtualServiceConfigL4TLS(check *C, edge *NsxtEdgeGateway, pool *NsxtAlbPool, seGroup *NsxtAlbServiceEngineGroup, vcd *TestVCD, client *VCDClient) { + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.org.Org.Name) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + + certificateConfigWithPrivateKey := &types.CertificateLibraryItem{ + Alias: check.TestName(), + Certificate: certificate, + PrivateKey: privateKey, + PrivateKeyPassphrase: "test", + } + openApiEndpoint, err := getEndpointByVersion(&vcd.client.Client) + check.Assert(err, IsNil) + createdCertificate, err := adminOrg.AddCertificateToLibrary(certificateConfigWithPrivateKey) + check.Assert(err, IsNil) + PrependToCleanupListOpenApi(createdCertificate.CertificateLibrary.Alias, check.TestName(), openApiEndpoint+createdCertificate.CertificateLibrary.Id) + + virtualServiceConfig := &types.NsxtAlbVirtualService{ + Name: check.TestName(), + Enabled: addrOf(true), + ApplicationProfile: types.NsxtAlbVirtualServiceApplicationProfile{ + SystemDefined: true, + Type: "L4_TLS", + }, + GatewayRef: types.OpenApiReference{ID: edge.EdgeGateway.ID}, + LoadBalancerPoolRef: types.OpenApiReference{ID: pool.NsxtAlbPool.ID}, + ServiceEngineGroupRef: types.OpenApiReference{ID: seGroup.NsxtAlbServiceEngineGroup.ID}, + CertificateRef: &types.OpenApiReference{ID: createdCertificate.CertificateLibrary.Id}, + ServicePorts: []types.NsxtAlbVirtualServicePort{ + { + PortStart: addrOf(80), + SslEnabled: addrOf(true), + }, + }, + VirtualIpAddress: edge.EdgeGateway.EdgeGatewayUplinks[0].Subnets.Values[0].PrimaryIP, + } + + virtualServiceConfigUpdated := &types.NsxtAlbVirtualService{ + Name: check.TestName(), + Description: "Updated", + Enabled: addrOf(true), + ApplicationProfile: types.NsxtAlbVirtualServiceApplicationProfile{ + SystemDefined: true, + Type: "L4_TLS", + }, + GatewayRef: types.OpenApiReference{ID: edge.EdgeGateway.ID}, + LoadBalancerPoolRef: types.OpenApiReference{ID: pool.NsxtAlbPool.ID}, + ServiceEngineGroupRef: types.OpenApiReference{ID: seGroup.NsxtAlbServiceEngineGroup.ID}, + CertificateRef: &types.OpenApiReference{ID: createdCertificate.CertificateLibrary.Id}, + ServicePorts: []types.NsxtAlbVirtualServicePort{ + { + PortStart: addrOf(443), + SslEnabled: addrOf(true), + TcpUdpProfile: &types.NsxtAlbVirtualServicePortTcpUdpProfile{ + SystemDefined: true, + Type: "TCP_PROXY", // The only possible type with L4_TLS + }, + }, + }, + // Use Primary IP of Edge Gateway as virtual service IP + VirtualIpAddress: edge.EdgeGateway.EdgeGatewayUplinks[0].Subnets.Values[0].PrimaryIP, + } + + testAlbVirtualServiceConfig(check, vcd, "L4-TLS", virtualServiceConfig, virtualServiceConfigUpdated, client) + + err = createdCertificate.Delete() + check.Assert(err, IsNil) +} + +func testVirtualServiceConfigWithCertHTTPS(check *C, edge *NsxtEdgeGateway, pool *NsxtAlbPool, seGroup *NsxtAlbServiceEngineGroup, vcd *TestVCD, client *VCDClient) { + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.org.Org.Name) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + + certificateConfigWithPrivateKey := &types.CertificateLibraryItem{ + Alias: check.TestName(), + Certificate: certificate, + PrivateKey: privateKey, + PrivateKeyPassphrase: "test", + } + + openApiEndpoint, err := getEndpointByVersion(&vcd.client.Client) + check.Assert(err, IsNil) + createdCertificate, err := adminOrg.AddCertificateToLibrary(certificateConfigWithPrivateKey) + check.Assert(err, IsNil) + PrependToCleanupListOpenApi(createdCertificate.CertificateLibrary.Alias, check.TestName(), openApiEndpoint+createdCertificate.CertificateLibrary.Id) + + virtualServiceConfig := &types.NsxtAlbVirtualService{ + Name: check.TestName(), + Enabled: addrOf(true), + ApplicationProfile: types.NsxtAlbVirtualServiceApplicationProfile{ + SystemDefined: true, + Type: "HTTPS", + }, + GatewayRef: types.OpenApiReference{ID: edge.EdgeGateway.ID}, + LoadBalancerPoolRef: types.OpenApiReference{ID: pool.NsxtAlbPool.ID}, + ServiceEngineGroupRef: types.OpenApiReference{ID: seGroup.NsxtAlbServiceEngineGroup.ID}, + CertificateRef: &types.OpenApiReference{ID: createdCertificate.CertificateLibrary.Id}, + ServicePorts: []types.NsxtAlbVirtualServicePort{{PortStart: addrOf(80), SslEnabled: addrOf(true)}}, + VirtualIpAddress: edge.EdgeGateway.EdgeGatewayUplinks[0].Subnets.Values[0].PrimaryIP, + } + + virtualServiceConfigUpdated := &types.NsxtAlbVirtualService{ + Name: check.TestName(), + Description: "Updated", + Enabled: addrOf(true), + ApplicationProfile: types.NsxtAlbVirtualServiceApplicationProfile{ + SystemDefined: true, + Type: "HTTPS", + }, + GatewayRef: types.OpenApiReference{ID: edge.EdgeGateway.ID}, + LoadBalancerPoolRef: types.OpenApiReference{ID: pool.NsxtAlbPool.ID}, + ServiceEngineGroupRef: types.OpenApiReference{ID: seGroup.NsxtAlbServiceEngineGroup.ID}, + CertificateRef: &types.OpenApiReference{ID: createdCertificate.CertificateLibrary.Id}, + ServicePorts: []types.NsxtAlbVirtualServicePort{ + { + PortStart: addrOf(80), + }, + { + PortStart: addrOf(443), + SslEnabled: addrOf(true), + }, + }, + // Use Primary IP of Edge Gateway as virtual service IP + VirtualIpAddress: edge.EdgeGateway.EdgeGatewayUplinks[0].Subnets.Values[0].PrimaryIP, + } + + testAlbVirtualServiceConfig(check, vcd, "WithCertHTTPS", virtualServiceConfig, virtualServiceConfigUpdated, client) + + err = createdCertificate.Delete() + check.Assert(err, IsNil) +} + +func testAlbVirtualServiceConfig(check *C, vcd *TestVCD, name string, setupConfig *types.NsxtAlbVirtualService, updateConfig *types.NsxtAlbVirtualService, client *VCDClient) { + fmt.Printf("# Running ALB Virtual Service test with config %s ('System' user: %t) ", name, client.Client.IsSysAdmin) + + edge, err := vcd.nsxtVdc.GetNsxtEdgeGatewayByName(vcd.config.VCD.Nsxt.EdgeGateway) + check.Assert(err, IsNil) + + createdVirtualService, err := client.CreateNsxtAlbVirtualService(setupConfig) + check.Assert(err, IsNil) + + // Verify mandatory fields + check.Assert(createdVirtualService.NsxtAlbVirtualService.ID, NotNil) + check.Assert(createdVirtualService.NsxtAlbVirtualService.Name, NotNil) + check.Assert(createdVirtualService.NsxtAlbVirtualService.GatewayRef.ID, NotNil) + + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbVirtualServices + createdVirtualService.NsxtAlbVirtualService.ID + PrependToCleanupListOpenApi(createdVirtualService.NsxtAlbVirtualService.Name, check.TestName(), openApiEndpoint) + + // Get By ID + virtualServiceById, err := client.GetAlbVirtualServiceById(createdVirtualService.NsxtAlbVirtualService.ID) + check.Assert(err, IsNil) + check.Assert(virtualServiceById.NsxtAlbVirtualService.ID, Equals, createdVirtualService.NsxtAlbVirtualService.ID) + + // Get By Name + virtualServiceByName, err := client.GetAlbVirtualServiceByName(edge.EdgeGateway.ID, createdVirtualService.NsxtAlbVirtualService.Name) + check.Assert(err, IsNil) + check.Assert(virtualServiceByName.NsxtAlbVirtualService.ID, Equals, createdVirtualService.NsxtAlbVirtualService.ID) + + //Get All Virtual Service summaries + allVirtualServiceSummaries, err := client.GetAllAlbVirtualServiceSummaries(edge.EdgeGateway.ID, nil) + check.Assert(err, IsNil) + check.Assert(len(allVirtualServiceSummaries) > 0, Equals, true) + + // Get All Pools + allVirtualServices, err := client.GetAllAlbVirtualServices(edge.EdgeGateway.ID, nil) + check.Assert(err, IsNil) + check.Assert(len(allVirtualServices) > 0, Equals, true) + + check.Assert(len(allVirtualServiceSummaries), Equals, len(allVirtualServices)) + + // Attempt an update if config is provided + if updateConfig != nil { + updateConfig.ID = createdVirtualService.NsxtAlbVirtualService.ID + updatedPool, err := createdVirtualService.Update(updateConfig) + check.Assert(err, IsNil) + check.Assert(createdVirtualService.NsxtAlbVirtualService.ID, Equals, updatedPool.NsxtAlbVirtualService.ID) + check.Assert(updatedPool.NsxtAlbVirtualService.Name, NotNil) + check.Assert(updatedPool.NsxtAlbVirtualService.GatewayRef.ID, NotNil) + } + + err = createdVirtualService.Delete() + check.Assert(err, IsNil) + fmt.Printf("Done.\n") +} + +func setupAlbVirtualServicePrerequisites(check *C, vcd *TestVCD) (*NsxtAlbController, *NsxtAlbCloud, *NsxtAlbServiceEngineGroup, *NsxtEdgeGateway, *NsxtAlbServiceEngineGroupAssignment, *NsxtAlbPool) { + controller, cloud, seGroup, edge, assignedSeGroup := setupAlbPoolPrerequisites(check, vcd) + + poolConfig := &types.NsxtAlbPool{ + Name: check.TestName(), + Enabled: addrOf(true), + GatewayRef: types.OpenApiReference{ID: edge.EdgeGateway.ID}, + } + + albPool, err := vcd.client.CreateNsxtAlbPool(poolConfig) + check.Assert(err, IsNil) + + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbPools + albPool.NsxtAlbPool.ID + PrependToCleanupListOpenApi(albPool.NsxtAlbPool.Name, check.TestName(), openApiEndpoint) + + return controller, cloud, seGroup, edge, assignedSeGroup, albPool +} + +func setupAlbPoolFirewallGroupMembers(check *C, vcd *TestVCD, edge *NsxtEdgeGateway) (*NsxtFirewallGroup, *NsxtAlbPool) { + // creates ip set + ipSetConfig := &types.NsxtFirewallGroup{ + Name: check.TestName(), + OwnerRef: &types.OpenApiReference{ID: edge.EdgeGateway.ID}, + Description: "Test IP Set", + Type: "IP_SET", + IpAddresses: []string{"1.1.1.1"}, + } + + ipSet, err := vcd.nsxtVdc.CreateNsxtFirewallGroup(ipSetConfig) + check.Assert(err, IsNil) + + // add ip set to cleanup list + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointFirewallGroups + ipSet.NsxtFirewallGroup.ID + PrependToCleanupListOpenApi(ipSet.NsxtFirewallGroup.Name, check.TestName(), openApiEndpoint) + + poolConfig := &types.NsxtAlbPool{ + Name: check.TestName() + "-member-group", + Enabled: addrOf(true), + GatewayRef: types.OpenApiReference{ID: edge.EdgeGateway.ID}, + MemberGroupRef: &types.OpenApiReference{ + ID: ipSet.NsxtFirewallGroup.ID, + }, + } + + albPool, err := vcd.client.CreateNsxtAlbPool(poolConfig) + check.Assert(err, IsNil) + + openApiEndpoint = types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbPools + albPool.NsxtAlbPool.ID + PrependToCleanupListOpenApi(albPool.NsxtAlbPool.Name, check.TestName(), openApiEndpoint) + + return ipSet, albPool +} + +func tearDownAlbVirtualServicePrerequisites(check *C, albPool *NsxtAlbPool, assignment *NsxtAlbServiceEngineGroupAssignment, edge *NsxtEdgeGateway, seGroup *NsxtAlbServiceEngineGroup, cloud *NsxtAlbCloud, controller *NsxtAlbController) { + err := albPool.Delete() + check.Assert(err, IsNil) + err = assignment.Delete() + check.Assert(err, IsNil) + err = edge.DisableAlb() + check.Assert(err, IsNil) + err = seGroup.Delete() + check.Assert(err, IsNil) + err = cloud.Delete() + check.Assert(err, IsNil) + err = controller.Delete() + check.Assert(err, IsNil) +} diff --git a/govcd/nsxt_application_profile.go b/govcd/nsxt_application_profile.go new file mode 100644 index 000000000..9d8b5c7e0 --- /dev/null +++ b/govcd/nsxt_application_profile.go @@ -0,0 +1,248 @@ +/* + * Copyright 2021 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" +) + +// NsxtAppPortProfile uses OpenAPI endpoint to operate NSX-T Application Port Profiles +// It can have 3 types of scopes: +// * SYSTEM - Read-only (The ones that are provided by SYSTEM). Constant `types.ApplicationPortProfileScopeSystem` +// * PROVIDER - Created by Provider on a particular network provider (NSX-T manager). Constant `types.ApplicationPortProfileScopeProvider` +// * TENANT (Created by Tenant at Org VDC level). Constant `types.ApplicationPortProfileScopeTenant` +// +// More details about scope in documentation for types.NsxtAppPortProfile +type NsxtAppPortProfile struct { + NsxtAppPortProfile *types.NsxtAppPortProfile + client *Client +} + +// CreateNsxtAppPortProfile allows users to create NSX-T Application Port Profile definition. +// It can have 3 types of scopes: +// * SYSTEM (The ones that are provided by SYSTEM) Read-only +// * PROVIDER (Created by Provider globally) +// * TENANT (Create by tenant at Org level) +// More details about scope in documentation for types.NsxtAppPortProfile +func (org *Org) CreateNsxtAppPortProfile(appPortProfileConfig *types.NsxtAppPortProfile) (*NsxtAppPortProfile, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAppPortProfiles + minimumApiVersion, err := org.client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := org.client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + returnObject := &NsxtAppPortProfile{ + NsxtAppPortProfile: &types.NsxtAppPortProfile{}, + client: org.client, + } + + err = org.client.OpenApiPostItem(minimumApiVersion, urlRef, nil, appPortProfileConfig, returnObject.NsxtAppPortProfile, nil) + if err != nil { + return nil, fmt.Errorf("error creating NSX-T Application Port Profile: %s", err) + } + + return returnObject, nil +} + +// GetAllNsxtAppPortProfiles returns all NSX-T Application Port Profiles for specific scope +// More details about scope in documentation for types.NsxtAppPortProfile +func (org *Org) GetAllNsxtAppPortProfiles(queryParameters url.Values, scope string) ([]*NsxtAppPortProfile, error) { + queryParams := copyOrNewUrlValues(queryParameters) + if scope != "" { + queryParams = queryParameterFilterAnd("scope=="+scope, queryParams) + } + + return getAllNsxtAppPortProfiles(org.client, queryParams) +} + +// GetNsxtAppPortProfileByName allows users to retrieve Application Port Profiles for specific scope. +// More details in documentation for types.NsxtAppPortProfile +// +// Note. Names are enforced to be unique per scope +func (org *Org) GetNsxtAppPortProfileByName(name, scope string) (*NsxtAppPortProfile, error) { + queryParameters := url.Values{} + if scope != "" { + queryParameters = queryParameterFilterAnd("scope=="+scope, queryParameters) + } + + return getNsxtAppPortProfileByName(org.client, name, queryParameters) +} + +// GetNsxtAppPortProfileByName allows users to retrieve Application Port Profiles for specific scope. +// More details in documentation for types.NsxtAppPortProfile +// +// Note. Names are enforced to be unique per scope +func (vdc *Vdc) GetNsxtAppPortProfileByName(name, scope string) (*NsxtAppPortProfile, error) { + queryParameters := copyOrNewUrlValues(nil) + queryParameters = queryParameterFilterAnd("_context=="+vdc.Vdc.ID, queryParameters) + if scope != "" { + queryParameters = queryParameterFilterAnd("scope=="+scope, queryParameters) + } + + return getNsxtAppPortProfileByName(vdc.client, name, queryParameters) +} + +// GetNsxtAppPortProfileByName allows users to retrieve Application Port Profiles for specific scope. +// More details in documentation for types.NsxtAppPortProfile +// +// Note. Names are enforced to be unique per scope +func (vdcGroup *VdcGroup) GetNsxtAppPortProfileByName(name, scope string) (*NsxtAppPortProfile, error) { + queryParameters := copyOrNewUrlValues(nil) + queryParameters = queryParameterFilterAnd("_context=="+vdcGroup.VdcGroup.Id, queryParameters) + + if scope != "" { + queryParameters = queryParameterFilterAnd("scope=="+scope, queryParameters) + } + + return getNsxtAppPortProfileByName(vdcGroup.client, name, queryParameters) +} + +// GetNsxtAppPortProfileById retrieves NSX-T Application Port Profile by ID +func (org *Org) GetNsxtAppPortProfileById(id string) (*NsxtAppPortProfile, error) { + return getNsxtAppPortProfileById(org.client, id) +} + +// Update allows users to update NSX-T Application Port Profile +func (appPortProfile *NsxtAppPortProfile) Update(appPortProfileConfig *types.NsxtAppPortProfile) (*NsxtAppPortProfile, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAppPortProfiles + minimumApiVersion, err := appPortProfile.client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + if appPortProfileConfig.ID == "" { + return nil, fmt.Errorf("cannot update NSX-T Application Port Profile without ID") + } + + urlRef, err := appPortProfile.client.OpenApiBuildEndpoint(endpoint, appPortProfileConfig.ID) + if err != nil { + return nil, err + } + + returnObject := &NsxtAppPortProfile{ + NsxtAppPortProfile: &types.NsxtAppPortProfile{}, + client: appPortProfile.client, + } + + err = appPortProfile.client.OpenApiPutItem(minimumApiVersion, urlRef, nil, appPortProfileConfig, returnObject.NsxtAppPortProfile, nil) + if err != nil { + return nil, fmt.Errorf("error updating NSX-T Application Port Profile : %s", err) + } + + return returnObject, nil +} + +// Delete allows users to delete NSX-T Application Port Profile +func (appPortProfile *NsxtAppPortProfile) Delete() error { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAppPortProfiles + minimumApiVersion, err := appPortProfile.client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return err + } + + if appPortProfile.NsxtAppPortProfile.ID == "" { + return fmt.Errorf("cannot delete NSX-T Application Port Profile without ID") + } + + urlRef, err := appPortProfile.client.OpenApiBuildEndpoint(endpoint, appPortProfile.NsxtAppPortProfile.ID) + if err != nil { + return err + } + + err = appPortProfile.client.OpenApiDeleteItem(minimumApiVersion, urlRef, nil, nil) + + if err != nil { + return fmt.Errorf("error deleting NSX-T Application Port Profile: %s", err) + } + + return nil +} + +func getNsxtAppPortProfileByName(client *Client, name string, queryParameters url.Values) (*NsxtAppPortProfile, error) { + queryParams := copyOrNewUrlValues(queryParameters) + queryParams = queryParameterFilterAnd("name=="+name, queryParams) + + allAppPortProfiles, err := getAllNsxtAppPortProfiles(client, queryParams) + if err != nil { + return nil, fmt.Errorf("could not find NSX-T Application Port Profile with name '%s': %s", name, err) + } + + if len(allAppPortProfiles) == 0 { + return nil, fmt.Errorf("%s: expected exactly one NSX-T Application Port Profile with name '%s'. Got %d", ErrorEntityNotFound, name, len(allAppPortProfiles)) + } + + if len(allAppPortProfiles) > 1 { + return nil, fmt.Errorf("expected exactly one NSX-T Application Port Profile with name '%s'. Got %d", name, len(allAppPortProfiles)) + } + + return getNsxtAppPortProfileById(client, allAppPortProfiles[0].NsxtAppPortProfile.ID) +} + +func getNsxtAppPortProfileById(client *Client, id string) (*NsxtAppPortProfile, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAppPortProfiles + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + if id == "" { + return nil, fmt.Errorf("empty NSX-T Application Port Profile ID specified") + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, id) + if err != nil { + return nil, err + } + + appPortProfile := &NsxtAppPortProfile{ + NsxtAppPortProfile: &types.NsxtAppPortProfile{}, + client: client, + } + + err = client.OpenApiGetItem(minimumApiVersion, urlRef, nil, appPortProfile.NsxtAppPortProfile, nil) + if err != nil { + return nil, err + } + + return appPortProfile, nil +} + +func getAllNsxtAppPortProfiles(client *Client, queryParameters url.Values) ([]*NsxtAppPortProfile, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAppPortProfiles + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + typeResponses := []*types.NsxtAppPortProfile{{}} + err = client.OpenApiGetAllItems(minimumApiVersion, urlRef, queryParameters, &typeResponses, nil) + if err != nil { + return nil, err + } + + // Wrap all typeResponses into NsxtAppPortProfile types with client + wrappedResponses := make([]*NsxtAppPortProfile, len(typeResponses)) + for sliceIndex := range typeResponses { + wrappedResponses[sliceIndex] = &NsxtAppPortProfile{ + NsxtAppPortProfile: typeResponses[sliceIndex], + client: client, + } + } + + return wrappedResponses, nil +} diff --git a/govcd/nsxt_application_profile_test.go b/govcd/nsxt_application_profile_test.go new file mode 100644 index 000000000..4e0c61886 --- /dev/null +++ b/govcd/nsxt_application_profile_test.go @@ -0,0 +1,202 @@ +//go:build network || nsxt || functional || openapi || ALL + +package govcd + +import ( + "fmt" + "net/url" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_NsxtApplicationPortProfileProvider(check *C) { + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointAppPortProfiles) + vcd.skipIfNotSysAdmin(check) + + appPortProfileConfig := getAppProfileProvider(vcd, check) + testAppPortProfile(appPortProfileConfig, types.ApplicationPortProfileScopeProvider, vcd, check) +} + +func (vcd *TestVCD) Test_NsxtApplicationPortProfileTenant(check *C) { + vcd.skipIfNotSysAdmin(check) + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointAppPortProfiles) + + appPortProfileConfig := getAppProfileTenant(vcd, check) + testAppPortProfile(appPortProfileConfig, types.ApplicationPortProfileScopeTenant, vcd, check) +} + +func (vcd *TestVCD) Test_NsxtApplicationPortProfileReadSystem(check *C) { + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointAppPortProfiles) + + testApplicationProfilesForScope(types.ApplicationPortProfileScopeSystem, check, vcd) +} + +func (vcd *TestVCD) Test_NsxtApplicationPortProfileReadProvider(check *C) { + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointAppPortProfiles) + + testApplicationProfilesForScope(types.ApplicationPortProfileScopeProvider, check, vcd) +} + +func (vcd *TestVCD) Test_NsxtApplicationPortProfileReadTenant(check *C) { + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointAppPortProfiles) + + testApplicationProfilesForScope(types.ApplicationPortProfileScopeTenant, check, vcd) +} + +func getAppProfileProvider(vcd *TestVCD, check *C) *types.NsxtAppPortProfile { + nsxtManager, err := vcd.client.QueryNsxtManagerByName(vcd.config.VCD.Nsxt.Manager) + check.Assert(err, IsNil) + + nsxtManagerUuid, err := GetUuidFromHref(nsxtManager[0].HREF, true) + check.Assert(err, IsNil) + + nsxtManagerUrn, err := BuildUrnWithUuid("urn:vcloud:nsxtmanager:", nsxtManagerUuid) + check.Assert(err, IsNil) + + // For PROVIDER scope application port profile must have ContextEntityId set as NSX-T Managers URN and no Org + appPortProfileConfig := &types.NsxtAppPortProfile{ + Name: check.TestName() + "PROVIDER", + Description: "Provider config", + ApplicationPorts: []types.NsxtAppPortProfilePort{ + types.NsxtAppPortProfilePort{ + Protocol: "TCP", + DestinationPorts: []string{"11000-12000"}, + }, + }, + ContextEntityId: nsxtManagerUrn, + Scope: types.ApplicationPortProfileScopeProvider, + } + return appPortProfileConfig +} + +func getAppProfileTenant(vcd *TestVCD, check *C) *types.NsxtAppPortProfile { + org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + + // For PROVIDER scope application port profile must have ContextEntityId set as NSX-T Managers URN and no Org + appPortProfileConfig := &types.NsxtAppPortProfile{ + Name: check.TestName() + "TENANT", + Description: "Provider config", + ApplicationPorts: []types.NsxtAppPortProfilePort{ + types.NsxtAppPortProfilePort{ + Protocol: "ICMPv4", + DestinationPorts: []string{"any"}, + }, + }, + OrgRef: &types.OpenApiReference{ID: org.Org.ID, Name: org.Org.Name}, + + ContextEntityId: vcd.nsxtVdc.Vdc.ID, // VDC ID + Scope: types.ApplicationPortProfileScopeTenant, + } + return appPortProfileConfig +} + +func testAppPortProfile(appPortProfileConfig *types.NsxtAppPortProfile, scope string, vcd *TestVCD, check *C) { + org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + appProfile, err := org.CreateNsxtAppPortProfile(appPortProfileConfig) + check.Assert(err, IsNil) + + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAppPortProfiles + appProfile.NsxtAppPortProfile.ID + AddToCleanupListOpenApi(appProfile.NsxtAppPortProfile.Name, check.TestName(), openApiEndpoint) + + appPortProfileConfig.ID = appProfile.NsxtAppPortProfile.ID // Inject ID into original creation + appPortProfileConfig.ContextEntityId = "" // Remove NSX-T Manager URN because read does not return it + check.Assert(appProfile.NsxtAppPortProfile, DeepEquals, appPortProfileConfig) + + // Check update + appProfile.NsxtAppPortProfile.Description = appProfile.NsxtAppPortProfile.Description + "-Update" + updatedAppProfile, err := appProfile.Update(appProfile.NsxtAppPortProfile) + check.Assert(err, IsNil) + check.Assert(updatedAppProfile.NsxtAppPortProfile, DeepEquals, appProfile.NsxtAppPortProfile) + + // Check lookup + foundAppProfileById, err := org.GetNsxtAppPortProfileById(appProfile.NsxtAppPortProfile.ID) + check.Assert(err, IsNil) + check.Assert(foundAppProfileById.NsxtAppPortProfile, DeepEquals, appProfile.NsxtAppPortProfile) + + foundAppProfileByName, err := org.GetNsxtAppPortProfileByName(appProfile.NsxtAppPortProfile.Name, scope) + check.Assert(err, IsNil) + check.Assert(foundAppProfileByName.NsxtAppPortProfile, DeepEquals, foundAppProfileById.NsxtAppPortProfile) + + // Check VDC and VDC Group lookup + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + vdc, vdcGroup := test_CreateVdcGroup(check, adminOrg, vcd) + + // Lookup by VDC + foundAppProfileByNameInVdc, err := vdc.GetNsxtAppPortProfileByName(appProfile.NsxtAppPortProfile.Name, scope) + check.Assert(err, IsNil) + check.Assert(foundAppProfileByNameInVdc.NsxtAppPortProfile, DeepEquals, foundAppProfileById.NsxtAppPortProfile) + + foundAppProfileByNameInVdcGroup, err := vdcGroup.GetNsxtAppPortProfileByName(appProfile.NsxtAppPortProfile.Name, scope) + check.Assert(err, IsNil) + check.Assert(foundAppProfileByNameInVdcGroup.NsxtAppPortProfile, DeepEquals, foundAppProfileById.NsxtAppPortProfile) + // Remove VDC group + err = vdcGroup.Delete() + check.Assert(err, IsNil) + err = vdc.DeleteWait(true, true) + check.Assert(err, IsNil) + + err = appProfile.Delete() + check.Assert(err, IsNil) + + // Expect a not found error + _, err = org.GetNsxtAppPortProfileById(appProfile.NsxtAppPortProfile.ID) + check.Assert(ContainsNotFound(err), Equals, true) + + _, err = org.GetNsxtAppPortProfileByName(appProfile.NsxtAppPortProfile.Name, scope) + check.Assert(ContainsNotFound(err), Equals, true) +} + +func testApplicationProfilesForScope(scope string, check *C, vcd *TestVCD) { + org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + + resultCount := getResultCountByScope(scope, check, vcd) + if testVerbose { + fmt.Printf("# API shows results for scope '%s': %d\n", scope, resultCount) + } + + appProfileSlice, err := org.GetAllNsxtAppPortProfiles(nil, scope) + check.Assert(err, IsNil) + + if testVerbose { + fmt.Printf("# Paginated item number for scope '%s': %d\n", scope, len(appProfileSlice)) + } + + // Ensure the amount of results is exactly the same as returned by getResultCountByScope which makes sure that + // pagination is not broken. + check.Assert(len(appProfileSlice), Equals, resultCount) +} + +func getResultCountByScope(scope string, check *C, vcd *TestVCD) int { + // Get element count by using a simple query and parse response directly to compare it against paginated list of items + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAppPortProfiles + skipOpenApiEndpointTest(vcd, check, endpoint) + apiVersion, err := vcd.client.Client.checkOpenApiEndpointCompatibility(endpoint) + check.Assert(err, IsNil) + + urlRef, err := vcd.client.Client.OpenApiBuildEndpoint(endpoint) + check.Assert(err, IsNil) + + // Limit search of audits trails to the last 12 hours so that it doesn't take too long and set pageSize to be 1 result + // to force following pages + queryParams := url.Values{} + queryParams.Add("filter", "scope=="+scope) + + result := struct { + Resulttotal int `json:"resultTotal"` + }{} + + err = vcd.vdc.client.OpenApiGetItem(apiVersion, urlRef, queryParams, &result, nil) + check.Assert(err, IsNil) + return result.Resulttotal +} diff --git a/govcd/nsxt_distributed_firewall.go b/govcd/nsxt_distributed_firewall.go new file mode 100644 index 000000000..188f4dd16 --- /dev/null +++ b/govcd/nsxt_distributed_firewall.go @@ -0,0 +1,360 @@ +package govcd + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + "github.com/vmware/go-vcloud-director/v2/util" +) + +const ( + labelDistributedFirewall = "NSX-T Distributed Firewall" + labelDistributedFirewallRule = "NSX-T Distributed Firewall Rule" +) + +// DistributedFirewall contains a types.DistributedFirewallRules which handles Distributed Firewall +// rules in a VDC Group +type DistributedFirewall struct { + DistributedFirewallRuleContainer *types.DistributedFirewallRules + client *Client + VdcGroup *VdcGroup +} + +// 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 (d DistributedFirewall) wrap(inner *types.DistributedFirewallRules) *DistributedFirewall { + d.DistributedFirewallRuleContainer = inner + return &d +} + +// DistributedFirewallRule is a representation of a single rule +type DistributedFirewallRule struct { + Rule *types.DistributedFirewallRule + client *Client + VdcGroup *VdcGroup +} + +// 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 (d DistributedFirewallRule) wrap(inner *types.DistributedFirewallRule) *DistributedFirewallRule { + d.Rule = inner + return &d +} + +// GetDistributedFirewall retrieves Distributed Firewall in a VDC Group which contains all rules +// +// Note. This function works only with `default` policy as this was the only supported when this +// functions was created +func (vdcGroup *VdcGroup) GetDistributedFirewall() (*DistributedFirewall, error) { + c := crudConfig{ + entityLabel: labelDistributedFirewall, + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroupsDfwRules, + endpointParams: []string{vdcGroup.VdcGroup.Id, types.DistributedFirewallPolicyDefault}, + } + + outerType := DistributedFirewall{client: vdcGroup.client, VdcGroup: vdcGroup} + return getOuterEntity[DistributedFirewall, types.DistributedFirewallRules](vdcGroup.client, outerType, c) +} + +// UpdateDistributedFirewall updates Distributed Firewall in a VDC Group +// +// Note. This function works only with `default` policy as this was the only supported when this +// functions was created +func (vdcGroup *VdcGroup) UpdateDistributedFirewall(dfwRules *types.DistributedFirewallRules) (*DistributedFirewall, error) { + c := crudConfig{ + entityLabel: labelDistributedFirewall, + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroupsDfwRules, + endpointParams: []string{vdcGroup.VdcGroup.Id, types.DistributedFirewallPolicyDefault}, + } + + outerType := DistributedFirewall{client: vdcGroup.client, VdcGroup: vdcGroup} + return updateOuterEntity[DistributedFirewall, types.DistributedFirewallRules](vdcGroup.client, outerType, c, dfwRules) +} + +// DeleteAllDistributedFirewallRules removes all Distributed Firewall rules +// +// Note. This function works only with `default` policy as this was the only supported when this +// functions was created +func (vdcGroup *VdcGroup) DeleteAllDistributedFirewallRules() error { + _, err := vdcGroup.UpdateDistributedFirewall(&types.DistributedFirewallRules{}) + return err +} + +// DeleteAllRules removes all Distributed Firewall rules +// +// Note. This function works only with `default` policy as this was the only supported when this +// functions was created +func (firewall *DistributedFirewall) DeleteAllRules() error { + if firewall.VdcGroup != nil && firewall.VdcGroup.VdcGroup != nil && firewall.VdcGroup.VdcGroup.Id == "" { + return errors.New("empty VDC Group ID for parent VDC Group") + } + + return firewall.VdcGroup.DeleteAllDistributedFirewallRules() +} + +// GetDistributedFirewallRuleById retrieves single Distributed Firewall Rule by ID +func (vdcGroup *VdcGroup) GetDistributedFirewallRuleById(id string) (*DistributedFirewallRule, error) { + c := crudConfig{ + entityLabel: labelDistributedFirewallRule, + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroupsDfwRules, + endpointParams: []string{vdcGroup.VdcGroup.Id, types.DistributedFirewallPolicyDefault, "/", id}, + } + + outerType := DistributedFirewallRule{client: vdcGroup.client, VdcGroup: vdcGroup} + return getOuterEntity[DistributedFirewallRule, types.DistributedFirewallRule](vdcGroup.client, outerType, c) +} + +// GetDistributedFirewallRuleByName retrieves single firewall rule by name +func (vdcGroup *VdcGroup) GetDistributedFirewallRuleByName(name string) (*DistributedFirewallRule, error) { + if name == "" { + return nil, fmt.Errorf("name must be specified") + } + + dfw, err := vdcGroup.GetDistributedFirewall() + if err != nil { + return nil, fmt.Errorf("error returning distributed firewall rules: %s", err) + } + + singleRuleByName, err := localFilterOneOrError(labelDistributedFirewallRule, dfw.DistributedFirewallRuleContainer.Values, "Name", name) + if err != nil { + return nil, err + } + + return vdcGroup.GetDistributedFirewallRuleById(singleRuleByName.ID) +} + +// CreateDistributedFirewallRule is a non-thread safe wrapper around +// "vdcGroups/%s/dfwPolicies/%s/rules" endpoint which handles all distributed firewall (DFW) rules +// at once. While there is no real endpoint to create single firewall rule, it is a requirements for +// some cases (e.g. using in Terraform) +// The code works by doing the following steps: +// +// 1. Getting all Distributed Firewall Rules and storing them in private intermediate +// type`distributedFirewallRulesRaw` which holds a []json.RawMessage (text) instead of exact types. +// This will prevent altering existing rules in any way (for example if a new field appears in +// schema in future VCD versions) +// +// 2. Converting the given `rule` into json.RawMessage so that it is provided in the same format as +// other already retrieved rules +// +// 3. Creating a new structure of []json.RawMessage which puts the new rule into one of places: +// 3.1. to the end of []json.RawMessage - bottom of the list +// 3.2. if `optionalAboveRuleId` argument is specified - identifying the position and placing new +// rule above it +// 4. Perform a PUT (update) call to the "vdcGroups/%s/dfwPolicies/%s/rules" endpoint using the +// newly constructed payload +// +// Note. Running this function concurrently will corrupt firewall rules as it uses an endpoint that +// manages all rules ("vdcGroups/%s/dfwPolicies/%s/rules") +func (vdcGroup *VdcGroup) CreateDistributedFirewallRule(optionalAboveRuleId string, rule *types.DistributedFirewallRule) (*DistributedFirewall, *DistributedFirewallRule, error) { + // 1. Getting all Distributed Firewall Rules and storing them in private intermediate + // type`distributedFirewallRulesRaw` which holds a []json.RawMessage (text) instead of exact types. + // This will prevent altering existing rules in any way (for example if a new field appears in + // schema in future VCD versions) + + c := crudConfig{ + entityLabel: labelDistributedFirewallRule, + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroupsDfwRules, + endpointParams: []string{vdcGroup.VdcGroup.Id, types.DistributedFirewallPolicyDefault}, + } + rawJsonExistingFirewallRules, err := getInnerEntity[distributedFirewallRulesRaw](vdcGroup.client, c) + if err != nil { + return nil, nil, err + } + + // 2. Converting the given `rule` (*types.DistributedFirewallRule) into json.RawMessage so that + // it is provided in the same format as other already retrieved rules + newRuleRawJson, err := firewallRuleToRawJson(rule) + if err != nil { + return nil, nil, err + } + + // dfwRuleUpdatePayload will contain complete request for Distributed Firewall Rule Update + // operation. Its content will be decided based on whether 'optionalAboveRuleId' parameter was + // specified or not. + var dfwRuleUpdatePayload []json.RawMessage + // newRuleSlicePosition will contain slice index to where new firewall rule will be put + var newRuleSlicePosition int + + // 3. Creating a new structure of []json.RawMessage which puts the new rule into one of places: + switch { + // 3.1. to the end of []json.RawMessage - bottom of the list (optionalAboveRuleId is empty) + case optionalAboveRuleId == "": + rawJsonExistingFirewallRules.Values = append(rawJsonExistingFirewallRules.Values, newRuleRawJson) + dfwRuleUpdatePayload = rawJsonExistingFirewallRules.Values + newRuleSlicePosition = len(dfwRuleUpdatePayload) - 1 // -1 to match for slice index + + // 3.2. if `optionalAboveRuleId` argument is specified - identifying the position and placing new + // rule above it + case optionalAboveRuleId != "": + // 3.2.1 Convert '[]json.Rawmessage' to 'types.DistributedFirewallRules' + dfwRules, err := convertRawJsonToFirewallRules(rawJsonExistingFirewallRules) + if err != nil { + return nil, nil, err + } + // 3.2.2 Find index for specified 'optionalAboveRuleId' rule + newFwRuleSliceIndex, err := getFirewallRuleIndexById(dfwRules, optionalAboveRuleId) + newRuleSlicePosition = newFwRuleSliceIndex // Set rule position for returning single firewall rule + if err != nil { + return nil, nil, err + } + + // 3.2.3 Compose new update (PUT) payload with all firewall rules and inject + // 'newRuleRawJson' into position 'newFwRuleSliceIndex' and shift other rules to the bottom + dfwRuleUpdatePayload, err = composeUpdatePayloadWithNewRulePosition(newFwRuleSliceIndex, rawJsonExistingFirewallRules, newRuleRawJson) + if err != nil { + return nil, nil, fmt.Errorf("error creating update payload with optionalAboveRuleId '%s' :%s", optionalAboveRuleId, err) + } + } + // 4. Perform a PUT (update) call to the "vdcGroups/%s/dfwPolicies/%s/rules" endpoint using the + // newly constructed payload + updateRequestPayload := &distributedFirewallRulesRaw{ + Values: dfwRuleUpdatePayload, + } + + c2 := crudConfig{ + entityLabel: labelDistributedFirewallRule, + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroupsDfwRules, + endpointParams: []string{vdcGroup.VdcGroup.Id, types.DistributedFirewallPolicyDefault}, + } + + updatedFirewallRules, err := updateInnerEntity(vdcGroup.client, c2, updateRequestPayload) + if err != nil { + return nil, nil, err + } + + dfwResults, err := convertRawJsonToFirewallRules(updatedFirewallRules) + if err != nil { + return nil, nil, err + } + + returnObjectSingleRule := &DistributedFirewallRule{ + client: vdcGroup.client, + VdcGroup: vdcGroup, + Rule: dfwResults.Values[newRuleSlicePosition], + } + + returnAllFirewallRules := &DistributedFirewall{ + DistributedFirewallRuleContainer: dfwResults, + client: vdcGroup.client, + VdcGroup: vdcGroup, + } + + return returnAllFirewallRules, returnObjectSingleRule, nil +} + +// Update a single Distributed Firewall Rule +func (dfwRule *DistributedFirewallRule) Update(rule *types.DistributedFirewallRule) (*DistributedFirewallRule, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroupsDfwRules, + endpointParams: []string{dfwRule.VdcGroup.VdcGroup.Id, types.DistributedFirewallPolicyDefault, "/", dfwRule.Rule.ID}, + entityLabel: labelDistributedFirewallRule, + } + outerType := DistributedFirewallRule{client: dfwRule.client, VdcGroup: dfwRule.VdcGroup} + return updateOuterEntity(dfwRule.client, outerType, c, rule) +} + +// Delete a single Distributed Firewall Rule +func (dfwRule *DistributedFirewallRule) Delete() error { + c := crudConfig{ + entityLabel: labelDistributedFirewallRule, + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroupsDfwRules, + endpointParams: []string{dfwRule.VdcGroup.VdcGroup.Id, types.DistributedFirewallPolicyDefault, "/", dfwRule.Rule.ID}, + } + return deleteEntityById(dfwRule.client, c) +} + +// getFirewallRuleIndexById searches for 'firewallRuleId' going through a list of available firewall +// rules and returns its index or error if the firewall rule is not found +func getFirewallRuleIndexById(dfwRules *types.DistributedFirewallRules, firewallRuleId string) (int, error) { + util.Logger.Printf("[DEBUG] CreateDistributedFirewallRule 'optionalAboveRuleId=%s'. Searching within '%d' items", + firewallRuleId, len(dfwRules.Values)) + var fwRuleSliceIndex *int + for index := range dfwRules.Values { + if dfwRules.Values[index].ID == firewallRuleId { + // using function `addrOf` to get copy of `index` value as taking a direct address + // of `&index` will shift before it is used in later code due to how Go range works + fwRuleSliceIndex = addrOf(index) + util.Logger.Printf("[DEBUG] CreateDistributedFirewallRule found existing Firewall Rule with ID '%s' at position '%d'", + firewallRuleId, index) + continue + } + } + + if fwRuleSliceIndex == nil { + return 0, fmt.Errorf("specified above rule ID '%s' does not exist in current Distributed Firewall Rule list", firewallRuleId) + } + + return *fwRuleSliceIndex, nil +} + +// firewallRuleToRawJson Marshal a single `types.DistributedFirewallRule` into `json.RawMessage` +// representation +func firewallRuleToRawJson(rule *types.DistributedFirewallRule) (json.RawMessage, error) { + ruleByteSlice, err := json.Marshal(rule) + if err != nil { + return nil, fmt.Errorf("error marshalling 'rule': %s", err) + } + ruleJsonMessage := json.RawMessage(string(ruleByteSlice)) + return ruleJsonMessage, nil +} + +// convertRawJsonToFirewallRules converts []json.RawMessage to +// types.DistributedFirewallRules.Values so that entries can be filtered by ID or other fields. +// Note. Slice order remains the same +func convertRawJsonToFirewallRules(rawBodyStructure *distributedFirewallRulesRaw) (*types.DistributedFirewallRules, error) { + var rawJsonBodies []string + for _, singleObject := range rawBodyStructure.Values { + rawJsonBodies = append(rawJsonBodies, string(singleObject)) + } + // rawJsonBodies contains a slice of all response objects and they must be formatted as a JSON slice (wrapped + // into `[]`, separated with semicolons) so that unmarshalling to specified `outType` works in one go + allResponses := `[` + strings.Join(rawJsonBodies, ",") + `]` + + // Convert the retrieved []json.RawMessage to *types.DistributedFirewallRules.Values so that IDs can be searched for + // Note. The main goal here is to have 2 slices - one with []json.RawMessage and other + // []*DistributedFirewallRule. One can look for IDs and capture firewall rule index + dfwRules := &types.DistributedFirewallRules{} + // Unmarshal all accumulated responses into `dfwRules` + if err := json.Unmarshal([]byte(allResponses), &dfwRules.Values); err != nil { + return nil, fmt.Errorf("error decoding values into type types.DistributedFirewallRules: %s", err) + } + + return dfwRules, nil +} + +// composeUpdatePayloadWithNewRulePosition takes a slice of existing firewall rules and injects new +// firewall rule at a given position `newRuleSlicePosition` +func composeUpdatePayloadWithNewRulePosition(newRuleSlicePosition int, rawBodyStructure *distributedFirewallRulesRaw, newRuleJsonMessage json.RawMessage) ([]json.RawMessage, error) { + // Create a new slice with additional capacity of 1 to add new firewall rule into existing list + newFwRuleSlice := make([]json.RawMessage, len(rawBodyStructure.Values)+1) + util.Logger.Printf("[DEBUG] CreateDistributedFirewallRule new container slice of size '%d' with previous element count '%d'", len(newFwRuleSlice), len(rawBodyStructure.Values)) + // if newRulePosition is not 0 (at the top), then previous rules need to be copied to the beginning of new slice + if newRuleSlicePosition != 0 { + util.Logger.Printf("[DEBUG] CreateDistributedFirewallRule copying first '%d' slice [:%d]", newRuleSlicePosition, newRuleSlicePosition) + copy(newFwRuleSlice[:newRuleSlicePosition], rawBodyStructure.Values[:newRuleSlicePosition]) + } + + // Insert the new element at specified index + util.Logger.Printf("[DEBUG] CreateDistributedFirewallRule inserting new element into position %d", newRuleSlicePosition) + newFwRuleSlice[newRuleSlicePosition] = newRuleJsonMessage + + // Copy the remaining elements after new rule + copy(newFwRuleSlice[newRuleSlicePosition+1:], rawBodyStructure.Values[newRuleSlicePosition:]) + util.Logger.Printf("[DEBUG] CreateDistributedFirewallRule copying remaining items '%d'", newRuleSlicePosition) + + return newFwRuleSlice, nil +} + +// distributedFirewallRulesRaw is a copy of `types.DistributedFirewallRules` so that values can be +// unmarshalled into json.RawMessage (as strings) instead of exact types `DistributedFirewallRule` +// It has Public field Values so that marshalling can work, but is not exported itself as it is only +// an intermediate type used in `VdcGroup.CreateDistributedFirewallRule` +type distributedFirewallRulesRaw struct { + Values []json.RawMessage `json:"values"` +} diff --git a/govcd/nsxt_distributed_firewall_test.go b/govcd/nsxt_distributed_firewall_test.go new file mode 100644 index 000000000..b43a383a0 --- /dev/null +++ b/govcd/nsxt_distributed_firewall_test.go @@ -0,0 +1,523 @@ +//go:build network || nsxt || functional || openapi || ALL + +package govcd + +import ( + "fmt" + "os" + "strconv" + "strings" + "text/tabwriter" + + "github.com/vmware/go-vcloud-director/v2/util" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +// Test_NsxtDistributedFirewall creates a list of distributed firewall rules with randomized +// parameters in two modes: +// * System user +// * Org Admin user +func (vcd *TestVCD) Test_NsxtDistributedFirewallRules(check *C) { + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointEdgeGateways) + vcd.skipIfNotSysAdmin(check) + + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(adminOrg, NotNil) + check.Assert(err, IsNil) + + nsxtExternalNetwork, err := GetExternalNetworkV2ByName(vcd.client, vcd.config.VCD.Nsxt.ExternalNetwork) + check.Assert(err, IsNil) + check.Assert(nsxtExternalNetwork, NotNil) + + vdc, vdcGroup := test_CreateVdcGroup(check, adminOrg, vcd) + check.Assert(vdc, NotNil) + check.Assert(vdcGroup, NotNil) + + // Run firewall tests as System user + fmt.Println("# Running Distributed Firewall tests as 'System' user") + test_NsxtDistributedFirewallRules(vcd, check, vdcGroup.VdcGroup.Id, vcd.client, vdc) + + // Prep Org admin user and run firewall tests + userName := strings.ToLower(check.TestName()) + fmt.Printf("# Running Distributed Firewall tests as Org Admin user '%s'\n", userName) + orgUserVcdClient, _, err := newOrgUserConnection(adminOrg, userName, "CHANGE-ME", vcd.config.Provider.Url, true) + check.Assert(err, IsNil) + orgUserOrgAdmin, err := orgUserVcdClient.GetAdminOrgById(adminOrg.AdminOrg.ID) + check.Assert(err, IsNil) + orgUserVdc, err := orgUserOrgAdmin.GetVDCById(vdc.Vdc.ID, false) + check.Assert(err, IsNil) + test_NsxtDistributedFirewallRules(vcd, check, vdcGroup.VdcGroup.Id, orgUserVcdClient, orgUserVdc) + + // Cleanup + err = vdcGroup.Delete() + check.Assert(err, IsNil) + err = vdc.DeleteWait(true, true) + check.Assert(err, IsNil) +} + +func test_NsxtDistributedFirewallRules(vcd *TestVCD, check *C, vdcGroupId string, vcdClient *VCDClient, vdc *Vdc) { + adminOrg, err := vcdClient.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(adminOrg, NotNil) + check.Assert(err, IsNil) + + vdcGroup, err := adminOrg.GetVdcGroupById(vdcGroupId) + check.Assert(err, IsNil) + + _, err = vdcGroup.ActivateDfw() + check.Assert(err, IsNil) + + // Get existing firewall rule configuration + fwRules, err := vdcGroup.GetDistributedFirewall() + check.Assert(err, IsNil) + check.Assert(fwRules.DistributedFirewallRuleContainer.Values, NotNil) + + // Create some prerequisites and generate firewall rule configurations to feed them into config + randomizedFwRuleDefs, ipSet, secGroup := createDistributedFirewallDefinitions(check, vcd, vdcGroup.VdcGroup.Id, vcdClient, vdc) + + fwRules.DistributedFirewallRuleContainer.Values = randomizedFwRuleDefs + + if testVerbose { + dumpDistributedFirewallRulesToScreen(randomizedFwRuleDefs) + } + + fwUpdated, err := vdcGroup.UpdateDistributedFirewall(fwRules.DistributedFirewallRuleContainer) + check.Assert(err, IsNil) + check.Assert(fwUpdated, Not(IsNil)) + + check.Assert(len(fwUpdated.DistributedFirewallRuleContainer.Values), Equals, len(randomizedFwRuleDefs)) + + // Check that all created rules have the same attributes and order + for index := range fwUpdated.DistributedFirewallRuleContainer.Values { + check.Assert(fwUpdated.DistributedFirewallRuleContainer.Values[index].Name, Equals, randomizedFwRuleDefs[index].Name) + check.Assert(fwUpdated.DistributedFirewallRuleContainer.Values[index].Direction, Equals, randomizedFwRuleDefs[index].Direction) + check.Assert(fwUpdated.DistributedFirewallRuleContainer.Values[index].IpProtocol, Equals, randomizedFwRuleDefs[index].IpProtocol) + check.Assert(fwUpdated.DistributedFirewallRuleContainer.Values[index].Enabled, Equals, randomizedFwRuleDefs[index].Enabled) + check.Assert(fwUpdated.DistributedFirewallRuleContainer.Values[index].Logging, Equals, randomizedFwRuleDefs[index].Logging) + check.Assert(fwUpdated.DistributedFirewallRuleContainer.Values[index].Comments, Equals, randomizedFwRuleDefs[index].Comments) + check.Assert(fwUpdated.DistributedFirewallRuleContainer.Values[index].ActionValue, Equals, randomizedFwRuleDefs[index].ActionValue) + + for fwGroupIndex := range fwUpdated.DistributedFirewallRuleContainer.Values[index].SourceFirewallGroups { + check.Assert(fwUpdated.DistributedFirewallRuleContainer.Values[index].SourceFirewallGroups[fwGroupIndex].ID, Equals, randomizedFwRuleDefs[index].SourceFirewallGroups[fwGroupIndex].ID) + } + + for fwGroupIndex := range fwUpdated.DistributedFirewallRuleContainer.Values[index].DestinationFirewallGroups { + check.Assert(fwUpdated.DistributedFirewallRuleContainer.Values[index].DestinationFirewallGroups[fwGroupIndex].ID, Equals, randomizedFwRuleDefs[index].DestinationFirewallGroups[fwGroupIndex].ID) + } + + // Ensure the same amount of Application Port Profiles are assigned and created + check.Assert(len(fwUpdated.DistributedFirewallRuleContainer.Values), Equals, len(randomizedFwRuleDefs)) + definedAppPortProfileIds := extractIdsFromOpenApiReferences(randomizedFwRuleDefs[index].ApplicationPortProfiles) + for _, appPortProfile := range fwUpdated.DistributedFirewallRuleContainer.Values[index].ApplicationPortProfiles { + check.Assert(contains(appPortProfile.ID, definedAppPortProfileIds), Equals, true) + } + + // Ensure the same amount of Network Context Profiles are assigned and created + definedNetContextProfileIds := extractIdsFromOpenApiReferences(randomizedFwRuleDefs[index].NetworkContextProfiles) + for _, networkContextProfile := range fwUpdated.DistributedFirewallRuleContainer.Values[index].NetworkContextProfiles { + check.Assert(contains(networkContextProfile.ID, definedNetContextProfileIds), Equals, true) + } + } + + // Cleanup + err = fwRules.DeleteAllRules() + check.Assert(err, IsNil) + // Check that rules were removed + newRules, err := vdcGroup.GetDistributedFirewall() + check.Assert(err, IsNil) + check.Assert(len(newRules.DistributedFirewallRuleContainer.Values) == 0, Equals, true) + + // Cleanup remaining setup + _, err = vdcGroup.DisableDefaultPolicy() + check.Assert(err, IsNil) + _, err = vdcGroup.DeactivateDfw() + check.Assert(err, IsNil) + err = ipSet.Delete() + check.Assert(err, IsNil) + err = secGroup.Delete() + check.Assert(err, IsNil) +} + +// createDistributedFirewallDefinitions creates some randomized firewall rule configurations to match possible configurations +func createDistributedFirewallDefinitions(check *C, vcd *TestVCD, vdcGroupId string, vcdClient *VCDClient, vdc *Vdc) ([]*types.DistributedFirewallRule, *NsxtFirewallGroup, *NsxtFirewallGroup) { + // This number does not impact performance because all rules are created at once in the API + numberOfRules := 40 + + // Pre-Create Firewall Groups (IP Set and Security Group to randomly configure them) + ipSet := preCreateVdcGroupIpSet(check, vcd, vdcGroupId, vdc) + secGroup := preCreateVdcGroupSecurityGroup(check, vcd, vdcGroupId, vdc) + fwGroupIds := []string{ipSet.NsxtFirewallGroup.ID, secGroup.NsxtFirewallGroup.ID} + fwGroupRefs := convertSliceOfStringsToOpenApiReferenceIds(fwGroupIds) + appPortProfileReferences := getRandomListOfAppPortProfiles(check, vcd) + networkContextProfiles := getRandomListOfNetworkContextProfiles(check, vcd, vcdClient) + + firewallRules := make([]*types.DistributedFirewallRule, numberOfRules) + for a := 0; a < numberOfRules; a++ { + + // Feed in empty value for source and destination or a firewall group + src := pickRandomOpenApiRefOrEmpty(fwGroupRefs) + var srcValue []types.OpenApiReference + dst := pickRandomOpenApiRefOrEmpty(fwGroupRefs) + var dstValue []types.OpenApiReference + if src != (types.OpenApiReference{}) { + srcValue = []types.OpenApiReference{src} + } + if dst != (types.OpenApiReference{}) { + dstValue = []types.OpenApiReference{dst} + } + + firewallRules[a] = &types.DistributedFirewallRule{ + Name: check.TestName() + strconv.Itoa(a), + ActionValue: pickRandomString([]string{"ALLOW", "DROP", "REJECT"}), + Enabled: a%2 == 0, + SourceFirewallGroups: srcValue, + DestinationFirewallGroups: dstValue, + ApplicationPortProfiles: appPortProfileReferences[0:a], + IpProtocol: pickRandomString([]string{"IPV6", "IPV4", "IPV4_IPV6"}), + Logging: a%2 == 1, + Direction: pickRandomString([]string{"IN", "OUT", "IN_OUT"}), + } + + // Network Context Profile can usually work with up to one Application Profile therefore this + // needs to be explicitly preset + if a%5 == 1 { // Every fifth rule + netCtxProfile := networkContextProfiles[0:a] + networkContextProfile := make([]types.OpenApiReference, 0) + for _, netCtxProf := range netCtxProfile { + if netCtxProf.ID != "" { + networkContextProfile = append(networkContextProfile, types.OpenApiReference{ID: netCtxProf.ID, Name: netCtxProf.Name}) + } + } + + firewallRules[a].NetworkContextProfiles = networkContextProfile + // firewallRules[a].ApplicationPortProfiles = appPortProfileReferences[0:1] + firewallRules[a].ApplicationPortProfiles = nil + + } + + // API V36.2 introduced new field Comment which is shown in UI + if vcd.client.Client.APIVCDMaxVersionIs(">= 36.2") { + firewallRules[a].Comments = "Comment Rule" + } + + } + + return firewallRules, ipSet, secGroup +} + +func preCreateVdcGroupIpSet(check *C, vcd *TestVCD, ownerId string, nsxtVdc *Vdc) *NsxtFirewallGroup { + ipSetDefinition := &types.NsxtFirewallGroup{ + Name: check.TestName() + "ipset", + Description: check.TestName() + "-Description", + Type: types.FirewallGroupTypeIpSet, + OwnerRef: &types.OpenApiReference{ID: ownerId}, + + IpAddresses: []string{ + "12.12.12.1", + "10.10.10.0/24", + "11.11.11.1-11.11.11.2", + // represents the block of IPv6 addresses from 2001:db8:0:0:0:0:0:0 to 2001:db8:0:ffff:ffff:ffff:ffff:ffff + "2001:db8::/48", + "2001:db6:0:0:0:0:0:0-2001:db6:0:ffff:ffff:ffff:ffff:ffff", + }, + } + + // Create IP Set and add to cleanup if it was created + createdIpSet, err := nsxtVdc.CreateNsxtFirewallGroup(ipSetDefinition) + check.Assert(err, IsNil) + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointFirewallGroups + createdIpSet.NsxtFirewallGroup.ID + PrependToCleanupListOpenApi(createdIpSet.NsxtFirewallGroup.Name, check.TestName(), openApiEndpoint) + + return createdIpSet +} + +func preCreateVdcGroupSecurityGroup(check *C, vcd *TestVCD, ownerId string, nsxtVdc *Vdc) *NsxtFirewallGroup { + fwGroupDefinition := &types.NsxtFirewallGroup{ + Name: check.TestName() + "security-group", + Description: check.TestName() + "-Description", + Type: types.FirewallGroupTypeSecurityGroup, + OwnerRef: &types.OpenApiReference{ID: ownerId}, + } + + // Create firewall group and add to cleanup if it was created + createdSecGroup, err := nsxtVdc.CreateNsxtFirewallGroup(fwGroupDefinition) + check.Assert(err, IsNil) + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointFirewallGroups + createdSecGroup.NsxtFirewallGroup.ID + PrependToCleanupListOpenApi(createdSecGroup.NsxtFirewallGroup.Name, check.TestName(), openApiEndpoint) + + return createdSecGroup +} + +func getRandomListOfNetworkContextProfiles(check *C, vcd *TestVCD, vdcClient *VCDClient) []types.OpenApiReference { + networkContextProfiles, err := GetAllNetworkContextProfiles(&vcd.client.Client, nil) + check.Assert(err, IsNil) + openApiRefs := make([]types.OpenApiReference, 1) + for _, networkContextProfile := range networkContextProfiles { + // Skipping network context profile which has hardcoded destinations and throws error when used in firewall rules with specified destinations + if strings.Contains(networkContextProfile.Description, "ALG") || strings.Contains(networkContextProfile.Description, "includes the URL categories") { + continue + } + openApiRef := types.OpenApiReference{ + ID: networkContextProfile.ID, + Name: networkContextProfile.Name, + } + + openApiRefs = append(openApiRefs, openApiRef) + } + + return openApiRefs +} + +func dumpDistributedFirewallRulesToScreen(rules []*types.DistributedFirewallRule) { + fmt.Println("# The following firewall rules will be created") + w := tabwriter.NewWriter(os.Stdout, 1, 1, 1, ' ', 0) + fmt.Fprintln(w, "Name\tDirection\tIP Protocol\tEnabled\tAction\tLogging\tSrc Count\tDst Count\tAppPortProfile Count\tNet Context Profile Count") + + for _, rule := range rules { + fmt.Fprintf(w, "%s\t%s\t%s\t%t\t%s\t%t\t%d\t%d\t%d\t%d\n", rule.Name, rule.Direction, rule.IpProtocol, + rule.Enabled, rule.Action, rule.Logging, len(rule.SourceFirewallGroups), len(rule.DestinationFirewallGroups), len(rule.ApplicationPortProfiles), len(rule.NetworkContextProfiles)) + } + err := w.Flush() + if err != nil { + util.Logger.Printf("Error while dumping Distributed Firewall rules to screen: %s", err) + } +} + +// Test_NsxtDistributedFirewallRule tests the capability of managing Firewall Rules one by one using +// `DistributedFirewallRule` type. +func (vcd *TestVCD) Test_NsxtDistributedFirewallRule(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointEdgeGateways) + + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(adminOrg, NotNil) + check.Assert(err, IsNil) + + nsxtExternalNetwork, err := GetExternalNetworkV2ByName(vcd.client, vcd.config.VCD.Nsxt.ExternalNetwork) + check.Assert(nsxtExternalNetwork, NotNil) + check.Assert(err, IsNil) + + vdc, vdcGroup := test_CreateVdcGroup(check, adminOrg, vcd) + check.Assert(vdc, NotNil) + check.Assert(vdcGroup, NotNil) + + defer func() { + // Cleanup + err = vdcGroup.Delete() + check.Assert(err, IsNil) + err = vdc.DeleteWait(true, true) + check.Assert(err, IsNil) + }() + + fmt.Println("# Running Distributed Firewall tests for single Rule") + test_NsxtDistributedFirewallRule(vcd, check, vdcGroup.VdcGroup.Id, vcd.client, vdc, true) +} + +func (vcd *TestVCD) Test_NsxtDistributedFirewallWithDefaultRule(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointEdgeGateways) + + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(adminOrg, NotNil) + check.Assert(err, IsNil) + + nsxtExternalNetwork, err := GetExternalNetworkV2ByName(vcd.client, vcd.config.VCD.Nsxt.ExternalNetwork) + check.Assert(nsxtExternalNetwork, NotNil) + check.Assert(err, IsNil) + + vdc, vdcGroup := test_CreateVdcGroup(check, adminOrg, vcd) + check.Assert(vdc, NotNil) + check.Assert(vdcGroup, NotNil) + + defer func() { + // Cleanup + err = vdcGroup.Delete() + check.Assert(err, IsNil) + err = vdc.DeleteWait(true, true) + check.Assert(err, IsNil) + }() + + fmt.Println("# Running Distributed Firewall tests for single Rule (with default DFW rule enabled)") + test_NsxtDistributedFirewallRuleAboveDefault(vcd, check, vdcGroup.VdcGroup.Id, vcd.client, vdc) +} + +func test_NsxtDistributedFirewallRule(vcd *TestVCD, check *C, vdcGroupId string, vcdClient *VCDClient, vdc *Vdc, deleteDefaultFirewallRule bool) { + adminOrg, err := vcdClient.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(adminOrg, NotNil) + check.Assert(err, IsNil) + + vdcGroup, err := adminOrg.GetVdcGroupById(vdcGroupId) + check.Assert(err, IsNil) + + _, err = vdcGroup.ActivateDfw() + check.Assert(err, IsNil) + + // Prep firewall rule sample to operate with + randomizedFwRuleDefs, ipSet, secGroup := createDistributedFirewallDefinitions(check, vcd, vdcGroup.VdcGroup.Id, vcdClient, vdc) + // defer cleanup function in case something goes wrong + defer func() { + dfw, err := vdcGroup.GetDistributedFirewall() + check.Assert(err, IsNil) + err = dfw.DeleteAllRules() + check.Assert(err, IsNil) + _, err = vdcGroup.DisableDefaultPolicy() + check.Assert(err, IsNil) + err = ipSet.Delete() + check.Assert(err, IsNil) + err = secGroup.Delete() + check.Assert(err, IsNil) + }() + + randomizedFwRuleSubSet := randomizedFwRuleDefs[0:5] // taking only first 5 rules to limit time of testing + + // removing default firewall rule which is created by VCD when vdcGroup.ActivateDfw() is executed + if deleteDefaultFirewallRule { + err = vdcGroup.DeleteAllDistributedFirewallRules() + check.Assert(err, IsNil) + } + + // Adding firewal rules one by one and checking that each of them is placed correctly + testDistributedFirewallRuleSequence(check, randomizedFwRuleSubSet, vdcGroup, false) + testDistributedFirewallRuleSequence(check, randomizedFwRuleSubSet, vdcGroup, true) +} + +func test_NsxtDistributedFirewallRuleAboveDefault(vcd *TestVCD, check *C, vdcGroupId string, vcdClient *VCDClient, vdc *Vdc) { + adminOrg, err := vcdClient.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(adminOrg, NotNil) + check.Assert(err, IsNil) + + vdcGroup, err := adminOrg.GetVdcGroupById(vdcGroupId) + check.Assert(err, IsNil) + + _, err = vdcGroup.ActivateDfw() + check.Assert(err, IsNil) + + // Prep firewall rule sample to operate with + randomizedFwRuleDefs, ipSet, secGroup := createDistributedFirewallDefinitions(check, vcd, vdcGroup.VdcGroup.Id, vcdClient, vdc) + // defer cleanup function in case something goes wrong + defer func() { + dfw, err := vdcGroup.GetDistributedFirewall() + check.Assert(err, IsNil) + err = dfw.DeleteAllRules() + check.Assert(err, IsNil) + _, err = vdcGroup.DisableDefaultPolicy() + check.Assert(err, IsNil) + err = ipSet.Delete() + check.Assert(err, IsNil) + err = secGroup.Delete() + check.Assert(err, IsNil) + }() + + // Get default rule by name (this name is automatically set by VCD) + defaultRuleName := fmt.Sprintf("Default_VdcGroup_%s", vdcGroup.VdcGroup.Name) + defaultRule, err := vdcGroup.GetDistributedFirewallRuleByName(defaultRuleName) + check.Assert(err, IsNil) + check.Assert(defaultRule, NotNil) + + _, rule1, err := vdcGroup.CreateDistributedFirewallRule(defaultRule.Rule.ID, randomizedFwRuleDefs[0]) + check.Assert(err, IsNil) + + _, rule2, err := vdcGroup.CreateDistributedFirewallRule(defaultRule.Rule.ID, randomizedFwRuleDefs[1]) + check.Assert(err, IsNil) + + // The order should be + // * rule1 (created first, inserted above default rule) + // * rule2 (created after rule1, inserted above default rule) + // * Default DFW rule + + allRules, err := vdcGroup.GetDistributedFirewall() + check.Assert(err, IsNil) + check.Assert(len(allRules.DistributedFirewallRuleContainer.Values), Equals, 3) + check.Assert(allRules.DistributedFirewallRuleContainer.Values[0].ID, Equals, rule1.Rule.ID) + check.Assert(allRules.DistributedFirewallRuleContainer.Values[1].ID, Equals, rule2.Rule.ID) + check.Assert(allRules.DistributedFirewallRuleContainer.Values[2].ID, Equals, defaultRule.Rule.ID) + + // Clean up created firewall rules for next phase + err = vdcGroup.DeleteAllDistributedFirewallRules() + check.Assert(err, IsNil) + +} + +// testDistributedFirewallRuleSequence tests the following: +// * create firewall rules one one by one +// * check that the order of firewall rules is the same as requested (or exactly reverse if +// reverseOrder=true) +// * check that all IDs of created firewall rules persisted during further updates (means that no +// firewall rules were recreated during addition of new ones) +func testDistributedFirewallRuleSequence(check *C, randomizedFwRuleSubSet []*types.DistributedFirewallRule, vdcGroup *VdcGroup, reverseOrder bool) { + createdIdsFound := make(map[string]bool) + fmt.Printf("# Creating '%d' rules one by one (reverseOrder: %t)\n", len(randomizedFwRuleSubSet), reverseOrder) + previousRuleId := "" + for _, rule := range randomizedFwRuleSubSet { + if testVerbose { + fmt.Printf("%s\t%s\t%s\t%t\t%s\t%t\t%d\t%d\t%d\t%d\n", rule.Name, rule.Direction, rule.IpProtocol, + rule.Enabled, rule.Action, rule.Logging, len(rule.SourceFirewallGroups), len(rule.DestinationFirewallGroups), len(rule.ApplicationPortProfiles), len(rule.NetworkContextProfiles)) + } + + completeDfw, singleCreatedFwRule, err := vdcGroup.CreateDistributedFirewallRule(previousRuleId, rule) + check.Assert(err, IsNil) + check.Assert(completeDfw, NotNil) + check.Assert(singleCreatedFwRule, NotNil) + createdIdsFound[singleCreatedFwRule.Rule.ID] = false + + // caching ID to use as previous rule in case + if reverseOrder { + previousRuleId = singleCreatedFwRule.Rule.ID + } + } + fmt.Printf("# Done creating '%d' rules one by one (reverseOrder: %t)\n", len(randomizedFwRuleSubSet), reverseOrder) + + // Retrieve all firewall rules and check that order matches + allRules, err := vdcGroup.GetDistributedFirewall() + check.Assert(err, IsNil) + check.Assert(len(allRules.DistributedFirewallRuleContainer.Values), Equals, len(randomizedFwRuleSubSet)) + + // check that rule order is exactly as expected (either reverse of randomizedFwRuleSubSet or exactly the same based on reverseOrder parameter) + if reverseOrder { + for ruleIndex, rule := range allRules.DistributedFirewallRuleContainer.Values { + reverseRuleIndex := len(randomizedFwRuleSubSet) - ruleIndex - 1 + check.Assert(rule.Name, Equals, randomizedFwRuleSubSet[reverseRuleIndex].Name) + createdIdsFound[rule.ID] = true + } + } else { + for ruleIndex, rule := range allRules.DistributedFirewallRuleContainer.Values { + check.Assert(rule.Name, Equals, randomizedFwRuleSubSet[ruleIndex].Name) + createdIdsFound[rule.ID] = true + } + } + + // Check that all created IDs are in the final output (none of the firewall rules were recreated) + for _, value := range createdIdsFound { + check.Assert(value, Equals, true) + } + + // Perform Update + ruleById, err := vdcGroup.GetDistributedFirewallRuleById(allRules.DistributedFirewallRuleContainer.Values[0].ID) + check.Assert(err, IsNil) + + updatedRuleName := check.TestName() + "-updated" + ruleById.Rule.Name = updatedRuleName + updatedRule, err := ruleById.Update(ruleById.Rule) + check.Assert(err, IsNil) + check.Assert(updatedRule.Rule.Name, Equals, updatedRuleName) + + // Delete + err = updatedRule.Delete() + check.Assert(err, IsNil) + + notFoundById, err := vdcGroup.GetDistributedFirewallRuleById(updatedRule.Rule.ID) + check.Assert(err, NotNil) + check.Assert(notFoundById, IsNil) + + // Clean up created firewall rules for next phase + err = vdcGroup.DeleteAllDistributedFirewallRules() + check.Assert(err, IsNil) +} diff --git a/govcd/nsxt_edge_cluster.go b/govcd/nsxt_edge_cluster.go index 028509f3a..382806c51 100644 --- a/govcd/nsxt_edge_cluster.go +++ b/govcd/nsxt_edge_cluster.go @@ -68,7 +68,6 @@ func filterNsxtEdgeClusters(name string, allNnsxtEdgeCluster []*NsxtEdgeCluster) } return filteredNsxtEdgeClusters - } // GetAllNsxtEdgeClusters retrieves all available Edge Clusters for a particular VDC @@ -77,28 +76,48 @@ func (vdc *Vdc) GetAllNsxtEdgeClusters(queryParameters url.Values) ([]*NsxtEdgeC return nil, fmt.Errorf("VDC must have ID populated to retrieve NSX-T edge clusters") } + // Get all NSX-T Edge clusters that are accessible to an organization VDC. The 'orgVdcId'filter + // key must be set with the ID of the VDC for which we want to get available Edge Clusters for. + // + // orgVdcId==urn:vcloud:vdc:09722307-aee0-4623-af95-7f8e577c9ebc + + // Create a copy of queryParameters so that original queryParameters are not mutated (because a map is always a + // reference) + queryParams := queryParameterFilterAnd("orgVdcId=="+vdc.Vdc.ID, queryParameters) + + return getAllNsxtEdgeClusters(vdc.client, queryParams) +} + +// GetAllNsxtEdgeClusters retrieves all NSX-T Edge Clusters in the system +// +// A filter is mandatory as otherwise request will fail +// orgVdcId - | The filter orgVdcId must be set equal to the id of the NSX-T backed Org vDC for +// which we want to get the edge clusters. Example: +// (orgVdcId==urn:vcloud:vdc:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) +// vdcGroupId - | The filter vdcGroupId must be set equal to the id of the NSX-T VDC Group for which +// we want to get the edge clusters. Example: +// (vdcGroupId==urn:vcloud:vdcGroup:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) +// pvdcId - | The filter pvdcId must be set equal to the id of the NSX-T backed Provider VDC for +// which we want to get the edge clusters. pvdcId filter is supported from version 35.2 Example: +// (pvdcId==urn:vcloud:providervdc:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) +func (vcdClient *VCDClient) GetAllNsxtEdgeClusters(queryParameters url.Values) ([]*NsxtEdgeCluster, error) { + return getAllNsxtEdgeClusters(&vcdClient.Client, queryParameters) +} + +func getAllNsxtEdgeClusters(client *Client, queryParams url.Values) ([]*NsxtEdgeCluster, error) { endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeClusters - minimumApiVersion, err := vdc.client.checkOpenApiEndpointCompatibility(endpoint) + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) if err != nil { return nil, err } - urlRef, err := vdc.client.OpenApiBuildEndpoint(endpoint) + urlRef, err := client.OpenApiBuildEndpoint(endpoint) if err != nil { return nil, err } - // Get all NSX-T Edge clusters that are accessible to an organization VDC. The “_context” filter key must be set with - // the ID of the VDC for which we want to get available Edge Clusters for. - // - // _context==urn:vcloud:vdc:09722307-aee0-4623-af95-7f8e577c9ebc - - // Create a copy of queryParameters so that original queryParameters are not mutated (because a map is always a - // reference) - queryParams := queryParameterFilterAnd("_context=="+vdc.Vdc.ID, queryParameters) - typeResponses := []*types.NsxtEdgeCluster{{}} - err = vdc.client.OpenApiGetAllItems(minimumApiVersion, urlRef, queryParams, &typeResponses) + err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParams, &typeResponses, nil) if err != nil { return nil, err } @@ -107,7 +126,7 @@ func (vdc *Vdc) GetAllNsxtEdgeClusters(queryParameters url.Values) ([]*NsxtEdgeC for sliceIndex := range typeResponses { returnObjects[sliceIndex] = &NsxtEdgeCluster{ NsxtEdgeCluster: typeResponses[sliceIndex], - client: vdc.client, + client: client, } } diff --git a/govcd/nsxt_edge_cluster_test.go b/govcd/nsxt_edge_cluster_test.go index e620931a6..b936f7ee8 100644 --- a/govcd/nsxt_edge_cluster_test.go +++ b/govcd/nsxt_edge_cluster_test.go @@ -1,4 +1,4 @@ -// +build network nsxt functional openapi ALL +//go:build network || nsxt || functional || openapi || ALL /* * Copyright 2020 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -8,15 +8,12 @@ package govcd import ( "fmt" + "net/url" . "gopkg.in/check.v1" ) func (vcd *TestVCD) Test_GetAllNsxtEdgeClusters(check *C) { - if vcd.client.Client.APIVCDMaxVersionIs("< 34") { - check.Skip("At least VCD 10.1 is required") - } - skipNoNsxtConfiguration(vcd, check) if vcd.skipAdminTests { @@ -26,17 +23,20 @@ func (vcd *TestVCD) Test_GetAllNsxtEdgeClusters(check *C) { nsxtVdc, err := vcd.org.GetVDCByNameOrId(vcd.config.VCD.Nsxt.Vdc, true) check.Assert(err, IsNil) - tier0Router, err := nsxtVdc.GetAllNsxtEdgeClusters(nil) + edgeClusters, err := nsxtVdc.GetAllNsxtEdgeClusters(nil) + check.Assert(err, IsNil) + check.Assert(edgeClusters, NotNil) + check.Assert(len(edgeClusters) > 0, Equals, true) + + queryParams := url.Values{} + queryParams.Add("filter", fmt.Sprintf("orgVdcId==%s", nsxtVdc.Vdc.ID)) + allEdgeClusters, err := vcd.client.GetAllNsxtEdgeClusters(queryParams) check.Assert(err, IsNil) - check.Assert(tier0Router, NotNil) - check.Assert(len(tier0Router) > 0, Equals, true) + check.Assert(allEdgeClusters, NotNil) + check.Assert(len(allEdgeClusters) > 0, Equals, true) } func (vcd *TestVCD) Test_GetNsxtEdgeClusterByName(check *C) { - if vcd.client.Client.APIVCDMaxVersionIs("< 34") { - check.Skip("At least VCD 10.1 is required") - } - skipNoNsxtConfiguration(vcd, check) if vcd.skipAdminTests { @@ -46,13 +46,9 @@ func (vcd *TestVCD) Test_GetNsxtEdgeClusterByName(check *C) { nsxtVdc, err := vcd.org.GetVDCByNameOrId(vcd.config.VCD.Nsxt.Vdc, true) check.Assert(err, IsNil) - allEdgeClusters, err := nsxtVdc.GetAllNsxtEdgeClusters(nil) + edgeCluster, err := nsxtVdc.GetNsxtEdgeClusterByName(vcd.config.VCD.Nsxt.NsxtEdgeCluster) check.Assert(err, IsNil) - check.Assert(allEdgeClusters, NotNil) - - edgeCluster, err := nsxtVdc.GetNsxtEdgeClusterByName(allEdgeClusters[0].NsxtEdgeCluster.Name) - check.Assert(err, IsNil) - check.Assert(allEdgeClusters, NotNil) - check.Assert(edgeCluster, DeepEquals, allEdgeClusters[0]) + check.Assert(edgeCluster, NotNil) + check.Assert(edgeCluster.NsxtEdgeCluster.Name, Equals, vcd.config.VCD.Nsxt.NsxtEdgeCluster) } diff --git a/govcd/nsxt_edgegateway.go b/govcd/nsxt_edgegateway.go index 426f2bc87..009e7dae7 100644 --- a/govcd/nsxt_edgegateway.go +++ b/govcd/nsxt_edgegateway.go @@ -6,9 +6,12 @@ package govcd import ( "fmt" + "net/netip" "net/url" + "time" "github.com/vmware/go-vcloud-director/v2/types/v56" + "github.com/vmware/go-vcloud-director/v2/util" ) // NsxtEdgeGateway uses OpenAPI endpoint to operate NSX-T Edge Gateways @@ -17,26 +20,26 @@ type NsxtEdgeGateway struct { client *Client } -// GetNsxtEdgeGatewayById allows to retrieve NSX-T edge gateway by ID for Org admins +// GetNsxtEdgeGatewayById allows retrieving NSX-T edge gateway by ID for Org admins func (adminOrg *AdminOrg) GetNsxtEdgeGatewayById(id string) (*NsxtEdgeGateway, error) { return getNsxtEdgeGatewayById(adminOrg.client, id, nil) } -// GetNsxtEdgeGatewayById allows to retrieve NSX-T edge gateway by ID for Org users +// GetNsxtEdgeGatewayById allows retrieving NSX-T edge gateway by ID for Org users func (org *Org) GetNsxtEdgeGatewayById(id string) (*NsxtEdgeGateway, error) { return getNsxtEdgeGatewayById(org.client, id, nil) } -// GetNsxtEdgeGatewayById allows to retrieve NSX-T edge gateway by ID for specific VDC +// GetNsxtEdgeGatewayById allows retrieving NSX-T edge gateway by ID for specific VDC func (vdc *Vdc) GetNsxtEdgeGatewayById(id string) (*NsxtEdgeGateway, error) { params := url.Values{} - filterParams := queryParameterFilterAnd("orgVdc.id=="+vdc.Vdc.ID, params) + filterParams := queryParameterFilterAnd("ownerRef.id=="+vdc.Vdc.ID, params) egw, err := getNsxtEdgeGatewayById(vdc.client, id, filterParams) if err != nil { return nil, err } - if egw.EdgeGateway.OrgVdc.ID != vdc.Vdc.ID { + if egw.EdgeGateway.OwnerRef.ID != vdc.Vdc.ID { return nil, fmt.Errorf("%s: no NSX-T Edge Gateway with ID '%s' found in VDC '%s'", ErrorEntityNotFound, id, vdc.Vdc.ID) } @@ -44,7 +47,7 @@ func (vdc *Vdc) GetNsxtEdgeGatewayById(id string) (*NsxtEdgeGateway, error) { return egw, nil } -// GetNsxtEdgeGatewayByName allows to retrieve NSX-T edge gateway by Name for Org admins +// GetNsxtEdgeGatewayByName allows retrieving NSX-T edge gateway by Name for Org admins func (adminOrg *AdminOrg) GetNsxtEdgeGatewayByName(name string) (*NsxtEdgeGateway, error) { queryParameters := url.Values{} queryParameters.Add("filter", "name=="+name) @@ -59,7 +62,7 @@ func (adminOrg *AdminOrg) GetNsxtEdgeGatewayByName(name string) (*NsxtEdgeGatewa return returnSingleNsxtEdgeGateway(name, onlyNsxtEdges) } -// GetNsxtEdgeGatewayByName allows to retrieve NSX-T edge gateway by Name for Org admins +// GetNsxtEdgeGatewayByName allows retrieving NSX-T edge gateway by Name for Org admins func (org *Org) GetNsxtEdgeGatewayByName(name string) (*NsxtEdgeGateway, error) { queryParameters := url.Values{} queryParameters.Add("filter", "name=="+name) @@ -74,6 +77,26 @@ func (org *Org) GetNsxtEdgeGatewayByName(name string) (*NsxtEdgeGateway, error) return returnSingleNsxtEdgeGateway(name, onlyNsxtEdges) } +// GetNsxtEdgeGatewayByNameAndOwnerId looks up NSX-T Edge Gateway by name and its owner ID (owner +// can be VDC or VDC Group). +func (org *Org) GetNsxtEdgeGatewayByNameAndOwnerId(edgeGatewayName, ownerId string) (*NsxtEdgeGateway, error) { + if edgeGatewayName == "" || ownerId == "" { + return nil, fmt.Errorf("'edgeGatewayName' and 'ownerId' must both be specified") + } + + queryParameters := url.Values{} + queryParameters.Add("filter", fmt.Sprintf("ownerRef.id==%s;name==%s", ownerId, edgeGatewayName)) + + allEdges, err := org.GetAllNsxtEdgeGateways(queryParameters) + if err != nil { + return nil, fmt.Errorf("unable to retrieve Edge Gateway by name '%s': %s", edgeGatewayName, err) + } + + onlyNsxtEdges := filterOnlyNsxtEdges(allEdges) + + return returnSingleNsxtEdgeGateway(edgeGatewayName, onlyNsxtEdges) +} + // GetNsxtEdgeGatewayByName allows to retrieve NSX-T edge gateway by Name for specific VDC func (vdc *Vdc) GetNsxtEdgeGatewayByName(name string) (*NsxtEdgeGateway, error) { queryParameters := url.Values{} @@ -87,6 +110,31 @@ func (vdc *Vdc) GetNsxtEdgeGatewayByName(name string) (*NsxtEdgeGateway, error) return returnSingleNsxtEdgeGateway(name, allEdges) } +// GetNsxtEdgeGatewayByName allows to retrieve NSX-T edge gateway by Name for specific VDC Group +func (vdcGroup *VdcGroup) GetNsxtEdgeGatewayByName(name string) (*NsxtEdgeGateway, error) { + if name == "" { + return nil, fmt.Errorf("'name' must be specified") + } + + queryParameters := url.Values{} + queryParameters.Add("filter", "name=="+name) + + allEdges, err := vdcGroup.GetAllNsxtEdgeGateways(queryParameters) + if err != nil { + return nil, fmt.Errorf("unable to retrieve Edge Gateway by name '%s': %s", name, err) + } + + return returnSingleNsxtEdgeGateway(name, allEdges) +} + +// GetAllNsxtEdgeGateways allows to retrieve all NSX-T Edge Gateways +func (vcdClient *VCDClient) GetAllNsxtEdgeGateways(queryParameters url.Values) ([]*NsxtEdgeGateway, error) { + if vcdClient == nil { + return nil, fmt.Errorf("vcdClient is empty") + } + return getAllNsxtEdgeGateways(&vcdClient.Client, queryParameters) +} + // GetAllNsxtEdgeGateways allows to retrieve all NSX-T edge gateways for Org Admins func (adminOrg *AdminOrg) GetAllNsxtEdgeGateways(queryParameters url.Values) ([]*NsxtEdgeGateway, error) { return getAllNsxtEdgeGateways(adminOrg.client, queryParameters) @@ -99,10 +147,16 @@ func (org *Org) GetAllNsxtEdgeGateways(queryParameters url.Values) ([]*NsxtEdgeG // GetAllNsxtEdgeGateways allows to retrieve all NSX-T edge gateways for specific VDC func (vdc *Vdc) GetAllNsxtEdgeGateways(queryParameters url.Values) ([]*NsxtEdgeGateway, error) { - filteredQueryParams := queryParameterFilterAnd("orgVdc.id=="+vdc.Vdc.ID, queryParameters) + filteredQueryParams := queryParameterFilterAnd("ownerRef.id=="+vdc.Vdc.ID, queryParameters) return getAllNsxtEdgeGateways(vdc.client, filteredQueryParams) } +// GetAllNsxtEdgeGateways allows to retrieve all NSX-T edge gateways for specific VDC +func (vdcGroup *VdcGroup) GetAllNsxtEdgeGateways(queryParameters url.Values) ([]*NsxtEdgeGateway, error) { + filteredQueryParams := queryParameterFilterAnd("ownerRef.id=="+vdcGroup.VdcGroup.Id, queryParameters) + return getAllNsxtEdgeGateways(vdcGroup.client, filteredQueryParams) +} + // CreateNsxtEdgeGateway allows to create NSX-T edge gateway for Org admins func (adminOrg *AdminOrg) CreateNsxtEdgeGateway(edgeGatewayConfig *types.OpenAPIEdgeGateway) (*NsxtEdgeGateway, error) { if !adminOrg.client.IsSysAdmin { @@ -110,7 +164,7 @@ func (adminOrg *AdminOrg) CreateNsxtEdgeGateway(edgeGatewayConfig *types.OpenAPI } endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGateways - minimumApiVersion, err := adminOrg.client.checkOpenApiEndpointCompatibility(endpoint) + minimumApiVersion, err := adminOrg.client.getOpenApiHighestElevatedVersion(endpoint) if err != nil { return nil, err } @@ -125,22 +179,47 @@ func (adminOrg *AdminOrg) CreateNsxtEdgeGateway(edgeGatewayConfig *types.OpenAPI client: adminOrg.client, } - err = adminOrg.client.OpenApiPostItem(minimumApiVersion, urlRef, nil, edgeGatewayConfig, returnEgw.EdgeGateway) + err = adminOrg.client.OpenApiPostItem(minimumApiVersion, urlRef, nil, edgeGatewayConfig, returnEgw.EdgeGateway, nil) if err != nil { return nil, fmt.Errorf("error creating Edge Gateway: %s", err) } + err = returnEgw.reorderUplinks() + if err != nil { + return nil, fmt.Errorf("error reordering Edge Gateway Uplinks after update operation: %s", err) + } + return returnEgw, nil } -// Update allows to update NSX-T edge gateway for Org admins +// Refresh reloads NSX-T Edge Gateway contents +func (egw *NsxtEdgeGateway) Refresh() error { + if egw.EdgeGateway == nil || egw.client == nil || egw.EdgeGateway.ID == "" { + return fmt.Errorf("cannot refresh Edge Gateway without ID") + } + + refreshedEdge, err := getNsxtEdgeGatewayById(egw.client, egw.EdgeGateway.ID, nil) + if err != nil { + return fmt.Errorf("error refreshing NSX-T Edge Gateway: %s", err) + } + egw.EdgeGateway = refreshedEdge.EdgeGateway + + err = egw.reorderUplinks() + if err != nil { + return fmt.Errorf("error reordering Edge Gateway Uplinks after refresh operation: %s", err) + } + + return nil +} + +// Update allows updating NSX-T edge gateway for Org admins func (egw *NsxtEdgeGateway) Update(edgeGatewayConfig *types.OpenAPIEdgeGateway) (*NsxtEdgeGateway, error) { if !egw.client.IsSysAdmin { return nil, fmt.Errorf("only System Administrator can update Edge Gateway") } endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGateways - minimumApiVersion, err := egw.client.checkOpenApiEndpointCompatibility(endpoint) + apiVersion, err := egw.client.getOpenApiHighestElevatedVersion(endpoint) if err != nil { return nil, err } @@ -159,22 +238,27 @@ func (egw *NsxtEdgeGateway) Update(edgeGatewayConfig *types.OpenAPIEdgeGateway) client: egw.client, } - err = egw.client.OpenApiPutItem(minimumApiVersion, urlRef, nil, edgeGatewayConfig, returnEgw.EdgeGateway) + err = egw.client.OpenApiPutItem(apiVersion, urlRef, nil, edgeGatewayConfig, returnEgw.EdgeGateway, nil) if err != nil { return nil, fmt.Errorf("error updating Edge Gateway: %s", err) } + err = egw.reorderUplinks() + if err != nil { + return nil, fmt.Errorf("error reordering Edge Gateway Uplinks after update operation: %s", err) + } + return returnEgw, nil } -// Update allows to delete NSX-T edge gateway for Org admins +// Delete allows deleting NSX-T edge gateway for sysadmins func (egw *NsxtEdgeGateway) Delete() error { if !egw.client.IsSysAdmin { - return fmt.Errorf("only Provider can update Edge Gateway") + return fmt.Errorf("only Provider can delete Edge Gateway") } endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGateways - minimumApiVersion, err := egw.client.checkOpenApiEndpointCompatibility(endpoint) + apiVersion, err := egw.client.getOpenApiHighestElevatedVersion(endpoint) if err != nil { return err } @@ -188,7 +272,7 @@ func (egw *NsxtEdgeGateway) Delete() error { return err } - err = egw.client.OpenApiDeleteItem(minimumApiVersion, urlRef, nil) + err = egw.client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) if err != nil { return fmt.Errorf("error deleting Edge Gateway: %s", err) @@ -197,13 +281,53 @@ func (egw *NsxtEdgeGateway) Delete() error { return nil } +// MoveToVdcOrVdcGroup moves NSX-T Edge Gateway to another VDC. This can cover such scenarios: +// * Move from VDC to VDC Group +// * Move from VDC Group to VDC (which is part of that VDC Group) +// +// This function is just an Update operation with OwnerRef changed to vdcGroupId, but it is more +// convenient to use it. +// Note. NSX-T Edge Gateway cannot be moved directly from one VDC to another +func (egw *NsxtEdgeGateway) MoveToVdcOrVdcGroup(vdcOrVdcGroupId string) (*NsxtEdgeGateway, error) { + edgeGatewayConfig := egw.EdgeGateway + edgeGatewayConfig.OwnerRef = &types.OpenApiReference{ID: vdcOrVdcGroupId} + // Explicitly unset VDC field because using it fails + edgeGatewayConfig.OrgVdc = nil + + return egw.Update(edgeGatewayConfig) +} + +// reorderUplinks will ensure that uplink at slice index 0 is the one backed by NSX-T Tier0 External network. +// NSX-T Edge Gateway can have many uplinks of different types (they are differentiated by 'backingType' field): +// * MANDATORY - exactly 1 uplink to Tier0 Gateway (External network backed by NSX-T T0 Gateway or NSX-T T0 Gateway VRF) [backingType==NSXT_TIER0 or NSXT_VRF_TIER0] +// * OPTIONAL - one or more External Network Uplinks (backed by NSX-T Segment backed External networks) [backingType==IMPORTED_T_LOGICAL_SWITCH] +// It is expected that the Tier0 gateway uplink is always at index 0, but we have seen where VCD API +// shuffles response values therefore it is important to ensure that uplink with +// backingType==NSXT_TIER0 or backingType==NSXT_VRF_TIER0 the element 0 in types.EdgeGatewayUplinks to avoid breaking functionality +// in upstream code. +// +// Note. This function wil be a noop in 10.4.0, because `backingType` was not present. However, this +// poses no risks because the can be only 1 uplink up to 10.4.1, when `backingType` was introduced. +func (egw *NsxtEdgeGateway) reorderUplinks() error { + if egw == nil || egw.EdgeGateway == nil { + return fmt.Errorf("edge gateway cannot be nil ") + } + + if len(egw.EdgeGateway.EdgeGatewayUplinks) == 0 { + return fmt.Errorf("no uplinks present in Edge Gateway") + } + + egw.EdgeGateway.EdgeGatewayUplinks = reorderEdgeGatewayUplinks(egw.EdgeGateway.EdgeGatewayUplinks) + return nil +} + // getNsxtEdgeGatewayById is a private parent for wrapped functions: // func (adminOrg *AdminOrg) GetNsxtEdgeGatewayByName(id string) (*NsxtEdgeGateway, error) // func (org *Org) GetNsxtEdgeGatewayByName(id string) (*NsxtEdgeGateway, error) // func (vdc *Vdc) GetNsxtEdgeGatewayById(id string) (*NsxtEdgeGateway, error) func getNsxtEdgeGatewayById(client *Client, id string, queryParameters url.Values) (*NsxtEdgeGateway, error) { endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGateways - minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + minimumApiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) if err != nil { return nil, err } @@ -222,7 +346,7 @@ func getNsxtEdgeGatewayById(client *Client, id string, queryParameters url.Value client: client, } - err = client.OpenApiGetItem(minimumApiVersion, urlRef, queryParameters, egw.EdgeGateway) + err = client.OpenApiGetItem(minimumApiVersion, urlRef, queryParameters, egw.EdgeGateway, nil) if err != nil { return nil, err } @@ -232,6 +356,11 @@ func getNsxtEdgeGatewayById(client *Client, id string, queryParameters url.Value ErrorEntityNotFound, egw.EdgeGateway.GatewayBacking.GatewayType) } + err = egw.reorderUplinks() + if err != nil { + return nil, fmt.Errorf("error reordering Edge Gateway Uplink after API retrieval") + } + return egw, nil } @@ -255,7 +384,7 @@ func returnSingleNsxtEdgeGateway(name string, allEdges []*NsxtEdgeGateway) (*Nsx // func (vdc *Vdc) GetAllNsxtEdgeGateways(queryParameters url.Values) ([]*NsxtEdgeGateway, error) func getAllNsxtEdgeGateways(client *Client, queryParameters url.Values) ([]*NsxtEdgeGateway, error) { endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGateways - minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + minimumApiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) if err != nil { return nil, err } @@ -266,7 +395,7 @@ func getAllNsxtEdgeGateways(client *Client, queryParameters url.Values) ([]*Nsxt } typeResponses := []*types.OpenAPIEdgeGateway{{}} - err = client.OpenApiGetAllItems(minimumApiVersion, urlRef, queryParameters, &typeResponses) + err = client.OpenApiGetAllItems(minimumApiVersion, urlRef, queryParameters, &typeResponses, nil) if err != nil { return nil, err } @@ -282,6 +411,15 @@ func getAllNsxtEdgeGateways(client *Client, queryParameters url.Values) ([]*Nsxt onlyNsxtEdges := filterOnlyNsxtEdges(wrappedResponses) + // Reorder uplink in all Edge Gateways + for edgeIndex := range onlyNsxtEdges { + err := onlyNsxtEdges[edgeIndex].reorderUplinks() + if err != nil { + return nil, fmt.Errorf("error reordering NSX-T Edge Gateway Uplinks for gateway '%s' ('%s'): %s", + onlyNsxtEdges[edgeIndex].EdgeGateway.Name, onlyNsxtEdges[edgeIndex].EdgeGateway.ID, err) + } + } + return onlyNsxtEdges, nil } @@ -300,3 +438,726 @@ func filterOnlyNsxtEdges(allEdges []*NsxtEdgeGateway) []*NsxtEdgeGateway { return filteredEdges } + +// GetUsedIpAddresses uses dedicated endpoint to retrieve used IP addresses in an Edge Gateway +func (egw *NsxtEdgeGateway) GetUsedIpAddresses(queryParameters url.Values) ([]*types.GatewayUsedIpAddress, error) { + if egw.EdgeGateway == nil || egw.EdgeGateway.ID == "" { + return nil, fmt.Errorf("edge gateway ID must be set to retrieve used IP addresses") + } + client := egw.client + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayUsedIpAddresses + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, egw.EdgeGateway.ID)) + if err != nil { + return nil, err + } + + typeResponse := make([]*types.GatewayUsedIpAddress, 0) + err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParameters, &typeResponse, nil) + if err != nil { + return nil, err + } + + return typeResponse, nil +} + +// GetUnusedExternalIPAddresses will retrieve a requiredIpCount of unused IP addresses for Edge +// Gateway +// Arguments: +// * `requiredIpCount` (how many unuseds IPs should be returned). It will fail and return an +// error if all IPs specified in 'requiredIpCount' cannot be found. +// * `optionalSubnet` is specified (CIDR notation, e.g. 192.168.1.0/24), it will look for an IP in +// this subnet only. +// * `refresh` defines if Edge Gateway structure should be retrieved with latest data before +// performing IP lookup operation +// +// Input and return arguments are using Go's native 'netip' package for IP addressing. This ensures +// correct support for IPv4 and IPv6 IPs. +// `netip.ParseAddr`, `netip.ParsePrefix`, `netip.Addr.String` functions can be used for conversion +// from/to strings +// +// This function performs below listed steps: +// 1. Retrieves a complete list of IPs in Edge Gateway uplinks (returns error if none are found) +// 2. if 'optionalSubnet' was specified - filter IP addresses to only fall into that subnet +// 3. Retrieves all used IP addresses in Edge Gateway using dedicated API endpoint +// 4. Subtracts used IP addresses from available list of IPs in uplink (optionally filtered by optionalSubnet in step 2) +// 5. Checks if 'requiredIpCount' criteria is met, returns error otherwise +// 6. Returns required amount of unused IPs (as defined in 'requiredIpCount') +// +// Notes: +// * This function uses Go's builtin `netip` package to avoid any string processing of IPs and +// supports IPv4 and IPv6 addressing. +// * If an unused IP is not found it will return 'netip.Addr{}' (not using *netip.Addr{} to match +// library semantics) and an error +// * It will return an error if any of uplink IP ranges End IP address is lower than Start IP +// address +func (egw *NsxtEdgeGateway) GetUnusedExternalIPAddresses(requiredIpCount int, optionalSubnet netip.Prefix, refresh bool) ([]netip.Addr, error) { + if refresh { + err := egw.Refresh() + if err != nil { + return nil, fmt.Errorf("error refreshing Edge Gateway: %s", err) + } + } + usedIpAddresses, err := egw.GetUsedIpAddresses(nil) + if err != nil { + return nil, fmt.Errorf("error getting used IP addresses for Edge Gateway: %s", err) + } + + return getUnusedExternalIPAddress(egw.EdgeGateway.EdgeGatewayUplinks, usedIpAddresses, requiredIpCount, optionalSubnet) +} + +// GetAllUnusedExternalIPAddresses will retrieve all unassigned IP addresses for Edge Gateway It is +// similar to GetUnusedExternalIPAddresses but returns all unused IPs instead of a specific amount +// +// Note. In case a very large subnet of IPv6 is present this function might exhaust memory. Please +// use GetUnusedExternalIPAddressesWithCountLimit in such cases +func (egw *NsxtEdgeGateway) GetAllUnusedExternalIPAddresses(refresh bool) ([]netip.Addr, error) { + if refresh { + err := egw.Refresh() + if err != nil { + return nil, fmt.Errorf("error refreshing Edge Gateway: %s", err) + } + } + usedIpAddresses, err := egw.GetUsedIpAddresses(nil) + if err != nil { + return nil, fmt.Errorf("error getting used IP addresses for Edge Gateway: %s", err) + } + + return getAllUnusedExternalIPAddresses(egw.EdgeGateway.EdgeGatewayUplinks, usedIpAddresses, netip.Prefix{}, 0) +} + +// GetUsedAndUnusedExternalIPAddressCountWithLimit will count IPs and can limit their total count up +// to 'limitTo' which can be used to count IPs with huge IPv6 subnets +// +// Return order - usedIpCount, unusedIpCount, error +func (egw *NsxtEdgeGateway) GetUsedAndUnusedExternalIPAddressCountWithLimit(refresh bool, limitTo int64) (int64, int64, error) { + if refresh { + err := egw.Refresh() + if err != nil { + return 0, 0, fmt.Errorf("error refreshing Edge Gateway: %s", err) + } + } + usedIpAddresses, err := egw.GetUsedIpAddresses(nil) + if err != nil { + return 0, 0, fmt.Errorf("error getting used IP addresses for Edge Gateway: %s", err) + } + + assignedIpAddresses, err := flattenEdgeGatewayUplinkToIpSlice(egw.EdgeGateway.EdgeGatewayUplinks, limitTo) + if err != nil { + return 0, 0, fmt.Errorf("error listing all IPs in Edge Gateway: %s", err) + } + + usedIpCount := int64(len(usedIpAddresses)) + assignedIpCount := int64(len(assignedIpAddresses)) + unusedIpCount := assignedIpCount - usedIpCount + + return usedIpCount, unusedIpCount, nil +} + +// GetAllocatedIpCount traverses all subnets in Edge Gateway and returns a count of allocated IP +// count for each subnet in each uplink +func (egw *NsxtEdgeGateway) GetAllocatedIpCount(refresh bool) (int, error) { + if refresh { + err := egw.Refresh() + if err != nil { + return 0, fmt.Errorf("error refreshing Edge Gateway: %s", err) + } + } + + allocatedIpCount := 0 + + for _, uplink := range egw.EdgeGateway.EdgeGatewayUplinks { + for _, subnet := range uplink.Subnets.Values { + if subnet.TotalIPCount != nil { + allocatedIpCount += *subnet.TotalIPCount + } + } + } + + return allocatedIpCount, nil +} + +// GetPrimaryNetworkAllocatedIpCount returns total count of allocated IPs for first NSX-T Edge +// Gateway uplink +func (egw *NsxtEdgeGateway) GetPrimaryNetworkAllocatedIpCount(refresh bool) (int, error) { + if refresh { + err := egw.Refresh() + if err != nil { + return 0, fmt.Errorf("error refreshing Edge Gateway: %s", err) + } + } + + allocatedIpCount := 0 + + for _, subnet := range egw.EdgeGateway.EdgeGatewayUplinks[0].Subnets.Values { + if subnet.TotalIPCount != nil { + allocatedIpCount += *subnet.TotalIPCount + } + } + + return allocatedIpCount, nil +} + +// GetAllocatedIpCountByUplinkType will return a sum of allocated IPs for particular `uplinkType` +// `uplinkType` can be one of 'NSXT_TIER0', 'NSXT_VRF_TIER0', 'IMPORTED_T_LOGICAL_SWITCH' +// +// Note. This function is based on BackingType field and requires at least VCD 10.4.1 +func (egw *NsxtEdgeGateway) GetAllocatedIpCountByUplinkType(refresh bool, uplinkType string) (int, error) { + if egw.client.APIVCDMaxVersionIs("< 37.1") { + return 0, fmt.Errorf("this function requires at least VCD 10.4.1 to work") + } + + if uplinkType != "NSXT_TIER0" && + uplinkType != "IMPORTED_T_LOGICAL_SWITCH" && + uplinkType != "NSXT_VRF_TIER0" { + return 0, fmt.Errorf("invalid 'uplinkType', expected 'NSXT_TIER0', 'IMPORTED_T_LOGICAL_SWITCH' or 'NSXT_VRF_TIER0', got: %s", uplinkType) + } + + if refresh { + err := egw.Refresh() + if err != nil { + return 0, fmt.Errorf("error refreshing Edge Gateway: %s", err) + } + } + + allocatedIpCount := 0 + + for _, uplink := range egw.EdgeGateway.EdgeGatewayUplinks { + // counting IPs only for specific uplink type + if uplink.BackingType != nil && *uplink.BackingType != uplinkType { + continue + } + for _, subnet := range uplink.Subnets.Values { + if subnet.TotalIPCount != nil { + allocatedIpCount += *subnet.TotalIPCount + } + } + } + + return allocatedIpCount, nil +} + +// GetUsedIpAddressSlice retrieves a list of used IP addresses in an Edge Gateway and returns it +// using native Go type '[]netip.Addr' +func (egw *NsxtEdgeGateway) GetUsedIpAddressSlice(refresh bool) ([]netip.Addr, error) { + if refresh { + err := egw.Refresh() + if err != nil { + return nil, fmt.Errorf("error refreshing Edge Gateway: %s", err) + } + } + usedIpAddresses, err := egw.GetUsedIpAddresses(nil) + if err != nil { + return nil, fmt.Errorf("error getting used IP addresses for Edge Gateway: %s", err) + } + + return flattenGatewayUsedIpAddressesToIpSlice(usedIpAddresses) +} + +// QuickDeallocateIpCount refreshes Edge Gateway structure and deallocates specified ipCount from it +// by modifying Uplink structure and calling Update() on it. +// +// Notes: +// * This is a reverse operation to QuickAllocateIpCount and is provided for convenience as the API +// does not support negative values for QuickAddAllocatedIPCount field +// * This function modifies Edge Gateway structure and calls update. To only modify structure, +// please use `NsxtEdgeGateway.DeallocateIpCount` function +func (egw *NsxtEdgeGateway) QuickDeallocateIpCount(ipCount int) (*NsxtEdgeGateway, error) { + if egw.EdgeGateway == nil { + return nil, fmt.Errorf("edge gateway is not initialized") + } + + err := egw.Refresh() + if err != nil { + return nil, fmt.Errorf("error refreshing Edge Gateway: %s", err) + } + + err = egw.DeallocateIpCount(ipCount) + if err != nil { + return nil, fmt.Errorf("error deallocating IP count: %s", err) + } + + return egw.Update(egw.EdgeGateway) +} + +// DeallocateIpCount modifies the structure to deallocate IP addresses from the Edge Gateway +// uplinks. +// +// Notes: +// * This function does not call Update() on the Edge Gateway and it is up to the caller to perform +// this operation (or use NsxtEdgeGateway.QuickDeallocateIpCount which wraps this function and +// performs API call) +// * Use `QuickAddAllocatedIPCount` field in the uplink structure to leverage VCD API directly for +// allocating IP addresses. +func (egw *NsxtEdgeGateway) DeallocateIpCount(deallocateIpCount int) error { + if deallocateIpCount < 0 { + return fmt.Errorf("deallocateIpCount must be greater than 0") + } + + if egw == nil || egw.EdgeGateway == nil { + return fmt.Errorf("edge gateway structure cannot be nil") + } + + edgeGatewayType := egw.EdgeGateway + + for uplinkIndex, uplink := range edgeGatewayType.EdgeGatewayUplinks { + for subnetIndex, subnet := range uplink.Subnets.Values { + + // TotalIPCount is an address of a variable so it needs to be dereferenced for easier arithmetic + // operations. In the end of processing the value is set back to the original location. + singleSubnetTotalIpCount := *edgeGatewayType.EdgeGatewayUplinks[uplinkIndex].Subnets.Values[subnetIndex].TotalIPCount + + if singleSubnetTotalIpCount > 0 { + util.Logger.Printf("[DEBUG] Edge Gateway deallocating IPs from subnet '%s', TotalIPCount '%d', deallocate IP count '%d'", + subnet.Gateway, subnet.TotalIPCount, deallocateIpCount) + + // If a subnet contains more allocated IPs than we need to deallocate - deallocate only what we need + if singleSubnetTotalIpCount >= deallocateIpCount { + singleSubnetTotalIpCount -= deallocateIpCount + + // To make deallocation work one must set this to true + edgeGatewayType.EdgeGatewayUplinks[uplinkIndex].Subnets.Values[subnetIndex].AutoAllocateIPRanges = true + + deallocateIpCount = 0 + } else { // If we have less IPs allocated than we need to deallocate - deallocate all of them + deallocateIpCount -= singleSubnetTotalIpCount + singleSubnetTotalIpCount = 0 + edgeGatewayType.EdgeGatewayUplinks[uplinkIndex].Subnets.Values[subnetIndex].AutoAllocateIPRanges = true // To make deallocation work one must set this to true + util.Logger.Printf("[DEBUG] Edge Gateway IP count after partial deallocation %d", edgeGatewayType.EdgeGatewayUplinks[uplinkIndex].Subnets.Values[subnetIndex].TotalIPCount) + } + } + + // Setting value back to original location after all operations + edgeGatewayType.EdgeGatewayUplinks[uplinkIndex].Subnets.Values[subnetIndex].TotalIPCount = &singleSubnetTotalIpCount + util.Logger.Printf("[DEBUG] Edge Gateway IP count after complete deallocation %d", edgeGatewayType.EdgeGatewayUplinks[uplinkIndex].Subnets.Values[subnetIndex].TotalIPCount) + + if deallocateIpCount == 0 { + break + } + } + } + + if deallocateIpCount > 0 { + return fmt.Errorf("not enough IPs allocated to deallocate requested '%d' IPs", deallocateIpCount) + } + + return nil +} + +// GetQoS retrieves QoS (rate limiting) configuration for an NSX-T Edge Gateway +func (egw *NsxtEdgeGateway) GetQoS() (*types.NsxtEdgeGatewayQos, error) { + if egw.EdgeGateway == nil || egw.client == nil || egw.EdgeGateway.ID == "" { + return nil, fmt.Errorf("cannot get QoS for NSX-T Edge Gateway without ID") + } + + client := egw.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayQos + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, egw.EdgeGateway.ID)) + if err != nil { + return nil, err + } + + qos := &types.NsxtEdgeGatewayQos{} + err = client.OpenApiGetItem(apiVersion, urlRef, nil, qos, nil) + if err != nil { + return nil, err + } + + return qos, nil +} + +// UpdateQoS updates QoS (rate limiting) configuration for an NSX-T Edge Gateway +func (egw *NsxtEdgeGateway) UpdateQoS(qosConfig *types.NsxtEdgeGatewayQos) (*types.NsxtEdgeGatewayQos, error) { + if egw.EdgeGateway == nil || egw.client == nil || egw.EdgeGateway.ID == "" { + return nil, fmt.Errorf("cannot update QoS for NSX-T Edge Gateway without ID") + } + + client := egw.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayQos + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, egw.EdgeGateway.ID)) + if err != nil { + return nil, err + } + + // update QoS with given qosConfig + updatedQos := &types.NsxtEdgeGatewayQos{} + err = client.OpenApiPutItem(apiVersion, urlRef, nil, qosConfig, updatedQos, nil) + if err != nil { + return nil, err + } + + return updatedQos, nil +} + +// GetDhcpForwarder gets DHCP forwarder configuration for an NSX-T Edge Gateway +func (egw *NsxtEdgeGateway) GetDhcpForwarder() (*types.NsxtEdgeGatewayDhcpForwarder, error) { + if egw.EdgeGateway == nil || egw.client == nil || egw.EdgeGateway.ID == "" { + return nil, fmt.Errorf("cannot get DHCP forwarder for NSX-T Edge Gateway without ID") + } + + client := egw.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayDhcpForwarder + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, egw.EdgeGateway.ID)) + if err != nil { + return nil, err + } + + dhcpForwarder := &types.NsxtEdgeGatewayDhcpForwarder{} + err = client.OpenApiGetItem(apiVersion, urlRef, nil, dhcpForwarder, nil) + if err != nil { + return nil, err + } + + return dhcpForwarder, nil +} + +// UpdateDhcpForwarder updates DHCP forwarder configuration for an NSX-T Edge Gateway +func (egw *NsxtEdgeGateway) UpdateDhcpForwarder(dhcpForwarderConfig *types.NsxtEdgeGatewayDhcpForwarder) (*types.NsxtEdgeGatewayDhcpForwarder, error) { + if egw.EdgeGateway == nil || egw.client == nil || egw.EdgeGateway.ID == "" { + return nil, fmt.Errorf("cannot update DHCP forwarder for NSX-T Edge Gateway without ID") + } + + client := egw.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayDhcpForwarder + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, egw.EdgeGateway.ID)) + if err != nil { + return nil, err + } + + // update DHCP forwarder with given dhcpForwarderConfig + updatedDhcpForwarder, err := egw.GetDhcpForwarder() + if err != nil { + return nil, err + } + dhcpForwarderConfig.Version = updatedDhcpForwarder.Version + + err = client.OpenApiPutItem(apiVersion, urlRef, nil, dhcpForwarderConfig, updatedDhcpForwarder, nil) + if err != nil { + return nil, err + } + + return updatedDhcpForwarder, nil +} + +// GetSlaacProfile gets SLAAC (Stateless Address Autoconfiguration) Profile configuration for an +// NSX-T Edge Gateway. +// Note. It represents DHCPv6 Edge Gateway configuration in UI +func (egw *NsxtEdgeGateway) GetSlaacProfile() (*types.NsxtEdgeGatewaySlaacProfile, error) { + if egw.EdgeGateway == nil || egw.client == nil || egw.EdgeGateway.ID == "" { + return nil, fmt.Errorf("cannot get SLAAC Profile for NSX-T Edge Gateway without ID") + } + + client := egw.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewaySlaacProfile + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, egw.EdgeGateway.ID)) + if err != nil { + return nil, err + } + + slaacProfile := &types.NsxtEdgeGatewaySlaacProfile{} + err = client.OpenApiGetItem(apiVersion, urlRef, nil, slaacProfile, nil) + if err != nil { + return nil, err + } + + return slaacProfile, nil +} + +// UpdateSlaacProfile creates a SLAAC (Stateless Address Autoconfiguration) profile or updates the +// existing one if it already exists. +// Note. It represents DHCPv6 Edge Gateway configuration in UI +func (egw *NsxtEdgeGateway) UpdateSlaacProfile(slaacProfileConfig *types.NsxtEdgeGatewaySlaacProfile) (*types.NsxtEdgeGatewaySlaacProfile, error) { + if egw.EdgeGateway == nil || egw.client == nil || egw.EdgeGateway.ID == "" { + return nil, fmt.Errorf("cannot update SLAAC Profile for NSX-T Edge Gateway without ID") + } + + client := egw.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewaySlaacProfile + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, egw.EdgeGateway.ID)) + if err != nil { + return nil, err + } + + updatedSlaacProfile := &types.NsxtEdgeGatewaySlaacProfile{} + err = client.OpenApiPutItem(apiVersion, urlRef, nil, slaacProfileConfig, updatedSlaacProfile, nil) + if err != nil { + return nil, err + } + + return updatedSlaacProfile, nil +} + +func getAllUnusedExternalIPAddresses(uplinks []types.EdgeGatewayUplinks, usedIpAddresses []*types.GatewayUsedIpAddress, optionalSubnet netip.Prefix, limitTo int64) ([]netip.Addr, error) { + // 1. Flatten all IP ranges in Edge Gateway using Go's native 'netip.Addr' IP container instead + // of plain strings because it is more robust (supports IPv4 and IPv6 and also comparison + // operator) + assignedIpSlice, err := flattenEdgeGatewayUplinkToIpSlice(uplinks, limitTo) + if err != nil { + return nil, fmt.Errorf("error listing all IPs in Edge Gateway: %s", err) + } + + if len(assignedIpSlice) == 0 { + return nil, fmt.Errorf("no IPs found in Edge Gateway configuration") + } + + // 2. Optionally filter given IP ranges by optionalSubnet value (if specified) + if optionalSubnet != (netip.Prefix{}) { + assignedIpSlice, err = filterIpSlicesBySubnet(assignedIpSlice, optionalSubnet) + if err != nil { + return nil, fmt.Errorf("error filtering ranges for given subnet '%s': %s", optionalSubnet, err) + } + } + + // 3. Get Used IP addresses in Edge Gateway in the same slice format + usedIpSlice, err := flattenGatewayUsedIpAddressesToIpSlice(usedIpAddresses) + if err != nil { + return nil, fmt.Errorf("could not flatten Edge Gateway used IP addresses: %s", err) + } + + // 4. Get all unused IPs + // (allIPs - allUsedIPs) = allUnusedIPs + unusedIps := ipSliceDifference(assignedIpSlice, usedIpSlice) + + return unusedIps, nil +} + +func getUnusedExternalIPAddress(uplinks []types.EdgeGatewayUplinks, usedIpAddresses []*types.GatewayUsedIpAddress, requiredIpCount int, optionalSubnet netip.Prefix) ([]netip.Addr, error) { + unusedIps, err := getAllUnusedExternalIPAddresses(uplinks, usedIpAddresses, optionalSubnet, 0) + if err != nil { + return nil, fmt.Errorf("error getting all unused IPs: %s", err) + } + + // 5. Check if 'requiredIpCount' criteria is met + if len(unusedIps) < requiredIpCount { + return nil, fmt.Errorf("not enough unused IPs found. Expected %d, got %d", requiredIpCount, len(unusedIps)) + } + + // 6. Return required amount of unused IPs + return unusedIps[:requiredIpCount], nil +} + +// flattenEdgeGatewayUplinkToIpSlice processes Edge Gateway Uplink structure and creates a slice of +// all available IPs +// Note. Having a huge IPv6 block might become a long running task and potentially exhaust system +// memory. One can use 'limitTo' setting to set upper limit for number of IPs that one wants to +// retrieve. Setting `limitTo` to 0 means that not limitation is applied. +func flattenEdgeGatewayUplinkToIpSlice(uplinks []types.EdgeGatewayUplinks, limitTo int64) ([]netip.Addr, error) { + start := time.Now() + util.Logger.Printf("[TRACE] flattenEdgeGatewayUplinkToIpSlice starting at %s with limitTo %d", start.String(), limitTo) + util.Logger.Printf("[TRACE] flattenEdgeGatewayUplinkToIpSlice Edge Gateway uplink count %d", len(uplinks)) + assignedIpSlice := make([]netip.Addr, 0) + + var counter int64 + + for _, edgeGatewayUplink := range uplinks { + for _, edgeGatewayUplinkSubnet := range edgeGatewayUplink.Subnets.Values { + for _, r := range edgeGatewayUplinkSubnet.IPRanges.Values { + // Convert IPs to netip.Addr + startIp, err := netip.ParseAddr(r.StartAddress) + if err != nil { + return nil, fmt.Errorf("error parsing start IP address in range '%s': %s", r.StartAddress, err) + } + + // if we have end address specified - a range of IPs must be expanded into slice + // with all IPs in that range + if r.EndAddress != "" { + endIp, err := netip.ParseAddr(r.EndAddress) + if err != nil { + return nil, fmt.Errorf("error parsing end IP address in range '%s': %s", r.EndAddress, err) + } + + // Check if EndAddress is lower than StartAddress ant report an error if so + if endIp.Less(startIp) { + return nil, fmt.Errorf("end IP is lower that start IP (%s < %s)", r.EndAddress, r.StartAddress) + } + + // loop over IPs in range from startIp to endIp and add them to the slice one by one + // Expression 'ip.Compare(endIp) == 1' means that 'ip > endIp' and the loop should stop + for ip := startIp; ip.Compare(endIp) != 1; ip = ip.Next() { + assignedIpSlice = append(assignedIpSlice, ip) + counter++ + if limitTo != 0 && counter >= limitTo { + util.Logger.Printf("[TRACE] flattenEdgeGatewayUplinkToIpSlice hit limitTo %d at %s with IP range", limitTo, time.Since(start)) + return assignedIpSlice, nil + } + } + } else { // if there is no end address in the range, then it is only a single IP - startIp + assignedIpSlice = append(assignedIpSlice, startIp) + counter++ + if limitTo != 0 && counter >= limitTo { + util.Logger.Printf("[TRACE] flattenEdgeGatewayUplinkToIpSlice hit limitTo %d at %s with single IP", limitTo, time.Since(start)) + return assignedIpSlice, nil + } + } + } + } + } + util.Logger.Printf("[TRACE] flattenEdgeGatewayUplinkToIpSlice finished %s", time.Since(start)) + + return assignedIpSlice, nil +} + +// ipSliceDifference performs mathematical subtraction for two slices of IPs +// The formula is (minuend − subtrahend = difference) +// +// Special behavior: +// * Passing nil minuend results in nil +// * Passing nil subtrahend will return minuendSlice +// +// NOTE. This function will mutate minuendSlice to save memory and avoid having a copy of all values +// which can become expensive if there are a lot of items +func ipSliceDifference(minuendSlice, subtrahendSlice []netip.Addr) []netip.Addr { + if minuendSlice == nil { + return nil + } + + if subtrahendSlice == nil { + return minuendSlice + } + + // Removal of elements from an empty slice results in an empty slice + if len(minuendSlice) == 0 { + return []netip.Addr{} + } + // Having an empty subtrahendSlice results in minuendSlice + if len(subtrahendSlice) == 0 { + return minuendSlice + } + + resultIpCount := 0 // count of IPs after removing items from subtrahendSlice + + // Loop over minuend IPs + for _, minuendIp := range minuendSlice { + + // Check if subtrahend has minuend element listed + var foundSubtrahend bool + for _, subtrahendIp := range subtrahendSlice { + if subtrahendIp == minuendIp { + // IP found in subtrahend, therefore breaking inner loop early + foundSubtrahend = true + break + } + } + + // Store the IP in `minuendSlice` at `resultIpCount` index and increment the index itself + if !foundSubtrahend { + // Add IP to the 'resultIpCount' index position + minuendSlice[resultIpCount] = minuendIp + resultIpCount++ + } + } + + // if all elements are removed - return nil + if resultIpCount == 0 { + return nil + } + + // cut off all values, greater than `resultIpCount` + minuendSlice = minuendSlice[:resultIpCount] + + return minuendSlice +} + +// filterIpSlicesBySubnet accepts 'ipRange' and returns a slice of IPs only that fall into given +// 'subnet' leaving everything out +// +// Special behavior: +// * Passing empty 'subnet' will return `nil` and an error +// * Passing empty 'ipRange' will return 'nil' and an error +// +// Note. This function does not enforce uniqueness of IPs in 'ipRange' and if there are duplicate +// IPs matching 'subnet' they will be in the resulting slice +func filterIpSlicesBySubnet(ipRange []netip.Addr, subnet netip.Prefix) ([]netip.Addr, error) { + if subnet == (netip.Prefix{}) { + return nil, fmt.Errorf("empty subnet specified") + } + + if len(ipRange) == 0 { + return nil, fmt.Errorf("empty IP Range specified") + } + + filteredRange := make([]netip.Addr, 0) + + for _, ip := range ipRange { + if subnet.Contains(ip) { + filteredRange = append(filteredRange, ip) + } + } + + return filteredRange, nil +} + +// flattenGatewayUsedIpAddressesToIpSlice accepts a slice of `GatewayUsedIpAddress` coming directly +// from the API and converts it to slice of Go's native '[]netip.Addr' which supports IPv4 and IPv6 +func flattenGatewayUsedIpAddressesToIpSlice(usedIpAddresses []*types.GatewayUsedIpAddress) ([]netip.Addr, error) { + usedIpSlice := make([]netip.Addr, len(usedIpAddresses)) + for usedIpIndex := range usedIpAddresses { + ip, err := netip.ParseAddr(usedIpAddresses[usedIpIndex].IPAddress) + if err != nil { + return nil, fmt.Errorf("error parsing IP '%s' in Edge Gateway used IP address list: %s", usedIpAddresses[usedIpIndex].IPAddress, err) + } + usedIpSlice[usedIpIndex] = ip + } + + return usedIpSlice, nil +} + +func reorderEdgeGatewayUplinks(edgeGatewayUplinks []types.EdgeGatewayUplinks) []types.EdgeGatewayUplinks { + // If only 1 uplink is present - there is nothing to reorder, because only mandatory uplink is present + if len(edgeGatewayUplinks) == 1 { + return edgeGatewayUplinks + } + + // Element 0 is External Network backed by Tier 0 Gateway or T0 Gateway VRF - nothing to do + if edgeGatewayUplinks[0].BackingType != nil && (*edgeGatewayUplinks[0].BackingType == "NSXT_TIER0" || *edgeGatewayUplinks[0].BackingType == "NSXT_VRF_TIER0") { + return edgeGatewayUplinks + } + + for uplinkIndex := range edgeGatewayUplinks { + if edgeGatewayUplinks[uplinkIndex].BackingType != nil && (*edgeGatewayUplinks[uplinkIndex].BackingType == "NSXT_TIER0" || *edgeGatewayUplinks[uplinkIndex].BackingType == "NSXT_VRF_TIER0") { + // Swap elements so that 'NSXT_TIER0' or 'NSXT_VRF_TIER0' is at position 0 + t0BackedUplink := edgeGatewayUplinks[uplinkIndex] + edgeGatewayUplinks[uplinkIndex] = edgeGatewayUplinks[0] + edgeGatewayUplinks[0] = t0BackedUplink + + return edgeGatewayUplinks + } + } + + return edgeGatewayUplinks +} diff --git a/govcd/nsxt_edgegateway_bgp_configuration.go b/govcd/nsxt_edgegateway_bgp_configuration.go new file mode 100644 index 000000000..931bb446f --- /dev/null +++ b/govcd/nsxt_edgegateway_bgp_configuration.go @@ -0,0 +1,85 @@ +/* + * Copyright 2022 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +// GetBgpConfiguration retrieves BGP Configuration for NSX-T Edge Gateway +func (egw *NsxtEdgeGateway) GetBgpConfiguration() (*types.EdgeBgpConfig, error) { + client := egw.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeBgpConfig + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + // Insert Edge Gateway ID into endpoint path "edgeGateways/%s/routing/bgp" + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, egw.EdgeGateway.ID)) + if err != nil { + return nil, err + } + + returnObject := &types.EdgeBgpConfig{} + + err = client.OpenApiGetItem(apiVersion, urlRef, nil, returnObject, nil) + if err != nil { + return nil, fmt.Errorf("error retrieving NSX-T Edge Gateway BGP Configuration: %s", err) + } + + return returnObject, nil +} + +// UpdateBgpConfiguration updates BGP configuration on NSX-T Edge Gateway +// +// Note. Update of BGP configuration requires version to be specified in 'Version' field. This +// function automatically handles it. +func (egw *NsxtEdgeGateway) UpdateBgpConfiguration(bgpConfig *types.EdgeBgpConfig) (*types.EdgeBgpConfig, error) { + client := egw.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeBgpConfig + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + // Insert Edge Gateway ID into endpoint path + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, egw.EdgeGateway.ID)) + if err != nil { + return nil, err + } + + // Update of BGP config requires version to be specified. This function automatically handles it. + existingBgpConfig, err := egw.GetBgpConfiguration() + if err != nil { + return nil, fmt.Errorf("error getting NSX-T Edge Gateway BGP Configuration: %s", err) + } + bgpConfig.Version = existingBgpConfig.Version + + returnObject := &types.EdgeBgpConfig{} + + err = client.OpenApiPutItem(apiVersion, urlRef, nil, bgpConfig, returnObject, nil) + if err != nil { + return nil, fmt.Errorf("error setting NSX-T Edge Gateway BGP Configuration: %s", err) + } + + return returnObject, nil +} + +// DisableBgpConfiguration performs an `UpdateBgpConfiguration` and preserve all field values as +// they were, but explicitly sets Enabled to false. +func (egw *NsxtEdgeGateway) DisableBgpConfiguration() error { + // Get existing BGP configuration so that when disabling it - other settings remain as they are + bgpConfig, err := egw.GetBgpConfiguration() + if err != nil { + return fmt.Errorf("error retrieving BGP configuration: %s", err) + } + bgpConfig.Enabled = false + + _, err = egw.UpdateBgpConfiguration(bgpConfig) + return err +} diff --git a/govcd/nsxt_edgegateway_bgp_configuration_test.go b/govcd/nsxt_edgegateway_bgp_configuration_test.go new file mode 100644 index 000000000..62ea29a6a --- /dev/null +++ b/govcd/nsxt_edgegateway_bgp_configuration_test.go @@ -0,0 +1,70 @@ +//go:build network || nsxt || functional || openapi || ALL + +package govcd + +import ( + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_NsxEdgeBgpConfiguration(check *C) { + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointEdgeBgpConfig) + vcd.skipIfNotSysAdmin(check) + + org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + nsxtVdc, err := org.GetVDCByName(vcd.config.VCD.Nsxt.Vdc, false) + check.Assert(err, IsNil) + edge, err := nsxtVdc.GetNsxtEdgeGatewayByName(vcd.config.VCD.Nsxt.EdgeGateway) + check.Assert(err, IsNil) + + // Switch Edge Gateway to use dedicated uplink for the time of this test and then turn it off + err = switchEdgeGatewayDedication(edge, true) // Turn on Dedicated Tier 0 gateway + check.Assert(err, IsNil) + defer switchEdgeGatewayDedication(edge, false) // Turn off Dedicated Tier 0 gateway + + // Get and store existing BGP configuration + bgpConfig, err := edge.GetBgpConfiguration() + check.Assert(err, IsNil) + check.Assert(bgpConfig, NotNil) + + // Disable BGP + err = edge.DisableBgpConfiguration() + check.Assert(err, IsNil) + + newBgpConfig := &types.EdgeBgpConfig{ + Enabled: true, + Ecmp: true, + LocalASNumber: "65420", + GracefulRestart: &types.EdgeBgpGracefulRestartConfig{ + StaleRouteTimer: 190, + RestartTimer: 600, + Mode: "HELPER_ONLY", + }, + } + + updatedBgpConfig, err := edge.UpdateBgpConfiguration(newBgpConfig) + check.Assert(err, IsNil) + check.Assert(updatedBgpConfig, NotNil) + + newBgpConfig.Version = updatedBgpConfig.Version // Version is constantly iterated and cant be checked + check.Assert(updatedBgpConfig, DeepEquals, newBgpConfig) + + // Check "disable" function which keeps all fields the same, but sets "Enabled: false" + err = edge.DisableBgpConfiguration() + check.Assert(err, IsNil) + + bgpConfigAfterDisabling, err := edge.GetBgpConfiguration() + check.Assert(err, IsNil) + check.Assert(bgpConfig, NotNil) + check.Assert(bgpConfigAfterDisabling.Enabled, Equals, false) + +} + +func switchEdgeGatewayDedication(edge *NsxtEdgeGateway, isDedicated bool) error { + edge.EdgeGateway.EdgeGatewayUplinks[0].Dedicated = isDedicated + _, err := edge.Update(edge.EdgeGateway) + + return err +} diff --git a/govcd/nsxt_edgegateway_bgp_ip_prefix_list.go b/govcd/nsxt_edgegateway_bgp_ip_prefix_list.go new file mode 100644 index 000000000..01aaac74f --- /dev/null +++ b/govcd/nsxt_edgegateway_bgp_ip_prefix_list.go @@ -0,0 +1,246 @@ +/* + * Copyright 2022 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" +) + +// EdgeBgpIpPrefixList helps to configure BGP IP Prefix Lists in NSX-T Edge Gateways +type EdgeBgpIpPrefixList struct { + EdgeBgpIpPrefixList *types.EdgeBgpIpPrefixList + client *Client + // edgeGatewayId is stored for usage in EdgeBgpIpPrefixList receiver functions + edgeGatewayId string +} + +// CreateBgpIpPrefixList creates a BGP IP Prefix List with supplied configuration +// +// Note. VCD 10.2 versions do not automatically return ID for created BGP IP Prefix List. To work around it this code +// automatically retrieves the entity by Name after the task is finished. This function may fail on VCD 10.2 if +// duplicate BGP IP Prefix Lists exist. +func (egw *NsxtEdgeGateway) CreateBgpIpPrefixList(bgpIpPrefixList *types.EdgeBgpIpPrefixList) (*EdgeBgpIpPrefixList, error) { + client := egw.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeBgpConfigPrefixLists + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + // Insert Edge Gateway ID into endpoint path + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, egw.EdgeGateway.ID)) + if err != nil { + return nil, err + } + + returnObject := &EdgeBgpIpPrefixList{ + client: egw.client, + edgeGatewayId: egw.EdgeGateway.ID, + EdgeBgpIpPrefixList: &types.EdgeBgpIpPrefixList{}, + } + + task, err := client.OpenApiPostItemAsync(apiVersion, urlRef, nil, bgpIpPrefixList) + if err != nil { + return nil, fmt.Errorf("error creating NSX-T Edge Gateway BGP IP Prefix List: %s", err) + } + + err = task.WaitTaskCompletion() + if err != nil { + return nil, fmt.Errorf("error creating NSX-T Edge Gateway BGP IP Prefix List: %s", err) + } + + // API is not consistent across different versions therefore explicit manual handling is + // required to lookup newly created object + // + // VCD 10.2 -> no ID for newly created object is returned at all + // VCD 10.3 -> `Details` field in task contains ID of newly created object + // To cover all cases this code will at first look for ID in `Details` field and fall back to + // lookup by name if `Details` field is empty. + // + // The drawback of this is that it is possible to create duplicate records with the same name on + // VCD versions that don't return IDs, but there is no better way for VCD versions that don't + // return IDs for created objects + + bgpIpPrefixListId := task.Task.Details + if bgpIpPrefixListId != "" { + getUrlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, egw.EdgeGateway.ID), bgpIpPrefixListId) + if err != nil { + return nil, err + } + err = client.OpenApiGetItem(apiVersion, getUrlRef, nil, returnObject.EdgeBgpIpPrefixList, nil) + if err != nil { + return nil, fmt.Errorf("error retrieving NSX-T Edge Gateway BGP IP Prefix List after creation: %s", err) + } + } else { + // ID after object creation was not returned therefore retrieving the entity by Name to lookup ID + // This has a risk of duplicate items, but is the only way to find the object when ID is not returned + bgpIpPrefixList, err := egw.GetBgpIpPrefixListByName(bgpIpPrefixList.Name) + if err != nil { + return nil, fmt.Errorf("error retrieving NSX-T Edge Gateway BGP IP Prefix List after creation: %s", err) + } + returnObject = bgpIpPrefixList + } + + return returnObject, nil +} + +// GetAllBgpIpPrefixLists retrieves all BGP IP Prefix Lists in a given NSX-T Edge Gateway with optional queryParameters +func (egw *NsxtEdgeGateway) GetAllBgpIpPrefixLists(queryParameters url.Values) ([]*EdgeBgpIpPrefixList, error) { + queryParams := copyOrNewUrlValues(queryParameters) + + client := egw.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeBgpConfigPrefixLists + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + // Insert Edge Gateway ID into endpoint path + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, egw.EdgeGateway.ID)) + if err != nil { + return nil, err + } + + typeResponses := []*types.EdgeBgpIpPrefixList{{}} + err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParams, &typeResponses, nil) + if err != nil { + return nil, err + } + + // Wrap all typeResponses into NsxtNatRule types with client + wrappedResponses := make([]*EdgeBgpIpPrefixList, len(typeResponses)) + for sliceIndex := range typeResponses { + wrappedResponses[sliceIndex] = &EdgeBgpIpPrefixList{ + EdgeBgpIpPrefixList: typeResponses[sliceIndex], + client: client, + edgeGatewayId: egw.EdgeGateway.ID, + } + } + + return wrappedResponses, nil +} + +// GetBgpIpPrefixListByName retrieves BGP IP Prefix List By Name +// It is meant to retrieve exactly one entry: +// * Will fail if more than one entry with the same name found +// * Will return an error containing `ErrorEntityNotFound` if no entries are found +// +// Note. API does not support filtering by 'name' field therefore filtering is performed on client +// side +func (egw *NsxtEdgeGateway) GetBgpIpPrefixListByName(name string) (*EdgeBgpIpPrefixList, error) { + if name == "" { + return nil, fmt.Errorf("name cannot be empty") + } + + allBgpIpPrefixLists, err := egw.GetAllBgpIpPrefixLists(nil) + if err != nil { + return nil, fmt.Errorf("error retrieving NSX-T Edge Gateway BGP IP Prefix List: %s", err) + } + + var filteredBgpIpPrefixLists []*EdgeBgpIpPrefixList + for _, bgpIpPrefixList := range allBgpIpPrefixLists { + if bgpIpPrefixList.EdgeBgpIpPrefixList.Name == name { + filteredBgpIpPrefixLists = append(filteredBgpIpPrefixLists, bgpIpPrefixList) + } + } + + if len(filteredBgpIpPrefixLists) > 1 { + return nil, fmt.Errorf("more than one NSX-T Edge Gateway BGP IP Prefix List found with Name '%s'", name) + } + + if len(filteredBgpIpPrefixLists) == 0 { + return nil, fmt.Errorf("%s: no NSX-T Edge Gateway BGP IP Prefix List found with name '%s'", ErrorEntityNotFound, name) + } + + return filteredBgpIpPrefixLists[0], nil +} + +// GetBgpIpPrefixListById retrieves BGP IP Prefix List By ID +func (egw *NsxtEdgeGateway) GetBgpIpPrefixListById(id string) (*EdgeBgpIpPrefixList, error) { + if id == "" { + return nil, fmt.Errorf("id is required") + } + + client := egw.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeBgpConfigPrefixLists + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + // Insert Edge Gateway ID into endpoint path + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, egw.EdgeGateway.ID), id) + if err != nil { + return nil, err + } + + returnObject := &EdgeBgpIpPrefixList{ + client: egw.client, + edgeGatewayId: egw.EdgeGateway.ID, + EdgeBgpIpPrefixList: &types.EdgeBgpIpPrefixList{}, + } + + err = client.OpenApiGetItem(apiVersion, urlRef, nil, returnObject.EdgeBgpIpPrefixList, nil) + if err != nil { + return nil, fmt.Errorf("error retrieving NSX-T Edge Gateway BGP IP Prefix List: %s", err) + } + + return returnObject, nil +} + +// Update updates existing BGP IP Prefix List with new configuration and returns it +func (bgpIpPrefixListCfg *EdgeBgpIpPrefixList) Update(bgpIpPrefixList *types.EdgeBgpIpPrefixList) (*EdgeBgpIpPrefixList, error) { + client := bgpIpPrefixListCfg.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeBgpConfigPrefixLists + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + // Insert Edge Gateway ID into endpoint path + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, bgpIpPrefixListCfg.edgeGatewayId), bgpIpPrefixList.ID) + if err != nil { + return nil, err + } + + returnObject := &EdgeBgpIpPrefixList{ + client: bgpIpPrefixListCfg.client, + edgeGatewayId: bgpIpPrefixListCfg.edgeGatewayId, + EdgeBgpIpPrefixList: &types.EdgeBgpIpPrefixList{}, + } + + err = client.OpenApiPutItem(apiVersion, urlRef, nil, bgpIpPrefixList, returnObject.EdgeBgpIpPrefixList, nil) + if err != nil { + return nil, fmt.Errorf("error setting NSX-T Edge Gateway BGP IP Prefix List: %s", err) + } + + return returnObject, nil +} + +// Delete deletes existing BGP IP Prefix List +func (bgpIpPrefixListCfg *EdgeBgpIpPrefixList) Delete() error { + client := bgpIpPrefixListCfg.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeBgpConfigPrefixLists + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return err + } + + // Insert Edge Gateway ID into endpoint path + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, bgpIpPrefixListCfg.edgeGatewayId), bgpIpPrefixListCfg.EdgeBgpIpPrefixList.ID) + if err != nil { + return err + } + + err = client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) + if err != nil { + return fmt.Errorf("error deleting NSX-T Edge Gateway BGP IP Prefix List: %s", err) + } + + return nil +} diff --git a/govcd/nsxt_edgegateway_bgp_ip_prefix_list_test.go b/govcd/nsxt_edgegateway_bgp_ip_prefix_list_test.go new file mode 100644 index 000000000..1095947ef --- /dev/null +++ b/govcd/nsxt_edgegateway_bgp_ip_prefix_list_test.go @@ -0,0 +1,91 @@ +//go:build network || nsxt || functional || openapi || ALL + +package govcd + +import ( + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +// Test_NsxEdgeBgpIpPrefixList tests CRUD operations for NSX-T Edge Gateway BGP IP Prefix Lists +func (vcd *TestVCD) Test_NsxEdgeBgpIpPrefixList(check *C) { + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointEdgeBgpConfigPrefixLists) + vcd.skipIfNotSysAdmin(check) + + org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + nsxtVdc, err := org.GetVDCByName(vcd.config.VCD.Nsxt.Vdc, false) + check.Assert(err, IsNil) + edge, err := nsxtVdc.GetNsxtEdgeGatewayByName(vcd.config.VCD.Nsxt.EdgeGateway) + check.Assert(err, IsNil) + + // Switch Edge Gateway to use dedicated uplink for the time of this test and then turn it off + err = switchEdgeGatewayDedication(edge, true) // Turn on Dedicated Tier 0 gateway + check.Assert(err, IsNil) + defer switchEdgeGatewayDedication(edge, false) // Turn off Dedicated Tier 0 gateway + + // Create a new BGP IP Prefix List + bgpIpPrefixList := &types.EdgeBgpIpPrefixList{ + Name: check.TestName(), + Description: "test-description", + Prefixes: []types.EdgeBgpConfigPrefixListPrefixes{ + { + Network: "1.1.1.0/24", + Action: "PERMIT", + }, + { + Network: "2.1.0.0/16", + Action: "PERMIT", + LessThanEqualTo: 29, + GreaterThanEqualTo: 24, + }, + }, + } + + bgpIpPrefix, err := edge.CreateBgpIpPrefixList(bgpIpPrefixList) + check.Assert(err, IsNil) + check.Assert(bgpIpPrefix, NotNil) + + // Get all BGP IP Prefix Lists + bgpIpPrefixLists, err := edge.GetAllBgpIpPrefixLists(nil) + check.Assert(err, IsNil) + check.Assert(bgpIpPrefixLists, NotNil) + check.Assert(len(bgpIpPrefixLists), Equals, 1) + check.Assert(bgpIpPrefixLists[0].EdgeBgpIpPrefixList.Name, Equals, bgpIpPrefixList.Name) + + // Get By Name + bgpPrefixListByName, err := edge.GetBgpIpPrefixListByName(bgpIpPrefixList.Name) + check.Assert(err, IsNil) + check.Assert(bgpPrefixListByName, NotNil) + + // Get By Id + bgpPrefixListById, err := edge.GetBgpIpPrefixListById(bgpIpPrefix.EdgeBgpIpPrefixList.ID) + check.Assert(err, IsNil) + check.Assert(bgpPrefixListById, NotNil) + + // Update + bgpIpPrefixList.Name = check.TestName() + "-updated" + bgpIpPrefixList.Description = "test-description-updated" + bgpIpPrefixList.ID = bgpIpPrefixLists[0].EdgeBgpIpPrefixList.ID + + updatedBgpIpPrefixList, err := bgpIpPrefixLists[0].Update(bgpIpPrefixList) + check.Assert(err, IsNil) + check.Assert(updatedBgpIpPrefixList, NotNil) + + check.Assert(updatedBgpIpPrefixList.EdgeBgpIpPrefixList.ID, Equals, bgpIpPrefixLists[0].EdgeBgpIpPrefixList.ID) + + // Delete + err = bgpIpPrefixLists[0].Delete() + check.Assert(err, IsNil) + + // Try to get once again and ensure it is not there + notFoundByName, err := edge.GetBgpIpPrefixListByName(bgpIpPrefixList.Name) + check.Assert(err, NotNil) + check.Assert(notFoundByName, IsNil) + + notFoundById, err := edge.GetBgpIpPrefixListById(bgpIpPrefix.EdgeBgpIpPrefixList.ID) + check.Assert(err, NotNil) + check.Assert(notFoundById, IsNil) + +} diff --git a/govcd/nsxt_edgegateway_bgp_neighbor.go b/govcd/nsxt_edgegateway_bgp_neighbor.go new file mode 100644 index 000000000..60bfc72ce --- /dev/null +++ b/govcd/nsxt_edgegateway_bgp_neighbor.go @@ -0,0 +1,239 @@ +/* + * Copyright 2022 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" +) + +// EdgeBgpNeighbor represents NSX-T Edge Gateway BGP Neighbor +type EdgeBgpNeighbor struct { + EdgeBgpNeighbor *types.EdgeBgpNeighbor + client *Client + // edgeGatewayId is stored for usage in EdgeBgpNeighbor receiver functions + edgeGatewayId string +} + +// CreateBgpNeighbor creates BGP Neighbor with the given configuration +func (egw *NsxtEdgeGateway) CreateBgpNeighbor(bgpNeighborConfig *types.EdgeBgpNeighbor) (*EdgeBgpNeighbor, error) { + client := egw.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeBgpNeighbor + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + // Insert Edge Gateway ID into endpoint path + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, egw.EdgeGateway.ID)) + if err != nil { + return nil, err + } + + returnObject := &EdgeBgpNeighbor{ + client: egw.client, + edgeGatewayId: egw.EdgeGateway.ID, + EdgeBgpNeighbor: &types.EdgeBgpNeighbor{}, + } + + task, err := client.OpenApiPostItemAsync(apiVersion, urlRef, nil, bgpNeighborConfig) + if err != nil { + return nil, fmt.Errorf("error creating NSX-T Edge Gateway BGP Neighbor: %s", err) + } + + err = task.WaitTaskCompletion() + if err != nil { + return nil, fmt.Errorf("error creating NSX-T Edge Gateway BGP Neighbor: %s", err) + } + + // API has problems therefore explicit manual handling is required to lookup newly created object + // VCD 10.2 -> no ID for newly created object is returned at all + // VCD 10.3 -> `Details` field in task contains ID of newly created object + // To cover all cases this code will at first look for ID in `Details` field and fall back to + // lookup by name if `Details` field is empty. + // + // The drawback of this is that it is possible to create duplicate records with the same name on VCDs that don't + // return IDs, but there is no better way for VCD versions that don't return API code + + bgpNeighborId := task.Task.Details + if bgpNeighborId != "" { + getUrlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, egw.EdgeGateway.ID), bgpNeighborId) + if err != nil { + return nil, err + } + err = client.OpenApiGetItem(apiVersion, getUrlRef, nil, returnObject.EdgeBgpNeighbor, nil) + if err != nil { + return nil, fmt.Errorf("error retrieving NSX-T Edge Gateway BGP Neighbor after creation: %s", err) + } + } else { + // ID after object creation was not returned therefore retrieving the entity by Name to lookup ID + // This has a risk of duplicate items, but is the only way to find the object when ID is not returned + bgpNeighbor, err := egw.GetBgpNeighborByIp(bgpNeighborConfig.NeighborAddress) + if err != nil { + return nil, fmt.Errorf("error retrieving NSX-T Edge Gateway BGP Neighbor after creation: %s", err) + } + returnObject = bgpNeighbor + } + + return returnObject, nil +} + +// GetAllBgpNeighbors retrieves all BGP Neighbors with an optional filter +func (egw *NsxtEdgeGateway) GetAllBgpNeighbors(queryParameters url.Values) ([]*EdgeBgpNeighbor, error) { + queryParams := copyOrNewUrlValues(queryParameters) + + client := egw.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeBgpNeighbor + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + // Insert Edge Gateway ID into endpoint path + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, egw.EdgeGateway.ID)) + if err != nil { + return nil, err + } + + typeResponses := []*types.EdgeBgpNeighbor{{}} + err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParams, &typeResponses, nil) + if err != nil { + return nil, err + } + + wrappedResponses := make([]*EdgeBgpNeighbor, len(typeResponses)) + for sliceIndex := range typeResponses { + wrappedResponses[sliceIndex] = &EdgeBgpNeighbor{ + EdgeBgpNeighbor: typeResponses[sliceIndex], + client: client, + edgeGatewayId: egw.EdgeGateway.ID, + } + } + + return wrappedResponses, nil +} + +// GetBgpNeighborByIp retrieves BGP Neighbor by Neighbor IP address +// It is meant to retrieve exactly one entry: +// * Will fail if more than one entry with the same Neighbor IP found (should not happen as uniqueness is +// enforced by API) +// * Will return an error containing `ErrorEntityNotFound` if no entries are found +// +// Note. API does not support filtering by 'neighborIpAddress' field therefore filtering is performed on client +// side +func (egw *NsxtEdgeGateway) GetBgpNeighborByIp(neighborIpAddress string) (*EdgeBgpNeighbor, error) { + if neighborIpAddress == "" { + return nil, fmt.Errorf("neighborIpAddress cannot be empty") + } + + allBgpNeighbors, err := egw.GetAllBgpNeighbors(nil) + if err != nil { + return nil, fmt.Errorf("error retrieving NSX-T Edge Gateway BGP Neighbor: %s", err) + } + + var filteredBgpNeighbors []*EdgeBgpNeighbor + for _, bgpNeighbor := range allBgpNeighbors { + if bgpNeighbor.EdgeBgpNeighbor.NeighborAddress == neighborIpAddress { + filteredBgpNeighbors = append(filteredBgpNeighbors, bgpNeighbor) + } + } + + if len(filteredBgpNeighbors) > 1 { + return nil, fmt.Errorf("more than one NSX-T Edge Gateway BGP Neighbor found with IP Address '%s'", neighborIpAddress) + } + + if len(filteredBgpNeighbors) == 0 { + return nil, fmt.Errorf("%s: no NSX-T Edge Gateway BGP Neighbor found with IP Address '%s'", ErrorEntityNotFound, neighborIpAddress) + } + + return filteredBgpNeighbors[0], nil +} + +// GetBgpNeighborById retrieves BGP Neighbor By ID +func (egw *NsxtEdgeGateway) GetBgpNeighborById(id string) (*EdgeBgpNeighbor, error) { + if id == "" { + return nil, fmt.Errorf("id is required") + } + + client := egw.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeBgpNeighbor + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + // Insert Edge Gateway ID into endpoint path + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, egw.EdgeGateway.ID), id) + if err != nil { + return nil, err + } + + returnObject := &EdgeBgpNeighbor{ + client: egw.client, + edgeGatewayId: egw.EdgeGateway.ID, + EdgeBgpNeighbor: &types.EdgeBgpNeighbor{}, + } + + err = client.OpenApiGetItem(apiVersion, urlRef, nil, returnObject.EdgeBgpNeighbor, nil) + if err != nil { + return nil, fmt.Errorf("error retrieving NSX-T Edge Gateway BGP Neighbor: %s", err) + } + + return returnObject, nil +} + +// Update updates existing BGP Neighbor with new configuration and returns it +func (bgpNeighbor *EdgeBgpNeighbor) Update(bgpNeighborConfig *types.EdgeBgpNeighbor) (*EdgeBgpNeighbor, error) { + client := bgpNeighbor.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeBgpNeighbor + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + // Insert Edge Gateway ID into endpoint path + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, bgpNeighbor.edgeGatewayId), bgpNeighborConfig.ID) + if err != nil { + return nil, err + } + + returnObject := &EdgeBgpNeighbor{ + client: bgpNeighbor.client, + edgeGatewayId: bgpNeighbor.edgeGatewayId, + EdgeBgpNeighbor: &types.EdgeBgpNeighbor{}, + } + + err = client.OpenApiPutItem(apiVersion, urlRef, nil, bgpNeighborConfig, returnObject.EdgeBgpNeighbor, nil) + if err != nil { + return nil, fmt.Errorf("error setting NSX-T Edge Gateway BGP Neighbor: %s", err) + } + + return returnObject, nil +} + +// Delete deletes existing BGP Neighbor +func (bgpNeighbor *EdgeBgpNeighbor) Delete() error { + client := bgpNeighbor.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeBgpNeighbor + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return err + } + + // Insert Edge Gateway ID into endpoint path + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, bgpNeighbor.edgeGatewayId), bgpNeighbor.EdgeBgpNeighbor.ID) + if err != nil { + return err + } + + err = client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) + if err != nil { + return fmt.Errorf("error deleting NSX-T Edge Gateway BGP Neighbor: %s", err) + } + + return nil +} diff --git a/govcd/nsxt_edgegateway_bgp_neighbor_test.go b/govcd/nsxt_edgegateway_bgp_neighbor_test.go new file mode 100644 index 000000000..1b04a4326 --- /dev/null +++ b/govcd/nsxt_edgegateway_bgp_neighbor_test.go @@ -0,0 +1,83 @@ +//go:build network || nsxt || functional || openapi || ALL + +package govcd + +import ( + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_NsxEdgeBgpNeighbor(check *C) { + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointEdgeBgpNeighbor) + vcd.skipIfNotSysAdmin(check) + + org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + nsxtVdc, err := org.GetVDCByName(vcd.config.VCD.Nsxt.Vdc, false) + check.Assert(err, IsNil) + edge, err := nsxtVdc.GetNsxtEdgeGatewayByName(vcd.config.VCD.Nsxt.EdgeGateway) + check.Assert(err, IsNil) + + // Switch Edge Gateway to use dedicated uplink for the time of this test and then turn it off + err = switchEdgeGatewayDedication(edge, true) // Turn on Dedicated Tier 0 gateway + check.Assert(err, IsNil) + defer switchEdgeGatewayDedication(edge, false) // Turn off Dedicated Tier 0 gateway + + // Create a new BGP IP Neighbor + bgpNeighbor := &types.EdgeBgpNeighbor{ + NeighborAddress: "11.11.11.11", + RemoteASNumber: "64123", + KeepAliveTimer: 80, + HoldDownTimer: 241, + NeighborPassword: "iQuee-ph2phe", + AllowASIn: true, + GracefulRestartMode: "HELPER_ONLY", + IpAddressTypeFiltering: "DISABLED", + } + + createdBgpNeighbor, err := edge.CreateBgpNeighbor(bgpNeighbor) + check.Assert(err, IsNil) + check.Assert(createdBgpNeighbor, NotNil) + + // Get all BGP Neighbors + BgpNeighbors, err := edge.GetAllBgpNeighbors(nil) + check.Assert(err, IsNil) + check.Assert(BgpNeighbors, NotNil) + check.Assert(len(BgpNeighbors), Equals, 1) + check.Assert(BgpNeighbors[0].EdgeBgpNeighbor.NeighborAddress, Equals, bgpNeighbor.NeighborAddress) + + // Get BGP Neighbor By Neighbor IP Address + bgpNeighborByIp, err := edge.GetBgpNeighborByIp(bgpNeighbor.NeighborAddress) + check.Assert(err, IsNil) + check.Assert(bgpNeighborByIp, NotNil) + + // Get BGP Neighbor By Id + bgpNeighborById, err := edge.GetBgpNeighborById(createdBgpNeighbor.EdgeBgpNeighbor.ID) + check.Assert(err, IsNil) + check.Assert(bgpNeighborById, NotNil) + + // Update BGP Neighbor + bgpNeighbor.NeighborAddress = "12.12.12.12" + bgpNeighbor.ID = BgpNeighbors[0].EdgeBgpNeighbor.ID + + updatedBgpNeighbor, err := BgpNeighbors[0].Update(bgpNeighbor) + check.Assert(err, IsNil) + check.Assert(updatedBgpNeighbor, NotNil) + + check.Assert(updatedBgpNeighbor.EdgeBgpNeighbor.ID, Equals, BgpNeighbors[0].EdgeBgpNeighbor.ID) + + // Delete BGP Neighbor + err = BgpNeighbors[0].Delete() + check.Assert(err, IsNil) + + // Try to get deleted BGP Neighbor once again and ensure it is not there + notFoundByIp, err := edge.GetBgpNeighborByIp(bgpNeighbor.NeighborAddress) + check.Assert(err, NotNil) + check.Assert(notFoundByIp, IsNil) + + notFoundById, err := edge.GetBgpNeighborById(createdBgpNeighbor.EdgeBgpNeighbor.ID) + check.Assert(err, NotNil) + check.Assert(notFoundById, IsNil) + +} diff --git a/govcd/nsxt_edgegateway_dns.go b/govcd/nsxt_edgegateway_dns.go new file mode 100644 index 000000000..25c50d9c6 --- /dev/null +++ b/govcd/nsxt_edgegateway_dns.go @@ -0,0 +1,115 @@ +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +// NsxtEdgeGatewayDns can be used to configure DNS on NSX-T Edge Gateway. +type NsxtEdgeGatewayDns struct { + NsxtEdgeGatewayDns *types.NsxtEdgeGatewayDns + client *Client + EdgeGatewayId string +} + +// GetDnsConfig retrieves the DNS configuration for the underlying edge gateway +func (egw *NsxtEdgeGateway) GetDnsConfig() (*NsxtEdgeGatewayDns, error) { + return getDnsConfig(egw.client, egw.EdgeGateway.ID) +} + +// UpdateDnsConfig updates the DNS configuration for the Edge Gateway +func (egw *NsxtEdgeGateway) UpdateDnsConfig(updatedConfig *types.NsxtEdgeGatewayDns) (*NsxtEdgeGatewayDns, error) { + return updateDnsConfig(updatedConfig, egw.client, egw.EdgeGateway.ID) +} + +// Update updates the DNS configuration for the underlying Edge Gateway +func (dns *NsxtEdgeGatewayDns) Update(updatedConfig *types.NsxtEdgeGatewayDns) (*NsxtEdgeGatewayDns, error) { + return updateDnsConfig(updatedConfig, dns.client, dns.EdgeGatewayId) +} + +// Refresh refreshes the DNS configuration for the Edge Gateway +func (dns *NsxtEdgeGatewayDns) Refresh() error { + updatedDns, err := getDnsConfig(dns.client, dns.EdgeGatewayId) + if err != nil { + return err + } + dns.NsxtEdgeGatewayDns = updatedDns.NsxtEdgeGatewayDns + + return nil +} + +// Delete deletes the DNS configuration for the Edge Gateway +func (dns *NsxtEdgeGatewayDns) Delete() error { + client := dns.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayDns + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, dns.EdgeGatewayId)) + if err != nil { + return err + } + + err = client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) + if err != nil { + return err + } + + return nil +} + +func getDnsConfig(client *Client, edgeGatewayId string) (*NsxtEdgeGatewayDns, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayDns + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, edgeGatewayId)) + if err != nil { + return nil, err + } + + dnsConfig := &NsxtEdgeGatewayDns{ + client: client, + EdgeGatewayId: edgeGatewayId, + } + err = client.OpenApiGetItem(apiVersion, urlRef, nil, &dnsConfig.NsxtEdgeGatewayDns, nil) + if err != nil { + return nil, err + } + + return dnsConfig, nil + +} + +func updateDnsConfig(updatedConfig *types.NsxtEdgeGatewayDns, client *Client, edgeGatewayId string) (*NsxtEdgeGatewayDns, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayDns + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, edgeGatewayId)) + if err != nil { + return nil, err + } + + dns := &NsxtEdgeGatewayDns{ + client: client, + EdgeGatewayId: edgeGatewayId, + } + err = client.OpenApiPutItem(apiVersion, urlRef, nil, updatedConfig, &dns.NsxtEdgeGatewayDns, nil) + if err != nil { + return nil, err + } + + return dns, nil +} diff --git a/govcd/nsxt_edgegateway_dns_test.go b/govcd/nsxt_edgegateway_dns_test.go new file mode 100644 index 000000000..2c7022348 --- /dev/null +++ b/govcd/nsxt_edgegateway_dns_test.go @@ -0,0 +1,126 @@ +//go:build ALL || openapi || functional || nsxt + +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_NsxtEdgeGatewayDns(check *C) { + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointEdgeGatewayDns) + skipNoNsxtConfiguration(vcd, check) + + org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + nsxtVdc, err := org.GetVDCByName(vcd.config.VCD.Nsxt.Vdc, false) + check.Assert(err, IsNil) + edge, err := nsxtVdc.GetNsxtEdgeGatewayByName(vcd.config.VCD.Nsxt.EdgeGateway) + check.Assert(err, IsNil) + AddToCleanupList(vcd.config.VCD.Nsxt.EdgeGateway, "nsxtEdgeGatewayDns", vcd.config.VCD.Org, check.TestName()) + + disabledDns, err := edge.GetDnsConfig() + check.Assert(err, IsNil) + check.Assert(disabledDns.NsxtEdgeGatewayDns.Enabled, Equals, false) + + enabledDnsConfig := &types.NsxtEdgeGatewayDns{ + Enabled: true, + DefaultForwarderZone: &types.NsxtDnsForwarderZoneConfig{ + DisplayName: "test", + UpstreamServers: []string{ + "1.2.3.4", + "2.3.4.5", + }, + }, + ConditionalForwarderZones: []*types.NsxtDnsForwarderZoneConfig{ + { + DisplayName: "test-conditional", + UpstreamServers: []string{ + "5.5.5.5", + "2.3.4.1", + }, + DnsDomainNames: []string{ + "test.com", + "abc.com", + "example.org", + }, + }, + }, + } + + enabledDns, err := disabledDns.Update(enabledDnsConfig) + check.Assert(err, IsNil) + dnsConfig := enabledDns.NsxtEdgeGatewayDns + check.Assert(dnsConfig.Enabled, Equals, true) + check.Assert(dnsConfig.DefaultForwarderZone.DisplayName, Equals, "test") + check.Assert(len(dnsConfig.DefaultForwarderZone.UpstreamServers), Equals, 2) + check.Assert(len(dnsConfig.ConditionalForwarderZones), Equals, 1) + check.Assert(dnsConfig.ConditionalForwarderZones[0].DisplayName, Equals, "test-conditional") + + updatedDnsConfig := &types.NsxtEdgeGatewayDns{ + Enabled: true, + DefaultForwarderZone: &types.NsxtDnsForwarderZoneConfig{ + DisplayName: "test", + UpstreamServers: []string{ + "1.2.3.5", + "2.3.4.6", + "2.3.4.5", + }, + }, + ConditionalForwarderZones: []*types.NsxtDnsForwarderZoneConfig{ + { + DisplayName: "test-conditional", + UpstreamServers: []string{ + "5.5.5.5", + "2.3.4.1", + }, + DnsDomainNames: []string{ + "test.com", + "abc.com", + "example.org", + }, + }, + { + DisplayName: "test-conditional-2", + UpstreamServers: []string{ + "1.2.3.4", + "4.3.2.1", + }, + DnsDomainNames: []string{ + "example.com", + }, + }, + }, + } + updatedDns, err := enabledDns.Update(updatedDnsConfig) + updatedDnsConfig = updatedDns.NsxtEdgeGatewayDns + check.Assert(err, IsNil) + check.Assert(updatedDnsConfig.Enabled, Equals, true) + check.Assert(updatedDnsConfig.DefaultForwarderZone.DisplayName, Equals, "test") + check.Assert(len(updatedDnsConfig.DefaultForwarderZone.UpstreamServers), Equals, 3) + conditionalZones := updatedDnsConfig.ConditionalForwarderZones + check.Assert(len(conditionalZones), Equals, 2) + // Flip the asserts in both cases of conditional zones arrays returned + if conditionalZones[0].DisplayName == "test-conditional" { + check.Assert(len(conditionalZones[0].UpstreamServers), Equals, 2) + check.Assert(len(conditionalZones[0].DnsDomainNames), Equals, 3) + check.Assert(len(conditionalZones[1].UpstreamServers), Equals, 2) + check.Assert(len(conditionalZones[1].DnsDomainNames), Equals, 1) + } else { + check.Assert(len(conditionalZones[1].UpstreamServers), Equals, 2) + check.Assert(len(conditionalZones[1].DnsDomainNames), Equals, 3) + check.Assert(len(conditionalZones[0].UpstreamServers), Equals, 2) + check.Assert(len(conditionalZones[0].DnsDomainNames), Equals, 1) + } + + err = enabledDns.Delete() + check.Assert(err, IsNil) + + deletedDns, err := edge.GetDnsConfig() + check.Assert(err, IsNil) + check.Assert(deletedDns.NsxtEdgeGatewayDns.Enabled, Equals, false) +} diff --git a/govcd/nsxt_edgegateway_qos_profile.go b/govcd/nsxt_edgegateway_qos_profile.go new file mode 100644 index 000000000..24a7c5e57 --- /dev/null +++ b/govcd/nsxt_edgegateway_qos_profile.go @@ -0,0 +1,108 @@ +/* + * 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" +) + +// NsxtEdgeGatewayQosProfiles uses OpenAPI endpoint to fetch NSX-T Edge Gateway QoS Profiles defined +// in NSX-T Manager. They can be used to configure QoS on NSX-T Edge Gateway. +type NsxtEdgeGatewayQosProfile struct { + NsxtEdgeGatewayQosProfile *types.NsxtEdgeGatewayQosProfile + client *Client +} + +// GetAllNsxtEdgeGatewayQosProfiles retrieves all NSX-T Edge Gateway QoS Profiles defined in NSX-T Manager +func (vcdClient *VCDClient) GetAllNsxtEdgeGatewayQosProfiles(nsxtManagerId string, queryParameters url.Values) ([]*NsxtEdgeGatewayQosProfile, error) { + if nsxtManagerId == "" { + return nil, fmt.Errorf("empty NSX-T manager ID") + } + + if !isUrn(nsxtManagerId) { + return nil, fmt.Errorf("NSX-T manager ID is not URN (e.g. 'urn:vcloud:nsxtmanager:09722307-aee0-4623-af95-7f8e577c9ebc)', got: %s", nsxtManagerId) + } + + client := vcdClient.Client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointQosProfiles + 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) + queryParams = queryParameterFilterAnd("nsxTManagerRef.id=="+nsxtManagerId, queryParams) + + typeResponses := []*types.NsxtEdgeGatewayQosProfile{{}} + err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParams, &typeResponses, nil) + if err != nil { + return nil, err + } + + returnObjects := make([]*NsxtEdgeGatewayQosProfile, len(typeResponses)) + for sliceIndex := range typeResponses { + returnObjects[sliceIndex] = &NsxtEdgeGatewayQosProfile{ + NsxtEdgeGatewayQosProfile: typeResponses[sliceIndex], + client: &client, + } + } + + return returnObjects, nil +} + +// GetNsxtEdgeGatewayQosProfileById retrieves NSX-T Edge Gateway QoS Profile by Display Name +func (vcdClient *VCDClient) GetNsxtEdgeGatewayQosProfileByDisplayName(nsxtManagerId, displayName string) (*NsxtEdgeGatewayQosProfile, error) { + if displayName == "" { + return nil, fmt.Errorf("empty QoS profile Display Name") + } + + // Ideally FIQL filter could be used to filter on server side and get only desired result, but filtering on + // 'displayName' is not yet supported. + /* + queryParameters := copyOrNewUrlValues(nil) + queryParameters.Add("filter", "displayName=="+displayName) + */ + nsxtEdgeClusters, err := vcdClient.GetAllNsxtEdgeGatewayQosProfiles(nsxtManagerId, nil) + if err != nil { + return nil, fmt.Errorf("could not find QoS profile with DisplayName '%s' for NSX-T Manager with ID '%s': %s", + displayName, nsxtManagerId, err) + } + + // TODO remove this when FIQL supports filtering on 'displayName' + nsxtEdgeClusters = filterQosProfiles(displayName, nsxtEdgeClusters) + // EOF TODO remove this when FIQL supports filtering on 'displayName' + + if len(nsxtEdgeClusters) == 0 { + // ErrorEntityNotFound is injected here for the ability to validate problem using ContainsNotFound() + return nil, fmt.Errorf("%s: no NSX-T QoS profiles with DisplayName '%s' for NSX-T Manager with ID '%s' found", + ErrorEntityNotFound, displayName, nsxtManagerId) + } + + if len(nsxtEdgeClusters) > 1 { + return nil, fmt.Errorf("more than one (%d) QoS profile with DisplayName '%s' for NSX-T Manager with ID '%s' found", + len(nsxtEdgeClusters), displayName, nsxtManagerId) + } + + return nsxtEdgeClusters[0], nil +} + +func filterQosProfiles(displayName string, allQosProfiles []*NsxtEdgeGatewayQosProfile) []*NsxtEdgeGatewayQosProfile { + filteredQosProfiles := make([]*NsxtEdgeGatewayQosProfile, 0) + for index, nsxtEdgeCluster := range allQosProfiles { + if allQosProfiles[index].NsxtEdgeGatewayQosProfile.DisplayName == displayName { + filteredQosProfiles = append(filteredQosProfiles, nsxtEdgeCluster) + } + } + + return filteredQosProfiles +} diff --git a/govcd/nsxt_edgegateway_qos_profile_test.go b/govcd/nsxt_edgegateway_qos_profile_test.go new file mode 100644 index 000000000..95f1f751b --- /dev/null +++ b/govcd/nsxt_edgegateway_qos_profile_test.go @@ -0,0 +1,49 @@ +//go:build ALL || openapi || functional || nsxt + +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_NsxtEdgeGatewayQosProfiles(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointQosProfiles) + + skipNoNsxtConfiguration(vcd, check) + if vcd.config.VCD.Nsxt.GatewayQosProfile == "" { + check.Skip("No NSX-T Edge Gateway QoS Profile configured") + } + + nsxtManagers, err := vcd.client.QueryNsxtManagerByName(vcd.config.VCD.Nsxt.Manager) + check.Assert(err, IsNil) + check.Assert(len(nsxtManagers), Equals, 1) + + uuid, err := GetUuidFromHref(nsxtManagers[0].HREF, true) + check.Assert(err, IsNil) + urn, err := BuildUrnWithUuid("urn:vcloud:nsxtmanager:", uuid) + check.Assert(err, IsNil) + + // Fetch all profiles + allQosProfiles, err := vcd.client.GetAllNsxtEdgeGatewayQosProfiles(urn, nil) + check.Assert(err, IsNil) + check.Assert(len(allQosProfiles) > 0, Equals, true) + + // Fetch one by one based on DisplayName + for _, profile := range allQosProfiles { + printVerbose("# Fetching QoS profile '%s' by Name\n", profile.NsxtEdgeGatewayQosProfile.DisplayName) + qosProfile, err := vcd.client.GetNsxtEdgeGatewayQosProfileByDisplayName(urn, profile.NsxtEdgeGatewayQosProfile.DisplayName) + check.Assert(err, IsNil) + check.Assert(qosProfile, NotNil) + check.Assert(qosProfile.NsxtEdgeGatewayQosProfile.ID, Equals, profile.NsxtEdgeGatewayQosProfile.ID) + } +} diff --git a/govcd/nsxt_edgegateway_static_route.go b/govcd/nsxt_edgegateway_static_route.go new file mode 100644 index 000000000..9ab9ca582 --- /dev/null +++ b/govcd/nsxt_edgegateway_static_route.go @@ -0,0 +1,270 @@ +/* + * Copyright 2022 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" +) + +// NsxtEdgeGatewayStaticRoute represents NSX-T Edge Gateway Static Route +type NsxtEdgeGatewayStaticRoute struct { + NsxtEdgeGatewayStaticRoute *types.NsxtEdgeGatewayStaticRoute + client *Client + // edgeGatewayId is stored for usage in NsxtEdgeGatewayStaticRoute receiver functions + edgeGatewayId string +} + +// CreateStaticRoute based on type definition +func (egw *NsxtEdgeGateway) CreateStaticRoute(staticRouteConfig *types.NsxtEdgeGatewayStaticRoute) (*NsxtEdgeGatewayStaticRoute, error) { + client := egw.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayStaticRoutes + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + // Insert Edge Gateway ID into endpoint path + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, egw.EdgeGateway.ID)) + if err != nil { + return nil, err + } + + returnObject := &NsxtEdgeGatewayStaticRoute{ + client: egw.client, + edgeGatewayId: egw.EdgeGateway.ID, + NsxtEdgeGatewayStaticRoute: &types.NsxtEdgeGatewayStaticRoute{}, + } + + // Non standard behavior of entity - `Details` field in task contains ID of newly created object while the Owner is Edge Gateway + task, err := client.OpenApiPostItemAsync(apiVersion, urlRef, nil, staticRouteConfig) + if err != nil { + return nil, fmt.Errorf("error creating NSX-T Edge Gateway Static Route: %s", err) + } + + err = task.WaitTaskCompletion() + if err != nil { + return nil, fmt.Errorf("error creating NSX-T Edge Gateway Static Route: %s", err) + } + + // API does not return an ID for created object - we know that it is expected to be in + // task.Task.Details therefore attempt to find it, but if it is empty - look for an entity by a + // set of requested parameters + staticRouteId := task.Task.Details + if staticRouteId != "" { + getUrlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, egw.EdgeGateway.ID), staticRouteId) + if err != nil { + return nil, err + } + err = client.OpenApiGetItem(apiVersion, getUrlRef, nil, returnObject.NsxtEdgeGatewayStaticRoute, nil) + if err != nil { + return nil, fmt.Errorf("error retrieving NSX-T Edge Gateway Static Route after creation: %s", err) + } + } else { + // ID was not present in response, therefore Static Route needs to be found manually. Using + // 'Name', 'Description' and 'NetworkCidr' for finding the entity. Duplicate entries can + // exist, but but it should be a good enough combination for finding unique entry until VCD API is fixed + allStaticRoutes, err := egw.GetAllStaticRoutes(nil) + if err != nil { + return nil, fmt.Errorf("error retrieving NSX-T Edge Gateway Static Route after creation: %s", err) + } + + var foundStaticRoute bool + for _, singleStaticRoute := range allStaticRoutes { + if singleStaticRoute.NsxtEdgeGatewayStaticRoute.Name == staticRouteConfig.Name && + singleStaticRoute.NsxtEdgeGatewayStaticRoute.NetworkCidr == staticRouteConfig.NetworkCidr && + singleStaticRoute.NsxtEdgeGatewayStaticRoute.Description == staticRouteConfig.Description { + foundStaticRoute = true + returnObject = singleStaticRoute + break + } + } + + if !foundStaticRoute { + return nil, fmt.Errorf("error finding Static Route after creation by Name '%s', NetworkCidr '%s', Description '%s'", + staticRouteConfig.Name, staticRouteConfig.NetworkCidr, staticRouteConfig.Description) + } + + } + + return returnObject, nil +} + +// GetAllStaticRoutes retrieves all Static Routes for a particular NSX-T Edge Gateway +func (egw *NsxtEdgeGateway) GetAllStaticRoutes(queryParameters url.Values) ([]*NsxtEdgeGatewayStaticRoute, error) { + client := egw.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayStaticRoutes + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + // Insert Edge Gateway ID into endpoint path + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, egw.EdgeGateway.ID)) + if err != nil { + return nil, err + } + + typeResponses := []*types.NsxtEdgeGatewayStaticRoute{{}} + err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParameters, &typeResponses, nil) + if err != nil { + return nil, err + } + + wrappedResponses := make([]*NsxtEdgeGatewayStaticRoute, len(typeResponses)) + for sliceIndex := range typeResponses { + wrappedResponses[sliceIndex] = &NsxtEdgeGatewayStaticRoute{ + NsxtEdgeGatewayStaticRoute: typeResponses[sliceIndex], + client: client, + edgeGatewayId: egw.EdgeGateway.ID, + } + } + + return wrappedResponses, nil +} + +// GetStaticRouteByNetworkCidr retrieves Static Route by network CIDR +// +// Note. It will return an error if more than one items is found +func (egw *NsxtEdgeGateway) GetStaticRouteByNetworkCidr(networkCidr string) (*NsxtEdgeGatewayStaticRoute, error) { + if networkCidr == "" { + return nil, fmt.Errorf("cidr cannot be empty") + } + + allStaticRoutes, err := egw.GetAllStaticRoutes(nil) + if err != nil { + return nil, fmt.Errorf("error retrieving NSX-T Edge Gateway Static Route: %s", err) + } + + filteredByNetworkCidr := make([]*NsxtEdgeGatewayStaticRoute, 0) + for _, sr := range allStaticRoutes { + if sr.NsxtEdgeGatewayStaticRoute.NetworkCidr == networkCidr { + filteredByNetworkCidr = append(filteredByNetworkCidr, sr) + } + } + + singleResult, err := oneOrError("networkCidr", networkCidr, filteredByNetworkCidr) + if err != nil { + return nil, err + } + + return singleResult, nil +} + +// GetStaticRouteByName retrieves Static Route by name +// +// Note. It will return an error if more than one items is found +func (egw *NsxtEdgeGateway) GetStaticRouteByName(name string) (*NsxtEdgeGatewayStaticRoute, error) { + if name == "" { + return nil, fmt.Errorf("cidr cannot be empty") + } + + allStaticRoutes, err := egw.GetAllStaticRoutes(nil) + if err != nil { + return nil, fmt.Errorf("error retrieving NSX-T Edge Gateway Static Route: %s", err) + } + + filteredByNetworkName := make([]*NsxtEdgeGatewayStaticRoute, 0) + // First - filter by name + for _, sr := range allStaticRoutes { + if sr.NsxtEdgeGatewayStaticRoute.Name == name { + filteredByNetworkName = append(filteredByNetworkName, sr) + } + } + + singleResult, err := oneOrError("name", name, filteredByNetworkName) + if err != nil { + return nil, err + } + + return singleResult, nil +} + +// GetStaticRouteById retrieves Static Route by given ID +func (egw *NsxtEdgeGateway) GetStaticRouteById(id string) (*NsxtEdgeGatewayStaticRoute, error) { + if id == "" { + return nil, fmt.Errorf("ID is required") + } + + client := egw.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayStaticRoutes + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + // Insert Edge Gateway ID into endpoint path + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, egw.EdgeGateway.ID), id) + if err != nil { + return nil, err + } + + returnObject := &NsxtEdgeGatewayStaticRoute{ + client: egw.client, + edgeGatewayId: egw.EdgeGateway.ID, + NsxtEdgeGatewayStaticRoute: &types.NsxtEdgeGatewayStaticRoute{}, + } + + err = client.OpenApiGetItem(apiVersion, urlRef, nil, returnObject.NsxtEdgeGatewayStaticRoute, nil) + if err != nil { + return nil, fmt.Errorf("error retrieving NSX-T Edge Gateway Static Route: %s", err) + } + + return returnObject, nil +} + +// Update Static Route +func (staticRoute *NsxtEdgeGatewayStaticRoute) Update(StaticRouteConfig *types.NsxtEdgeGatewayStaticRoute) (*NsxtEdgeGatewayStaticRoute, error) { + client := staticRoute.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayStaticRoutes + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + // Insert Edge Gateway ID into endpoint path + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, staticRoute.edgeGatewayId), StaticRouteConfig.ID) + if err != nil { + return nil, err + } + + returnObject := &NsxtEdgeGatewayStaticRoute{ + client: staticRoute.client, + edgeGatewayId: staticRoute.edgeGatewayId, + NsxtEdgeGatewayStaticRoute: &types.NsxtEdgeGatewayStaticRoute{}, + } + + err = client.OpenApiPutItem(apiVersion, urlRef, nil, StaticRouteConfig, returnObject.NsxtEdgeGatewayStaticRoute, nil) + if err != nil { + return nil, fmt.Errorf("error setting NSX-T Edge Gateway Static Route: %s", err) + } + + return returnObject, nil +} + +// Delete Static Route +func (staticRoute *NsxtEdgeGatewayStaticRoute) Delete() error { + client := staticRoute.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayStaticRoutes + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return err + } + + // Insert Edge Gateway ID into endpoint path + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, staticRoute.edgeGatewayId), staticRoute.NsxtEdgeGatewayStaticRoute.ID) + if err != nil { + return err + } + + err = client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) + if err != nil { + return fmt.Errorf("error deleting NSX-T Edge Gateway Static Route: %s", err) + } + + return nil +} diff --git a/govcd/nsxt_edgegateway_static_route_test.go b/govcd/nsxt_edgegateway_static_route_test.go new file mode 100644 index 000000000..1709fa377 --- /dev/null +++ b/govcd/nsxt_edgegateway_static_route_test.go @@ -0,0 +1,108 @@ +//go:build network || nsxt || functional || openapi || ALL + +package govcd + +import ( + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_NsxEdgeStaticRoute(check *C) { + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointEdgeGatewayStaticRoutes) + vcd.skipIfNotSysAdmin(check) + + org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + nsxtVdc, err := org.GetVDCByName(vcd.config.VCD.Nsxt.Vdc, false) + check.Assert(err, IsNil) + edge, err := nsxtVdc.GetNsxtEdgeGatewayByName(vcd.config.VCD.Nsxt.EdgeGateway) + check.Assert(err, IsNil) + + // Switch Edge Gateway to use dedicated uplink for the time of this test and then turn it off + err = switchEdgeGatewayDedication(edge, true) // Turn on Dedicated Tier 0 gateway + check.Assert(err, IsNil) + defer func() { + err = switchEdgeGatewayDedication(edge, false) + check.Assert(err, IsNil) + }() + + // Get Org VDC routed network + orgVdcNet, err := nsxtVdc.GetOpenApiOrgVdcNetworkByName(vcd.config.VCD.Nsxt.RoutedNetwork) + check.Assert(err, IsNil) + check.Assert(orgVdcNet, NotNil) + + staticRouteConfig := &types.NsxtEdgeGatewayStaticRoute{ + Name: check.TestName(), + Description: "description", + NetworkCidr: "1.1.1.0/24", + NextHops: []types.NsxtEdgeGatewayStaticRouteNextHops{ + { + IPAddress: orgVdcNet.OpenApiOrgVdcNetwork.Subnets.Values[0].Gateway, + AdminDistance: 4, + Scope: &types.NsxtEdgeGatewayStaticRouteNextHopScope{ + ID: orgVdcNet.OpenApiOrgVdcNetwork.ID, + ScopeType: "NETWORK", + }, + }, + }, + } + + staticRoute, err := edge.CreateStaticRoute(staticRouteConfig) + check.Assert(err, IsNil) + check.Assert(staticRoute, NotNil) + + // Get all BGP IP Prefix Lists + staticRouteList, err := edge.GetAllStaticRoutes(nil) + check.Assert(err, IsNil) + check.Assert(staticRouteList, NotNil) + check.Assert(len(staticRouteList), Equals, 1) + check.Assert(staticRouteList[0].NsxtEdgeGatewayStaticRoute.Name, Equals, staticRoute.NsxtEdgeGatewayStaticRoute.Name) + + // Get By Name + staticRouteByName, err := edge.GetStaticRouteByName(staticRoute.NsxtEdgeGatewayStaticRoute.Name) + check.Assert(err, IsNil) + check.Assert(staticRouteByName, NotNil) + + // Get By Id + staticRouteById, err := edge.GetStaticRouteById(staticRoute.NsxtEdgeGatewayStaticRoute.ID) + check.Assert(err, IsNil) + check.Assert(staticRouteById, NotNil) + + // Get By Network CIDR + staticRouteByNetworkCidr, err := edge.GetStaticRouteByNetworkCidr(staticRoute.NsxtEdgeGatewayStaticRoute.NetworkCidr) + check.Assert(err, IsNil) + check.Assert(staticRouteByNetworkCidr, NotNil) + + // Update + staticRouteConfig.Name = check.TestName() + "-updated" + staticRouteConfig.Description = "test-description-updated" + staticRouteConfig.ID = staticRouteByNetworkCidr.NsxtEdgeGatewayStaticRoute.ID + + // staticRoute + updatedStaticRoute, err := staticRoute.Update(staticRouteConfig) + check.Assert(err, IsNil) + check.Assert(updatedStaticRoute, NotNil) + + check.Assert(updatedStaticRoute.NsxtEdgeGatewayStaticRoute.ID, Equals, staticRouteByName.NsxtEdgeGatewayStaticRoute.ID) + + // Delete + err = staticRoute.Delete() + check.Assert(err, IsNil) + + // Try to get once again and ensure it is not there + notFoundByName, err := edge.GetStaticRouteByName(staticRoute.NsxtEdgeGatewayStaticRoute.Name) + check.Assert(err, NotNil) + check.Assert(ContainsNotFound(err), Equals, true) + check.Assert(notFoundByName, IsNil) + + notFoundById, err := edge.GetStaticRouteById(staticRoute.NsxtEdgeGatewayStaticRoute.ID) + check.Assert(err, NotNil) + check.Assert(ContainsNotFound(err), Equals, true) + check.Assert(notFoundById, IsNil) + + notFoundByCidr, err := edge.GetStaticRouteByNetworkCidr(staticRoute.NsxtEdgeGatewayStaticRoute.NetworkCidr) + check.Assert(err, NotNil) + check.Assert(ContainsNotFound(err), Equals, true) + check.Assert(notFoundByCidr, IsNil) +} diff --git a/govcd/nsxt_edgegateway_test.go b/govcd/nsxt_edgegateway_test.go index 1b1dad068..f74028d87 100644 --- a/govcd/nsxt_edgegateway_test.go +++ b/govcd/nsxt_edgegateway_test.go @@ -1,9 +1,10 @@ -// +build network nsxt functional openapi ALL +//go:build network || nsxt || functional || openapi || ALL package govcd import ( "fmt" + "net/netip" "net/url" "github.com/vmware/go-vcloud-director/v2/types/v56" @@ -13,23 +14,29 @@ import ( func (vcd *TestVCD) Test_NsxtEdgeCreate(check *C) { skipNoNsxtConfiguration(vcd, check) skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointEdgeGateways) + vcd.skipIfNotSysAdmin(check) adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) check.Assert(err, IsNil) + check.Assert(org, NotNil) nsxvVdc, err := adminOrg.GetVDCByName(vcd.config.VCD.Vdc, false) check.Assert(err, IsNil) + check.Assert(nsxvVdc, NotNil) nsxtVdc, err := adminOrg.GetVDCByName(vcd.config.VCD.Nsxt.Vdc, false) if ContainsNotFound(err) { check.Skip(fmt.Sprintf("No NSX-T VDC (%s) found - skipping test", vcd.config.VCD.Nsxt.Vdc)) } check.Assert(err, IsNil) + check.Assert(nsxtVdc, NotNil) nsxtExternalNetwork, err := GetExternalNetworkV2ByName(vcd.client, vcd.config.VCD.Nsxt.ExternalNetwork) check.Assert(err, IsNil) + check.Assert(nsxtExternalNetwork, NotNil) egwDefinition := &types.OpenAPIEdgeGateway{ Name: "nsx-t-edge", @@ -37,7 +44,7 @@ func (vcd *TestVCD) Test_NsxtEdgeCreate(check *C) { OrgVdc: &types.OpenApiReference{ ID: nsxtVdc.Vdc.ID, }, - EdgeGatewayUplinks: []types.EdgeGatewayUplinks{types.EdgeGatewayUplinks{ + EdgeGatewayUplinks: []types.EdgeGatewayUplinks{{ UplinkID: nsxtExternalNetwork.ExternalNetwork.ID, Subnets: types.OpenAPIEdgeGatewaySubnets{Values: []types.OpenAPIEdgeGatewaySubnetValue{{ Gateway: "1.1.1.1", @@ -50,10 +57,20 @@ func (vcd *TestVCD) Test_NsxtEdgeCreate(check *C) { } createdEdge, err := adminOrg.CreateNsxtEdgeGateway(egwDefinition) - check.Assert(err, IsNil) check.Assert(createdEdge.EdgeGateway.Name, Equals, egwDefinition.Name) + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGateways + createdEdge.EdgeGateway.ID + AddToCleanupListOpenApi(createdEdge.EdgeGateway.Name, check.TestName(), openApiEndpoint) + + edgeCluster, err := nsxtVdc.GetNsxtEdgeClusterByName(vcd.config.VCD.Nsxt.NsxtEdgeCluster) + check.Assert(err, IsNil) + check.Assert(edgeCluster, NotNil) + createdEdge.EdgeGateway.EdgeClusterConfig = &types.OpenAPIEdgeGatewayEdgeClusterConfig{ + PrimaryEdgeCluster: types.OpenAPIEdgeGatewayEdgeCluster{ + BackingID: edgeCluster.NsxtEdgeCluster.ID, + }, + } createdEdge.EdgeGateway.Name = "renamed-edge" updatedEdge, err := createdEdge.Update(createdEdge.EdgeGateway) check.Assert(err, IsNil) @@ -70,16 +87,22 @@ func (vcd *TestVCD) Test_NsxtEdgeCreate(check *C) { // Lookup using different available methods e1, err := adminOrg.GetNsxtEdgeGatewayByName(updatedEdge.EdgeGateway.Name) check.Assert(err, IsNil) + check.Assert(e1, NotNil) e2, err := org.GetNsxtEdgeGatewayByName(updatedEdge.EdgeGateway.Name) check.Assert(err, IsNil) + check.Assert(e2, NotNil) e3, err := nsxtVdc.GetNsxtEdgeGatewayByName(updatedEdge.EdgeGateway.Name) check.Assert(err, IsNil) + check.Assert(e3, NotNil) e4, err := adminOrg.GetNsxtEdgeGatewayById(updatedEdge.EdgeGateway.ID) check.Assert(err, IsNil) + check.Assert(e4, NotNil) e5, err := org.GetNsxtEdgeGatewayById(updatedEdge.EdgeGateway.ID) check.Assert(err, IsNil) + check.Assert(e5, NotNil) e6, err := nsxtVdc.GetNsxtEdgeGatewayById(updatedEdge.EdgeGateway.ID) check.Assert(err, IsNil) + check.Assert(e6, NotNil) // Try to search for NSX-T edge gateway in NSX-V VDC and expect it to be not found expectNil, err := nsxvVdc.GetNsxtEdgeGatewayByName(updatedEdge.EdgeGateway.Name) @@ -96,6 +119,752 @@ func (vcd *TestVCD) Test_NsxtEdgeCreate(check *C) { check.Assert(e1.EdgeGateway.ID, Equals, e5.EdgeGateway.ID) check.Assert(e1.EdgeGateway.ID, Equals, e6.EdgeGateway.ID) + // Try out GetUsedIpAddresses function + usedIPs, err := updatedEdge.GetUsedIpAddresses(nil) + check.Assert(err, IsNil) + check.Assert(usedIPs, NotNil) + + ipAddr, err := updatedEdge.GetUnusedExternalIPAddresses(1, netip.Prefix{}, false) + // Expect an error as no ranges were assigned + check.Assert(err, NotNil) + check.Assert(ipAddr, DeepEquals, []netip.Addr(nil)) + + // Try a refresh operation + err = updatedEdge.Refresh() + check.Assert(err, IsNil) + check.Assert(updatedEdge.EdgeGateway.ID, Equals, e1.EdgeGateway.ID) + + // Cleanup err = updatedEdge.Delete() check.Assert(err, IsNil) } + +func (vcd *TestVCD) Test_NsxtEdgeVdcGroup(check *C) { + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointEdgeGateways) + vcd.skipIfNotSysAdmin(check) + + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + + org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(org, NotNil) + + nsxtExternalNetwork, err := GetExternalNetworkV2ByName(vcd.client, vcd.config.VCD.Nsxt.ExternalNetwork) + check.Assert(err, IsNil) + check.Assert(nsxtExternalNetwork, NotNil) + + vdc, vdcGroup := test_CreateVdcGroup(check, adminOrg, vcd) + + egwDefinition := &types.OpenAPIEdgeGateway{ + Name: "nsx-t-edge", + Description: "nsx-t-edge-description", + OwnerRef: &types.OpenApiReference{ + ID: vdc.Vdc.ID, + }, + EdgeGatewayUplinks: []types.EdgeGatewayUplinks{{ + UplinkID: nsxtExternalNetwork.ExternalNetwork.ID, + Subnets: types.OpenAPIEdgeGatewaySubnets{Values: []types.OpenAPIEdgeGatewaySubnetValue{{ + Gateway: "1.1.1.1", + PrefixLength: 24, + Enabled: true, + }}}, + Connected: true, + Dedicated: false, + }}, + } + + // Create Edge Gateway in VDC + createdEdge, err := adminOrg.CreateNsxtEdgeGateway(egwDefinition) + check.Assert(err, IsNil) + check.Assert(createdEdge.EdgeGateway.OwnerRef.ID, Matches, `^urn:vcloud:vdc:.*`) + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGateways + createdEdge.EdgeGateway.ID + PrependToCleanupListOpenApi(createdEdge.EdgeGateway.Name, check.TestName(), openApiEndpoint) + + check.Assert(createdEdge.EdgeGateway.Name, Equals, egwDefinition.Name) + check.Assert(createdEdge.EdgeGateway.OwnerRef.ID, Equals, egwDefinition.OwnerRef.ID) + + // Move Edge Gateway to VDC Group + movedGateway, err := createdEdge.MoveToVdcOrVdcGroup(vdcGroup.VdcGroup.Id) + check.Assert(err, IsNil) + check.Assert(movedGateway.EdgeGateway.OwnerRef.ID, Equals, vdcGroup.VdcGroup.Id) + check.Assert(movedGateway.EdgeGateway.OwnerRef.ID, Matches, `^urn:vcloud:vdcGroup:.*`) + + // Get by name and owner ID + edgeByNameAndOwnerId, err := org.GetNsxtEdgeGatewayByNameAndOwnerId(createdEdge.EdgeGateway.Name, vdcGroup.VdcGroup.Id) + check.Assert(err, IsNil) + + // Check lookup of Edge Gateways in VDC Groups + edgeInVdcGroup, err := vdcGroup.GetNsxtEdgeGatewayByName(createdEdge.EdgeGateway.Name) + check.Assert(err, IsNil) + + // Ensure both methods for retrieval get the same object + check.Assert(edgeByNameAndOwnerId.EdgeGateway, DeepEquals, edgeInVdcGroup.EdgeGateway) + + // Masking known variables that have change for deep check + edgeInVdcGroup.EdgeGateway.OwnerRef.Name = "" + check.Assert(edgeInVdcGroup.EdgeGateway, DeepEquals, createdEdge.EdgeGateway) + + // Move Edge Gateway back to VDC from VDC Group + egwDefinition.OwnerRef.ID = vdc.Vdc.ID + egwDefinition.ID = movedGateway.EdgeGateway.ID + + movedBackToVdcEdge, err := movedGateway.Update(egwDefinition) + check.Assert(err, IsNil) + check.Assert(movedBackToVdcEdge.EdgeGateway.OwnerRef.ID, Matches, `^urn:vcloud:vdc:.*`) + + // Ignore known to differ fields, but check that whole Edge Gateway structure remains the same + // as it is important to perform update operations without impacting configuration itself + + // Fields to ignore on both sides + createdEdge.EdgeGateway.OrgVdc = movedBackToVdcEdge.EdgeGateway.OrgVdc + createdEdge.EdgeGateway.OwnerRef = movedBackToVdcEdge.EdgeGateway.OwnerRef + check.Assert(movedBackToVdcEdge.EdgeGateway, DeepEquals, createdEdge.EdgeGateway) + + // Remove Edge Gateway + err = movedBackToVdcEdge.Delete() + check.Assert(err, IsNil) + + // Remove VDC Group + err = vdcGroup.Delete() + check.Assert(err, IsNil) + + // Remove VDC + err = vdc.DeleteWait(true, true) + check.Assert(err, IsNil) +} + +func (vcd *TestVCD) Test_NsxtEdgeGatewayUsedAndUnusedIPs(check *C) { + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointEdgeGateways) + vcd.skipIfNotSysAdmin(check) + + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + + org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(org, NotNil) + + nsxvVdc, err := adminOrg.GetVDCByName(vcd.config.VCD.Vdc, false) + check.Assert(err, IsNil) + check.Assert(nsxvVdc, NotNil) + nsxtVdc, err := adminOrg.GetVDCByName(vcd.config.VCD.Nsxt.Vdc, false) + if ContainsNotFound(err) { + check.Skip(fmt.Sprintf("No NSX-T VDC (%s) found - skipping test", vcd.config.VCD.Nsxt.Vdc)) + } + check.Assert(err, IsNil) + check.Assert(nsxtVdc, NotNil) + + // NSX-T details + man, err := vcd.client.QueryNsxtManagerByName(vcd.config.VCD.Nsxt.Manager) + check.Assert(err, IsNil) + nsxtManagerId, err := BuildUrnWithUuid("urn:vcloud:nsxtmanager:", extractUuid(man[0].HREF)) + check.Assert(err, IsNil) + + tier0RouterVrf, err := vcd.client.GetImportableNsxtTier0RouterByName(vcd.config.VCD.Nsxt.Tier0router, nsxtManagerId) + check.Assert(err, IsNil) + backingId := tier0RouterVrf.NsxtTier0Router.ID + + netNsxt := &types.ExternalNetworkV2{ + Name: check.TestName(), + Subnets: types.ExternalNetworkV2Subnets{Values: []types.ExternalNetworkV2Subnet{ + { + Gateway: "1.1.1.1", + PrefixLength: 24, + IPRanges: types.ExternalNetworkV2IPRanges{Values: []types.ExternalNetworkV2IPRange{ + { + StartAddress: "1.1.1.3", + EndAddress: "1.1.1.25", + }, + }}, + Enabled: true, + }, + }}, + NetworkBackings: types.ExternalNetworkV2Backings{Values: []types.ExternalNetworkV2Backing{ + { + BackingID: backingId, + NetworkProvider: types.NetworkProvider{ + ID: nsxtManagerId, + }, + BackingTypeValue: types.ExternalNetworkBackingTypeNsxtTier0Router, + }, + }}, + } + createdNet, err := CreateExternalNetworkV2(vcd.client, netNsxt) + check.Assert(err, IsNil) + + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointExternalNetworks + createdNet.ExternalNetwork.ID + AddToCleanupListOpenApi(createdNet.ExternalNetwork.Name, check.TestName(), openApiEndpoint) + + egwDefinition := &types.OpenAPIEdgeGateway{ + Name: check.TestName(), + OrgVdc: &types.OpenApiReference{ + ID: nsxtVdc.Vdc.ID, + }, + EdgeGatewayUplinks: []types.EdgeGatewayUplinks{{ + UplinkID: createdNet.ExternalNetwork.ID, + Subnets: types.OpenAPIEdgeGatewaySubnets{Values: []types.OpenAPIEdgeGatewaySubnetValue{{ + Gateway: createdNet.ExternalNetwork.Subnets.Values[0].Gateway, + PrefixLength: createdNet.ExternalNetwork.Subnets.Values[0].PrefixLength, + Enabled: true, + IPRanges: &types.OpenApiIPRanges{ + Values: []types.OpenApiIPRangeValues{ + { + StartAddress: createdNet.ExternalNetwork.Subnets.Values[0].IPRanges.Values[0].StartAddress, + EndAddress: createdNet.ExternalNetwork.Subnets.Values[0].IPRanges.Values[0].EndAddress, + }, + }, + }, + }}}, + Connected: true, + Dedicated: false, + }}, + } + + createdEdge, err := adminOrg.CreateNsxtEdgeGateway(egwDefinition) + check.Assert(err, IsNil) + check.Assert(createdEdge.EdgeGateway.Name, Equals, egwDefinition.Name) + openApiEndpoint = types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGateways + createdEdge.EdgeGateway.ID + PrependToCleanupListOpenApi(createdEdge.EdgeGateway.Name, check.TestName(), openApiEndpoint) + + // Try out GetUsedIpAddresses function + usedIPs, err := createdEdge.GetUsedIpAddresses(nil) + check.Assert(err, IsNil) + check.Assert(usedIPs, NotNil) + + // Edge Gateway always allocates 1 IP as its primary + check.Assert(usedIPs[0].IPAddress, Equals, "1.1.1.3") + check.Assert(usedIPs[0].Category, Equals, "PRIMARY_IP") + + // Attempt to get 1 unallocated IP + ipAddr, err := createdEdge.GetUnusedExternalIPAddresses(1, netip.Prefix{}, false) + check.Assert(err, IsNil) + ipsCompared := compareEachIpElementAndOrder(ipAddr, []netip.Addr{netip.MustParseAddr("1.1.1.4")}) + check.Assert(ipsCompared, Equals, true) + + // Attempt to get 10 unallocated IPs + ipAddr, err = createdEdge.GetUnusedExternalIPAddresses(10, netip.Prefix{}, true) + check.Assert(err, IsNil) + ipsCompared = compareEachIpElementAndOrder(ipAddr, []netip.Addr{ + netip.MustParseAddr("1.1.1.4"), + netip.MustParseAddr("1.1.1.5"), + netip.MustParseAddr("1.1.1.6"), + netip.MustParseAddr("1.1.1.7"), + netip.MustParseAddr("1.1.1.8"), + netip.MustParseAddr("1.1.1.9"), + netip.MustParseAddr("1.1.1.10"), + netip.MustParseAddr("1.1.1.11"), + netip.MustParseAddr("1.1.1.12"), + netip.MustParseAddr("1.1.1.13"), + }) + check.Assert(ipsCompared, Equals, true) + + // Attempt to get IP but filter it off by prefix + ipAddr, err = createdEdge.GetUnusedExternalIPAddresses(1, netip.MustParsePrefix("192.168.1.1/24"), false) + // Expect an error because Edge Gateway does not have IPs from required subnet 192.168.1.1/24 + check.Assert(err, NotNil) + check.Assert(ipAddr, IsNil) + + // Attempt to get all unused IPs + allIps, err := createdEdge.GetAllUnusedExternalIPAddresses(true) + check.Assert(err, IsNil) + ipsCompared = compareEachIpElementAndOrder(allIps, []netip.Addr{ + netip.MustParseAddr("1.1.1.4"), + netip.MustParseAddr("1.1.1.5"), + netip.MustParseAddr("1.1.1.6"), + netip.MustParseAddr("1.1.1.7"), + netip.MustParseAddr("1.1.1.8"), + netip.MustParseAddr("1.1.1.9"), + netip.MustParseAddr("1.1.1.10"), + netip.MustParseAddr("1.1.1.11"), + netip.MustParseAddr("1.1.1.12"), + netip.MustParseAddr("1.1.1.13"), + netip.MustParseAddr("1.1.1.14"), + netip.MustParseAddr("1.1.1.15"), + netip.MustParseAddr("1.1.1.16"), + netip.MustParseAddr("1.1.1.17"), + netip.MustParseAddr("1.1.1.18"), + netip.MustParseAddr("1.1.1.19"), + netip.MustParseAddr("1.1.1.20"), + netip.MustParseAddr("1.1.1.21"), + netip.MustParseAddr("1.1.1.22"), + netip.MustParseAddr("1.1.1.23"), + netip.MustParseAddr("1.1.1.24"), + netip.MustParseAddr("1.1.1.25"), + }) + check.Assert(ipsCompared, Equals, true) + check.Assert(len(allIps), Equals, 22) + + // Get used and unused IP counts + usedIpCount, unusedIpCount, err := createdEdge.GetUsedAndUnusedExternalIPAddressCountWithLimit(false, 5) + check.Assert(err, IsNil) + check.Assert(unusedIpCount, Equals, int64(4)) + check.Assert(usedIpCount, Equals, int64(1)) + + // Verify that GetAllocatedIpCount returns correct number of allocated IPs + totalAllocationIpCount, err := createdEdge.GetAllocatedIpCount(true) + check.Assert(err, IsNil) + check.Assert(totalAllocationIpCount, NotNil) + check.Assert(totalAllocationIpCount, Equals, 23) // 22 unused IPs + 1 primary + + // Try to deallocate more IPs than allocated + failedDeallocationIpCount, err := createdEdge.QuickDeallocateIpCount(24) + check.Assert(err, NotNil) + check.Assert(failedDeallocationIpCount, IsNil) + + // Check that failed deallocation did not change the number of allocated IPs + allocatedIpCountAfterFailedDeallocation, err := createdEdge.GetAllocatedIpCount(true) + check.Assert(err, IsNil) + check.Assert(allocatedIpCountAfterFailedDeallocation, NotNil) + check.Assert(allocatedIpCountAfterFailedDeallocation, Equals, 23) // 22 unused IPs + 1 primary + + // Try to deallocate all IPs including primary. Expect a failure as an Edge Gateway must always + // have a primary IP + failedDeallocationIpCount, err = createdEdge.QuickDeallocateIpCount(23) + check.Assert(err, NotNil) + check.Assert(failedDeallocationIpCount, IsNil) + + // Deallocate 22 IP addresses + deallocatedEdge, err := createdEdge.QuickDeallocateIpCount(22) + check.Assert(err, IsNil) + + allocatedIpCountAfterDeallocation, err := deallocatedEdge.GetAllocatedIpCount(true) + check.Assert(err, IsNil) + check.Assert(allocatedIpCountAfterDeallocation, NotNil) + check.Assert(allocatedIpCountAfterDeallocation, Equals, 1) // 1 primary + + // Cleanup + err = createdEdge.Delete() + check.Assert(err, IsNil) + + err = createdNet.Delete() + check.Assert(err, IsNil) +} + +// compareEachIpElementAndOrder performs comparison of IPs in a slice as default check.Assert +// functions is not able to perform this comparison +func compareEachIpElementAndOrder(ipSlice1, ipSlice2 []netip.Addr) bool { + if len(ipSlice1) != len(ipSlice2) { + return false + } + + for index := range ipSlice1 { + if ipSlice1[index] != ipSlice2[index] { + return false + } + } + + return true +} + +// Test_NsxtEdgeQoS tests QoS config (NSX-T Edge Gateway Rate Limiting) retrieval and update +func (vcd *TestVCD) Test_NsxtEdgeQoS(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointQosProfiles) + if vcd.config.VCD.Nsxt.GatewayQosProfile == "" { + check.Skip("No NSX-T Edge Gateway QoS Profile configured") + } + + // Get QoS profile to use + nsxtManagers, err := vcd.client.QueryNsxtManagerByName(vcd.config.VCD.Nsxt.Manager) + check.Assert(err, IsNil) + check.Assert(len(nsxtManagers), Equals, 1) + + uuid, err := GetUuidFromHref(nsxtManagers[0].HREF, true) + check.Assert(err, IsNil) + urn, err := BuildUrnWithUuid("urn:vcloud:nsxtmanager:", uuid) + check.Assert(err, IsNil) + + qosProfile, err := vcd.client.GetNsxtEdgeGatewayQosProfileByDisplayName(urn, vcd.config.VCD.Nsxt.GatewayQosProfile) + check.Assert(err, IsNil) + check.Assert(qosProfile, NotNil) + + org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + nsxtVdc, err := org.GetVDCByName(vcd.config.VCD.Nsxt.Vdc, false) + check.Assert(err, IsNil) + edge, err := nsxtVdc.GetNsxtEdgeGatewayByName(vcd.config.VCD.Nsxt.EdgeGateway) + check.Assert(err, IsNil) + + // Fetch current QoS config + qosConfig, err := edge.GetQoS() + check.Assert(err, IsNil) + check.Assert(qosConfig, NotNil) + check.Assert(qosConfig.EgressProfile, IsNil) + check.Assert(qosConfig.IngressProfile, IsNil) + + // Create new QoS config + newQosConfig := &types.NsxtEdgeGatewayQos{ + IngressProfile: &types.OpenApiReference{ID: qosProfile.NsxtEdgeGatewayQosProfile.ID}, + EgressProfile: &types.OpenApiReference{ID: qosProfile.NsxtEdgeGatewayQosProfile.ID}, + } + + // Update QoS config + updatedEdgeQosConfig, err := edge.UpdateQoS(newQosConfig) + check.Assert(err, IsNil) + check.Assert(updatedEdgeQosConfig, NotNil) + + // Check that updates were applied + check.Assert(updatedEdgeQosConfig.EgressProfile.ID, Equals, newQosConfig.EgressProfile.ID) + check.Assert(updatedEdgeQosConfig.IngressProfile.ID, Equals, newQosConfig.IngressProfile.ID) + + // Remove QoS config + updatedEdgeQosConfig, err = edge.UpdateQoS(&types.NsxtEdgeGatewayQos{}) + check.Assert(err, IsNil) + check.Assert(updatedEdgeQosConfig, NotNil) +} + +func (vcd *TestVCD) Test_NsxtEdgeDhcpForwarder(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointEdgeGatewayDhcpForwarder) + + org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + nsxtVdc, err := org.GetVDCByName(vcd.config.VCD.Nsxt.Vdc, false) + check.Assert(err, IsNil) + edge, err := nsxtVdc.GetNsxtEdgeGatewayByName(vcd.config.VCD.Nsxt.EdgeGateway) + check.Assert(err, IsNil) + AddToCleanupList(vcd.config.VCD.Nsxt.EdgeGateway, "nsxtDhcpForwarder", vcd.config.VCD.Org, check.TestName()) + + // Fetch current DHCP forwarder config + dhcpForwarderConfig, err := edge.GetDhcpForwarder() + check.Assert(err, IsNil) + check.Assert(dhcpForwarderConfig.Enabled, Equals, false) + check.Assert(dhcpForwarderConfig.DhcpServers, DeepEquals, []string(nil)) + + // Create new DHCP Forwarder config + testDhcpServers := []string{ + "1.1.1.1", + "192.168.2.254", + "fe80::abcd", + } + + newDhcpForwarderConfig := &types.NsxtEdgeGatewayDhcpForwarder{ + Enabled: true, + DhcpServers: testDhcpServers, + } + + // Update DHCP forwarder config + updatedEdgeDhcpForwarderConfig, err := edge.UpdateDhcpForwarder(newDhcpForwarderConfig) + check.Assert(err, IsNil) + check.Assert(updatedEdgeDhcpForwarderConfig, NotNil) + + // Check that updates were applied + check.Assert(updatedEdgeDhcpForwarderConfig.Enabled, Equals, true) + check.Assert(updatedEdgeDhcpForwarderConfig.DhcpServers, DeepEquals, testDhcpServers) + + // remove the last dhcp server from the list + testDhcpServers = testDhcpServers[0:2] + newDhcpForwarderConfig.DhcpServers = testDhcpServers + + updatedEdgeDhcpForwarderConfig, err = edge.UpdateDhcpForwarder(newDhcpForwarderConfig) + check.Assert(err, IsNil) + check.Assert(updatedEdgeDhcpForwarderConfig, NotNil) + + // Check that updates were applied + check.Assert(updatedEdgeDhcpForwarderConfig.Enabled, Equals, true) + check.Assert(updatedEdgeDhcpForwarderConfig.DhcpServers, DeepEquals, testDhcpServers) + + // Add servers to the list + testDhcpServers = append(testDhcpServers, "192.254.0.2") + newDhcpForwarderConfig.DhcpServers = testDhcpServers + + updatedEdgeDhcpForwarderConfig, err = edge.UpdateDhcpForwarder(newDhcpForwarderConfig) + check.Assert(err, IsNil) + check.Assert(updatedEdgeDhcpForwarderConfig, NotNil) + + // Check that updates were applied + check.Assert(updatedEdgeDhcpForwarderConfig.Enabled, Equals, true) + check.Assert(updatedEdgeDhcpForwarderConfig.DhcpServers, DeepEquals, testDhcpServers) + + // Disable DHCP forwarder config + newDhcpForwarderConfig.Enabled = false + + // Update DHCP forwarder config + updatedEdgeDhcpForwarderConfig, err = edge.UpdateDhcpForwarder(newDhcpForwarderConfig) + check.Assert(err, IsNil) + check.Assert(updatedEdgeDhcpForwarderConfig, NotNil) + + // Check that updates were applied + check.Assert(updatedEdgeDhcpForwarderConfig.Enabled, Equals, false) + check.Assert(updatedEdgeDhcpForwarderConfig.DhcpServers, DeepEquals, testDhcpServers) + + _, err = edge.UpdateDhcpForwarder(&types.NsxtEdgeGatewayDhcpForwarder{}) + check.Assert(err, IsNil) +} + +// Test_NsxtEdgeSlaacProfile tests SLAAC profile (NSX-T Edge Gateway DHCPv6) retrieval and update +func (vcd *TestVCD) Test_NsxtEdgeSlaacProfile(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointEdgeGatewaySlaacProfile) + + edge, err := vcd.nsxtVdc.GetNsxtEdgeGatewayByName(vcd.config.VCD.Nsxt.EdgeGateway) + check.Assert(err, IsNil) + AddToCleanupList(vcd.config.VCD.Nsxt.EdgeGateway, "slaacProfile", vcd.config.VCD.Org, check.TestName()) + + // Fetch current SLAAC Profile + slaacProfile, err := edge.GetSlaacProfile() + check.Assert(err, IsNil) + check.Assert(slaacProfile, NotNil) + check.Assert(slaacProfile.Enabled, Equals, false) + + // Create new SLAAC config in SLAAC mode + newSlaacProfile := &types.NsxtEdgeGatewaySlaacProfile{ + Enabled: true, + Mode: "SLAAC", + DNSConfig: types.NsxtEdgeGatewaySlaacProfileDNSConfig{ + DNSServerIpv6Addresses: []string{"2001:4860:4860::8888", "2001:4860:4860::8844"}, + DomainNames: []string{"non-existing.org.tld", "fake.org.tld"}, + }, + } + + // Update SLAAC profile + updatedSlaacProfile, err := edge.UpdateSlaacProfile(newSlaacProfile) + check.Assert(err, IsNil) + check.Assert(updatedSlaacProfile, NotNil) + check.Assert(updatedSlaacProfile, DeepEquals, newSlaacProfile) + + // Create new SLAAC config in DHCPv6 mode + newSlaacProfileDhcpv6 := &types.NsxtEdgeGatewaySlaacProfile{ + Enabled: true, + Mode: "DHCPv6", + DNSConfig: types.NsxtEdgeGatewaySlaacProfileDNSConfig{ + DNSServerIpv6Addresses: []string{}, + DomainNames: []string{}, + }, + } + + // Update SLAAC profile + updatedSlaacProfileDhcpv6, err := edge.UpdateSlaacProfile(newSlaacProfileDhcpv6) + check.Assert(err, IsNil) + check.Assert(updatedSlaacProfileDhcpv6, NotNil) + check.Assert(updatedSlaacProfileDhcpv6, DeepEquals, newSlaacProfileDhcpv6) + + // Cleanup + updatedSlaacProfile, err = edge.UpdateSlaacProfile(&types.NsxtEdgeGatewaySlaacProfile{Enabled: false, Mode: "DISABLED"}) + check.Assert(err, IsNil) + check.Assert(updatedSlaacProfile, NotNil) +} + +// Test_NsxtEdgeCreateWithT0AndExternalNetworks checks that IP Allocation counts and External +// Network attachment works well with NSX-T T0 Gateway backed external network +func (vcd *TestVCD) Test_NsxtEdgeCreateWithT0AndExternalNetworks(check *C) { + test_NsxtEdgeCreateWithExternalNetworks(vcd, check, vcd.config.VCD.Nsxt.Tier0router, types.ExternalNetworkBackingTypeNsxtTier0Router) +} + +// Test_NsxtEdgeCreateWithT0VrfAndExternalNetworks checks that IP Allocation counts and External +// Network attachment works well with NSX-T T0 VRF Gateway backed external network +func (vcd *TestVCD) Test_NsxtEdgeCreateWithT0VrfAndExternalNetworks(check *C) { + test_NsxtEdgeCreateWithExternalNetworks(vcd, check, vcd.config.VCD.Nsxt.Tier0routerVrf, types.ExternalNetworkBackingTypeNsxtVrfTier0Router) +} + +func test_NsxtEdgeCreateWithExternalNetworks(vcd *TestVCD, check *C, backingRouter, backingRouterType string) { + if vcd.client.Client.APIVCDMaxVersionIs("< 37.1") { + check.Skip("Segment Backed External Network uplinks are supported in VCD 10.4.1+") + } + + if vcd.config.VCD.Nsxt.NsxtImportSegment == "" || vcd.config.VCD.Nsxt.NsxtImportSegment2 == "" { + check.Skip("NSX-T Imported Segments are not configured") + } + + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointEdgeGateways) + vcd.skipIfNotSysAdmin(check) + + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + + nsxtVdc, err := adminOrg.GetVDCByName(vcd.config.VCD.Nsxt.Vdc, false) + if ContainsNotFound(err) { + check.Skip(fmt.Sprintf("No NSX-T VDC (%s) found - skipping test", vcd.config.VCD.Nsxt.Vdc)) + } + check.Assert(err, IsNil) + check.Assert(nsxtVdc, NotNil) + + // Setup 2 NSX-T Segment backed External Networks and 1 T0 or T0 VRF backed networks + nsxtManager, err := vcd.client.QueryNsxtManagerByName(vcd.config.VCD.Nsxt.Manager) + check.Assert(err, IsNil) + nsxtManagerId, err := BuildUrnWithUuid("urn:vcloud:nsxtmanager:", extractUuid(nsxtManager[0].HREF)) + check.Assert(err, IsNil) + + // T0 backed external network + backingExtNet := getBackingIdByNameAndType(check, backingRouter, backingRouterType, vcd, nsxtManagerId) + nsxtExternalNetworkCfg := t0vrfBackedExternalNetworkConfig(vcd, check.TestName()+"-t0", "89.1.1", backingRouterType, backingExtNet, nsxtManagerId) + nsxtExternalNetwork, err := CreateExternalNetworkV2(vcd.client, nsxtExternalNetworkCfg) + check.Assert(err, IsNil) + check.Assert(nsxtExternalNetwork, NotNil) + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointExternalNetworks + nsxtExternalNetwork.ExternalNetwork.ID + AddToCleanupListOpenApi(nsxtExternalNetwork.ExternalNetwork.Name, check.TestName(), openApiEndpoint) + + // First NSX-T Segment backed network + backingId1 := getBackingIdByNameAndType(check, vcd.config.VCD.Nsxt.NsxtImportSegment, types.ExternalNetworkBackingTypeNsxtSegment, vcd, nsxtManagerId) + segmentBackedNet1Cfg := t0vrfBackedExternalNetworkConfig(vcd, check.TestName()+"-1", "1.1.1", types.ExternalNetworkBackingTypeNsxtSegment, backingId1, nsxtManagerId) + segmentBackedNet1, err := CreateExternalNetworkV2(vcd.client, segmentBackedNet1Cfg) + check.Assert(err, IsNil) + check.Assert(segmentBackedNet1, NotNil) + openApiEndpoint = types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointExternalNetworks + segmentBackedNet1.ExternalNetwork.ID + AddToCleanupListOpenApi(segmentBackedNet1.ExternalNetwork.Name, check.TestName(), openApiEndpoint) + + // Second NSX-T Segment backed network + backingId2 := getBackingIdByNameAndType(check, vcd.config.VCD.Nsxt.NsxtImportSegment2, types.ExternalNetworkBackingTypeNsxtSegment, vcd, nsxtManagerId) + segmentBackedNet2Cfg := t0vrfBackedExternalNetworkConfig(vcd, check.TestName()+"-2", "4.4.4", types.ExternalNetworkBackingTypeNsxtSegment, backingId2, nsxtManagerId) + segmentBackedNet2, err := CreateExternalNetworkV2(vcd.client, segmentBackedNet2Cfg) + check.Assert(err, IsNil) + check.Assert(segmentBackedNet2, NotNil) + openApiEndpoint = types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointExternalNetworks + segmentBackedNet2.ExternalNetwork.ID + AddToCleanupListOpenApi(segmentBackedNet1.ExternalNetwork.Name, check.TestName(), openApiEndpoint) + // Setup 2 NSX-T Segment backed External Networks and 1 T0 or T0 VRF backed networks + + egwDefinition := &types.OpenAPIEdgeGateway{ + Name: "nsx-t-edge", + Description: "nsx-t-edge-description", + OrgVdc: &types.OpenApiReference{ + ID: nsxtVdc.Vdc.ID, + }, + EdgeGatewayUplinks: []types.EdgeGatewayUplinks{ + { + UplinkID: nsxtExternalNetwork.ExternalNetwork.ID, + Subnets: types.OpenAPIEdgeGatewaySubnets{Values: []types.OpenAPIEdgeGatewaySubnetValue{{ + Gateway: "5.1.1.1", + PrefixLength: 24, + Enabled: true, + }}}, + Connected: true, + Dedicated: false, + }, + { + UplinkID: segmentBackedNet1.ExternalNetwork.ID, + Subnets: types.OpenAPIEdgeGatewaySubnets{Values: []types.OpenAPIEdgeGatewaySubnetValue{{ + Gateway: "1.1.1.1", + PrefixLength: 24, + Enabled: true, + AutoAllocateIPRanges: true, + PrimaryIP: "1.1.1.5", + TotalIPCount: addrOf(4), + }}}, + Connected: true, + Dedicated: false, + }, + { + UplinkID: segmentBackedNet2.ExternalNetwork.ID, + Subnets: types.OpenAPIEdgeGatewaySubnets{Values: []types.OpenAPIEdgeGatewaySubnetValue{{ + Gateway: "4.4.4.1", + PrefixLength: 24, + Enabled: true, + AutoAllocateIPRanges: true, + TotalIPCount: addrOf(7), + }}}, + Connected: true, + Dedicated: false, + }, + }, + } + + createdEdge, err := adminOrg.CreateNsxtEdgeGateway(egwDefinition) + check.Assert(err, IsNil) + check.Assert(createdEdge.EdgeGateway.Name, Equals, egwDefinition.Name) + openApiEndpoint = types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGateways + createdEdge.EdgeGateway.ID + PrependToCleanupListOpenApi(createdEdge.EdgeGateway.Name, check.TestName(), openApiEndpoint) + + // Retrieve edge gateway + retrievedEdge, err := adminOrg.GetNsxtEdgeGatewayById(createdEdge.EdgeGateway.ID) + check.Assert(err, IsNil) + check.Assert(retrievedEdge, NotNil) + + // Check IP allocation in NSX-T Segment backed networks + totalAllocatedIpCountSegmentBacked, err := retrievedEdge.GetAllocatedIpCountByUplinkType(false, types.ExternalNetworkBackingTypeNsxtSegment) + check.Assert(err, IsNil) + check.Assert(totalAllocatedIpCountSegmentBacked, Equals, (4 + 7)) + + // Check IP allocation in NSX-T T0 backed networks + totalAllocatedIpCountT0backed, err := retrievedEdge.GetAllocatedIpCountByUplinkType(false, backingRouterType) + check.Assert(err, IsNil) + check.Assert(totalAllocatedIpCountT0backed, Equals, 1) + + totalAllocatedIpCountForPrimaryUplink, err := retrievedEdge.GetPrimaryNetworkAllocatedIpCount(false) + check.Assert(err, IsNil) + check.Assert(totalAllocatedIpCountForPrimaryUplink, Equals, 1) + + // Check IP allocation for all subnets + totalAllocatedIpCount, err := retrievedEdge.GetAllocatedIpCount(false) + check.Assert(err, IsNil) + check.Assert(totalAllocatedIpCount, Equals, (1 + 4 + 7)) + + createdEdge.EdgeGateway.Name = check.TestName() + "-renamed-edge" + updatedEdge, err := createdEdge.Update(createdEdge.EdgeGateway) + check.Assert(err, IsNil) + check.Assert(updatedEdge.EdgeGateway.Name, Equals, createdEdge.EdgeGateway.Name) + + // Check IP allocation in NSX-T Segment backed networks + totalAllocatedIpCountSegmentBacked, err = updatedEdge.GetAllocatedIpCountByUplinkType(false, types.ExternalNetworkBackingTypeNsxtSegment) + check.Assert(err, IsNil) + check.Assert(totalAllocatedIpCountSegmentBacked, Equals, (4 + 7)) + + // Check IP allocation in NSX-T T0 backed networks + totalAllocatedIpCountT0backed, err = updatedEdge.GetAllocatedIpCountByUplinkType(false, backingRouterType) + check.Assert(err, IsNil) + check.Assert(totalAllocatedIpCountT0backed, Equals, 1) + + // Check IP allocation for all subnets + totalAllocatedIpCount, err = updatedEdge.GetAllocatedIpCount(false) + check.Assert(err, IsNil) + check.Assert(totalAllocatedIpCount, Equals, (1 + 4 + 7)) + + // Cleanup + err = updatedEdge.Delete() + check.Assert(err, IsNil) + + err = segmentBackedNet2.Delete() + check.Assert(err, IsNil) + + err = segmentBackedNet1.Delete() + check.Assert(err, IsNil) + + err = nsxtExternalNetwork.Delete() + check.Assert(err, IsNil) + +} + +func t0vrfBackedExternalNetworkConfig(vcd *TestVCD, name, ipPrefix string, backingType, backingId, NetworkProviderId string) *types.ExternalNetworkV2 { + net := &types.ExternalNetworkV2{ + Name: name, + Subnets: types.ExternalNetworkV2Subnets{Values: []types.ExternalNetworkV2Subnet{ + { + Gateway: ipPrefix + ".1", + PrefixLength: 24, + IPRanges: types.ExternalNetworkV2IPRanges{Values: []types.ExternalNetworkV2IPRange{ + { + StartAddress: ipPrefix + ".3", + EndAddress: ipPrefix + ".50", + }, + }}, + Enabled: true, + }, + }}, + NetworkBackings: types.ExternalNetworkV2Backings{Values: []types.ExternalNetworkV2Backing{ + { + BackingID: backingId, + NetworkProvider: types.NetworkProvider{ + ID: NetworkProviderId, + }, + BackingTypeValue: backingType, + }, + }}, + } + + return net +} diff --git a/govcd/nsxt_edgegateway_unit_test.go b/govcd/nsxt_edgegateway_unit_test.go new file mode 100644 index 000000000..7d803ae02 --- /dev/null +++ b/govcd/nsxt_edgegateway_unit_test.go @@ -0,0 +1,1817 @@ +//go:build unit || ALL + +/* +* Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "net/netip" + "reflect" + "testing" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +func Test_filterIpSlicesBySubnet(t *testing.T) { + type args struct { + ipRange []netip.Addr + subnet netip.Prefix + } + tests := []struct { + name string + args args + want []netip.Addr + wantErr bool + }{ + {name: "BothArgsEmpty", args: args{}, want: nil, wantErr: true}, + {name: "EmptyRange", args: args{subnet: netip.MustParsePrefix("10.10.10.1/24")}, want: nil, wantErr: true}, + {name: "EmptySubnet", args: args{ipRange: []netip.Addr{netip.MustParseAddr("10.1.1.1")}}, want: nil, wantErr: true}, + { + name: "SingleIpMatchingSubnet", + args: args{ + ipRange: []netip.Addr{netip.MustParseAddr("10.0.0.2")}, + subnet: netip.MustParsePrefix("10.0.0.1/24"), + }, + want: []netip.Addr{netip.MustParseAddr("10.0.0.2")}, + wantErr: false, + }, + { + name: "SingleIpNotMatchingSubnet", + args: args{ + ipRange: []netip.Addr{netip.MustParseAddr("10.0.0.2")}, + subnet: netip.MustParsePrefix("20.0.0.1/24"), + }, + want: []netip.Addr{}, + wantErr: false, + }, + { + name: "ManyIPsSomeMatch", + args: args{ + ipRange: []netip.Addr{ + netip.MustParseAddr("10.0.0.2"), + netip.MustParseAddr("192.0.0.2"), + netip.MustParseAddr("11.0.0.2"), + netip.MustParseAddr("20.0.0.2"), + netip.MustParseAddr("20.0.0.3"), + netip.MustParseAddr("10.0.0.2"), + }, + subnet: netip.MustParsePrefix("20.0.0.1/24"), + }, + want: []netip.Addr{ + netip.MustParseAddr("20.0.0.2"), + netip.MustParseAddr("20.0.0.3"), + }, + wantErr: false, + }, + { + name: "DuplicateIPsInRange", + args: args{ + ipRange: []netip.Addr{ + netip.MustParseAddr("10.0.0.2"), + netip.MustParseAddr("10.0.0.2"), + netip.MustParseAddr("192.0.0.2"), + netip.MustParseAddr("11.0.0.2"), + netip.MustParseAddr("20.0.0.2"), + netip.MustParseAddr("20.0.0.3"), + netip.MustParseAddr("20.0.0.3"), + netip.MustParseAddr("10.0.0.2"), + }, + subnet: netip.MustParsePrefix("20.0.0.1/24"), + }, + want: []netip.Addr{ + netip.MustParseAddr("20.0.0.2"), + netip.MustParseAddr("20.0.0.3"), + netip.MustParseAddr("20.0.0.3"), + }, + wantErr: false, + }, + // IPv6 + { + name: "IPv6SingleMatchingSubnet", + args: args{ + ipRange: []netip.Addr{netip.MustParseAddr("2001:0DB8:0000:000b:0000:0000:0000:0001")}, + subnet: netip.MustParsePrefix("2001:0DB8:0000:000b::/64"), + }, + want: []netip.Addr{netip.MustParseAddr("2001:0DB8:0000:000b:0000:0000:0000:0001")}, + wantErr: false, + }, + { + name: "IPv6SingleNotMatchingSubnet", + args: args{ + ipRange: []netip.Addr{netip.MustParseAddr("2001:0DB6:0000:000b:0000:0000:0000:0001")}, + subnet: netip.MustParsePrefix("2001:0DB8:0000:000b::/64"), + }, + want: []netip.Addr{}, + wantErr: false, + }, + { + name: "IPv6ManyIPsSomeMatch", + args: args{ + ipRange: []netip.Addr{ + netip.MustParseAddr("2001:1111:0000:000b:0000:0000:0000:0001"), + netip.MustParseAddr("2222:0DB8:0000:000b:0000:0000:0000:0001"), + netip.MustParseAddr("2001:0DB8:0000:000b:0000:0000:0000:0001"), + netip.MustParseAddr("2001:0DB8:0000:000b:0000:0000:0000:0002"), + netip.MustParseAddr("2001:0DB8:0000:000b:0000:0000:0000:0003"), + netip.MustParseAddr("4001:0DB8:0000:000b:0000:0000:0000:0001"), + netip.MustParseAddr("4001:0DB8:0000:000b:0000:0000:0000:0001"), + }, + subnet: netip.MustParsePrefix("2001:0DB8:0000:000b::/64"), + }, + want: []netip.Addr{ + netip.MustParseAddr("2001:0DB8:0000:000b:0000:0000:0000:0001"), + netip.MustParseAddr("2001:0DB8:0000:000b:0000:0000:0000:0002"), + netip.MustParseAddr("2001:0DB8:0000:000b:0000:0000:0000:0003"), + }, + wantErr: false, + }, + { + name: "IPv6ManyIPsSomeDuplicatesMatch", + args: args{ + ipRange: []netip.Addr{ + netip.MustParseAddr("2001:1111:0000:000b:0000:0000:0000:0001"), + netip.MustParseAddr("2222:0DB8:0000:000b:0000:0000:0000:0001"), + netip.MustParseAddr("2001:0DB8:0000:000b:0000:0000:0000:0001"), + netip.MustParseAddr("2001:0DB8:0000:000b:0000:0000:0000:0002"), + netip.MustParseAddr("2001:0DB8:0000:000b:0000:0000:0000:0002"), + netip.MustParseAddr("2001:0DB8:0000:000b:0000:0000:0000:0003"), + netip.MustParseAddr("4001:0DB8:0000:000b:0000:0000:0000:0001"), + netip.MustParseAddr("4001:0DB8:0000:000b:0000:0000:0000:0001"), + }, + subnet: netip.MustParsePrefix("2001:0DB8:0000:000b::/64"), + }, + want: []netip.Addr{ + netip.MustParseAddr("2001:0DB8:0000:000b:0000:0000:0000:0001"), + netip.MustParseAddr("2001:0DB8:0000:000b:0000:0000:0000:0002"), + netip.MustParseAddr("2001:0DB8:0000:000b:0000:0000:0000:0002"), + netip.MustParseAddr("2001:0DB8:0000:000b:0000:0000:0000:0003"), + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := filterIpSlicesBySubnet(tt.args.ipRange, tt.args.subnet) + if (err != nil) != tt.wantErr { + t.Errorf("filterIpRangesInSubnet() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("filterIpRangesInSubnet() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_ipSliceDifference(t *testing.T) { + type args struct { + minuendSlice []netip.Addr + subtrahendSlice []netip.Addr + } + tests := []struct { + name string + args args + want []netip.Addr + }{ + { + name: "BothParamsNil", + args: args{ + minuendSlice: nil, + subtrahendSlice: nil, + }, + want: nil, + }, + { + name: "MinuendNil", + args: args{ + minuendSlice: nil, + subtrahendSlice: []netip.Addr{ + netip.MustParseAddr("10.0.0.1"), + }, + }, + want: nil, + }, + { + name: "MinuendEmptySliceNilSubtrahend", + args: args{ + minuendSlice: make([]netip.Addr, 0), + subtrahendSlice: nil, + }, + want: make([]netip.Addr, 0), + }, + { + name: "MinuendEmptySlice", + args: args{ + minuendSlice: []netip.Addr{{}}, + subtrahendSlice: []netip.Addr{ + netip.MustParseAddr("10.0.0.1"), + }, + }, + want: []netip.Addr{{}}, + }, + { + name: "SubtrahendNil", + args: args{ + minuendSlice: []netip.Addr{ + netip.MustParseAddr("10.0.0.1"), + }, + subtrahendSlice: nil, + }, + want: []netip.Addr{ + netip.MustParseAddr("10.0.0.1"), + }, + }, + { + name: "SubtractUnavailableIP", + args: args{ + minuendSlice: []netip.Addr{ + netip.MustParseAddr("10.0.0.1"), + netip.MustParseAddr("10.0.0.2"), + }, + subtrahendSlice: []netip.Addr{ + netip.MustParseAddr("20.0.0.1"), + }, + }, + want: []netip.Addr{ + netip.MustParseAddr("10.0.0.1"), + netip.MustParseAddr("10.0.0.2"), + }, + }, + { + name: "SubtractIP", + args: args{ + minuendSlice: []netip.Addr{ + netip.MustParseAddr("10.0.0.1"), + netip.MustParseAddr("10.0.0.2"), + }, + subtrahendSlice: []netip.Addr{ + netip.MustParseAddr("10.0.0.2"), + }, + }, + want: []netip.Addr{ + netip.MustParseAddr("10.0.0.1"), + }, + }, + { + name: "RemoveAll", + args: args{ + minuendSlice: []netip.Addr{ + netip.MustParseAddr("10.0.0.1"), + netip.MustParseAddr("10.0.0.2"), + }, + subtrahendSlice: []netip.Addr{ + netip.MustParseAddr("10.0.0.1"), + netip.MustParseAddr("10.0.0.2"), + }, + }, + want: nil, + }, + { + name: "SubtractIPWithDuplicates", + args: args{ + minuendSlice: []netip.Addr{ + netip.MustParseAddr("10.0.0.1"), + netip.MustParseAddr("10.0.0.2"), + netip.MustParseAddr("10.0.0.2"), + }, + subtrahendSlice: []netip.Addr{ + netip.MustParseAddr("10.0.0.2"), + }, + }, + want: []netip.Addr{ + netip.MustParseAddr("10.0.0.1"), + }, + }, + // IPv6 + { + name: "IPv6MinuendNil", + args: args{ + minuendSlice: nil, + subtrahendSlice: []netip.Addr{ + netip.MustParseAddr("4001:0DB8:0000:000b:0000:0000:0000:0001"), + }, + }, + want: nil, + }, + { + name: "IPv6SubtrahendNil", + args: args{ + minuendSlice: []netip.Addr{ + netip.MustParseAddr("4001:0DB8:0000:000b:0000:0000:0000:0001"), + }, + subtrahendSlice: nil, + }, + want: []netip.Addr{ + netip.MustParseAddr("4001:0DB8:0000:000b:0000:0000:0000:0001"), + }, + }, + { + name: "IPv6SubtractUnavailableIP", + args: args{ + minuendSlice: []netip.Addr{ + netip.MustParseAddr("4001:0DB8:0000:000b:0000:0000:0000:0001"), + netip.MustParseAddr("4001:0DB8:0000:000b:0000:0000:0000:0002"), + }, + subtrahendSlice: []netip.Addr{ + netip.MustParseAddr("9001:0DB8:0000:000b:0000:0000:0000:0002"), + }, + }, + want: []netip.Addr{ + netip.MustParseAddr("4001:0DB8:0000:000b:0000:0000:0000:0001"), + netip.MustParseAddr("4001:0DB8:0000:000b:0000:0000:0000:0002"), + }, + }, + { + name: "IPv6SubtractIP", + args: args{ + minuendSlice: []netip.Addr{ + netip.MustParseAddr("4001:0DB8:0000:000b:0000:0000:0000:0001"), + netip.MustParseAddr("4001:0DB8:0000:000b:0000:0000:0000:0002"), + }, + subtrahendSlice: []netip.Addr{ + netip.MustParseAddr("4001:0DB8:0000:000b:0000:0000:0000:0002"), + }, + }, + want: []netip.Addr{ + netip.MustParseAddr("4001:0DB8:0000:000b:0000:0000:0000:0001"), + }, + }, + { + name: "IPv6SubtractIPWithDuplicates", + args: args{ + minuendSlice: []netip.Addr{ + netip.MustParseAddr("4001:0DB8:0000:000b:0000:0000:0000:0001"), + netip.MustParseAddr("4001:0DB8:0000:000b:0000:0000:0000:0002"), + netip.MustParseAddr("4001:0DB8:0000:000b:0000:0000:0000:0002"), + }, + subtrahendSlice: []netip.Addr{ + netip.MustParseAddr("4001:0DB8:0000:000b:0000:0000:0000:0002"), + }, + }, + want: []netip.Addr{ + netip.MustParseAddr("4001:0DB8:0000:000b:0000:0000:0000:0001"), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ipSliceDifference(tt.args.minuendSlice, tt.args.subtrahendSlice); !reflect.DeepEqual(got, tt.want) { + t.Errorf("ipRangeDifference() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_flattenEdgeGatewayUplinkToIpSlice(t *testing.T) { + type args struct { + uplinks []types.EdgeGatewayUplinks + limitTo int64 + } + tests := []struct { + name string + args args + want []netip.Addr + wantErr bool + }{ + { + name: "SingleStartAndEndAddresses", + args: args{ + uplinks: []types.EdgeGatewayUplinks{ + { + Subnets: types.OpenAPIEdgeGatewaySubnets{ + Values: []types.OpenAPIEdgeGatewaySubnetValue{ + { + IPRanges: &types.OpenApiIPRanges{ + Values: []types.OpenApiIPRangeValues{ + { + StartAddress: "10.10.10.1", + EndAddress: "10.10.10.2", + }, + }, + }, + }, + }, + }, + }, + }, + }, + want: []netip.Addr{ + netip.MustParseAddr("10.10.10.1"), + netip.MustParseAddr("10.10.10.2"), + }, + wantErr: false, + }, + { + name: "IPv6BigSubnetLimit3", + args: args{ + uplinks: []types.EdgeGatewayUplinks{ + { + Subnets: types.OpenAPIEdgeGatewaySubnets{ + Values: []types.OpenAPIEdgeGatewaySubnetValue{ + { + IPRanges: &types.OpenApiIPRanges{ + Values: []types.OpenApiIPRangeValues{ + { + StartAddress: "2a02:a404:11:0:0:0:0:1", + EndAddress: "2a02:a404:11:0:ffff:ffff:ffff:fffd", + }, + }, + }, + }, + }, + }, + }, + }, + limitTo: 3, + }, + want: []netip.Addr{ + netip.MustParseAddr("2a02:a404:11:0:0:0:0:1"), + netip.MustParseAddr("2a02:a404:11:0:0:0:0:2"), + netip.MustParseAddr("2a02:a404:11:0:0:0:0:3"), + }, + wantErr: false, + }, + { + name: "IPv6BigSubnetLimit1", + args: args{ + uplinks: []types.EdgeGatewayUplinks{ + { + Subnets: types.OpenAPIEdgeGatewaySubnets{ + Values: []types.OpenAPIEdgeGatewaySubnetValue{ + { + IPRanges: &types.OpenApiIPRanges{ + Values: []types.OpenApiIPRangeValues{ + { + StartAddress: "2a02:a404:11:0:0:0:0:1", + EndAddress: "2a02:a404:11:0:ffff:ffff:ffff:fffd", + }, + }, + }, + }, + }, + }, + }, + }, + limitTo: 1, + }, + want: []netip.Addr{ + netip.MustParseAddr("2a02:a404:11:0:0:0:0:1"), + }, + wantErr: false, + }, + { + name: "ReverseStartAndEnd", + args: args{ + uplinks: []types.EdgeGatewayUplinks{ + { + Subnets: types.OpenAPIEdgeGatewaySubnets{ + Values: []types.OpenAPIEdgeGatewaySubnetValue{ + { + IPRanges: &types.OpenApiIPRanges{ + Values: []types.OpenApiIPRangeValues{ + { + StartAddress: "10.10.10.2", + EndAddress: "10.10.10.1", + }, + }, + }, + }, + }, + }, + }, + }, + }, + want: nil, + wantErr: true, + }, + { + name: "SameStartAndEndAddresses", + args: args{ + uplinks: []types.EdgeGatewayUplinks{ + { + Subnets: types.OpenAPIEdgeGatewaySubnets{ + Values: []types.OpenAPIEdgeGatewaySubnetValue{ + { + IPRanges: &types.OpenApiIPRanges{ + Values: []types.OpenApiIPRangeValues{ + { + StartAddress: "10.10.10.1", + EndAddress: "10.10.10.1", + }, + }, + }, + }, + }, + }, + }, + }, + }, + want: []netip.Addr{ + netip.MustParseAddr("10.10.10.1"), + }, + wantErr: false, + }, + { + name: "StartAddressOnly", + args: args{ + uplinks: []types.EdgeGatewayUplinks{ + { + Subnets: types.OpenAPIEdgeGatewaySubnets{ + Values: []types.OpenAPIEdgeGatewaySubnetValue{ + { + IPRanges: &types.OpenApiIPRanges{ + Values: []types.OpenApiIPRangeValues{ + { + StartAddress: "10.10.10.1", + }, + }, + }, + }, + }, + }, + }, + }, + }, + want: []netip.Addr{ + netip.MustParseAddr("10.10.10.1"), + }, + wantErr: false, + }, + { + name: "EmptyUplink", + args: args{ + uplinks: []types.EdgeGatewayUplinks{ + {}, + }, + }, + want: make([]netip.Addr, 0), + wantErr: false, + }, + { + name: "EmptySubnets", + args: args{ + uplinks: []types.EdgeGatewayUplinks{ + { + Subnets: types.OpenAPIEdgeGatewaySubnets{}, + }, + }, + }, + want: make([]netip.Addr, 0), + wantErr: false, + }, + { + name: "EmptySubnetValues", + args: args{ + uplinks: []types.EdgeGatewayUplinks{ + { + Subnets: types.OpenAPIEdgeGatewaySubnets{ + Values: []types.OpenAPIEdgeGatewaySubnetValue{}, + }, + }, + }, + }, + want: make([]netip.Addr, 0), + wantErr: false, + }, + { + name: "EmptySubnetValueIpRanges", + args: args{ + uplinks: []types.EdgeGatewayUplinks{ + { + Subnets: types.OpenAPIEdgeGatewaySubnets{ + Values: []types.OpenAPIEdgeGatewaySubnetValue{ + { + IPRanges: &types.OpenApiIPRanges{}, + }, + }, + }, + }, + }, + }, + want: make([]netip.Addr, 0), + wantErr: false, + }, + { + name: "EmptySubnetValueIpRangeValues", + args: args{ + uplinks: []types.EdgeGatewayUplinks{ + { + Subnets: types.OpenAPIEdgeGatewaySubnets{ + Values: []types.OpenAPIEdgeGatewaySubnetValue{ + { + IPRanges: &types.OpenApiIPRanges{ + Values: []types.OpenApiIPRangeValues{}, + }, + }, + }, + }, + }, + }, + }, + want: make([]netip.Addr, 0), + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := flattenEdgeGatewayUplinkToIpSlice(tt.args.uplinks, tt.args.limitTo) + if (err != nil) != tt.wantErr { + t.Errorf("ipSliceFromEdgeGatewayUplinks() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ipSliceFromEdgeGatewayUplinks() = %v, want %v", got, tt.want) + } + }) + } +} + +// buildSimpleUplinkStructure helps to avoid deep indentation in Test_getUnusedExternalIPAddress +// where the structure itself is simple enough that has only one subnet and one IP range. Other +// tests in this table test still contain the full structure as it would be less readable if it was +// wrapped into multiple function calls. +func buildSimpleUplinkStructure(ipRangeValues []types.OpenApiIPRangeValues) []types.EdgeGatewayUplinks { + return []types.EdgeGatewayUplinks{ + { + Subnets: types.OpenAPIEdgeGatewaySubnets{ + Values: []types.OpenAPIEdgeGatewaySubnetValue{ + { + IPRanges: &types.OpenApiIPRanges{ + Values: ipRangeValues, + }, + }, + }, + }, + }, + } +} + +func Test_getUnusedExternalIPAddress(t *testing.T) { + type args struct { + uplinks []types.EdgeGatewayUplinks + usedIpAddresses []*types.GatewayUsedIpAddress + requiredCount int + optionalSubnet netip.Prefix + } + tests := []struct { + name string + args args + want []netip.Addr + wantErr bool + }{ + { + name: "EmptyStructureError", + args: args{ + uplinks: []types.EdgeGatewayUplinks{}, + usedIpAddresses: []*types.GatewayUsedIpAddress{{}}, + requiredCount: 1, + optionalSubnet: netip.Prefix{}, + }, + want: nil, + wantErr: true, + }, + { + name: "SingleIpAvailable", + args: args{ + uplinks: buildSimpleUplinkStructure([]types.OpenApiIPRangeValues{ + { + StartAddress: "10.10.10.1", + EndAddress: "10.10.10.1", + }, + }), + usedIpAddresses: []*types.GatewayUsedIpAddress{}, + requiredCount: 1, + optionalSubnet: netip.Prefix{}, + }, + want: []netip.Addr{ + netip.MustParseAddr("10.10.10.1"), + }, + wantErr: false, + }, + { + name: "AvailableIPsFilteredOff", + args: args{ + uplinks: buildSimpleUplinkStructure([]types.OpenApiIPRangeValues{ + { + StartAddress: "10.10.10.1", + EndAddress: "10.10.10.10", + }, + }), + usedIpAddresses: []*types.GatewayUsedIpAddress{}, + requiredCount: 1, + optionalSubnet: netip.MustParsePrefix("20.10.10.0/24"), + }, + want: nil, + wantErr: true, + }, + { + name: "AvailableIPsFilteredOffAndUsed", + args: args{ + uplinks: buildSimpleUplinkStructure([]types.OpenApiIPRangeValues{ + { + StartAddress: "10.10.10.1", + EndAddress: "10.10.10.10", + }, + }), + usedIpAddresses: []*types.GatewayUsedIpAddress{{IPAddress: "10.10.10.1"}}, + requiredCount: 1, + optionalSubnet: netip.MustParsePrefix("20.10.10.0/24"), + }, + want: nil, + wantErr: true, + }, + { + name: "SingleIpFromMany", + args: args{ + uplinks: buildSimpleUplinkStructure([]types.OpenApiIPRangeValues{ + { + StartAddress: "10.10.10.15", + EndAddress: "10.10.10.200", + }, + }), + usedIpAddresses: []*types.GatewayUsedIpAddress{}, + requiredCount: 1, + optionalSubnet: netip.Prefix{}, + }, + want: []netip.Addr{ + netip.MustParseAddr("10.10.10.15"), + }, + wantErr: false, + }, + { + name: "CrossBoundary", + args: args{ + uplinks: buildSimpleUplinkStructure([]types.OpenApiIPRangeValues{ + { + StartAddress: "10.10.10.255", + EndAddress: "10.10.11.1", + }, + }), + usedIpAddresses: []*types.GatewayUsedIpAddress{}, + requiredCount: 3, + optionalSubnet: netip.Prefix{}, + }, + want: []netip.Addr{ + netip.MustParseAddr("10.10.10.255"), + netip.MustParseAddr("10.10.11.0"), + netip.MustParseAddr("10.10.11.1"), + }, + wantErr: false, + }, + { + name: "CrossBoundaryPrefix", + args: args{ + uplinks: buildSimpleUplinkStructure([]types.OpenApiIPRangeValues{ + { + StartAddress: "10.10.10.255", + EndAddress: "10.10.11.1", + }, + }), + usedIpAddresses: []*types.GatewayUsedIpAddress{}, + requiredCount: 2, + optionalSubnet: netip.MustParsePrefix("10.10.11.0/24"), + }, + want: []netip.Addr{ + netip.MustParseAddr("10.10.11.0"), + netip.MustParseAddr("10.10.11.1"), + }, + wantErr: false, + }, + { + name: "CrossBoundaryPrefixAndUsed", + args: args{ + uplinks: buildSimpleUplinkStructure([]types.OpenApiIPRangeValues{ + { + StartAddress: "10.10.10.255", + EndAddress: "10.10.11.1", + }, + }), + usedIpAddresses: []*types.GatewayUsedIpAddress{{IPAddress: "10.10.11.0"}}, + requiredCount: 1, + optionalSubnet: netip.MustParsePrefix("10.10.11.0/24"), + }, + want: []netip.Addr{ + netip.MustParseAddr("10.10.11.1"), + }, + wantErr: false, + }, + { + name: "IPv6SingleIpAvailable", + args: args{ + uplinks: buildSimpleUplinkStructure([]types.OpenApiIPRangeValues{ + { + StartAddress: "4001:0DB8:0000:000b:0000:0000:0000:0001", + EndAddress: "4001:0DB8:0000:000b:0000:0000:0000:0001", + }, + }), + usedIpAddresses: []*types.GatewayUsedIpAddress{}, + requiredCount: 1, + optionalSubnet: netip.Prefix{}, + }, + want: []netip.Addr{ + netip.MustParseAddr("4001:0DB8:0000:000b:0000:0000:0000:0001"), + }, + wantErr: false, + }, + { + name: "SingleIpAvailableStartOnly", + args: args{ + uplinks: buildSimpleUplinkStructure([]types.OpenApiIPRangeValues{ + { + StartAddress: "10.10.10.1", + }, + }), + usedIpAddresses: []*types.GatewayUsedIpAddress{}, + requiredCount: 1, + optionalSubnet: netip.Prefix{}, + }, + want: []netip.Addr{ + netip.MustParseAddr("10.10.10.1"), + }, + wantErr: false, + }, + { + name: "IPv6SingleIpAvailableStartOnly", + args: args{ + uplinks: buildSimpleUplinkStructure([]types.OpenApiIPRangeValues{ + { + StartAddress: "4001:0DB8:0000:000b:0000:0000:0000:0001", + }, + }), + usedIpAddresses: []*types.GatewayUsedIpAddress{}, + requiredCount: 1, + optionalSubnet: netip.Prefix{}, + }, + want: []netip.Addr{ + netip.MustParseAddr("4001:0DB8:0000:000b:0000:0000:0000:0001"), + }, + wantErr: false, + }, + { + name: "InvalidIpRange", + args: args{ + uplinks: buildSimpleUplinkStructure([]types.OpenApiIPRangeValues{ + { + // Start Address is higher than end IP address + StartAddress: "10.10.10.200", + EndAddress: "10.10.10.1", + }, + }), + usedIpAddresses: []*types.GatewayUsedIpAddress{}, + requiredCount: 1, + optionalSubnet: netip.Prefix{}, + }, + want: nil, + wantErr: true, + }, + { + name: "InsufficientIPs", + args: args{ + uplinks: buildSimpleUplinkStructure([]types.OpenApiIPRangeValues{ + { + StartAddress: "10.10.10.1", + EndAddress: "10.10.10.6", + }, + }), + usedIpAddresses: []*types.GatewayUsedIpAddress{}, + requiredCount: 7, + optionalSubnet: netip.Prefix{}, + }, + want: nil, + wantErr: true, + }, + { + name: "InsufficientIPsWithUsed", + args: args{ + uplinks: buildSimpleUplinkStructure([]types.OpenApiIPRangeValues{ + { + StartAddress: "10.10.10.1", + EndAddress: "10.10.10.6", + }, + }), + usedIpAddresses: []*types.GatewayUsedIpAddress{ + {IPAddress: "10.10.10.1"}, + }, + requiredCount: 6, + optionalSubnet: netip.Prefix{}, + }, + want: nil, + wantErr: true, + }, + { + name: "MultipleUplinks", + args: args{ + uplinks: []types.EdgeGatewayUplinks{ + { + Subnets: types.OpenAPIEdgeGatewaySubnets{ + Values: []types.OpenAPIEdgeGatewaySubnetValue{ + { + IPRanges: &types.OpenApiIPRanges{ + Values: []types.OpenApiIPRangeValues{ + { + StartAddress: "10.10.10.1", + EndAddress: "10.10.10.6", + }, + }, + }, + }, + }, + }, + }, + { + Subnets: types.OpenAPIEdgeGatewaySubnets{ + Values: []types.OpenAPIEdgeGatewaySubnetValue{ + { + IPRanges: &types.OpenApiIPRanges{ + Values: []types.OpenApiIPRangeValues{ + { + StartAddress: "20.10.10.1", + EndAddress: "20.10.10.6", + }, + }, + }, + }, + }, + }, + }, + { + Subnets: types.OpenAPIEdgeGatewaySubnets{ + Values: []types.OpenAPIEdgeGatewaySubnetValue{ + { + IPRanges: &types.OpenApiIPRanges{ + Values: []types.OpenApiIPRangeValues{ + { + StartAddress: "30.10.10.1", + EndAddress: "30.10.10.6", + }, + }, + }, + }, + }, + }, + }, + }, + usedIpAddresses: []*types.GatewayUsedIpAddress{}, + requiredCount: 18, + optionalSubnet: netip.Prefix{}, + }, + want: []netip.Addr{ + netip.MustParseAddr("10.10.10.1"), + netip.MustParseAddr("10.10.10.2"), + netip.MustParseAddr("10.10.10.3"), + netip.MustParseAddr("10.10.10.4"), + netip.MustParseAddr("10.10.10.5"), + netip.MustParseAddr("10.10.10.6"), + netip.MustParseAddr("20.10.10.1"), + netip.MustParseAddr("20.10.10.2"), + netip.MustParseAddr("20.10.10.3"), + netip.MustParseAddr("20.10.10.4"), + netip.MustParseAddr("20.10.10.5"), + netip.MustParseAddr("20.10.10.6"), + netip.MustParseAddr("30.10.10.1"), + netip.MustParseAddr("30.10.10.2"), + netip.MustParseAddr("30.10.10.3"), + netip.MustParseAddr("30.10.10.4"), + netip.MustParseAddr("30.10.10.5"), + netip.MustParseAddr("30.10.10.6"), + }, + wantErr: false, + }, + { + name: "MultipleUplinksWithUsedIPsInsufficient", + args: args{ + uplinks: []types.EdgeGatewayUplinks{ + { + Subnets: types.OpenAPIEdgeGatewaySubnets{ + Values: []types.OpenAPIEdgeGatewaySubnetValue{ + { + IPRanges: &types.OpenApiIPRanges{ + Values: []types.OpenApiIPRangeValues{ + { + StartAddress: "10.10.10.1", + EndAddress: "10.10.10.6", + }, + }, + }, + }, + }, + }, + }, + { + Subnets: types.OpenAPIEdgeGatewaySubnets{ + Values: []types.OpenAPIEdgeGatewaySubnetValue{ + { + IPRanges: &types.OpenApiIPRanges{ + Values: []types.OpenApiIPRangeValues{ + { + StartAddress: "20.10.10.1", + EndAddress: "20.10.10.6", + }, + }, + }, + }, + }, + }, + }, + { + Subnets: types.OpenAPIEdgeGatewaySubnets{ + Values: []types.OpenAPIEdgeGatewaySubnetValue{ + { + IPRanges: &types.OpenApiIPRanges{ + Values: []types.OpenApiIPRangeValues{ + { + StartAddress: "30.10.10.1", + EndAddress: "30.10.10.6", + }, + }, + }, + }, + }, + }, + }, + }, + usedIpAddresses: []*types.GatewayUsedIpAddress{ + {IPAddress: "10.10.10.1"}, + }, + requiredCount: 18, + optionalSubnet: netip.Prefix{}, + }, + want: nil, + wantErr: true, + }, + { + name: "MultipleUplinksAndRanges", + args: args{ + uplinks: []types.EdgeGatewayUplinks{ + { + Subnets: types.OpenAPIEdgeGatewaySubnets{ + Values: []types.OpenAPIEdgeGatewaySubnetValue{ + { + IPRanges: &types.OpenApiIPRanges{ + Values: []types.OpenApiIPRangeValues{ + { + StartAddress: "10.10.10.1", + EndAddress: "10.10.10.2", + }, + { + StartAddress: "10.10.10.10", + EndAddress: "10.10.10.12", + }, + }, + }, + }, + }, + }, + }, + { + Subnets: types.OpenAPIEdgeGatewaySubnets{ + Values: []types.OpenAPIEdgeGatewaySubnetValue{ + { + IPRanges: &types.OpenApiIPRanges{ + Values: []types.OpenApiIPRangeValues{ + { + StartAddress: "20.10.10.1", + EndAddress: "20.10.10.6", + }, + { + StartAddress: "20.10.10.200", + EndAddress: "20.10.10.201", + }, + { + StartAddress: "20.10.10.251", + EndAddress: "20.10.10.252", + }, + { + StartAddress: "20.10.10.255", + }, + }, + }, + }, + }, + }, + }, + { + Subnets: types.OpenAPIEdgeGatewaySubnets{ + Values: []types.OpenAPIEdgeGatewaySubnetValue{ + { + IPRanges: &types.OpenApiIPRanges{ + Values: []types.OpenApiIPRangeValues{ + { + StartAddress: "30.10.10.1", + EndAddress: "30.10.10.2", + }, + }, + }, + }, + }, + }, + }, + }, + usedIpAddresses: []*types.GatewayUsedIpAddress{}, + requiredCount: 18, + optionalSubnet: netip.Prefix{}, + }, + want: []netip.Addr{ + netip.MustParseAddr("10.10.10.1"), + netip.MustParseAddr("10.10.10.2"), + netip.MustParseAddr("10.10.10.10"), + netip.MustParseAddr("10.10.10.11"), + netip.MustParseAddr("10.10.10.12"), + netip.MustParseAddr("20.10.10.1"), + netip.MustParseAddr("20.10.10.2"), + netip.MustParseAddr("20.10.10.3"), + netip.MustParseAddr("20.10.10.4"), + netip.MustParseAddr("20.10.10.5"), + netip.MustParseAddr("20.10.10.6"), + netip.MustParseAddr("20.10.10.200"), + netip.MustParseAddr("20.10.10.201"), + netip.MustParseAddr("20.10.10.251"), + netip.MustParseAddr("20.10.10.252"), + netip.MustParseAddr("20.10.10.255"), + netip.MustParseAddr("30.10.10.1"), + netip.MustParseAddr("30.10.10.2"), + }, + wantErr: false, + }, + { + name: "MultipleUplinksAndRangesInsufficientIPs", + args: args{ + uplinks: []types.EdgeGatewayUplinks{ + { + Subnets: types.OpenAPIEdgeGatewaySubnets{ + Values: []types.OpenAPIEdgeGatewaySubnetValue{ + { + IPRanges: &types.OpenApiIPRanges{ + Values: []types.OpenApiIPRangeValues{ + { + StartAddress: "10.10.10.1", + EndAddress: "10.10.10.2", + }, + { + StartAddress: "10.10.10.10", + EndAddress: "10.10.10.12", + }, + }, + }, + }, + }, + }, + }, + { + Subnets: types.OpenAPIEdgeGatewaySubnets{ + Values: []types.OpenAPIEdgeGatewaySubnetValue{ + { + IPRanges: &types.OpenApiIPRanges{ + Values: []types.OpenApiIPRangeValues{ + { + StartAddress: "20.10.10.1", + EndAddress: "20.10.10.6", + }, + { + StartAddress: "20.10.10.200", + EndAddress: "20.10.10.201", + }, + { + StartAddress: "20.10.10.251", + EndAddress: "20.10.10.252", + }, + { + StartAddress: "20.10.10.255", + }, + }, + }, + }, + }, + }, + }, + { + Subnets: types.OpenAPIEdgeGatewaySubnets{ + Values: []types.OpenAPIEdgeGatewaySubnetValue{ + { + IPRanges: &types.OpenApiIPRanges{ + Values: []types.OpenApiIPRangeValues{ + { + StartAddress: "30.10.10.1", + EndAddress: "30.10.10.2", + }, + }, + }, + }, + }, + }, + }, + }, + usedIpAddresses: []*types.GatewayUsedIpAddress{}, + requiredCount: 25, + optionalSubnet: netip.Prefix{}, + }, + want: nil, + wantErr: true, + }, + { + name: "MultipleUplinksAndRangesInSubnet24", + args: args{ + uplinks: []types.EdgeGatewayUplinks{ + { + Subnets: types.OpenAPIEdgeGatewaySubnets{ + Values: []types.OpenAPIEdgeGatewaySubnetValue{ + { + IPRanges: &types.OpenApiIPRanges{ + Values: []types.OpenApiIPRangeValues{ + { + StartAddress: "10.10.10.1", + EndAddress: "10.10.10.2", + }, + { + StartAddress: "10.10.10.10", + EndAddress: "10.10.10.12", + }, + }, + }, + }, + }, + }, + }, + { + Subnets: types.OpenAPIEdgeGatewaySubnets{ + Values: []types.OpenAPIEdgeGatewaySubnetValue{ + { + IPRanges: &types.OpenApiIPRanges{ + Values: []types.OpenApiIPRangeValues{ + { + StartAddress: "20.10.10.1", + EndAddress: "20.10.10.6", + }, + { + StartAddress: "20.10.10.200", + EndAddress: "20.10.10.201", + }, + { + StartAddress: "20.10.10.251", + EndAddress: "20.10.10.252", + }, + { + StartAddress: "20.10.10.255", + }, + }, + }, + }, + }, + }, + }, + { + Subnets: types.OpenAPIEdgeGatewaySubnets{ + Values: []types.OpenAPIEdgeGatewaySubnetValue{ + { + IPRanges: &types.OpenApiIPRanges{ + Values: []types.OpenApiIPRangeValues{ + { + StartAddress: "30.10.10.1", + EndAddress: "30.10.10.2", + }, + }, + }, + }, + }, + }, + }, + }, + usedIpAddresses: []*types.GatewayUsedIpAddress{}, + requiredCount: 2, + optionalSubnet: netip.MustParsePrefix("30.10.10.1/24"), + }, + want: []netip.Addr{ + netip.MustParseAddr("30.10.10.1"), + netip.MustParseAddr("30.10.10.2"), + }, + wantErr: false, + }, + { + name: "MultipleUplinksAndRangesInSubnet28", + args: args{ + uplinks: []types.EdgeGatewayUplinks{ + { + Subnets: types.OpenAPIEdgeGatewaySubnets{ + Values: []types.OpenAPIEdgeGatewaySubnetValue{ + { + IPRanges: &types.OpenApiIPRanges{ + Values: []types.OpenApiIPRangeValues{ + { + StartAddress: "10.10.10.1", + EndAddress: "10.10.10.2", + }, + { + StartAddress: "10.10.10.10", + EndAddress: "10.10.10.12", + }, + }, + }, + }, + }, + }, + }, + { + Subnets: types.OpenAPIEdgeGatewaySubnets{ + Values: []types.OpenAPIEdgeGatewaySubnetValue{ + { + IPRanges: &types.OpenApiIPRanges{ + Values: []types.OpenApiIPRangeValues{ + { + StartAddress: "20.10.10.1", + EndAddress: "20.10.10.6", + }, + { + StartAddress: "20.10.10.200", + EndAddress: "20.10.10.201", + }, + { + StartAddress: "20.10.10.251", + EndAddress: "20.10.10.252", + }, + { + StartAddress: "20.10.10.255", + }, + }, + }, + }, + }, + }, + }, + { + Subnets: types.OpenAPIEdgeGatewaySubnets{ + Values: []types.OpenAPIEdgeGatewaySubnetValue{ + { + IPRanges: &types.OpenApiIPRanges{ + Values: []types.OpenApiIPRangeValues{ + { + StartAddress: "30.10.10.1", + EndAddress: "30.10.10.2", + }, + }, + }, + }, + }, + }, + }, + }, + usedIpAddresses: []*types.GatewayUsedIpAddress{}, + requiredCount: 6, + optionalSubnet: netip.MustParsePrefix("20.10.10.1/28"), + }, + want: []netip.Addr{ + netip.MustParseAddr("20.10.10.1"), + netip.MustParseAddr("20.10.10.2"), + netip.MustParseAddr("20.10.10.3"), + netip.MustParseAddr("20.10.10.4"), + netip.MustParseAddr("20.10.10.5"), + netip.MustParseAddr("20.10.10.6"), + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := getUnusedExternalIPAddress(tt.args.uplinks, tt.args.usedIpAddresses, tt.args.requiredCount, tt.args.optionalSubnet) + if (err != nil) != tt.wantErr { + t.Errorf("getUnusedExternalIPAddress() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("getUnusedExternalIPAddress() = %v, want %v", got, tt.want) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("getUnusedExternalIPAddress() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_flattenGatewayUsedIpAddressesToIpSlice(t *testing.T) { + type args struct { + usedIpAddresses []*types.GatewayUsedIpAddress + } + tests := []struct { + name string + args args + want []netip.Addr + wantErr bool + }{ + { + name: "SingleIP", + args: args{usedIpAddresses: []*types.GatewayUsedIpAddress{{IPAddress: "10.0.0.1"}}}, + want: []netip.Addr{netip.MustParseAddr("10.0.0.1")}, + wantErr: false, + }, + { + name: "DuplicateIPs", + args: args{ + usedIpAddresses: []*types.GatewayUsedIpAddress{ + {IPAddress: "10.0.0.1"}, + {IPAddress: "10.0.0.1"}, + }, + }, + want: []netip.Addr{ + netip.MustParseAddr("10.0.0.1"), + netip.MustParseAddr("10.0.0.1"), + }, + wantErr: false, + }, + { + name: "NilSlice", + args: args{usedIpAddresses: nil}, + want: []netip.Addr{}, + wantErr: false, + }, + { + name: "InvalidIp", + args: args{usedIpAddresses: []*types.GatewayUsedIpAddress{{IPAddress: "ASD"}}}, + want: nil, + wantErr: true, + }, + { + name: "ManyIPs", + args: args{ + usedIpAddresses: []*types.GatewayUsedIpAddress{ + {IPAddress: "10.0.0.1"}, + {IPAddress: "10.0.0.2"}, + {IPAddress: "10.0.0.3"}, + {IPAddress: "10.0.0.4"}, + }, + }, + want: []netip.Addr{ + netip.MustParseAddr("10.0.0.1"), + netip.MustParseAddr("10.0.0.2"), + netip.MustParseAddr("10.0.0.3"), + netip.MustParseAddr("10.0.0.4"), + }, + wantErr: false, + }, + { + name: "IPv6SingleIP", + args: args{usedIpAddresses: []*types.GatewayUsedIpAddress{{IPAddress: "684D:1111:222:3333:4444:5555:6:77"}}}, + want: []netip.Addr{netip.MustParseAddr("684D:1111:222:3333:4444:5555:6:77")}, + wantErr: false, + }, + { + name: "IPv6ManyIPs", + args: args{ + usedIpAddresses: []*types.GatewayUsedIpAddress{ + {IPAddress: "2001:db8:3333:4444:5555:6666:7777:8888"}, + {IPAddress: "2002:db8:3333:4444:5555:6666:7777:8888"}, + {IPAddress: "2003:db8:3333:4444:5555:6666:7777:8888"}, + {IPAddress: "2004:db8:3333:4444:5555:6666:7777:8888"}, + {IPAddress: "2001:db8::68"}, + }, + }, + want: []netip.Addr{ + netip.MustParseAddr("2001:db8:3333:4444:5555:6666:7777:8888"), + netip.MustParseAddr("2002:db8:3333:4444:5555:6666:7777:8888"), + netip.MustParseAddr("2003:db8:3333:4444:5555:6666:7777:8888"), + netip.MustParseAddr("2004:db8:3333:4444:5555:6666:7777:8888"), + netip.MustParseAddr("2001:db8::68"), + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := flattenGatewayUsedIpAddressesToIpSlice(tt.args.usedIpAddresses) + if (err != nil) != tt.wantErr { + t.Errorf("flattenGatewayUsedIpAddressesToIpSlice() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("flattenGatewayUsedIpAddressesToIpSlice() = %v, want %v", got, tt.want) + } + }) + } +} + +// TestOpenAPIEdgeGateway_DeallocateIpCount tests that the function +// OpenAPIEdgeGateway.DeallocateIpCount is correctly processing the Edge Gateway uplink structure +func TestOpenAPIEdgeGateway_DeallocateIpCount(t *testing.T) { + type fields struct { + EdgeGatewayUplinks []types.EdgeGatewayUplinks + } + type args struct { + deallocateIpCount int + expectedCount int + } + tests := []struct { + name string + fields fields + args args + wantErr bool + }{ + { + name: "SingleStartAndEndAddresses", + fields: fields{ + EdgeGatewayUplinks: []types.EdgeGatewayUplinks{ + { + Subnets: types.OpenAPIEdgeGatewaySubnets{ + Values: []types.OpenAPIEdgeGatewaySubnetValue{ + { + IPRanges: &types.OpenApiIPRanges{ + Values: []types.OpenApiIPRangeValues{ + { + StartAddress: "10.10.10.1", + EndAddress: "10.10.10.2", + }, + }, + }, + TotalIPCount: addrOf(2), + }, + }, + }, + }, + }, + }, + args: args{ + deallocateIpCount: 1, + expectedCount: 1, + }, + }, + { + // Here we check that the function is able to deallocate exactly one IP address (the + // last). The API will return an error during such operation because Edge Gateway must + // have at least one IP address allocated. + name: "ExactlyOnIp", + fields: fields{ + EdgeGatewayUplinks: []types.EdgeGatewayUplinks{ + { + Subnets: types.OpenAPIEdgeGatewaySubnets{ + Values: []types.OpenAPIEdgeGatewaySubnetValue{ + { + IPRanges: &types.OpenApiIPRanges{ + Values: []types.OpenApiIPRangeValues{ + { + StartAddress: "10.10.10.1", + EndAddress: "10.10.10.1", + }, + }, + }, + TotalIPCount: addrOf(1), + }, + }, + }, + }, + }, + }, + args: args{ + deallocateIpCount: 1, + expectedCount: 0, + }, + }, + { + name: "NegativeAllocationImpossible", + fields: fields{ + EdgeGatewayUplinks: []types.EdgeGatewayUplinks{ + { + Subnets: types.OpenAPIEdgeGatewaySubnets{ + Values: []types.OpenAPIEdgeGatewaySubnetValue{ + { + IPRanges: &types.OpenApiIPRanges{ + Values: []types.OpenApiIPRangeValues{ + { + StartAddress: "10.10.10.1", + EndAddress: "10.10.10.2", + }, + }, + }, + TotalIPCount: addrOf(2), + }, + }, + }, + }, + }, + }, + args: args{ + deallocateIpCount: -1, + }, + wantErr: true, + }, + { + name: "MultipleSubnets", + fields: fields{ + EdgeGatewayUplinks: []types.EdgeGatewayUplinks{ + { + Subnets: types.OpenAPIEdgeGatewaySubnets{ + Values: []types.OpenAPIEdgeGatewaySubnetValue{ + { + IPRanges: &types.OpenApiIPRanges{ + Values: []types.OpenApiIPRangeValues{ + { + StartAddress: "10.10.10.1", + EndAddress: "10.10.10.2", + }, + }, + }, + TotalIPCount: addrOf(2), + }, + { + IPRanges: &types.OpenApiIPRanges{ + Values: []types.OpenApiIPRangeValues{ + { + StartAddress: "10.20.10.1", + EndAddress: "10.20.10.2", + }, + }, + }, + TotalIPCount: addrOf(2), + }, + }, + }, + }, + }, + }, + args: args{ + deallocateIpCount: 3, + expectedCount: 1, + }, + }, + { + name: "RemoveMoreThanAvailable", + fields: fields{ + EdgeGatewayUplinks: []types.EdgeGatewayUplinks{ + { + Subnets: types.OpenAPIEdgeGatewaySubnets{ + Values: []types.OpenAPIEdgeGatewaySubnetValue{ + { + IPRanges: &types.OpenApiIPRanges{ + Values: []types.OpenApiIPRangeValues{ + { + StartAddress: "10.10.10.1", + EndAddress: "10.10.10.2", + }, + }, + }, + TotalIPCount: addrOf(2), + }, + { + IPRanges: &types.OpenApiIPRanges{ + Values: []types.OpenApiIPRangeValues{ + { + StartAddress: "10.20.10.1", + EndAddress: "10.20.10.2", + }, + }, + }, + TotalIPCount: addrOf(2), + }, + }, + }, + }, + }, + }, + args: args{ + deallocateIpCount: 5, // only 4 IPs are available + expectedCount: 1, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + egw := &NsxtEdgeGateway{ + EdgeGateway: &types.OpenAPIEdgeGateway{ + EdgeGatewayUplinks: tt.fields.EdgeGatewayUplinks, + }, + } + var err error + if err = egw.DeallocateIpCount(tt.args.deallocateIpCount); (err != nil) != tt.wantErr { + t.Errorf("OpenAPIEdgeGateway.DeallocateIpCount() error = %v, wantErr %v", err, tt.wantErr) + } + + // Skip other validations if an error was expected + if err != nil && tt.wantErr { + return + } + + allocatedIpCount, err := egw.GetAllocatedIpCount(false) + if err != nil { + t.Errorf("NsxtEdgeGateway.GetAllocatedIpCount() error = %v", err) + } + + if allocatedIpCount != tt.args.expectedCount { + t.Errorf("Allocated IP count %d != desired IP count %d", allocatedIpCount, tt.args.expectedCount) + } + + }) + } +} + +func Test_reorderEdgeGatewayUplinks(t *testing.T) { + type args struct { + edgeGatewayUplinks []types.EdgeGatewayUplinks + } + tests := []struct { + name string + args args + want args + }{ + { + name: "OneT0Uplink", + args: args{edgeGatewayUplinks: []types.EdgeGatewayUplinks{{BackingType: addrOf("NSXT_TIER0"), UplinkID: "1"}}}, + want: args{edgeGatewayUplinks: []types.EdgeGatewayUplinks{{BackingType: addrOf("NSXT_TIER0"), UplinkID: "1"}}}, + }, + { + name: "ImpossibleOneSegmentUplink", + args: args{edgeGatewayUplinks: []types.EdgeGatewayUplinks{{BackingType: addrOf("IMPORTED_T_LOGICAL_SWITCH"), UplinkID: "1"}}}, + want: args{edgeGatewayUplinks: []types.EdgeGatewayUplinks{{BackingType: addrOf("IMPORTED_T_LOGICAL_SWITCH"), UplinkID: "1"}}}, + }, + { + name: "OrderedOneT0OneSegmentUplink", + args: args{edgeGatewayUplinks: []types.EdgeGatewayUplinks{ + {BackingType: addrOf("NSXT_TIER0"), UplinkID: "1"}, + {BackingType: addrOf("IMPORTED_T_LOGICAL_SWITCH"), UplinkID: "2"}, + }}, + want: args{edgeGatewayUplinks: []types.EdgeGatewayUplinks{ + {BackingType: addrOf("NSXT_TIER0"), UplinkID: "1"}, + {BackingType: addrOf("IMPORTED_T_LOGICAL_SWITCH"), UplinkID: "2"}, + }}, + }, + { + name: "OrderedOneT0OneManySegments", + args: args{edgeGatewayUplinks: []types.EdgeGatewayUplinks{ + {BackingType: addrOf("NSXT_TIER0"), UplinkID: "1"}, + {BackingType: addrOf("IMPORTED_T_LOGICAL_SWITCH"), UplinkID: "2"}, + {BackingType: addrOf("IMPORTED_T_LOGICAL_SWITCH"), UplinkID: "3"}, + {BackingType: addrOf("IMPORTED_T_LOGICAL_SWITCH"), UplinkID: "4"}, + {BackingType: addrOf("IMPORTED_T_LOGICAL_SWITCH"), UplinkID: "5"}, + }}, + want: args{edgeGatewayUplinks: []types.EdgeGatewayUplinks{ + {BackingType: addrOf("NSXT_TIER0"), UplinkID: "1"}, + {BackingType: addrOf("IMPORTED_T_LOGICAL_SWITCH"), UplinkID: "2"}, + {BackingType: addrOf("IMPORTED_T_LOGICAL_SWITCH"), UplinkID: "3"}, + {BackingType: addrOf("IMPORTED_T_LOGICAL_SWITCH"), UplinkID: "4"}, + {BackingType: addrOf("IMPORTED_T_LOGICAL_SWITCH"), UplinkID: "5"}, + }}, + }, + { + name: "ReverseOneT0OneSegmentUplink", + args: args{edgeGatewayUplinks: []types.EdgeGatewayUplinks{ + {BackingType: addrOf("IMPORTED_T_LOGICAL_SWITCH"), UplinkID: "1"}, + {BackingType: addrOf("NSXT_TIER0"), UplinkID: "2"}, + }}, + want: args{edgeGatewayUplinks: []types.EdgeGatewayUplinks{ + {BackingType: addrOf("NSXT_TIER0"), UplinkID: "2"}, + {BackingType: addrOf("IMPORTED_T_LOGICAL_SWITCH"), UplinkID: "1"}, + }}, + }, + { + name: "ReverseOneT0ManySegmentUplinks", + args: args{edgeGatewayUplinks: []types.EdgeGatewayUplinks{ + {BackingType: addrOf("IMPORTED_T_LOGICAL_SWITCH"), UplinkID: "1"}, + {BackingType: addrOf("IMPORTED_T_LOGICAL_SWITCH"), UplinkID: "2"}, + {BackingType: addrOf("IMPORTED_T_LOGICAL_SWITCH"), UplinkID: "3"}, + {BackingType: addrOf("IMPORTED_T_LOGICAL_SWITCH"), UplinkID: "4"}, + {BackingType: addrOf("NSXT_TIER0"), UplinkID: "5"}, + }}, + want: args{edgeGatewayUplinks: []types.EdgeGatewayUplinks{ + {BackingType: addrOf("NSXT_TIER0"), UplinkID: "5"}, + {BackingType: addrOf("IMPORTED_T_LOGICAL_SWITCH"), UplinkID: "2"}, + {BackingType: addrOf("IMPORTED_T_LOGICAL_SWITCH"), UplinkID: "3"}, + {BackingType: addrOf("IMPORTED_T_LOGICAL_SWITCH"), UplinkID: "4"}, + {BackingType: addrOf("IMPORTED_T_LOGICAL_SWITCH"), UplinkID: "1"}, + }}, + }, + { + name: "ReverseOneT0ManySegmentUplinksVRF", + args: args{edgeGatewayUplinks: []types.EdgeGatewayUplinks{ + {BackingType: addrOf("IMPORTED_T_LOGICAL_SWITCH"), UplinkID: "1"}, + {BackingType: addrOf("IMPORTED_T_LOGICAL_SWITCH"), UplinkID: "2"}, + {BackingType: addrOf("IMPORTED_T_LOGICAL_SWITCH"), UplinkID: "3"}, + {BackingType: addrOf("IMPORTED_T_LOGICAL_SWITCH"), UplinkID: "4"}, + {BackingType: addrOf("NSXT_VRF_TIER0"), UplinkID: "5"}, + }}, + want: args{edgeGatewayUplinks: []types.EdgeGatewayUplinks{ + {BackingType: addrOf("NSXT_VRF_TIER0"), UplinkID: "5"}, + {BackingType: addrOf("IMPORTED_T_LOGICAL_SWITCH"), UplinkID: "2"}, + {BackingType: addrOf("IMPORTED_T_LOGICAL_SWITCH"), UplinkID: "3"}, + {BackingType: addrOf("IMPORTED_T_LOGICAL_SWITCH"), UplinkID: "4"}, + {BackingType: addrOf("IMPORTED_T_LOGICAL_SWITCH"), UplinkID: "1"}, + }}, + }, + // A failing test example - commented on purpose + // { + // name: "FailingTest", + // args: args{edgeGatewayUplinks: []types.EdgeGatewayUplinks{ + // types.EdgeGatewayUplinks{BackingType: addrOf("IMPORTED_T_LOGICAL_SWITCH")}, + // types.EdgeGatewayUplinks{BackingType: addrOf("NSXT_TIER0")}, + // }}, + // want: args{edgeGatewayUplinks: []types.EdgeGatewayUplinks{ + // types.EdgeGatewayUplinks{BackingType: addrOf("IMPORTED_T_LOGICAL_SWITCH")}, + // types.EdgeGatewayUplinks{BackingType: addrOf("NSXT_TIER0")}, + // }}, + // }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reorderedUplinks := reorderEdgeGatewayUplinks(tt.args.edgeGatewayUplinks) + + if !reflect.DeepEqual(reorderedUplinks, tt.want.edgeGatewayUplinks) { + t.Errorf("Expected %+v, got %+v", tt.args.edgeGatewayUplinks, tt.want.edgeGatewayUplinks) + } + }) + } +} diff --git a/govcd/nsxt_firewall.go b/govcd/nsxt_firewall.go new file mode 100644 index 000000000..4fe9daf48 --- /dev/null +++ b/govcd/nsxt_firewall.go @@ -0,0 +1,136 @@ +/* + * Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +// NsxtFirewall contains a types.NsxtFirewallRuleContainer which encloses three types of rules - +// system, default and user defined rules. User defined rules are the only ones that can be modified, others are +// read-only. +type NsxtFirewall struct { + NsxtFirewallRuleContainer *types.NsxtFirewallRuleContainer + client *Client + // edgeGatewayId is stored for usage in NsxtFirewall receiver functions + edgeGatewayId string +} + +// UpdateNsxtFirewall allows user to set new firewall rules or update existing ones. The API does not have POST endpoint +// and always uses PUT endpoint for creating and updating. +func (egw *NsxtEdgeGateway) UpdateNsxtFirewall(firewallRules *types.NsxtFirewallRuleContainer) (*NsxtFirewall, error) { + client := egw.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtFirewallRules + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + // Insert Edge Gateway ID into endpoint path edgeGateways/%s/firewall/rules + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, egw.EdgeGateway.ID)) + if err != nil { + return nil, err + } + + returnObject := &NsxtFirewall{ + NsxtFirewallRuleContainer: &types.NsxtFirewallRuleContainer{}, + client: client, + edgeGatewayId: egw.EdgeGateway.ID, + } + + err = client.OpenApiPutItem(minimumApiVersion, urlRef, nil, firewallRules, returnObject.NsxtFirewallRuleContainer, nil) + if err != nil { + return nil, fmt.Errorf("error setting NSX-T Firewall: %s", err) + } + + return returnObject, nil +} + +// GetNsxtFirewall retrieves all firewall rules system, default and user defined rules +func (egw *NsxtEdgeGateway) GetNsxtFirewall() (*NsxtFirewall, error) { + client := egw.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtFirewallRules + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + // Insert Edge Gateway ID into endpoint path edgeGateways/%s/firewall/rules + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, egw.EdgeGateway.ID)) + if err != nil { + return nil, err + } + + returnObject := &NsxtFirewall{ + NsxtFirewallRuleContainer: &types.NsxtFirewallRuleContainer{}, + client: client, + edgeGatewayId: egw.EdgeGateway.ID, + } + + err = client.OpenApiGetItem(minimumApiVersion, urlRef, nil, returnObject.NsxtFirewallRuleContainer, nil) + if err != nil { + return nil, fmt.Errorf("error retrieving NSX-T Firewall rules: %s", err) + } + + // Store Edge Gateway ID for later operations + returnObject.edgeGatewayId = egw.EdgeGateway.ID + + return returnObject, nil +} + +// DeleteAllRules allows users to delete all NSX-T Firewall rules in a particular Edge Gateway +func (firewall *NsxtFirewall) DeleteAllRules() error { + + if firewall.edgeGatewayId == "" { + return fmt.Errorf("missing Edge Gateway ID") + } + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtFirewallRules + minimumApiVersion, err := firewall.client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return err + } + + urlRef, err := firewall.client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, firewall.edgeGatewayId)) + if err != nil { + return err + } + + err = firewall.client.OpenApiDeleteItem(minimumApiVersion, urlRef, nil, nil) + + if err != nil { + return fmt.Errorf("error deleting all NSX-T Firewall Rules: %s", err) + } + + return nil +} + +// DeleteRuleById allows users to delete NSX-T Firewall Rule By ID +func (firewall *NsxtFirewall) DeleteRuleById(id string) error { + if id == "" { + return fmt.Errorf("empty ID specified") + } + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtFirewallRules + minimumApiVersion, err := firewall.client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return err + } + + urlRef, err := firewall.client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, firewall.edgeGatewayId), "/", id) + if err != nil { + return err + } + + err = firewall.client.OpenApiDeleteItem(minimumApiVersion, urlRef, nil, nil) + + if err != nil { + return fmt.Errorf("error deleting NSX-T Firewall Rule with ID '%s': %s", id, err) + } + + return nil +} diff --git a/govcd/nsxt_firewall_group.go b/govcd/nsxt_firewall_group.go new file mode 100644 index 000000000..55e2eb9c3 --- /dev/null +++ b/govcd/nsxt_firewall_group.go @@ -0,0 +1,413 @@ +/* + * Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "errors" + "fmt" + "net/url" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +// NsxtFirewallGroup uses OpenAPI endpoint to operate NSX-T Security Groups and IP Sets which use +// the same Firewall Group API endpoint +// +// IP sets are groups of objects to which the firewall rules apply. Combining multiple objects into +// IP sets helps reduce the total number of firewall rules to be created. +// +// Security groups are groups of Org Vdc networks to which distributed firewall rules apply. +// Grouping networks helps you to reduce the total number of distributed firewall rules to be +// created. +type NsxtFirewallGroup struct { + NsxtFirewallGroup *types.NsxtFirewallGroup + client *Client +} + +// CreateNsxtFirewallGroup allows users to create NSX-T Firewall Group +func (vdc *Vdc) CreateNsxtFirewallGroup(firewallGroupConfig *types.NsxtFirewallGroup) (*NsxtFirewallGroup, error) { + return createNsxtFirewallGroup(vdc.client, firewallGroupConfig) +} + +// CreateNsxtFirewallGroup allows users to create NSX-T Firewall Group +func (egw *NsxtEdgeGateway) CreateNsxtFirewallGroup(firewallGroupConfig *types.NsxtFirewallGroup) (*NsxtFirewallGroup, error) { + return createNsxtFirewallGroup(egw.client, firewallGroupConfig) +} + +// CreateNsxtFirewallGroup allows users to create NSX-T Firewall Group +func (vdcGroup *VdcGroup) CreateNsxtFirewallGroup(firewallGroupConfig *types.NsxtFirewallGroup) (*NsxtFirewallGroup, error) { + return createNsxtFirewallGroup(vdcGroup.client, firewallGroupConfig) +} + +// GetAllNsxtFirewallGroups allows users to retrieve all Firewall Groups for Org +// firewallGroupType can be one of the following: +// * types.FirewallGroupTypeSecurityGroup - for NSX-T Security Groups +// * types.FirewallGroupTypeIpSet - for NSX-T IP Sets +// * "" (empty) - search will not be limited and will get both - IP Sets and Security Groups +// +// It is possible to add additional filtering by using queryParameters of type 'url.Values'. +// One special filter is `_context==` filtering. Value can be one of the following: +// +// * Org Vdc Network ID (_context==networkId) - Returns all the firewall groups which the specified +// network is a member of. +// +// * Edge Gateway ID (_context==edgeGatewayId) - Returns all the firewall groups which are available +// to the specific edge gateway. Or use a shorthand NsxtEdgeGateway.GetAllNsxtFirewallGroups() which +// automatically injects this filter. +// +// * Network Provider ID (_context==networkProviderId) - Returns all the firewall groups which are +// available under a specific network provider. This context requires system admin privilege. +// 'networkProviderId' is NSX-T manager ID +func (org *Org) GetAllNsxtFirewallGroups(queryParameters url.Values, firewallGroupType string) ([]*NsxtFirewallGroup, error) { + queryParams := copyOrNewUrlValues(queryParameters) + if firewallGroupType != "" { + queryParams = queryParameterFilterAnd(fmt.Sprintf("typeValue==%s", firewallGroupType), queryParameters) + } + + return getAllNsxtFirewallGroups(org.client, queryParams) +} + +// GetAllNsxtFirewallGroups allows users to retrieve all NSX-T Firewall Groups +func (vdc *Vdc) GetAllNsxtFirewallGroups(queryParameters url.Values, firewallGroupType string) ([]*NsxtFirewallGroup, error) { + if vdc.IsNsxv() { + return nil, errors.New("only NSX-T VDCs support Firewall Groups") + } + return getAllNsxtFirewallGroups(vdc.client, queryParameters) +} + +// GetAllNsxtFirewallGroups allows users to retrieve all NSX-T Firewall Groups in a particular Edge Gateway +// firewallGroupType can be one of the following: +// * types.FirewallGroupTypeSecurityGroup - for NSX-T Security Groups +// * types.FirewallGroupTypeIpSet - for NSX-T IP Sets +// * "" (empty) - search will not be limited and will get both - IP Sets and Security Groups +func (egw *NsxtEdgeGateway) GetAllNsxtFirewallGroups(queryParameters url.Values, firewallGroupType string) ([]*NsxtFirewallGroup, error) { + queryParams := copyOrNewUrlValues(queryParameters) + + if firewallGroupType != "" { + queryParams = queryParameterFilterAnd(fmt.Sprintf("typeValue==%s", firewallGroupType), queryParameters) + } + + // Automatically inject Edge Gateway filter because this is an Edge Gateway scoped query + queryParams = queryParameterFilterAnd("_context=="+egw.EdgeGateway.ID, queryParams) + + return getAllNsxtFirewallGroups(egw.client, queryParams) +} + +// GetNsxtFirewallGroupByName allows users to retrieve Firewall Group by Name +// firewallGroupType can be one of the following: +// * types.FirewallGroupTypeSecurityGroup - for NSX-T Security Groups +// * types.FirewallGroupTypeIpSet - for NSX-T IP Sets +// * "" (empty) - search will not be limited and will get both - IP Sets and Security Groups +// +// Note. One might get an error if IP Set and Security Group exist with the same name (two objects +// of the same type cannot exist) and firewallGroupType is left empty. +func (org *Org) GetNsxtFirewallGroupByName(name, firewallGroupType string) (*NsxtFirewallGroup, error) { + queryParameters := url.Values{} + if firewallGroupType != "" { + queryParameters = queryParameterFilterAnd(fmt.Sprintf("typeValue==%s", firewallGroupType), queryParameters) + } + + return getNsxtFirewallGroupByName(org.client, name, queryParameters) +} + +// GetNsxtFirewallGroupByName allows users to retrieve Firewall Group by Name +// firewallGroupType can be one of the following: +// * types.FirewallGroupTypeSecurityGroup - for NSX-T Security Groups +// * types.FirewallGroupTypeIpSet - for NSX-T IP Sets +// * "" (empty) - search will not be limited and will get both - IP Sets and Security Groups +// +// Note. One might get an error if IP Set and Security Group exist with the same name (two objects +// of the same type cannot exist) and firewallGroupType is left empty. +func (vdc *Vdc) GetNsxtFirewallGroupByName(name, firewallGroupType string) (*NsxtFirewallGroup, error) { + + queryParameters := url.Values{} + if firewallGroupType != "" { + queryParameters = queryParameterFilterAnd(fmt.Sprintf("typeValue==%s", firewallGroupType), queryParameters) + } + return getNsxtFirewallGroupByName(vdc.client, name, queryParameters) +} + +// GetNsxtFirewallGroupByName allows users to retrieve Firewall Group by Name in a particular VDC Group +// firewallGroupType can be one of the following: +// * types.FirewallGroupTypeSecurityGroup - for NSX-T Static Security Groups +// * types.FirewallGroupTypeVmCriteria - for NSX-T Dynamic Security Groups +// * types.FirewallGroupTypeIpSet - for NSX-T IP Sets +// * "" (empty) - search will not be limited and will get both - IP Sets and Security Groups +// +// Note. One might get an error if IP Set and Security Group exist with the same name (two objects +// of the same type cannot exist) and firewallGroupType is left empty. +func (vdcGroup *VdcGroup) GetNsxtFirewallGroupByName(name string, firewallGroupType string) (*NsxtFirewallGroup, error) { + queryParameters := url.Values{} + + if firewallGroupType != "" { + queryParameters = queryParameterFilterAnd(fmt.Sprintf("typeValue==%s", firewallGroupType), queryParameters) + } + + // Automatically inject Edge Gateway filter because this is an Edge Gateway scoped query + queryParameters = queryParameterFilterAnd("ownerRef.id=="+vdcGroup.VdcGroup.Id, queryParameters) + + return getNsxtFirewallGroupByName(vdcGroup.client, name, queryParameters) +} + +// GetNsxtFirewallGroupByName allows users to retrieve Firewall Group by Name in a particular Edge Gateway +// firewallGroupType can be one of the following: +// * types.FirewallGroupTypeSecurityGroup - for NSX-T Security Groups +// * types.FirewallGroupTypeIpSet - for NSX-T IP Sets +// * "" (empty) - search will not be limited and will get both - IP Sets and Security Groups +// +// Note. One might get an error if IP Set and Security Group exist with the same name (two objects +// of the same type cannot exist) and firewallGroupType is left empty. +func (egw *NsxtEdgeGateway) GetNsxtFirewallGroupByName(name string, firewallGroupType string) (*NsxtFirewallGroup, error) { + queryParameters := url.Values{} + + if firewallGroupType != "" { + queryParameters = queryParameterFilterAnd(fmt.Sprintf("typeValue==%s", firewallGroupType), queryParameters) + } + + // Automatically inject Edge Gateway filter because this is an Edge Gateway scoped query + queryParameters = queryParameterFilterAnd("_context=="+egw.EdgeGateway.ID, queryParameters) + + return getNsxtFirewallGroupByName(egw.client, name, queryParameters) +} + +// GetNsxtFirewallGroupById retrieves NSX-T Firewall Group by ID +func (org *Org) GetNsxtFirewallGroupById(id string) (*NsxtFirewallGroup, error) { + return getNsxtFirewallGroupById(org.client, id) +} + +// GetNsxtFirewallGroupById retrieves NSX-T Firewall Group by ID +func (vdc *Vdc) GetNsxtFirewallGroupById(id string) (*NsxtFirewallGroup, error) { + return getNsxtFirewallGroupById(vdc.client, id) +} + +// GetNsxtFirewallGroupById retrieves NSX-T Firewall Group by ID +func (egw *NsxtEdgeGateway) GetNsxtFirewallGroupById(id string) (*NsxtFirewallGroup, error) { + return getNsxtFirewallGroupById(egw.client, id) +} + +// GetNsxtFirewallGroupById retrieves NSX-T Firewall Group by ID +func (vdcGroup *VdcGroup) GetNsxtFirewallGroupById(id string) (*NsxtFirewallGroup, error) { + return getNsxtFirewallGroupById(vdcGroup.client, id) +} + +// Update allows users to update NSX-T Firewall Group +func (firewallGroup *NsxtFirewallGroup) Update(firewallGroupConfig *types.NsxtFirewallGroup) (*NsxtFirewallGroup, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointFirewallGroups + apiVersion, err := firewallGroup.client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + if firewallGroupConfig.ID == "" { + return nil, fmt.Errorf("cannot update NSX-T Firewall Group without ID") + } + + urlRef, err := firewallGroup.client.OpenApiBuildEndpoint(endpoint, firewallGroupConfig.ID) + if err != nil { + return nil, err + } + + returnObject := &NsxtFirewallGroup{ + NsxtFirewallGroup: &types.NsxtFirewallGroup{}, + client: firewallGroup.client, + } + + err = firewallGroup.client.OpenApiPutItem(apiVersion, urlRef, nil, firewallGroupConfig, returnObject.NsxtFirewallGroup, nil) + if err != nil { + return nil, fmt.Errorf("error updating NSX-T firewall group: %s", err) + } + + return returnObject, nil +} + +// Delete allows users to delete NSX-T Firewall Group +func (firewallGroup *NsxtFirewallGroup) Delete() error { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointFirewallGroups + apiVersion, err := firewallGroup.client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return err + } + + if firewallGroup.NsxtFirewallGroup.ID == "" { + return fmt.Errorf("cannot delete NSX-T Firewall Group without ID") + } + + urlRef, err := firewallGroup.client.OpenApiBuildEndpoint(endpoint, firewallGroup.NsxtFirewallGroup.ID) + if err != nil { + return err + } + + err = firewallGroup.client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) + + if err != nil { + return fmt.Errorf("error deleting NSX-T Firewall Group: %s", err) + } + + return nil +} + +// GetAssociatedVms allows users to retrieve a list of references to child VMs (with vApps when they exist). +// +// Note. Only Security Groups have associated VMs. Executing it on an IP Set will return an error +// similar to: "only Security Groups have associated VMs. This Firewall Group has type 'IP_SET'" +func (firewallGroup *NsxtFirewallGroup) GetAssociatedVms() ([]*types.NsxtFirewallGroupMemberVms, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointFirewallGroups + apiVersion, err := firewallGroup.client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + if firewallGroup.NsxtFirewallGroup.ID == "" { + return nil, fmt.Errorf("cannot retrieve associated VMs for NSX-T Firewall Group without ID") + } + + if !firewallGroup.IsSecurityGroup() && !firewallGroup.IsDynamicSecurityGroup() { + return nil, fmt.Errorf("only Security Groups have associated VMs. This Firewall Group has type '%s'", + firewallGroup.NsxtFirewallGroup.Type) + } + + urlRef, err := firewallGroup.client.OpenApiBuildEndpoint(endpoint, firewallGroup.NsxtFirewallGroup.ID, "/associatedVMs") + if err != nil { + return nil, err + } + + associatedVms := []*types.NsxtFirewallGroupMemberVms{{}} + + err = firewallGroup.client.OpenApiGetAllItems(apiVersion, urlRef, nil, &associatedVms, nil) + + if err != nil { + return nil, fmt.Errorf("error retrieving associated VMs: %s", err) + } + + return associatedVms, nil +} + +// IsSecurityGroup allows users to check if Firewall Group is a Static Security Group +func (firewallGroup *NsxtFirewallGroup) IsSecurityGroup() bool { + return firewallGroup.NsxtFirewallGroup.Type == types.FirewallGroupTypeSecurityGroup +} + +// IsDynamicSecurityGroup allows users to check if Firewall Group is a Dynamic Security Group +func (firewallGroup *NsxtFirewallGroup) IsDynamicSecurityGroup() bool { + return firewallGroup.NsxtFirewallGroup.TypeValue == types.FirewallGroupTypeVmCriteria +} + +// IsIpSet allows users to check if Firewall Group is an IP Set +func (firewallGroup *NsxtFirewallGroup) IsIpSet() bool { + return firewallGroup.NsxtFirewallGroup.Type == types.FirewallGroupTypeIpSet +} + +func getNsxtFirewallGroupByName(client *Client, name string, queryParameters url.Values) (*NsxtFirewallGroup, error) { + queryParams := copyOrNewUrlValues(queryParameters) + queryParams = queryParameterFilterAnd("name=="+name, queryParams) + + allGroups, err := getAllNsxtFirewallGroups(client, queryParams) + if err != nil { + return nil, fmt.Errorf("could not find NSX-T Firewall Group with name '%s': %s", name, err) + } + + if len(allGroups) == 0 { + return nil, fmt.Errorf("%s: expected exactly one NSX-T Firewall Group with name '%s'. Got %d", ErrorEntityNotFound, name, len(allGroups)) + } + + if len(allGroups) > 1 { + return nil, fmt.Errorf("expected exactly one NSX-T Firewall Group with name '%s'. Got %d", name, len(allGroups)) + } + + // TODO API V36.0 - maybe it is fixed + // There is a bug that not all data is present (e.g. missing IpAddresses field for IP_SET) when + // using "getAll" endpoint therefore after finding the object by name we must retrieve it once + // again using its direct endpoint. + // + // return allGroups[0], nil + + return getNsxtFirewallGroupById(client, allGroups[0].NsxtFirewallGroup.ID) +} + +func getNsxtFirewallGroupById(client *Client, id string) (*NsxtFirewallGroup, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointFirewallGroups + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + if id == "" { + return nil, fmt.Errorf("empty NSX-T Firewall Group ID specified") + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, id) + if err != nil { + return nil, err + } + + fwGroup := &NsxtFirewallGroup{ + NsxtFirewallGroup: &types.NsxtFirewallGroup{}, + client: client, + } + + err = client.OpenApiGetItem(apiVersion, urlRef, nil, fwGroup.NsxtFirewallGroup, nil) + if err != nil { + return nil, err + } + + return fwGroup, nil +} + +func getAllNsxtFirewallGroups(client *Client, queryParameters url.Values) ([]*NsxtFirewallGroup, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointFirewallGroups + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + // This Object does not follow regular REST scheme and for get the endpoint must be + // 1.0.0/firewallGroups/summaries therefore bellow "summaries" is appended to the path + urlRef, err := client.OpenApiBuildEndpoint(endpoint, "summaries") + if err != nil { + return nil, err + } + + typeResponses := []*types.NsxtFirewallGroup{{}} + err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParameters, &typeResponses, nil) + if err != nil { + return nil, err + } + + // Wrap all typeResponses into NsxtEdgeGateway types with client + wrappedResponses := make([]*NsxtFirewallGroup, len(typeResponses)) + for sliceIndex := range typeResponses { + wrappedResponses[sliceIndex] = &NsxtFirewallGroup{ + NsxtFirewallGroup: typeResponses[sliceIndex], + client: client, + } + } + + return wrappedResponses, nil +} + +func createNsxtFirewallGroup(client *Client, firewallGroupConfig *types.NsxtFirewallGroup) (*NsxtFirewallGroup, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointFirewallGroups + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + returnObject := &NsxtFirewallGroup{ + NsxtFirewallGroup: &types.NsxtFirewallGroup{}, + client: client, + } + + err = client.OpenApiPostItem(apiVersion, urlRef, nil, firewallGroupConfig, returnObject.NsxtFirewallGroup, nil) + if err != nil { + return nil, fmt.Errorf("error creating NSX-T Firewall Group: %s", err) + } + + return returnObject, nil +} diff --git a/govcd/nsxt_firewall_group_dynamic_security_group_test.go b/govcd/nsxt_firewall_group_dynamic_security_group_test.go new file mode 100644 index 000000000..40aa0c0bf --- /dev/null +++ b/govcd/nsxt_firewall_group_dynamic_security_group_test.go @@ -0,0 +1,96 @@ +//go:build network || nsxt || functional || openapi || ALL + +package govcd + +import ( + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +// Test_NsxtDynamicSecurityGroup tests out CRUD of Dynamic NSX-T Security Group +// +// Note. Dynamic Security Group is one type of Firewall Group. Other types are IP-Set and Static +// Security Group. +func (vcd *TestVCD) Test_NsxtDynamicSecurityGroup(check *C) { + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointFirewallGroups) + + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + + vdcGroup, err := adminOrg.GetVdcGroupByName(vcd.config.VCD.Nsxt.VdcGroup) + check.Assert(err, IsNil) + + dynamicSecGroupDefinition := &types.NsxtFirewallGroup{ + Name: check.TestName(), + Description: check.TestName() + "-Description", + TypeValue: types.FirewallGroupTypeVmCriteria, + OwnerRef: &types.OpenApiReference{ID: vdcGroup.VdcGroup.Id}, + VmCriteria: []types.NsxtFirewallGroupVmCriteria{ + { + VmCriteriaRule: []types.NsxtFirewallGroupVmCriteriaRule{ + { + AttributeType: "VM_TAG", + Operator: "EQUALS", + AttributeValue: "string", + }, // Boolean AND + { + AttributeType: "VM_TAG", + Operator: "CONTAINS", + AttributeValue: "substring", + }, // Boolean AND + { + AttributeType: "VM_TAG", + Operator: "STARTS_WITH", + AttributeValue: "substring", + }, // Boolean AND + { + AttributeType: "VM_TAG", + Operator: "ENDS_WITH", + AttributeValue: "substring", + }, // Boolean AND + }, + }, // Boolean OR + { + VmCriteriaRule: []types.NsxtFirewallGroupVmCriteriaRule{ + { + AttributeType: "VM_NAME", + Operator: "CONTAINS", + AttributeValue: "substring", + }, // Boolean AND + { + AttributeType: "VM_NAME", + Operator: "STARTS_WITH", + AttributeValue: "substring", + }, // Boolean AND + }, + }, + }, + } + + createdDynamicGroup, err := vdcGroup.CreateNsxtFirewallGroup(dynamicSecGroupDefinition) + check.Assert(err, IsNil) + check.Assert(createdDynamicGroup, NotNil) + + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointFirewallGroups + createdDynamicGroup.NsxtFirewallGroup.ID + AddToCleanupListOpenApi(createdDynamicGroup.NsxtFirewallGroup.Name, check.TestName(), openApiEndpoint) + + check.Assert(createdDynamicGroup.NsxtFirewallGroup.ID, Not(Equals), "") + check.Assert(createdDynamicGroup.NsxtFirewallGroup.OwnerRef.Name, Equals, vcd.config.VCD.Nsxt.VdcGroup) + check.Assert(createdDynamicGroup.NsxtFirewallGroup.TypeValue, Equals, types.FirewallGroupTypeVmCriteria) + + // Update + createdDynamicGroup.NsxtFirewallGroup.Description = "updated-description" + createdDynamicGroup.NsxtFirewallGroup.Name = check.TestName() + "-updated" + + updatedDynamicGroup, err := createdDynamicGroup.Update(createdDynamicGroup.NsxtFirewallGroup) + check.Assert(err, IsNil) + check.Assert(updatedDynamicGroup, NotNil) + check.Assert(updatedDynamicGroup.NsxtFirewallGroup, DeepEquals, createdDynamicGroup.NsxtFirewallGroup) + + check.Assert(updatedDynamicGroup, DeepEquals, createdDynamicGroup) + + // Remove Dynamic Security Group + err = updatedDynamicGroup.Delete() + check.Assert(err, IsNil) +} diff --git a/govcd/nsxt_firewall_group_ip_set_test.go b/govcd/nsxt_firewall_group_ip_set_test.go new file mode 100644 index 000000000..62ec655b5 --- /dev/null +++ b/govcd/nsxt_firewall_group_ip_set_test.go @@ -0,0 +1,195 @@ +//go:build network || nsxt || functional || openapi || ALL + +package govcd + +import ( + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +// Test_NsxtIpSet tests out IP Set capabilities using Firewall Group endpoint +func (vcd *TestVCD) Test_NsxtIpSet(check *C) { + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointFirewallGroups) + vcd.skipIfNotSysAdmin(check) + + org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + + nsxtVdc, err := org.GetVDCByName(vcd.config.VCD.Nsxt.Vdc, false) + check.Assert(err, IsNil) + + edge, err := nsxtVdc.GetNsxtEdgeGatewayByName(vcd.config.VCD.Nsxt.EdgeGateway) + check.Assert(err, IsNil) + + ipSetDefinition := &types.NsxtFirewallGroup{ + Name: check.TestName(), + Description: check.TestName() + "-Description", + Type: types.FirewallGroupTypeIpSet, + OwnerRef: &types.OpenApiReference{ID: edge.EdgeGateway.ID}, + + IpAddresses: []string{ + "12.12.12.1", + "10.10.10.0/24", + "11.11.11.1-11.11.11.2", + // represents the block of IPv6 addresses from 2001:db8:0:0:0:0:0:0 to 2001:db8:0:ffff:ffff:ffff:ffff:ffff + "2001:db8::/48", + "2001:db6:0:0:0:0:0:0-2001:db6:0:ffff:ffff:ffff:ffff:ffff", + }, + } + + // Create IP Set and add to cleanup if it was created + createdIpSet, err := nsxtVdc.CreateNsxtFirewallGroup(ipSetDefinition) + check.Assert(err, IsNil) + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointFirewallGroups + createdIpSet.NsxtFirewallGroup.ID + AddToCleanupListOpenApi(createdIpSet.NsxtFirewallGroup.Name, check.TestName(), openApiEndpoint) + + check.Assert(createdIpSet.NsxtFirewallGroup.ID, Not(Equals), "") + check.Assert(createdIpSet.NsxtFirewallGroup.EdgeGatewayRef.Name, Equals, vcd.config.VCD.Nsxt.EdgeGateway) + + check.Assert(createdIpSet.NsxtFirewallGroup.Description, Equals, ipSetDefinition.Description) + check.Assert(createdIpSet.NsxtFirewallGroup.Name, Equals, ipSetDefinition.Name) + check.Assert(createdIpSet.NsxtFirewallGroup.Type, Equals, ipSetDefinition.Type) + + // Update and compare + createdIpSet.NsxtFirewallGroup.Description = "updated-description" + createdIpSet.NsxtFirewallGroup.Name = check.TestName() + "-updated" + + updatedIpSet, err := createdIpSet.Update(createdIpSet.NsxtFirewallGroup) + check.Assert(err, IsNil) + check.Assert(updatedIpSet.NsxtFirewallGroup, DeepEquals, createdIpSet.NsxtFirewallGroup) + + check.Assert(updatedIpSet, DeepEquals, createdIpSet) + + // Get all Firewall Groups and check if the created one is there + allIpSets, err := org.GetAllNsxtFirewallGroups(nil, types.FirewallGroupTypeIpSet) + check.Assert(err, IsNil) + fwGroupFound := false + for i := range allIpSets { + if allIpSets[i].NsxtFirewallGroup.ID == updatedIpSet.NsxtFirewallGroup.ID { + fwGroupFound = true + break + } + } + check.Assert(fwGroupFound, Equals, true) + + // Check if all retrieval functions get the same + orgIpSetByName, err := org.GetNsxtFirewallGroupByName(updatedIpSet.NsxtFirewallGroup.Name, types.FirewallGroupTypeIpSet) + check.Assert(err, IsNil) + orgIpSetById, err := org.GetNsxtFirewallGroupById(updatedIpSet.NsxtFirewallGroup.ID) + check.Assert(err, IsNil) + check.Assert(orgIpSetByName.NsxtFirewallGroup, DeepEquals, orgIpSetById.NsxtFirewallGroup) + + // Get Firewall Group using VDC + vdcIpSetByName, err := nsxtVdc.GetNsxtFirewallGroupByName(updatedIpSet.NsxtFirewallGroup.Name, types.FirewallGroupTypeIpSet) + check.Assert(err, IsNil) + vdcIpSetById, err := nsxtVdc.GetNsxtFirewallGroupById(updatedIpSet.NsxtFirewallGroup.ID) + check.Assert(err, IsNil) + check.Assert(vdcIpSetByName.NsxtFirewallGroup, DeepEquals, vdcIpSetById.NsxtFirewallGroup) + check.Assert(vdcIpSetById.NsxtFirewallGroup, DeepEquals, orgIpSetById.NsxtFirewallGroup) + + // Get Firewall Group using Edge Gateway + edgeIpSetByName, err := edge.GetNsxtFirewallGroupByName(updatedIpSet.NsxtFirewallGroup.Name, types.FirewallGroupTypeIpSet) + check.Assert(err, IsNil) + edgeIpSetById, err := edge.GetNsxtFirewallGroupById(updatedIpSet.NsxtFirewallGroup.ID) + check.Assert(err, IsNil) + check.Assert(edgeIpSetByName.NsxtFirewallGroup, DeepEquals, orgIpSetByName.NsxtFirewallGroup) + check.Assert(edgeIpSetById.NsxtFirewallGroup, DeepEquals, edgeIpSetByName.NsxtFirewallGroup) + + // Get Firewall Group using VDC Group + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + + nsxtExternalNetwork, err := GetExternalNetworkV2ByName(vcd.client, vcd.config.VCD.Nsxt.ExternalNetwork) + check.Assert(err, IsNil) + check.Assert(nsxtExternalNetwork, NotNil) + + vdc, vdcGroup := test_CreateVdcGroup(check, adminOrg, vcd) + egwDefinition := &types.OpenAPIEdgeGateway{ + Name: "nsx-for-IpSet-edge", + Description: "nsx-for-IpSet-edge-description", + OwnerRef: &types.OpenApiReference{ + ID: vdc.Vdc.ID, + }, + EdgeGatewayUplinks: []types.EdgeGatewayUplinks{{ + UplinkID: nsxtExternalNetwork.ExternalNetwork.ID, + Subnets: types.OpenAPIEdgeGatewaySubnets{Values: []types.OpenAPIEdgeGatewaySubnetValue{{ + Gateway: "1.1.1.1", + PrefixLength: 24, + Enabled: true, + }}}, + Connected: true, + Dedicated: false, + }}, + } + + // Create Edge Gateway in VDC Group + createdEdge, err := adminOrg.CreateNsxtEdgeGateway(egwDefinition) + check.Assert(err, IsNil) + check.Assert(createdEdge.EdgeGateway.OwnerRef.ID, Matches, `^urn:vcloud:vdc:.*`) + openApiEndpoint = types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGateways + createdEdge.EdgeGateway.ID + PrependToCleanupListOpenApi(createdEdge.EdgeGateway.Name, check.TestName(), openApiEndpoint) + + check.Assert(createdEdge.EdgeGateway.Name, Equals, egwDefinition.Name) + check.Assert(createdEdge.EdgeGateway.OwnerRef.ID, Equals, egwDefinition.OwnerRef.ID) + + movedGateway, err := createdEdge.MoveToVdcOrVdcGroup(vdcGroup.VdcGroup.Id) + check.Assert(err, IsNil) + check.Assert(movedGateway.EdgeGateway.OwnerRef.ID, Equals, vdcGroup.VdcGroup.Id) + check.Assert(movedGateway.EdgeGateway.OwnerRef.ID, Matches, `^urn:vcloud:vdcGroup:.*`) + + ipSetDefinition.Name = check.TestName() + "VdcGroup" + ipSetDefinition.OwnerRef.ID = vdcGroup.VdcGroup.Id + createdIpSetInVdcGroup, err := createdEdge.CreateNsxtFirewallGroup(ipSetDefinition) + check.Assert(err, IsNil) + check.Assert(createdIpSetInVdcGroup, NotNil) + openApiEndpoint = types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointFirewallGroups + createdIpSetInVdcGroup.NsxtFirewallGroup.ID + AddToCleanupListOpenApi(createdIpSet.NsxtFirewallGroup.Name, check.TestName(), openApiEndpoint) + vdcGroupIpSetByName, err := vdcGroup.GetNsxtFirewallGroupByName(createdIpSetInVdcGroup.NsxtFirewallGroup.Name, types.FirewallGroupTypeIpSet) + check.Assert(err, IsNil) + vdcGroupIpSetById, err := vdcGroup.GetNsxtFirewallGroupById(createdIpSetInVdcGroup.NsxtFirewallGroup.ID) + check.Assert(err, IsNil) + check.Assert(vdcGroupIpSetByName.NsxtFirewallGroup, DeepEquals, vdcGroupIpSetById.NsxtFirewallGroup) + check.Assert(vdcGroupIpSetById.NsxtFirewallGroup, DeepEquals, vdcGroupIpSetByName.NsxtFirewallGroup) + + associatedVms, err := edgeIpSetByName.GetAssociatedVms() + // IP_SET type Firewall Groups do not have VM associations and throw an error on API call. + // The error is: only Security Groups have associated VMs. This Firewall Group has type 'IP_SET' + // Not hardcodeing it here because it may change and break the test. + check.Assert(err, NotNil) + check.Assert(associatedVms, IsNil) + + // Remove + err = createdIpSet.Delete() + check.Assert(err, IsNil) + err = vdcGroupIpSetByName.Delete() + check.Assert(err, IsNil) + + // Create IP Set using Edge Gateway method + ipSetDefinition.Name = check.TestName() + "-using-edge-gateway-type" + ipSetDefinition.OwnerRef.ID = edge.EdgeGateway.ID + + // Create IP Set and add to cleanup if it was created + edgeCreatedIpSet, err := nsxtVdc.CreateNsxtFirewallGroup(ipSetDefinition) + check.Assert(err, IsNil) + openApiEndpoint = types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointFirewallGroups + edgeCreatedIpSet.NsxtFirewallGroup.ID + AddToCleanupListOpenApi(createdIpSet.NsxtFirewallGroup.Name, check.TestName(), openApiEndpoint) + + check.Assert(edgeCreatedIpSet.NsxtFirewallGroup.ID, Not(Equals), "") + check.Assert(edgeCreatedIpSet.NsxtFirewallGroup.OwnerRef.Name, Equals, edge.EdgeGateway.Name) + + err = edgeCreatedIpSet.Delete() + check.Assert(err, IsNil) + + // Remove Edge Gateway + err = movedGateway.Delete() + check.Assert(err, IsNil) + + // Remove VDC group and VDC + err = vdcGroup.Delete() + check.Assert(err, IsNil) + task, err := vdc.Delete(true, true) + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) +} diff --git a/govcd/nsxt_firewall_group_static_security_group_test.go b/govcd/nsxt_firewall_group_static_security_group_test.go new file mode 100644 index 000000000..3c5c8de41 --- /dev/null +++ b/govcd/nsxt_firewall_group_static_security_group_test.go @@ -0,0 +1,373 @@ +//go:build network || nsxt || functional || openapi || ALL + +package govcd + +import ( + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +// Test_NsxtSecurityGroup tests out CRUD of Static NSX-T Security Group +// +// Note. Security Group is one type of Firewall Group +func (vcd *TestVCD) Test_NsxtStaticSecurityGroup(check *C) { + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointFirewallGroups) + + org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + + nsxtVdc, err := org.GetVDCByName(vcd.config.VCD.Nsxt.Vdc, false) + check.Assert(err, IsNil) + + edge, err := nsxtVdc.GetNsxtEdgeGatewayByName(vcd.config.VCD.Nsxt.EdgeGateway) + check.Assert(err, IsNil) + + fwGroupDefinition := &types.NsxtFirewallGroup{ + Name: check.TestName(), + Description: check.TestName() + "-Description", + Type: types.FirewallGroupTypeSecurityGroup, + EdgeGatewayRef: &types.OpenApiReference{ID: edge.EdgeGateway.ID}, + } + + // Create firewall group and add to cleanup if it was created + createdSecGroup, err := nsxtVdc.CreateNsxtFirewallGroup(fwGroupDefinition) + check.Assert(err, IsNil) + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointFirewallGroups + createdSecGroup.NsxtFirewallGroup.ID + AddToCleanupListOpenApi(createdSecGroup.NsxtFirewallGroup.Name, check.TestName(), openApiEndpoint) + + check.Assert(createdSecGroup.NsxtFirewallGroup.ID, Not(Equals), "") + check.Assert(createdSecGroup.NsxtFirewallGroup.EdgeGatewayRef.Name, Equals, vcd.config.VCD.Nsxt.EdgeGateway) + + check.Assert(createdSecGroup.NsxtFirewallGroup.Description, Equals, fwGroupDefinition.Description) + check.Assert(createdSecGroup.NsxtFirewallGroup.Name, Equals, fwGroupDefinition.Name) + check.Assert(createdSecGroup.NsxtFirewallGroup.Type, Equals, fwGroupDefinition.Type) + + // Update and compare + createdSecGroup.NsxtFirewallGroup.Description = "updated-description" + createdSecGroup.NsxtFirewallGroup.Name = check.TestName() + "-updated" + + updatedSecGroup, err := createdSecGroup.Update(createdSecGroup.NsxtFirewallGroup) + check.Assert(err, IsNil) + check.Assert(updatedSecGroup.NsxtFirewallGroup, DeepEquals, createdSecGroup.NsxtFirewallGroup) + + check.Assert(updatedSecGroup, DeepEquals, createdSecGroup) + + // Get all Firewall Groups and check if the created one is there + allSecGroups, err := org.GetAllNsxtFirewallGroups(nil, types.FirewallGroupTypeSecurityGroup) + check.Assert(err, IsNil) + fwGroupFound := false + for i := range allSecGroups { + if allSecGroups[i].NsxtFirewallGroup.ID == updatedSecGroup.NsxtFirewallGroup.ID { + fwGroupFound = true + break + } + } + check.Assert(fwGroupFound, Equals, true) + + // Get firewall group by name using Org + secGroupByName, err := org.GetNsxtFirewallGroupByName(updatedSecGroup.NsxtFirewallGroup.Name, types.FirewallGroupTypeSecurityGroup) + check.Assert(err, IsNil) + + secGroupById, err := org.GetNsxtFirewallGroupById(updatedSecGroup.NsxtFirewallGroup.ID) + check.Assert(err, IsNil) + check.Assert(secGroupById.NsxtFirewallGroup, DeepEquals, secGroupByName.NsxtFirewallGroup) + + // // Get firewall group by name using Vdc + vdcSecGroupByName, err := nsxtVdc.GetNsxtFirewallGroupByName(updatedSecGroup.NsxtFirewallGroup.Name, types.FirewallGroupTypeSecurityGroup) + check.Assert(err, IsNil) + + vdcSecGroupById, err := nsxtVdc.GetNsxtFirewallGroupById(updatedSecGroup.NsxtFirewallGroup.ID) + check.Assert(err, IsNil) + check.Assert(vdcSecGroupById.NsxtFirewallGroup.ID, Not(Equals), "") + check.Assert(vdcSecGroupByName.NsxtFirewallGroup, DeepEquals, vdcSecGroupById.NsxtFirewallGroup) + check.Assert(vdcSecGroupByName.NsxtFirewallGroup, DeepEquals, secGroupById.NsxtFirewallGroup) + + // Get Security Group using Edge Gateway + edgeSecGroup, err := edge.GetNsxtFirewallGroupByName(updatedSecGroup.NsxtFirewallGroup.Name, types.FirewallGroupTypeSecurityGroup) + check.Assert(err, IsNil) + check.Assert(edgeSecGroup.NsxtFirewallGroup, DeepEquals, secGroupByName.NsxtFirewallGroup) + + associatedVms, err := edgeSecGroup.GetAssociatedVms() + // Try to list associated VMs and expect an empty list (because no Org VDC network is attached) + check.Assert(err, IsNil) + check.Assert(len(associatedVms), Equals, 0) + + // Remove + err = createdSecGroup.Delete() + check.Assert(err, IsNil) +} + +// Test_NsxtSecurityGroupGetAssociatedVms tests if member routed Org VDC networks are added correctly to +// Security Groups and if associated VMs are correctly reported back +// +// Note. Security Group is one type of Firewall Group +func (vcd *TestVCD) Test_NsxtSecurityGroupGetAssociatedVms(check *C) { + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointFirewallGroups) + + org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + + nsxtVdc, err := org.GetVDCByName(vcd.config.VCD.Nsxt.Vdc, false) + check.Assert(err, IsNil) + + edge, err := nsxtVdc.GetNsxtEdgeGatewayByName(vcd.config.VCD.Nsxt.EdgeGateway) + check.Assert(err, IsNil) + + // Setup prerequisites - Routed Org VDC and add 2 VMs. With vApp and standalone + routedNet := createNsxtRoutedNetwork(check, vcd, nsxtVdc, edge.EdgeGateway.ID) + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointOrgVdcNetworks + routedNet.OpenApiOrgVdcNetwork.ID + AddToCleanupListOpenApi(routedNet.OpenApiOrgVdcNetwork.Name, check.TestName(), openApiEndpoint) + + vapp, vappVm := createVappVmAndAttachNetwork(check, vcd, nsxtVdc, routedNet) + PrependToCleanupList(vapp.VApp.Name, "vapp", vcd.nsxtVdc.Vdc.Name, check.TestName()) + + // VMs are prependend to clean up list to make sure they are removed before routed network + standaloneVm := createStandaloneVm(check, vcd, nsxtVdc, routedNet) + PrependToCleanupList(standaloneVm.VM.ID, "standaloneVm", "", check.TestName()) + + secGroupDefinition := &types.NsxtFirewallGroup{ + Name: check.TestName(), + Description: check.TestName() + "-Description", + Type: types.FirewallGroupTypeSecurityGroup, + EdgeGatewayRef: &types.OpenApiReference{ID: edge.EdgeGateway.ID}, + Members: []types.OpenApiReference{ + {ID: routedNet.OpenApiOrgVdcNetwork.ID}, + }, + } + + // Create firewall group and add to cleanup if it was created + createdSecGroup, err := nsxtVdc.CreateNsxtFirewallGroup(secGroupDefinition) + check.Assert(err, IsNil) + openApiEndpoint = types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointFirewallGroups + createdSecGroup.NsxtFirewallGroup.ID + AddToCleanupListOpenApi(createdSecGroup.NsxtFirewallGroup.Name, check.TestName(), openApiEndpoint) + + // Expect to see VM created in associated VM query + associatedVms, err := createdSecGroup.GetAssociatedVms() + check.Assert(err, IsNil) + + check.Assert(len(associatedVms), Equals, 2) + + foundStandalone := false + foundVappVm := false + for i := range associatedVms { + if associatedVms[i].VmRef.ID == standaloneVm.VM.ID { + foundStandalone = true + } + + if associatedVms[i].VappRef != nil && associatedVms[i].VmRef.ID == vappVm.VM.ID && + associatedVms[i].VappRef.ID == vapp.VApp.ID { + foundVappVm = true + } + } + + check.Assert(foundStandalone, Equals, true) + check.Assert(foundVappVm, Equals, true) + task, err := vapp.Delete() + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) + err = standaloneVm.Delete() + check.Assert(err, IsNil) + err = createdSecGroup.Delete() + check.Assert(err, IsNil) + err = routedNet.Delete() + check.Assert(err, IsNil) +} + +func createNsxtRoutedNetwork(check *C, vcd *TestVCD, vdc *Vdc, edgeGatewayId string) *OpenApiOrgVdcNetwork { + orgVdcNetworkConfig := &types.OpenApiOrgVdcNetwork{ + Name: check.TestName() + "routed-net", + Description: check.TestName() + "-description", + + // On v35.0 orgVdc is not supported anymore. Using ownerRef instead. + OwnerRef: &types.OpenApiReference{ID: vcd.nsxtVdc.Vdc.ID}, + + NetworkType: types.OrgVdcNetworkTypeRouted, + + // Connection is used for "routed" network + Connection: &types.Connection{ + RouterRef: types.OpenApiReference{ + ID: edgeGatewayId, + }, + ConnectionType: "INTERNAL", + }, + Subnets: types.OrgVdcNetworkSubnets{ + Values: []types.OrgVdcNetworkSubnetValues{ + { + Gateway: "2.1.1.1", + PrefixLength: 24, + IPRanges: types.OrgVdcNetworkSubnetIPRanges{ + Values: []types.OrgVdcNetworkSubnetIPRangeValues{ + { + StartAddress: "2.1.1.20", + EndAddress: "2.1.1.30", + }, + }}, + }, + }, + }, + } + + orgVdcNet, err := vdc.CreateOpenApiOrgVdcNetwork(orgVdcNetworkConfig) + check.Assert(err, IsNil) + return orgVdcNet +} + +func createStandaloneVm(check *C, vcd *TestVCD, vdc *Vdc, net *OpenApiOrgVdcNetwork) *VM { + params := types.CreateVmParams{ + Name: check.TestName() + "-standalone", + PowerOn: false, + CreateVm: &types.Vm{ + Name: check.TestName() + "-standalone", + VirtualHardwareSection: nil, + NetworkConnectionSection: &types.NetworkConnectionSection{ + Info: "Network Configuration for VM", + PrimaryNetworkConnectionIndex: 0, + NetworkConnection: []*types.NetworkConnection{ + &types.NetworkConnection{ + Network: net.OpenApiOrgVdcNetwork.Name, + NeedsCustomization: false, + NetworkConnectionIndex: 0, + IPAddress: "any", + IsConnected: true, + IPAddressAllocationMode: "DHCP", + NetworkAdapterType: "VMXNET3", + }, + }, + Link: nil, + }, + VmSpecSection: &types.VmSpecSection{ + Modified: addrOf(true), + Info: "Virtual Machine specification", + OsType: "debian10Guest", + NumCpus: addrOf(1), + NumCoresPerSocket: addrOf(1), + CpuResourceMhz: &types.CpuResourceMhz{ + Configured: 0, + }, + MemoryResourceMb: &types.MemoryResourceMb{ + Configured: 512, + }, + DiskSection: &types.DiskSection{ + DiskSettings: []*types.DiskSettings{ + &types.DiskSettings{ + SizeMb: 1024, + UnitNumber: 0, + BusNumber: 0, + AdapterType: "5", + ThinProvisioned: addrOf(true), + OverrideVmDefault: false, + }, + }, + }, + + HardwareVersion: &types.HardwareVersion{Value: "vmx-14"}, + VmToolsVersion: "", + VirtualCpuType: "VM32", + }, + GuestCustomizationSection: &types.GuestCustomizationSection{ + Info: "Specifies Guest OS Customization Settings", + ComputerName: "standalone1", + }, + }, + Xmlns: types.XMLNamespaceVCloud, + } + + vm, err := vdc.CreateStandaloneVm(¶ms) + check.Assert(err, IsNil) + check.Assert(vm, NotNil) + return vm +} + +func createVappVmAndAttachNetwork(check *C, vcd *TestVCD, vdc *Vdc, net *OpenApiOrgVdcNetwork) (*VApp, *VM) { + vapp, err := vdc.CreateRawVApp(check.TestName(), check.TestName()+"description") + check.Assert(err, IsNil) + + check.Assert(vapp, NotNil) + + // Attach network to vApp + orgVdcNetworkWithHREF, err := vdc.GetOrgVdcNetworkById(net.OpenApiOrgVdcNetwork.ID, true) + check.Assert(err, IsNil) + + networkConfigurations := vapp.VApp.NetworkConfigSection.NetworkConfig + vappConfiguration := types.VAppNetworkConfiguration{ + NetworkName: net.OpenApiOrgVdcNetwork.Name, + Configuration: &types.NetworkConfiguration{ + ParentNetwork: &types.Reference{ + HREF: orgVdcNetworkWithHREF.OrgVDCNetwork.HREF, + }, + RetainNetInfoAcrossDeployments: addrOf(false), + FenceMode: types.FenceModeBridged, + }, + IsDeployed: false, + } + + networkConfigurations = append(networkConfigurations, + vappConfiguration) + + task, err := updateNetworkConfigurations(vapp, networkConfigurations) + check.Assert(err, IsNil) + + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) + // EOF Attach network to vApp + + desiredNetConfig := &types.NetworkConnectionSection{} + desiredNetConfig.PrimaryNetworkConnectionIndex = 0 + desiredNetConfig.NetworkConnection = append(desiredNetConfig.NetworkConnection, + &types.NetworkConnection{ + IsConnected: true, + IPAddressAllocationMode: types.IPAllocationModePool, + Network: net.OpenApiOrgVdcNetwork.Name, + NetworkConnectionIndex: 0, + }, + ) + + emptyVmDefinition := &types.RecomposeVAppParamsForEmptyVm{ + CreateItem: &types.CreateItem{ + Name: check.TestName(), + Description: "created by " + check.TestName(), + GuestCustomizationSection: nil, + VmSpecSection: &types.VmSpecSection{ + Modified: addrOf(true), + Info: "Virtual Machine specification", + OsType: "debian10Guest", + NumCpus: addrOf(2), + NumCoresPerSocket: addrOf(1), + CpuResourceMhz: &types.CpuResourceMhz{Configured: 1}, + MemoryResourceMb: &types.MemoryResourceMb{Configured: 1024}, + DiskSection: &types.DiskSection{DiskSettings: []*types.DiskSettings{ + &types.DiskSettings{ + AdapterType: "5", + SizeMb: int64(16384), + BusNumber: 0, + UnitNumber: 0, + ThinProvisioned: addrOf(true), + OverrideVmDefault: true, + }, + }}, + HardwareVersion: &types.HardwareVersion{Value: "vmx-13"}, // need support older version vCD + VmToolsVersion: "", + VirtualCpuType: "VM32", + TimeSyncWithHost: nil, + }, + }, + AllEULAsAccepted: true, + } + + createdVm, err := vapp.AddEmptyVm(emptyVmDefinition) + check.Assert(err, IsNil) + + // Network could have been configured while creating VM, but on some slow systems + // the network is not yet found just after creating it so creating a VM without network and + // adding it later buys some time + err = createdVm.UpdateNetworkConnectionSection(desiredNetConfig) + check.Assert(err, IsNil) + + check.Assert(err, IsNil) + check.Assert(createdVm, NotNil) + + return vapp, createdVm +} diff --git a/govcd/nsxt_firewall_test.go b/govcd/nsxt_firewall_test.go new file mode 100644 index 000000000..9f9d75a01 --- /dev/null +++ b/govcd/nsxt_firewall_test.go @@ -0,0 +1,248 @@ +//go:build network || nsxt || functional || openapi || ALL + +package govcd + +import ( + "crypto/rand" + "fmt" + "math/big" + "os" + "strconv" + "text/tabwriter" + + "github.com/vmware/go-vcloud-director/v2/util" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +// Test_NsxtFirewall creates 20 firewall rules with randomized parameters +func (vcd *TestVCD) Test_NsxtFirewall(check *C) { + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointFirewallGroups) + + org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + nsxtVdc, err := org.GetVDCByName(vcd.config.VCD.Nsxt.Vdc, false) + check.Assert(err, IsNil) + edge, err := nsxtVdc.GetNsxtEdgeGatewayByName(vcd.config.VCD.Nsxt.EdgeGateway) + check.Assert(err, IsNil) + + // Get existing firewall rule configuration + fwRules, err := edge.GetNsxtFirewall() + check.Assert(err, IsNil) + + existingDefaultRuleCount := len(fwRules.NsxtFirewallRuleContainer.DefaultRules) + existingSystemRuleCount := len(fwRules.NsxtFirewallRuleContainer.SystemRules) + + // Create some prerequisites and generate firewall rule configurations to feed them into config + randomizedFwRuleDefs := createFirewallDefinitions(check, vcd) + fwRules.NsxtFirewallRuleContainer.UserDefinedRules = randomizedFwRuleDefs + + if testVerbose { + dumpFirewallRulesToScreen(randomizedFwRuleDefs) + } + + fwCreated, err := edge.UpdateNsxtFirewall(fwRules.NsxtFirewallRuleContainer) + check.Assert(err, IsNil) + + openApiEndpoint := types.OpenApiPathVersion1_0_0 + fmt.Sprintf(types.OpenApiEndpointNsxtFirewallRules, edge.EdgeGateway.ID) + PrependToCleanupList(openApiEndpoint, "OpenApiEntityFirewall", edge.EdgeGateway.Name, check.TestName()) + + check.Assert(fwCreated, Not(IsNil)) + check.Assert(len(fwCreated.NsxtFirewallRuleContainer.UserDefinedRules), Equals, len(randomizedFwRuleDefs)) + + // Check that all created rules are have the same attributes and order + for index := range fwCreated.NsxtFirewallRuleContainer.UserDefinedRules { + check.Assert(fwCreated.NsxtFirewallRuleContainer.UserDefinedRules[index].Name, Equals, randomizedFwRuleDefs[index].Name) + check.Assert(fwCreated.NsxtFirewallRuleContainer.UserDefinedRules[index].Direction, Equals, randomizedFwRuleDefs[index].Direction) + check.Assert(fwCreated.NsxtFirewallRuleContainer.UserDefinedRules[index].IpProtocol, Equals, randomizedFwRuleDefs[index].IpProtocol) + check.Assert(fwCreated.NsxtFirewallRuleContainer.UserDefinedRules[index].Enabled, Equals, randomizedFwRuleDefs[index].Enabled) + check.Assert(fwCreated.NsxtFirewallRuleContainer.UserDefinedRules[index].ActionValue, Equals, randomizedFwRuleDefs[index].ActionValue) + if vcd.client.Client.IsSysAdmin { + // Only system administrator can handle logging + check.Assert(fwCreated.NsxtFirewallRuleContainer.UserDefinedRules[index].Logging, Equals, randomizedFwRuleDefs[index].Logging) + } + + for fwGroupIndex := range fwCreated.NsxtFirewallRuleContainer.UserDefinedRules[index].SourceFirewallGroups { + check.Assert(fwCreated.NsxtFirewallRuleContainer.UserDefinedRules[index].SourceFirewallGroups[fwGroupIndex].ID, Equals, randomizedFwRuleDefs[index].SourceFirewallGroups[fwGroupIndex].ID) + } + + for fwGroupIndex := range fwCreated.NsxtFirewallRuleContainer.UserDefinedRules[index].DestinationFirewallGroups { + check.Assert(fwCreated.NsxtFirewallRuleContainer.UserDefinedRules[index].DestinationFirewallGroups[fwGroupIndex].ID, Equals, randomizedFwRuleDefs[index].DestinationFirewallGroups[fwGroupIndex].ID) + } + + // Ensure the same amount of Application Port Profiles are assigned and created + check.Assert(len(fwCreated.NsxtFirewallRuleContainer.UserDefinedRules), Equals, len(randomizedFwRuleDefs)) + definedAppPortProfileIds := extractIdsFromOpenApiReferences(randomizedFwRuleDefs[index].ApplicationPortProfiles) + for _, appPortProfile := range fwCreated.NsxtFirewallRuleContainer.UserDefinedRules[index].ApplicationPortProfiles { + check.Assert(contains(appPortProfile.ID, definedAppPortProfileIds), Equals, true) + } + } + + // Delete a single rule by ID and check for two things: + // * Rule with deleted ID should not be found in list post deletion + // * There should be one less rule in the list + deleteRuleId := fwCreated.NsxtFirewallRuleContainer.UserDefinedRules[3].ID + err = fwCreated.DeleteRuleById(deleteRuleId) + check.Assert(err, IsNil) + + allRulesPostDeletion, err := edge.GetNsxtFirewall() + check.Assert(err, IsNil) + + check.Assert(len(allRulesPostDeletion.NsxtFirewallRuleContainer.UserDefinedRules), Equals, len(fwCreated.NsxtFirewallRuleContainer.UserDefinedRules)-1) + for _, rule := range allRulesPostDeletion.NsxtFirewallRuleContainer.UserDefinedRules { + check.Assert(rule.ID, Not(Equals), deleteRuleId) + } + + err = fwRules.DeleteAllRules() + check.Assert(err, IsNil) + + // Ensure no firewall rules left in user space post deletion, but the same amount of default and system rules still exist + postDeleteCheck, err := edge.GetNsxtFirewall() + check.Assert(err, IsNil) + check.Assert(len(postDeleteCheck.NsxtFirewallRuleContainer.UserDefinedRules), Equals, 0) + check.Assert(len(postDeleteCheck.NsxtFirewallRuleContainer.DefaultRules), Equals, existingDefaultRuleCount) + check.Assert(len(postDeleteCheck.NsxtFirewallRuleContainer.SystemRules), Equals, existingSystemRuleCount) + +} + +// createFirewallDefinitions creates some randomized firewall rule configurations to match possible configurations +func createFirewallDefinitions(check *C, vcd *TestVCD) []*types.NsxtFirewallRule { + // This number does not impact performance because all rules are created at once in the API + numberOfRules := 20 + + // Pre-Create Firewall Groups (IP Set and Security Group to randomly configure them) + ipSet := preCreateIpSet(check, vcd) + secGroup := preCreateSecurityGroup(check, vcd) + fwGroupIds := []string{ipSet.NsxtFirewallGroup.ID, secGroup.NsxtFirewallGroup.ID} + fwGroupRefs := convertSliceOfStringsToOpenApiReferenceIds(fwGroupIds) + appPortProfileReferences := getRandomListOfAppPortProfiles(check, vcd) + + firewallRules := make([]*types.NsxtFirewallRule, numberOfRules) + for a := 0; a < numberOfRules; a++ { + + // Feed in empty value for source and destination or a firewall group + src := pickRandomOpenApiRefOrEmpty(fwGroupRefs) + var srcValue []types.OpenApiReference + dst := pickRandomOpenApiRefOrEmpty(fwGroupRefs) + var dstValue []types.OpenApiReference + if src != (types.OpenApiReference{}) { + srcValue = []types.OpenApiReference{src} + } + if dst != (types.OpenApiReference{}) { + dstValue = []types.OpenApiReference{dst} + } + + firewallRules[a] = &types.NsxtFirewallRule{ + Name: check.TestName() + strconv.Itoa(a), + ActionValue: pickRandomString([]string{"ALLOW", "DROP", "REJECT"}), + Enabled: a%2 == 0, + SourceFirewallGroups: srcValue, + DestinationFirewallGroups: dstValue, + ApplicationPortProfiles: appPortProfileReferences[0:a], + IpProtocol: pickRandomString([]string{"IPV6", "IPV4", "IPV4_IPV6"}), + Logging: a%2 == 1, + Direction: pickRandomString([]string{"IN", "OUT", "IN_OUT"}), + } + } + + return firewallRules +} + +func pickRandomString(in []string) string { + randomIndex, _ := rand.Int(rand.Reader, big.NewInt(int64(len(in)))) + return in[randomIndex.Uint64()] +} + +// pickRandomOpenApiRefOrEmpty picks a random OpenAPI entity or an empty one +func pickRandomOpenApiRefOrEmpty(in []types.OpenApiReference) types.OpenApiReference { + // Random value can be up to len+1 (len+1 is the special case when it should return an empty reference) + randomIndex, _ := rand.Int(rand.Reader, big.NewInt(int64(len(in)+1))) + if randomIndex.Uint64() == uint64(len(in)) { + return types.OpenApiReference{} + } + return in[randomIndex.Uint64()] +} + +func preCreateIpSet(check *C, vcd *TestVCD) *NsxtFirewallGroup { + nsxtVdc := vcd.nsxtVdc + edge, err := nsxtVdc.GetNsxtEdgeGatewayByName(vcd.config.VCD.Nsxt.EdgeGateway) + check.Assert(err, IsNil) + + ipSetDefinition := &types.NsxtFirewallGroup{ + Name: check.TestName() + "ipset", + Description: check.TestName() + "-Description", + Type: types.FirewallGroupTypeIpSet, + EdgeGatewayRef: &types.OpenApiReference{ID: edge.EdgeGateway.ID}, + + IpAddresses: []string{ + "12.12.12.1", + "10.10.10.0/24", + "11.11.11.1-11.11.11.2", + // represents the block of IPv6 addresses from 2001:db8:0:0:0:0:0:0 to 2001:db8:0:ffff:ffff:ffff:ffff:ffff + "2001:db8::/48", + "2001:db6:0:0:0:0:0:0-2001:db6:0:ffff:ffff:ffff:ffff:ffff", + }, + } + + // Create IP Set and add to cleanup if it was created + createdIpSet, err := nsxtVdc.CreateNsxtFirewallGroup(ipSetDefinition) + check.Assert(err, IsNil) + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointFirewallGroups + createdIpSet.NsxtFirewallGroup.ID + AddToCleanupListOpenApi(createdIpSet.NsxtFirewallGroup.Name, check.TestName(), openApiEndpoint) + + return createdIpSet +} + +func preCreateSecurityGroup(check *C, vcd *TestVCD) *NsxtFirewallGroup { + nsxtVdc := vcd.nsxtVdc + edge, err := nsxtVdc.GetNsxtEdgeGatewayByName(vcd.config.VCD.Nsxt.EdgeGateway) + check.Assert(err, IsNil) + + fwGroupDefinition := &types.NsxtFirewallGroup{ + Name: check.TestName() + "security-group", + Description: check.TestName() + "-Description", + Type: types.FirewallGroupTypeSecurityGroup, + EdgeGatewayRef: &types.OpenApiReference{ID: edge.EdgeGateway.ID}, + } + + // Create firewall group and add to cleanup if it was created + createdSecGroup, err := nsxtVdc.CreateNsxtFirewallGroup(fwGroupDefinition) + check.Assert(err, IsNil) + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointFirewallGroups + createdSecGroup.NsxtFirewallGroup.ID + AddToCleanupListOpenApi(check.TestName()+"sec-group", check.TestName(), openApiEndpoint) + + return createdSecGroup +} + +func getRandomListOfAppPortProfiles(check *C, vcd *TestVCD) []types.OpenApiReference { + org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + + appProfileSlice, err := org.GetAllNsxtAppPortProfiles(nil, types.ApplicationPortProfileScopeSystem) + check.Assert(err, IsNil) + + openApiRefs := make([]types.OpenApiReference, len(appProfileSlice)) + for index, appPortProfile := range appProfileSlice { + openApiRefs[index].ID = appPortProfile.NsxtAppPortProfile.ID + openApiRefs[index].Name = appPortProfile.NsxtAppPortProfile.Name + } + + return openApiRefs +} + +func dumpFirewallRulesToScreen(rules []*types.NsxtFirewallRule) { + fmt.Println("# The following firewall rules will be created") + w := tabwriter.NewWriter(os.Stdout, 1, 1, 1, ' ', 0) + fmt.Fprintln(w, "Name\tDirection\tIP Protocol\tEnabled\tAction\tLogging\tSrc Count\tDst Count\tAppPortProfile Count") + + for _, rule := range rules { + fmt.Fprintf(w, "%s\t%s\t%s\t%t\t%s\t%t\t%d\t%d\t%d\n", rule.Name, rule.Direction, rule.IpProtocol, + rule.Enabled, rule.ActionValue, rule.Logging, len(rule.SourceFirewallGroups), len(rule.DestinationFirewallGroups), len(rule.ApplicationPortProfiles)) + } + err := w.Flush() + if err != nil { + util.Logger.Printf("Error while dumping Firewall rules to screen: %s", err) + } +} diff --git a/govcd/nsxt_importable_switch.go b/govcd/nsxt_importable_switch.go index 9e2853796..a3ac5ff71 100644 --- a/govcd/nsxt_importable_switch.go +++ b/govcd/nsxt_importable_switch.go @@ -6,10 +6,12 @@ package govcd import ( "fmt" + "io" "net/http" "net/url" "github.com/vmware/go-vcloud-director/v2/types/v56" + "github.com/vmware/go-vcloud-director/v2/util" ) // NsxtImportableSwitch is a read only object to retrieve NSX-T segments (importable switches) to be used for Org VDC @@ -55,6 +57,62 @@ func (vdc *Vdc) GetNsxtImportableSwitchByName(name string) (*NsxtImportableSwitc return filteredNsxtImportableSwitches[0], nil } +// GetNsxtImportableSwitchByName retrieves a particular NSX-T Segment by name available for that VDC +// +// Note. OpenAPI endpoint does not exist for this resource and by default endpoint +// "/network/orgvdcnetworks/importableswitches" returns only unused NSX-T importable switches (the ones that are not +// already consumed in Org VDC networks) and there is no way to get them all (including the used ones). +func (vdcGroup *VdcGroup) GetNsxtImportableSwitchByName(name string) (*NsxtImportableSwitch, error) { + if name == "" { + return nil, fmt.Errorf("empty NSX-T Importable Switch name specified") + } + + allNsxtImportableSwitches, err := vdcGroup.GetAllNsxtImportableSwitches() + if err != nil { + return nil, fmt.Errorf("error getting all NSX-T Importable Switches for VDC Group '%s': %s", vdcGroup.VdcGroup.Name, err) + } + + var filteredNsxtImportableSwitches []*NsxtImportableSwitch + for _, nsxtImportableSwitch := range allNsxtImportableSwitches { + if nsxtImportableSwitch.NsxtImportableSwitch.Name == name { + filteredNsxtImportableSwitches = append(filteredNsxtImportableSwitches, nsxtImportableSwitch) + } + } + + if len(filteredNsxtImportableSwitches) == 0 { + // ErrorEntityNotFound is injected here for the ability to validate problem using ContainsNotFound() + return nil, fmt.Errorf("%s: no NSX-T Importable Switch with name '%s' for VDC Group with ID '%s' found", + ErrorEntityNotFound, name, vdcGroup.VdcGroup.Id) + } + + if len(filteredNsxtImportableSwitches) > 1 { + return nil, fmt.Errorf("more than one (%d) NSX-T Importable Switch with name '%s' for VDC Group with ID '%s' found", + len(filteredNsxtImportableSwitches), name, vdcGroup.VdcGroup.Id) + } + + return filteredNsxtImportableSwitches[0], nil +} + +// GetAllNsxtImportableSwitches retrieves all available importable switches which can be consumed for creating NSX-T +// "Imported" Org VDC network +// +// Note. OpenAPI endpoint does not exist for this resource and by default endpoint +// "/network/orgvdcnetworks/importableswitches" returns only unused NSX-T importable switches (the ones that are not +// already consumed in Org VDC networks) and there is no way to get them all. +func (vdcGroup *VdcGroup) GetAllNsxtImportableSwitches() ([]*NsxtImportableSwitch, error) { + if vdcGroup.VdcGroup.Id == "" { + return nil, fmt.Errorf("VDC Group must have ID populated to retrieve NSX-T importable switches") + } + // request requires Org VDC Group ID to be specified as UUID, not as URN + orgVdcGroupId, err := getBareEntityUuid(vdcGroup.VdcGroup.Id) + if err != nil { + return nil, fmt.Errorf("could not get UUID from URN '%s': %s", vdcGroup.VdcGroup.Id, err) + } + filter := map[string]string{"vdcGroup": orgVdcGroupId} + + return getFilteredNsxtImportableSwitches(filter, vdcGroup.client) +} + // GetAllNsxtImportableSwitches retrieves all available importable switches which can be consumed for creating NSX-T // "Imported" Org VDC network // @@ -65,30 +123,84 @@ func (vdc *Vdc) GetAllNsxtImportableSwitches() ([]*NsxtImportableSwitch, error) if vdc.Vdc.ID == "" { return nil, fmt.Errorf("VDC must have ID populated to retrieve NSX-T importable switches") } - - apiEndpoint := vdc.client.VCDHREF - endpoint := apiEndpoint.Scheme + "://" + apiEndpoint.Host + "/network/orgvdcnetworks/importableswitches" - // error below is ignored because it is a static endpoint - urlRef, _ := url.Parse(endpoint) - // request requires Org VDC ID to be specified as UUID, not as URN orgVdcId, err := getBareEntityUuid(vdc.Vdc.ID) if err != nil { return nil, fmt.Errorf("could not get UUID from URN '%s': %s", vdc.Vdc.ID, err) } + filter := map[string]string{"orgVdc": orgVdcId} + + return getFilteredNsxtImportableSwitches(filter, vdc.client) +} + +// GetFilteredNsxtImportableSwitches returns all available importable switches. +// One of the filters below is required (using plain UUID - not URN): +// * orgVdc +// * nsxTManager (only in VCD 10.3.0+) +// +// Note. OpenAPI endpoint does not exist for this resource and by default endpoint +// "/network/orgvdcnetworks/importableswitches" returns only unused NSX-T importable switches (the ones that are not +// already consumed in Org VDC networks) and there is no way to get them all. +func (vcdClient *VCDClient) GetFilteredNsxtImportableSwitches(filter map[string]string) ([]*NsxtImportableSwitch, error) { + return getFilteredNsxtImportableSwitches(filter, &vcdClient.Client) +} + +// GetFilteredNsxtImportableSwitchesByName builds on top of GetFilteredNsxtImportableSwitches and additionally performs +// client side filtering by Name +func (vcdClient *VCDClient) GetFilteredNsxtImportableSwitchesByName(filter map[string]string, name string) (*NsxtImportableSwitch, error) { + importableSwitches, err := getFilteredNsxtImportableSwitches(filter, &vcdClient.Client) + if err != nil { + return nil, fmt.Errorf("error getting list of filtered Importable Switches: %s", err) + } + + var foundImportableSwitch bool + var foundSwitches []*NsxtImportableSwitch + + for index, impSwitch := range importableSwitches { + if importableSwitches[index].NsxtImportableSwitch.Name == name { + foundImportableSwitch = true + foundSwitches = append(foundSwitches, impSwitch) + } + } + + if !foundImportableSwitch { + return nil, fmt.Errorf("%s: Importable Switch with name '%s' not found", ErrorEntityNotFound, name) + } + + if len(foundSwitches) > 1 { + return nil, fmt.Errorf("found multiple Importable Switches with name '%s'", name) + } + + return foundSwitches[0], nil +} + +// getFilteredNsxtImportableSwitches is extracted so that it can be reused across multiple functions +func getFilteredNsxtImportableSwitches(filter map[string]string, client *Client) ([]*NsxtImportableSwitch, error) { + apiEndpoint := client.VCDHREF + endpoint := apiEndpoint.Scheme + "://" + apiEndpoint.Host + "/network/orgvdcnetworks/importableswitches/" + // error below is ignored because it is a static endpoint + urlRef, err := url.Parse(endpoint) + if err != nil { + util.Logger.Printf("[DEBUG - getFilteredNsxtImportableSwitches] error parsing URL: %s", err) + } headAccept := http.Header{} headAccept.Set("Accept", types.JSONMime) - request := vdc.client.newRequest(map[string]string{"orgVdc": orgVdcId}, nil, http.MethodGet, *urlRef, nil, vdc.client.APIVersion, headAccept) + request := client.newRequest(filter, nil, http.MethodGet, *urlRef, nil, client.APIVersion, headAccept) request.Header.Set("Accept", types.JSONMime) - response, err := checkResp(vdc.client.Http.Do(request)) + response, err := checkResp(client.Http.Do(request)) if err != nil { return nil, err } - defer response.Body.Close() + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + util.Logger.Printf("error closing response Body [getFilteredNsxtImportableSwitches]: %s", err) + } + }(response.Body) - nsxtImportableSwitches := []*types.NsxtImportableSwitch{} + var nsxtImportableSwitches []*types.NsxtImportableSwitch if err = decodeBody(types.BodyTypeJSON, response, &nsxtImportableSwitches); err != nil { return nil, err } @@ -97,7 +209,7 @@ func (vdc *Vdc) GetAllNsxtImportableSwitches() ([]*NsxtImportableSwitch, error) for sliceIndex := range nsxtImportableSwitches { wrappedNsxtImportableSwitches[sliceIndex] = &NsxtImportableSwitch{ NsxtImportableSwitch: nsxtImportableSwitches[sliceIndex], - client: vdc.client, + client: client, } } diff --git a/govcd/nsxt_importable_switch_test.go b/govcd/nsxt_importable_switch_test.go index e0b4d2dbb..cac77f64f 100644 --- a/govcd/nsxt_importable_switch_test.go +++ b/govcd/nsxt_importable_switch_test.go @@ -1,4 +1,4 @@ -// +build network nsxt functional ALL +//go:build network || nsxt || functional || ALL /* * Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -17,9 +17,6 @@ func (vcd *TestVCD) Test_GetAllNsxtImportableSwitches(check *C) { check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) } - if vcd.client.Client.APIVCDMaxVersionIs("< 34") { - check.Skip("At least VCD 10.1 is required") - } skipNoNsxtConfiguration(vcd, check) nsxtVdc, err := vcd.org.GetVDCByNameOrId(vcd.config.VCD.Nsxt.Vdc, true) @@ -35,9 +32,6 @@ func (vcd *TestVCD) Test_GetNsxtImportableSwitchByName(check *C) { check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) } - if vcd.client.Client.APIVCDMaxVersionIs("< 34") { - check.Skip("At least VCD 10.1 is required") - } skipNoNsxtConfiguration(vcd, check) nsxtVdc, err := vcd.org.GetVDCByNameOrId(vcd.config.VCD.Nsxt.Vdc, true) @@ -47,3 +41,40 @@ func (vcd *TestVCD) Test_GetNsxtImportableSwitchByName(check *C) { check.Assert(err, IsNil) check.Assert(logicalSwitch.NsxtImportableSwitch.Name, Equals, vcd.config.VCD.Nsxt.NsxtImportSegment) } + +func (vcd *TestVCD) Test_GetFilteredNsxtImportableSwitches(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + + skipNoNsxtConfiguration(vcd, check) + + // Check that nil filter returns error. This will work as a safeguard to also detect if future versions start accepting + // empty filter value + results, err := vcd.client.GetFilteredNsxtImportableSwitches(nil) + check.Assert(err, Not(IsNil)) + check.Assert(results, IsNil) + + // Filter by VDC ID + bareVdcId, err := getBareEntityUuid(vcd.nsxtVdc.Vdc.ID) + check.Assert(err, IsNil) + filter := map[string]string{"orgVdc": bareVdcId} + results, err = vcd.client.GetFilteredNsxtImportableSwitches(filter) + check.Assert(err, IsNil) + check.Assert(len(results) > 0, Equals, true) + + nsxtManagers, err := vcd.client.QueryNsxtManagerByName(vcd.config.VCD.Nsxt.Manager) + check.Assert(err, IsNil) + check.Assert(len(nsxtManagers) > 0, Equals, true) + + uuid := extractUuid(nsxtManagers[0].HREF) + filter = map[string]string{"nsxTManager": uuid} + results, err = vcd.client.GetFilteredNsxtImportableSwitches(filter) + check.Assert(err, IsNil) + check.Assert(len(results) > 0, Equals, true) + + switchByName, err := vcd.client.GetFilteredNsxtImportableSwitchesByName(filter, vcd.config.VCD.Nsxt.NsxtImportSegment) + check.Assert(err, IsNil) + check.Assert(switchByName.NsxtImportableSwitch.Name, Equals, vcd.config.VCD.Nsxt.NsxtImportSegment) + +} diff --git a/govcd/nsxt_ipsec_vpn_tunnel.go b/govcd/nsxt_ipsec_vpn_tunnel.go new file mode 100644 index 000000000..abea6540e --- /dev/null +++ b/govcd/nsxt_ipsec_vpn_tunnel.go @@ -0,0 +1,342 @@ +/* + * Copyright 2021 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" + "github.com/vmware/go-vcloud-director/v2/util" +) + +// NsxtIpSecVpnTunnel offers site-to-site connectivity between an Edge Gateway and remote sites which also use NSX-T +// Data Center or which have either third-party hardware routers or VPN gateways that support IPsec. Policy-based IPsec +// VPN requires a VPN policy to be applied to packets to determine which traffic is to be protected by IPsec before +// being passed through a VPN tunnel. This type of VPN is considered static because when a local network topology and +// configuration change, the VPN policy settings must also be updated to accommodate the changes. NSX-T Data Center Edge +// Gateways support split tunnel configuration, with IPsec traffic taking routing precedence. VMware Cloud Director +// supports automatic route redistribution when you use IPsec VPN on an NSX-T edge gateway. +type NsxtIpSecVpnTunnel struct { + NsxtIpSecVpn *types.NsxtIpSecVpnTunnel + client *Client + // edgeGatewayId is stored here so that pointer receiver functions can embed edge gateway ID into path + edgeGatewayId string +} + +// GetAllIpSecVpnTunnels returns all IPsec VPN Tunnel configurations +func (egw *NsxtEdgeGateway) GetAllIpSecVpnTunnels(queryParameters url.Values) ([]*NsxtIpSecVpnTunnel, error) { + client := egw.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSecVpnTunnel + apiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, egw.EdgeGateway.ID)) + if err != nil { + return nil, err + } + + typeResponses := []*types.NsxtIpSecVpnTunnel{{}} + err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParameters, &typeResponses, nil) + if err != nil { + return nil, err + } + + // Wrap all typeResponses into NsxtIpSecVpnTunnel types with client + wrappedResponses := make([]*NsxtIpSecVpnTunnel, len(typeResponses)) + for sliceIndex := range typeResponses { + wrappedResponses[sliceIndex] = &NsxtIpSecVpnTunnel{ + NsxtIpSecVpn: typeResponses[sliceIndex], + client: client, + edgeGatewayId: egw.EdgeGateway.ID, + } + } + + return wrappedResponses, nil +} + +// GetIpSecVpnTunnelById retrieves single IPsec VPN Tunnel by ID +func (egw *NsxtEdgeGateway) GetIpSecVpnTunnelById(id string) (*NsxtIpSecVpnTunnel, error) { + if id == "" { + return nil, fmt.Errorf("canot find NSX-T IPsec VPN Tunnel configuration without ID") + } + + client := egw.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSecVpnTunnel + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, egw.EdgeGateway.ID), id) + if err != nil { + return nil, err + } + + returnObject := &NsxtIpSecVpnTunnel{ + NsxtIpSecVpn: &types.NsxtIpSecVpnTunnel{}, + client: client, + edgeGatewayId: egw.EdgeGateway.ID, + } + + err = client.OpenApiGetItem(minimumApiVersion, urlRef, nil, returnObject.NsxtIpSecVpn, nil) + if err != nil { + return nil, err + } + + return returnObject, nil +} + +// GetIpSecVpnTunnelByName retrieves single IPsec VPN Tunnel by Name. +// +// Note. Name uniqueness is not enforced therefore it might exist a few IPsec VPN Tunnels with the same name. +// An error will be returned in that case. +func (egw *NsxtEdgeGateway) GetIpSecVpnTunnelByName(name string) (*NsxtIpSecVpnTunnel, error) { + if name == "" { + return nil, fmt.Errorf("canot find NSX-T IPsec VPN Tunnel configuration without Name") + } + + allVpns, err := egw.GetAllIpSecVpnTunnels(nil) + if err != nil { + return nil, fmt.Errorf("error retrieving all NSX-T IPsec VPN Tunnel configurations: %s", err) + } + + var allResults []*NsxtIpSecVpnTunnel + + for _, vpnConfig := range allVpns { + if vpnConfig.NsxtIpSecVpn.Name == name { + allResults = append(allResults, vpnConfig) + } + } + + if len(allResults) > 1 { + return nil, fmt.Errorf("error - found %d NSX-T IPsec VPN Tunnel configuratios with Name '%s'. Expected 1", + len(allResults), name) + } + + if len(allResults) == 0 { + return nil, ErrorEntityNotFound + } + + // Retrieving again the object by ID, because only it includes Pre-shared Key + return egw.GetIpSecVpnTunnelById(allResults[0].NsxtIpSecVpn.ID) +} + +// CreateIpSecVpnTunnel creates IPsec VPN Tunnel and returns it +func (egw *NsxtEdgeGateway) CreateIpSecVpnTunnel(ipSecVpnConfig *types.NsxtIpSecVpnTunnel) (*NsxtIpSecVpnTunnel, error) { + client := egw.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSecVpnTunnel + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, egw.EdgeGateway.ID)) + if err != nil { + return nil, err + } + + task, err := client.OpenApiPostItemAsync(minimumApiVersion, urlRef, nil, ipSecVpnConfig) + if err != nil { + return nil, fmt.Errorf("error creating NSX-T IPsec VPN Tunnel configuration: %s", err) + } + + err = task.WaitTaskCompletion() + if err != nil { + return nil, fmt.Errorf("task failed while creating NSX-T IPsec VPN Tunnel configuration: %s", err) + } + + // filtering even by Name is not supported on VCD side + allVpns, err := egw.GetAllIpSecVpnTunnels(nil) + if err != nil { + return nil, fmt.Errorf("error retrieving all NSX-T IPsec VPN Tunnel configuration after creation: %s", err) + } + + for index, singleConfig := range allVpns { + if singleConfig.IsEqualTo(ipSecVpnConfig) { + // retrieve exact value by ID, because only this endpoint includes private key + ipSecVpn, err := egw.GetIpSecVpnTunnelById(allVpns[index].NsxtIpSecVpn.ID) + if err != nil { + return nil, fmt.Errorf("error retrieving NSX-T IPsec VPN Tunnel configuration: %s", err) + } + + return ipSecVpn, nil + } + } + + return nil, fmt.Errorf("error finding NSX-T IPsec VPN Tunnel configuration after creation: %s", ErrorEntityNotFound) +} + +// Update updates NSX-T IPsec VPN Tunnel configuration with newly supplied data. +func (ipSecVpn *NsxtIpSecVpnTunnel) Update(ipSecVpnConfig *types.NsxtIpSecVpnTunnel) (*NsxtIpSecVpnTunnel, error) { + client := ipSecVpn.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSecVpnTunnel + apiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + if ipSecVpn.NsxtIpSecVpn.ID == "" { + return nil, fmt.Errorf("cannot update NSX-T IPsec VPN Tunnel configuration without ID") + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, ipSecVpn.edgeGatewayId), ipSecVpn.NsxtIpSecVpn.ID) + if err != nil { + return nil, err + } + + returnObject := &NsxtIpSecVpnTunnel{ + NsxtIpSecVpn: &types.NsxtIpSecVpnTunnel{}, + client: client, + edgeGatewayId: ipSecVpn.edgeGatewayId, + } + + err = client.OpenApiPutItem(apiVersion, urlRef, nil, ipSecVpnConfig, returnObject.NsxtIpSecVpn, nil) + if err != nil { + return nil, fmt.Errorf("error updating NSX-T IPsec VPN Tunnel configuration: %s", err) + } + + return returnObject, nil +} + +// Delete allows users to delete NSX-T IPsec VPN Tunnel +func (ipSecVpn *NsxtIpSecVpnTunnel) Delete() error { + client := ipSecVpn.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSecVpnTunnel + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return err + } + + if ipSecVpn.NsxtIpSecVpn.ID == "" { + return fmt.Errorf("cannot delete NSX-T IPsec VPN Tunnel configuration without ID") + } + + urlRef, err := ipSecVpn.client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, ipSecVpn.edgeGatewayId), ipSecVpn.NsxtIpSecVpn.ID) + if err != nil { + return err + } + + err = ipSecVpn.client.OpenApiDeleteItem(minimumApiVersion, urlRef, nil, nil) + if err != nil { + return fmt.Errorf("error deleting NSX-T IPsec VPN Tunnel configuration: %s", err) + } + + return nil +} + +// GetStatus returns status of IPsec VPN Tunnel. +// +// Note. This is not being immediately populated and may appear after some time depending on +// NsxtIpSecVpnTunnelSecurityProfile.DpdConfiguration +func (ipSecVpn *NsxtIpSecVpnTunnel) GetStatus() (*types.NsxtIpSecVpnTunnelStatus, error) { + client := ipSecVpn.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSecVpnTunnelStatus + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + if ipSecVpn.NsxtIpSecVpn.ID == "" { + return nil, fmt.Errorf("cannot get NSX-T IPsec VPN Tunnel status without ID") + } + + urlRef, err := ipSecVpn.client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, ipSecVpn.edgeGatewayId, ipSecVpn.NsxtIpSecVpn.ID)) + if err != nil { + return nil, err + } + + ipSecVpnTunnelStatus := &types.NsxtIpSecVpnTunnelStatus{} + + err = ipSecVpn.client.OpenApiGetItem(minimumApiVersion, urlRef, nil, ipSecVpnTunnelStatus, nil) + if err != nil { + return nil, fmt.Errorf("error getting NSX-T IPsec VPN Tunnel status: %s", err) + } + + return ipSecVpnTunnelStatus, nil +} + +// UpdateTunnelConnectionProperties allows user to customize IPsec VPN Tunnel Security Profile when the default one +// does not fit requirements. +func (ipSecVpn *NsxtIpSecVpnTunnel) UpdateTunnelConnectionProperties(ipSecVpnTunnelConnectionProperties *types.NsxtIpSecVpnTunnelSecurityProfile) (*types.NsxtIpSecVpnTunnelSecurityProfile, error) { + client := ipSecVpn.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSecVpnTunnelConnectionProperties + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + if ipSecVpn.NsxtIpSecVpn.ID == "" { + return nil, fmt.Errorf("cannot update NSX-T IPsec VPN Connection Properties without ID") + } + + urlRef, err := ipSecVpn.client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, ipSecVpn.edgeGatewayId, ipSecVpn.NsxtIpSecVpn.ID)) + if err != nil { + return nil, err + } + + ipSecVpnTunnelProfile := &types.NsxtIpSecVpnTunnelSecurityProfile{} + err = ipSecVpn.client.OpenApiPutItem(minimumApiVersion, urlRef, nil, ipSecVpnTunnelConnectionProperties, ipSecVpnTunnelProfile, nil) + if err != nil { + return nil, fmt.Errorf("error updating NSX-T IPsec VPN Connection Properties: %s", err) + } + + return ipSecVpnTunnelProfile, nil +} + +// GetTunnelConnectionProperties retrieves IPsec VPN Tunnel Security Profile +func (ipSecVpn *NsxtIpSecVpnTunnel) GetTunnelConnectionProperties() (*types.NsxtIpSecVpnTunnelSecurityProfile, error) { + client := ipSecVpn.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSecVpnTunnelConnectionProperties + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + if ipSecVpn.NsxtIpSecVpn.ID == "" { + return nil, fmt.Errorf("cannot get NSX-T IPsec VPN Connection Properties without ID") + } + + urlRef, err := ipSecVpn.client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, ipSecVpn.edgeGatewayId, ipSecVpn.NsxtIpSecVpn.ID)) + if err != nil { + return nil, err + } + + ipSecVpnTunnelProfile := &types.NsxtIpSecVpnTunnelSecurityProfile{} + err = ipSecVpn.client.OpenApiGetItem(minimumApiVersion, urlRef, nil, ipSecVpnTunnelProfile, nil) + if err != nil { + return nil, fmt.Errorf("error retrieving NSX-T IPsec VPN Connection Properties: %s", err) + } + + return ipSecVpnTunnelProfile, nil +} + +// IsEqualTo helps to find NSX-T IPsec VPN Tunnel Configuration +// Combination of LocalAddress and RemoteAddress has to be unique (enforced by API). This is a list of fields compared: +// * LocalEndpoint.LocalAddress +// * RemoteEndpoint.RemoteAddress +func (ipSecVpn *NsxtIpSecVpnTunnel) IsEqualTo(vpnConfig *types.NsxtIpSecVpnTunnel) bool { + return ipSetVpnRulesEqual(ipSecVpn.NsxtIpSecVpn, vpnConfig) +} + +// ipSetVpnRulesEqual performs comparison of two NSX-T IPsec VPN Tunnels to ease lookup. This is a list of fields compared: +// * LocalEndpoint.LocalAddress +// * RemoteEndpoint.RemoteAddress +func ipSetVpnRulesEqual(first, second *types.NsxtIpSecVpnTunnel) bool { + util.Logger.Println("comparing NSX-T IP Sev VPN configuration:") + util.Logger.Printf("%+v\n", first) + util.Logger.Println("against:") + util.Logger.Printf("%+v\n", second) + + // These fields should be enough to cover uniqueness + if first.LocalEndpoint.LocalAddress == second.LocalEndpoint.LocalAddress && + first.RemoteEndpoint.RemoteAddress == second.RemoteEndpoint.RemoteAddress { + return true + } + + return false +} diff --git a/govcd/nsxt_ipsec_vpn_tunnel_test.go b/govcd/nsxt_ipsec_vpn_tunnel_test.go new file mode 100644 index 000000000..bcd77f9d9 --- /dev/null +++ b/govcd/nsxt_ipsec_vpn_tunnel_test.go @@ -0,0 +1,319 @@ +//go:build network || nsxt || functional || openapi || ALL + +package govcd + +import ( + "fmt" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_NsxtIpSecVpn(check *C) { + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointFirewallGroups) + + org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + + nsxtVdc, err := org.GetVDCByName(vcd.config.VCD.Nsxt.Vdc, false) + check.Assert(err, IsNil) + + edge, err := nsxtVdc.GetNsxtEdgeGatewayByName(vcd.config.VCD.Nsxt.EdgeGateway) + check.Assert(err, IsNil) + + ipSecDef := &types.NsxtIpSecVpnTunnel{ + Name: check.TestName(), + Description: check.TestName() + "-description", + Enabled: true, + LocalEndpoint: types.NsxtIpSecVpnTunnelLocalEndpoint{ + LocalAddress: edge.EdgeGateway.EdgeGatewayUplinks[0].Subnets.Values[0].PrimaryIP, + LocalNetworks: []string{"10.10.10.0/24"}, + }, + RemoteEndpoint: types.NsxtIpSecVpnTunnelRemoteEndpoint{ + RemoteId: "192.168.140.1", + RemoteAddress: "192.168.140.1", + RemoteNetworks: []string{"20.20.20.0/24"}, + }, + PreSharedKey: "PSK-Sec", + SecurityType: "DEFAULT", + Logging: true, + } + + runIpSecVpnTests(check, edge, ipSecDef) + +} + +func (vcd *TestVCD) Test_NsxtIpSecVpnCustomSecurityProfile(check *C) { + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointFirewallGroups) + + org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + + nsxtVdc, err := org.GetVDCByName(vcd.config.VCD.Nsxt.Vdc, false) + check.Assert(err, IsNil) + + edge, err := nsxtVdc.GetNsxtEdgeGatewayByName(vcd.config.VCD.Nsxt.EdgeGateway) + check.Assert(err, IsNil) + + ipSecDef := &types.NsxtIpSecVpnTunnel{ + Name: check.TestName(), + Description: check.TestName() + "-description", + Enabled: true, + AuthenticationMode: types.NsxtIpSecVpnAuthenticationModePSK, // Default value even when it is unset + LocalEndpoint: types.NsxtIpSecVpnTunnelLocalEndpoint{ + LocalAddress: edge.EdgeGateway.EdgeGatewayUplinks[0].Subnets.Values[0].PrimaryIP, + LocalNetworks: []string{"10.10.10.0/24"}, + }, + RemoteEndpoint: types.NsxtIpSecVpnTunnelRemoteEndpoint{ + RemoteId: "192.168.140.1", + RemoteAddress: "192.168.140.1", + RemoteNetworks: []string{"20.20.20.0/24"}, + }, + PreSharedKey: "PSK-Sec", + SecurityType: "DEFAULT", + Logging: false, + } + + createdIpSecVpn, err := edge.CreateIpSecVpnTunnel(ipSecDef) + check.Assert(err, IsNil) + openApiEndpoint := types.OpenApiPathVersion1_0_0 + fmt.Sprintf(types.OpenApiEndpointIpSecVpnTunnel, createdIpSecVpn.edgeGatewayId) + createdIpSecVpn.NsxtIpSecVpn.ID + AddToCleanupListOpenApi(createdIpSecVpn.NsxtIpSecVpn.Name, check.TestName(), openApiEndpoint) + + // Customize Security Profile + secProfile := &types.NsxtIpSecVpnTunnelSecurityProfile{ + SecurityType: "CUSTOM", + IkeConfiguration: types.NsxtIpSecVpnTunnelProfileIkeConfiguration{ + IkeVersion: "IKE_V2", + EncryptionAlgorithms: []string{"AES_128"}, + DigestAlgorithms: []string{"SHA2_256"}, + DhGroups: []string{"GROUP14"}, + SaLifeTime: addrOf(86400), + }, + TunnelConfiguration: types.NsxtIpSecVpnTunnelProfileTunnelConfiguration{ + PerfectForwardSecrecyEnabled: true, + DfPolicy: "CLEAR", + EncryptionAlgorithms: []string{"AES_256"}, + DigestAlgorithms: []string{"SHA2_256"}, + DhGroups: []string{"GROUP14"}, + SaLifeTime: addrOf(3600), + }, + DpdConfiguration: types.NsxtIpSecVpnTunnelProfileDpdConfiguration{ProbeInterval: 3}, + } + setSecProfile, err := createdIpSecVpn.UpdateTunnelConnectionProperties(secProfile) + check.Assert(err, IsNil) + check.Assert(setSecProfile, DeepEquals, secProfile) + + // Check if status endpoint works properly, but cannot rely on returned status as it is not immediately returned and + // it can hold on for a long time before available. At least validate that this function does not return error. + _, err = createdIpSecVpn.GetStatus() + check.Assert(err, IsNil) + + //Latest Version + latestSecProfile, err := edge.GetIpSecVpnTunnelById(createdIpSecVpn.NsxtIpSecVpn.ID) + check.Assert(err, IsNil) + + // Reset security profile to default + latestSecProfile.NsxtIpSecVpn.SecurityType = "DEFAULT" + updatedIpSecVpn, err := createdIpSecVpn.Update(latestSecProfile.NsxtIpSecVpn) + check.Assert(err, IsNil) + // All fields should be the same, except version + latestSecProfile.NsxtIpSecVpn.Version = updatedIpSecVpn.NsxtIpSecVpn.Version + check.Assert(updatedIpSecVpn.NsxtIpSecVpn, DeepEquals, latestSecProfile.NsxtIpSecVpn) + + // Remove object + err = createdIpSecVpn.Delete() + check.Assert(err, IsNil) +} + +// Test_NsxtIpSecVpnUniqueness checks that uniqueness is enforced at API level on LocalAddress+RemoteAddress by creating +// two IPsec VPN tunnels with different field values but the same LocalAddress and RemoteAddress without any other +// fields clashing. +func (vcd *TestVCD) Test_NsxtIpSecVpnUniqueness(check *C) { + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointFirewallGroups) + + org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + + nsxtVdc, err := org.GetVDCByName(vcd.config.VCD.Nsxt.Vdc, false) + check.Assert(err, IsNil) + + edge, err := nsxtVdc.GetNsxtEdgeGatewayByName(vcd.config.VCD.Nsxt.EdgeGateway) + check.Assert(err, IsNil) + + ipSecDef := &types.NsxtIpSecVpnTunnel{ + Name: check.TestName(), + Description: check.TestName() + "-description", + Enabled: true, + LocalEndpoint: types.NsxtIpSecVpnTunnelLocalEndpoint{ + LocalAddress: edge.EdgeGateway.EdgeGatewayUplinks[0].Subnets.Values[0].PrimaryIP, + LocalNetworks: []string{"10.10.10.0/24"}, + }, + RemoteEndpoint: types.NsxtIpSecVpnTunnelRemoteEndpoint{ + RemoteId: "192.168.170.1", + RemoteAddress: "192.168.170.1", + RemoteNetworks: []string{"20.20.20.0/24"}, + }, + PreSharedKey: "PSK-Sec", + SecurityType: "DEFAULT", + Logging: true, + } + + // Create first IPsec VPN Tunnel + createdIpSecVpn, err := edge.CreateIpSecVpnTunnel(ipSecDef) + check.Assert(err, IsNil) + openApiEndpoint := types.OpenApiPathVersion1_0_0 + fmt.Sprintf(types.OpenApiEndpointIpSecVpnTunnel, createdIpSecVpn.edgeGatewayId) + createdIpSecVpn.NsxtIpSecVpn.ID + AddToCleanupListOpenApi(createdIpSecVpn.NsxtIpSecVpn.Name, check.TestName(), openApiEndpoint) + + // Try to create second IPsec VPN Tunnel with the same localAddress and RemoteAddress and expect an error + ipSecDef2 := &types.NsxtIpSecVpnTunnel{ + Name: check.TestName() + "2", + Description: check.TestName() + "-description2", + Enabled: true, + LocalEndpoint: types.NsxtIpSecVpnTunnelLocalEndpoint{ + LocalAddress: edge.EdgeGateway.EdgeGatewayUplinks[0].Subnets.Values[0].PrimaryIP, + LocalNetworks: []string{"40.10.10.0/24"}, + }, + RemoteEndpoint: types.NsxtIpSecVpnTunnelRemoteEndpoint{ + RemoteId: "192.168.170.1", + RemoteAddress: "192.168.170.1", + RemoteNetworks: []string{"50.20.20.0/24"}, + }, + PreSharedKey: "PSK-Sec", + SecurityType: "DEFAULT", + Logging: true, + } + + // Ensure that the IsEqual matches those definitions as equal ones + check.Assert(createdIpSecVpn.IsEqualTo(ipSecDef2), Equals, true) + + createdIpSecVpn2, err := edge.CreateIpSecVpnTunnel(ipSecDef2) + check.Assert(err.Error(), Matches, ".*IPSec VPN Tunnel with local address .* and remote address .* is already in use.*") + check.Assert(createdIpSecVpn2, IsNil) + + // Removing the first IPsec VPN tunnel + err = createdIpSecVpn.Delete() + check.Assert(err, IsNil) +} + +func runIpSecVpnTests(check *C, edge *NsxtEdgeGateway, ipSecDef *types.NsxtIpSecVpnTunnel) { + createdIpSecVpn, err := edge.CreateIpSecVpnTunnel(ipSecDef) + check.Assert(err, IsNil) + openApiEndpoint := types.OpenApiPathVersion1_0_0 + fmt.Sprintf(types.OpenApiEndpointIpSecVpnTunnel, createdIpSecVpn.edgeGatewayId) + createdIpSecVpn.NsxtIpSecVpn.ID + AddToCleanupListOpenApi(createdIpSecVpn.NsxtIpSecVpn.Name, check.TestName(), openApiEndpoint) + + foundIpSecVpnById, err := edge.GetIpSecVpnTunnelById(createdIpSecVpn.NsxtIpSecVpn.ID) + check.Assert(err, IsNil) + check.Assert(foundIpSecVpnById.NsxtIpSecVpn, DeepEquals, createdIpSecVpn.NsxtIpSecVpn) + + foundIpSecVpnByName, err := edge.GetIpSecVpnTunnelByName(createdIpSecVpn.NsxtIpSecVpn.Name) + check.Assert(err, IsNil) + check.Assert(foundIpSecVpnByName.NsxtIpSecVpn, DeepEquals, createdIpSecVpn.NsxtIpSecVpn) + check.Assert(foundIpSecVpnByName.NsxtIpSecVpn, DeepEquals, foundIpSecVpnById.NsxtIpSecVpn) + + check.Assert(createdIpSecVpn.NsxtIpSecVpn.ID, Not(Equals), "") + + ipSecDef.Name = check.TestName() + "-updated" + ipSecDef.RemoteEndpoint.RemoteAddress = "192.168.40.1" + ipSecDef.ID = createdIpSecVpn.NsxtIpSecVpn.ID + + updatedIpSecVpn, err := createdIpSecVpn.Update(ipSecDef) + check.Assert(err, IsNil) + check.Assert(updatedIpSecVpn.NsxtIpSecVpn.Name, Equals, ipSecDef.Name) + check.Assert(updatedIpSecVpn.NsxtIpSecVpn.ID, Equals, ipSecDef.ID) + check.Assert(updatedIpSecVpn.NsxtIpSecVpn.RemoteEndpoint.RemoteAddress, Equals, ipSecDef.RemoteEndpoint.RemoteAddress) + + err = createdIpSecVpn.Delete() + check.Assert(err, IsNil) + + // Ensure rule does not exist in the list + allVpnConfigs, err := edge.GetAllIpSecVpnTunnels(nil) + check.Assert(err, IsNil) + for _, vpnConfig := range allVpnConfigs { + check.Assert(vpnConfig.IsEqualTo(updatedIpSecVpn.NsxtIpSecVpn), Equals, false) + } +} + +func (vcd *TestVCD) Test_NsxtIpSecVpnCertificateAuth(check *C) { + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointFirewallGroups) + vcd.skipIfNotSysAdmin(check) + + org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + + nsxtVdc, err := org.GetVDCByName(vcd.config.VCD.Nsxt.Vdc, false) + check.Assert(err, IsNil) + + edge, err := nsxtVdc.GetNsxtEdgeGatewayByName(vcd.config.VCD.Nsxt.EdgeGateway) + check.Assert(err, IsNil) + + // Upload Certificates to use in the test + aliasForPrivateKey := check.TestName() + "cert-with-private-key" + privateKeyPassphrase := "test" + certificateWithPrivateKeyConfig := &types.CertificateLibraryItem{ + Alias: aliasForPrivateKey, + Certificate: certificate, + PrivateKey: privateKey, + PrivateKeyPassphrase: privateKeyPassphrase, + } + + certWithKey, err := adminOrg.AddCertificateToLibrary(certificateWithPrivateKeyConfig) + check.Assert(err, IsNil) + openApiEndpoint, err := getEndpointByVersion(&vcd.client.Client) + check.Assert(err, IsNil) + check.Assert(openApiEndpoint, NotNil) + PrependToCleanupListOpenApi(certWithKey.CertificateLibrary.Alias, check.TestName(), + openApiEndpoint+certWithKey.CertificateLibrary.Id) + + // Upload CA Certificate to use in the test + aliasForCaCertificate := check.TestName() + "ca-certificate" + caCertificateConfig := &types.CertificateLibraryItem{ + Alias: aliasForCaCertificate, + Certificate: rootCaCertificate, + } + + caCert, err := adminOrg.AddCertificateToLibrary(caCertificateConfig) + check.Assert(err, IsNil) + PrependToCleanupListOpenApi(caCert.CertificateLibrary.Alias, check.TestName(), + openApiEndpoint+caCert.CertificateLibrary.Id) + + // Create IPSec VPN configuration with certificate authentication mode + ipSecDef := &types.NsxtIpSecVpnTunnel{ + Name: check.TestName(), + Description: check.TestName() + "-description", + Enabled: true, + AuthenticationMode: types.NsxtIpSecVpnAuthenticationModeCertificate, + CertificateRef: &types.OpenApiReference{ + ID: certWithKey.CertificateLibrary.Id, + }, + CaCertificateRef: &types.OpenApiReference{ + ID: caCert.CertificateLibrary.Id, + }, + + LocalEndpoint: types.NsxtIpSecVpnTunnelLocalEndpoint{ + LocalAddress: edge.EdgeGateway.EdgeGatewayUplinks[0].Subnets.Values[0].PrimaryIP, + LocalNetworks: []string{"10.10.10.0/24"}, + }, + RemoteEndpoint: types.NsxtIpSecVpnTunnelRemoteEndpoint{ + RemoteId: "custom-remote-id", + RemoteAddress: "192.168.140.1", + RemoteNetworks: []string{"20.20.20.0/24"}, + }, + SecurityType: "DEFAULT", + Logging: true, + } + + runIpSecVpnTests(check, edge, ipSecDef) + + // cleanup uploaded certificates + err = certWithKey.Delete() + check.Assert(err, IsNil) + err = caCert.Delete() + check.Assert(err, IsNil) +} diff --git a/govcd/nsxt_l2_vpn_tunnel.go b/govcd/nsxt_l2_vpn_tunnel.go new file mode 100644 index 000000000..3e3591420 --- /dev/null +++ b/govcd/nsxt_l2_vpn_tunnel.go @@ -0,0 +1,283 @@ +package govcd + +import ( + "fmt" + "net/url" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +// NsxtL2VpnTunnel extends an organization VDC by enabling virtual machines to +// maintain their network connectivity across geographical boundaries while keeping +// the same IP address. The connection is secured with a route-based IPSec tunnel between the two sides of the tunnel. +// The L2 VPN service can be configured on an NSX-T edge gateway in a VMware Cloud Director environment +// to create a L2 VPN tunnel. Virtual machines remain on the same subnet, which extends +// the organization VDC by stretching its network. This way, an edge gateway at one site can provide +// all services to virtual machines on the other site. +type NsxtL2VpnTunnel struct { + NsxtL2VpnTunnel *types.NsxtL2VpnTunnel + client *Client + // edgeGatewayId is stored for usage in NsxtFirewall receiver functions + edgeGatewayId string +} + +// CreateL2VpnTunnel creates a L2 VPN Tunnel on the provided NSX-T Edge Gateway and returns +// the tunnel +func (egw *NsxtEdgeGateway) CreateL2VpnTunnel(tunnel *types.NsxtL2VpnTunnel) (*NsxtL2VpnTunnel, error) { + if egw.EdgeGateway == nil || egw.client == nil || egw.EdgeGateway.ID == "" { + return nil, fmt.Errorf("cannot create L2 VPN tunnel for NSX-T Edge Gateway without ID") + } + + client := egw.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayL2VpnTunnel + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, egw.EdgeGateway.ID)) + if err != nil { + return nil, err + } + + // When creating a L2 VPN tunnel, its ID is stored in the creation task Details section, + // so we need to fetch the newly created tunnel manually + task, err := client.OpenApiPostItemAsync(apiVersion, urlRef, nil, tunnel) + if err != nil { + return nil, fmt.Errorf("error creating L2 VPN tunnel: %s", err) + } + + err = task.WaitTaskCompletion() + if err != nil { + return nil, fmt.Errorf("error waiting for L2 VPN tunnel to be created: %s", err) + } + + newTunnel, err := egw.GetL2VpnTunnelById(task.Task.Details) + if err != nil { + return nil, fmt.Errorf("error getting L2 VPN tunnel with id %s: %s", task.Task.Details, err) + } + + return newTunnel, nil +} + +// Refresh updates the provided NsxtL2VpnTunnel and returns an error if it failed +func (l2Vpn *NsxtL2VpnTunnel) Refresh() error { + client := l2Vpn.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayL2VpnTunnel + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, l2Vpn.edgeGatewayId), l2Vpn.NsxtL2VpnTunnel.ID) + if err != nil { + return err + } + + refreshedTunnel := &types.NsxtL2VpnTunnel{} + err = client.OpenApiGetItem(apiVersion, urlRef, nil, &refreshedTunnel, nil) + if err != nil { + return err + } + l2Vpn.NsxtL2VpnTunnel = refreshedTunnel + + return nil +} + +// GetAllL2VpnTunnels fetches all L2 VPN tunnels that are created on the Edge Gateway. +func (egw *NsxtEdgeGateway) GetAllL2VpnTunnels(queryParameters url.Values) ([]*NsxtL2VpnTunnel, error) { + if egw.EdgeGateway == nil || egw.client == nil || egw.EdgeGateway.ID == "" { + return nil, fmt.Errorf("cannot get L2 VPN tunnels for NSX-T Edge Gateway without ID") + } + + client := egw.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayL2VpnTunnel + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + typeResponses := []*types.NsxtL2VpnTunnel{{}} + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, egw.EdgeGateway.ID)) + if err != nil { + return nil, err + } + + err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParameters, &typeResponses, nil) + if err != nil { + return nil, err + } + + // Wrap all typeResponses into NsxtL2VpnTunnel types with client + results := make([]*NsxtL2VpnTunnel, len(typeResponses)) + for sliceIndex := range typeResponses { + results[sliceIndex] = &NsxtL2VpnTunnel{ + NsxtL2VpnTunnel: typeResponses[sliceIndex], + edgeGatewayId: egw.EdgeGateway.ID, + client: egw.client, + } + } + + return results, nil +} + +// GetL2VpnTunnelByName gets the L2 VPN Tunnel by name +func (egw *NsxtEdgeGateway) GetL2VpnTunnelByName(name string) (*NsxtL2VpnTunnel, error) { + results, err := egw.GetAllL2VpnTunnels(nil) + if err != nil { + return nil, err + } + + foundTunnels := make([]*NsxtL2VpnTunnel, 0) + for _, tunnel := range results { + if tunnel.NsxtL2VpnTunnel.Name == name { + foundTunnels = append(foundTunnels, tunnel) + } + } + + return oneOrError("name", name, foundTunnels) +} + +// GetL2VpnTunnelById gets the L2 VPN Tunnel by its ID +func (egw *NsxtEdgeGateway) GetL2VpnTunnelById(id string) (*NsxtL2VpnTunnel, error) { + if egw.EdgeGateway == nil || egw.client == nil || egw.EdgeGateway.ID == "" { + return nil, fmt.Errorf("cannot get L2 VPN tunnel for NSX-T Edge Gateway without ID") + } + + client := egw.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayL2VpnTunnel + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, egw.EdgeGateway.ID), id) + if err != nil { + return nil, err + } + + tunnel := &NsxtL2VpnTunnel{ + client: egw.client, + edgeGatewayId: egw.EdgeGateway.ID, + } + err = client.OpenApiGetItem(apiVersion, urlRef, nil, &tunnel.NsxtL2VpnTunnel, nil) + if err != nil { + return nil, err + } + + return tunnel, nil +} + +// Statistics retrieves connection statistics for a given L2 VPN Tunnel configured on an Edge Gateway. +func (l2Vpn *NsxtL2VpnTunnel) Statistics() (*types.EdgeL2VpnTunnelStatistics, error) { + client := l2Vpn.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayL2VpnTunnelStatistics + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, l2Vpn.edgeGatewayId, l2Vpn.NsxtL2VpnTunnel.ID)) + if err != nil { + return nil, err + } + + statistics := &types.EdgeL2VpnTunnelStatistics{} + err = client.OpenApiGetItem(apiVersion, urlRef, nil, &statistics, nil) + if err != nil { + return nil, err + } + + return statistics, nil +} + +// Status retrieves status of a given L2 VPN Tunnel. +func (l2Vpn *NsxtL2VpnTunnel) Status() (*types.EdgeL2VpnTunnelStatus, error) { + client := l2Vpn.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayL2VpnTunnelStatus + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, l2Vpn.edgeGatewayId, l2Vpn.NsxtL2VpnTunnel.ID)) + if err != nil { + return nil, err + } + + status := &types.EdgeL2VpnTunnelStatus{} + err = client.OpenApiGetItem(apiVersion, urlRef, nil, &status, nil) + if err != nil { + return nil, err + } + + return status, nil +} + +// Update updates the L2 VPN tunnel with the provided parameters as the argument +func (l2Vpn *NsxtL2VpnTunnel) Update(tunnelParams *types.NsxtL2VpnTunnel) (*NsxtL2VpnTunnel, error) { + if l2Vpn.NsxtL2VpnTunnel.SessionMode != tunnelParams.SessionMode { + return nil, fmt.Errorf("error updating the L2 VPN Tunnel: session mode can't be changed after creation") + } + + if tunnelParams.SessionMode == "CLIENT" && !tunnelParams.Enabled { + // There is a known bug up to 10.5.0, the CLIENT sessions can't be + // disabled and can result in unexpected behaviour for the following + // operations + if l2Vpn.client.APIVCDMaxVersionIs("<= 38.0") { + return nil, fmt.Errorf("client sessions can't be disabled on VCD versions up to 10.5.0") + } + } + + client := l2Vpn.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayL2VpnTunnel + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, l2Vpn.edgeGatewayId), l2Vpn.NsxtL2VpnTunnel.ID) + if err != nil { + return nil, err + } + + tunnelParams.Version.Version = l2Vpn.NsxtL2VpnTunnel.Version.Version + + newTunnel := &NsxtL2VpnTunnel{ + client: l2Vpn.client, + edgeGatewayId: l2Vpn.edgeGatewayId, + } + err = client.OpenApiPutItem(apiVersion, urlRef, nil, tunnelParams, &newTunnel.NsxtL2VpnTunnel, nil) + if err != nil { + return nil, err + } + + return newTunnel, nil +} + +// Delete deletes the L2 VPN Tunnel +// On versions up to 10.5.0 (as of writing) there is a bug with deleting +// CLIENT tunnels. If there are any networks attached to the tunnel, the +// DELETE call will fail the amount of times the resource was updated, +// so the best choice is to remove the networks and then call Delete(), or +// call Delete() in a loop until it's successful. +func (l2Vpn *NsxtL2VpnTunnel) Delete() error { + client := l2Vpn.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayL2VpnTunnel + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, l2Vpn.edgeGatewayId), l2Vpn.NsxtL2VpnTunnel.ID) + if err != nil { + return err + } + + err = client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) + if err != nil { + return err + } + + return nil +} diff --git a/govcd/nsxt_l2_vpn_tunnel_test.go b/govcd/nsxt_l2_vpn_tunnel_test.go new file mode 100644 index 000000000..efc6bfcbc --- /dev/null +++ b/govcd/nsxt_l2_vpn_tunnel_test.go @@ -0,0 +1,215 @@ +//go:build network || nsxt || functional || openapi || ALL + +package govcd + +import ( + "fmt" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +// Test_NsxtL2VpnTunnel tests NSX-T Edge Gateway L2 VPN Tunnels. +// 1. It creates/gets/updates/deletes a SERVER type Tunnel, also the peer code (encoded configuration of the tunnel) +// is saved for creation of CLIENT type tunnel. +// 2. Creates/gets/updates/deletes a CLIENT type Tunnel +func (vcd *TestVCD) Test_NsxtL2VpnTunnel(check *C) { + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointEdgeGatewayL2VpnTunnel) + vcd.skipIfNotSysAdmin(check) + + org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + + nsxtVdc, err := org.GetVDCByName(vcd.config.VCD.Nsxt.Vdc, false) + check.Assert(err, IsNil) + + edge, err := nsxtVdc.GetNsxtEdgeGatewayByName(vcd.config.VCD.Nsxt.EdgeGateway) + check.Assert(err, IsNil) + + network, err := nsxtVdc.GetOrgVdcNetworkByName(vcd.config.VCD.Nsxt.RoutedNetwork, false) + check.Assert(err, IsNil) + + // Get the auto-allocated IP of the Edge Gateway + localEndpointIp, err := edge.GetUsedIpAddresses(nil) + check.Assert(err, IsNil) + + // SERVER Tunnel part + serverTunnelParams := &types.NsxtL2VpnTunnel{ + Name: check.TestName(), + Description: check.TestName(), + SessionMode: "SERVER", + Enabled: true, + LocalEndpointIp: localEndpointIp[0].IPAddress, + RemoteEndpointIp: "1.1.1.1", + TunnelInterface: "", + ConnectorInitiationMode: "ON_DEMAND", + PreSharedKey: check.TestName(), + StretchedNetworks: []types.EdgeL2VpnStretchedNetwork{ + { + NetworkRef: types.OpenApiReference{ + Name: network.OrgVDCNetwork.Name, + ID: network.OrgVDCNetwork.ID, + }, + }, + }, + Logging: false, + } + + serverTunnel, err := edge.CreateL2VpnTunnel(serverTunnelParams) + check.Assert(err, IsNil) + check.Assert(serverTunnel, NotNil) + AddToCleanupListOpenApi(serverTunnel.NsxtL2VpnTunnel.ID, check.TestName(), + fmt.Sprintf(types.OpenApiPathVersion1_0_0+ + types.OpenApiEndpointEdgeGatewayL2VpnTunnel+ + serverTunnel.NsxtL2VpnTunnel.ID, edge.EdgeGateway.ID)) + + // Save the peer code to create a Client tunnel for testing + peerCode := serverTunnel.NsxtL2VpnTunnel.PeerCode + + check.Assert(serverTunnel.NsxtL2VpnTunnel.Name, Equals, check.TestName()) + check.Assert(serverTunnel.NsxtL2VpnTunnel.Description, Equals, check.TestName()) + check.Assert(serverTunnel.NsxtL2VpnTunnel.SessionMode, Equals, "SERVER") + check.Assert(serverTunnel.NsxtL2VpnTunnel.Enabled, Equals, true) + check.Assert(serverTunnel.NsxtL2VpnTunnel.LocalEndpointIp, Equals, localEndpointIp[0].IPAddress) + check.Assert(serverTunnel.NsxtL2VpnTunnel.RemoteEndpointIp, Equals, "1.1.1.1") + check.Assert(serverTunnel.NsxtL2VpnTunnel.ConnectorInitiationMode, Equals, "ON_DEMAND") + if is10511plus, err := vcd.client.Client.VersionEqualOrGreater("10.5.1.23400185", 4); err == nil && is10511plus { + // VCD 10.5.1.1+ return 6 asterisks instead of PreSharedKey + check.Assert(serverTunnel.NsxtL2VpnTunnel.PreSharedKey, Equals, "******") + } else { + check.Assert(serverTunnel.NsxtL2VpnTunnel.PreSharedKey, Equals, check.TestName()) + } + + fetchedServerTunnel, err := edge.GetL2VpnTunnelById(serverTunnel.NsxtL2VpnTunnel.ID) + check.Assert(err, IsNil) + check.Assert(fetchedServerTunnel, DeepEquals, serverTunnel) + + updatedServerTunnelParams := serverTunnelParams + updatedServerTunnelParams.ConnectorInitiationMode = "INITIATOR" + updatedServerTunnelParams.RemoteEndpointIp = "2.2.2.2" + updatedServerTunnelParams.TunnelInterface = "192.168.0.1/24" + + updatedServerTunnel, err := serverTunnel.Update(updatedServerTunnelParams) + check.Assert(err, IsNil) + check.Assert(updatedServerTunnel, NotNil) + + check.Assert(updatedServerTunnel.NsxtL2VpnTunnel.Name, Equals, check.TestName()) + check.Assert(updatedServerTunnel.NsxtL2VpnTunnel.Description, Equals, check.TestName()) + check.Assert(updatedServerTunnel.NsxtL2VpnTunnel.SessionMode, Equals, "SERVER") + check.Assert(updatedServerTunnel.NsxtL2VpnTunnel.Enabled, Equals, true) + check.Assert(updatedServerTunnel.NsxtL2VpnTunnel.LocalEndpointIp, Equals, localEndpointIp[0].IPAddress) + check.Assert(updatedServerTunnel.NsxtL2VpnTunnel.RemoteEndpointIp, Equals, "2.2.2.2") + check.Assert(updatedServerTunnel.NsxtL2VpnTunnel.TunnelInterface, Equals, "192.168.0.1/24") + check.Assert(updatedServerTunnel.NsxtL2VpnTunnel.ConnectorInitiationMode, Equals, "INITIATOR") + if is10511plus, err := vcd.client.Client.VersionEqualOrGreater("10.5.1.23400185", 4); err == nil && is10511plus { + // VCD 10.5.1.1+ return 6 asterisks instead of PreSharedKey + check.Assert(serverTunnel.NsxtL2VpnTunnel.PreSharedKey, Equals, "******") + } else { + check.Assert(serverTunnel.NsxtL2VpnTunnel.PreSharedKey, Equals, check.TestName()) + } + + tunnelByName, err := edge.GetL2VpnTunnelByName(serverTunnel.NsxtL2VpnTunnel.Name) + check.Assert(err, IsNil) + check.Assert(tunnelByName.NsxtL2VpnTunnel.ID, Equals, serverTunnel.NsxtL2VpnTunnel.ID) + + nonexistentTunnel, err := edge.GetL2VpnTunnelByName("nonexistent-tunnel") + check.Assert(err, NotNil) + check.Assert(nonexistentTunnel, IsNil) + + err = updatedServerTunnel.Delete() + check.Assert(err, IsNil) + + deletedServerTunnel, err := edge.GetL2VpnTunnelById(serverTunnel.NsxtL2VpnTunnel.ID) + check.Assert(err, NotNil) + check.Assert(deletedServerTunnel, IsNil) + + // CLIENT Tunnel part + clientTunnelParams := &types.NsxtL2VpnTunnel{ + Name: check.TestName(), + Description: check.TestName(), + SessionMode: "CLIENT", + Enabled: true, + LocalEndpointIp: localEndpointIp[0].IPAddress, + RemoteEndpointIp: "1.1.1.1", + PreSharedKey: check.TestName(), + PeerCode: peerCode, + StretchedNetworks: []types.EdgeL2VpnStretchedNetwork{ + { + NetworkRef: types.OpenApiReference{ + Name: network.OrgVDCNetwork.Name, + ID: network.OrgVDCNetwork.ID, + }, + TunnelID: 1, + }, + }, + Logging: false, + } + + clientTunnel, err := edge.CreateL2VpnTunnel(clientTunnelParams) + check.Assert(err, IsNil) + check.Assert(clientTunnel, NotNil) + AddToCleanupListOpenApi(clientTunnel.NsxtL2VpnTunnel.ID, check.TestName(), + fmt.Sprintf(types.OpenApiPathVersion1_0_0+ + types.OpenApiEndpointEdgeGatewayL2VpnTunnel+ + clientTunnel.NsxtL2VpnTunnel.ID, edge.EdgeGateway.ID)) + + check.Assert(clientTunnel.NsxtL2VpnTunnel.Name, Equals, check.TestName()) + check.Assert(clientTunnel.NsxtL2VpnTunnel.Description, Equals, check.TestName()) + check.Assert(clientTunnel.NsxtL2VpnTunnel.SessionMode, Equals, "CLIENT") + check.Assert(clientTunnel.NsxtL2VpnTunnel.Enabled, Equals, true) + check.Assert(clientTunnel.NsxtL2VpnTunnel.LocalEndpointIp, Equals, localEndpointIp[0].IPAddress) + check.Assert(clientTunnel.NsxtL2VpnTunnel.RemoteEndpointIp, Equals, "1.1.1.1") + if is10511plus, err := vcd.client.Client.VersionEqualOrGreater("10.5.1.23400185", 4); err == nil && is10511plus { + // VCD 10.5.1.1+ return 6 asterisks instead of PreSharedKey + check.Assert(serverTunnel.NsxtL2VpnTunnel.PreSharedKey, Equals, "******") + } else { + check.Assert(serverTunnel.NsxtL2VpnTunnel.PreSharedKey, Equals, check.TestName()) + } + + fetchedClientTunnel, err := edge.GetL2VpnTunnelById(clientTunnel.NsxtL2VpnTunnel.ID) + check.Assert(err, IsNil) + check.Assert(fetchedClientTunnel, DeepEquals, clientTunnel) + + updatedClientTunnelParams := clientTunnelParams + updatedClientTunnelParams.RemoteEndpointIp = "2.2.2.2" + + updatedClientTunnel, err := clientTunnel.Update(updatedClientTunnelParams) + check.Assert(err, IsNil) + check.Assert(updatedClientTunnel, NotNil) + + check.Assert(updatedClientTunnel.NsxtL2VpnTunnel.Name, Equals, check.TestName()) + check.Assert(updatedClientTunnel.NsxtL2VpnTunnel.Description, Equals, check.TestName()) + check.Assert(updatedClientTunnel.NsxtL2VpnTunnel.SessionMode, Equals, "CLIENT") + check.Assert(updatedClientTunnel.NsxtL2VpnTunnel.Enabled, Equals, true) + check.Assert(updatedClientTunnel.NsxtL2VpnTunnel.LocalEndpointIp, Equals, localEndpointIp[0].IPAddress) + check.Assert(updatedClientTunnel.NsxtL2VpnTunnel.RemoteEndpointIp, Equals, "2.2.2.2") + + // Check if the bug exists in versions above 38.0, so the testsuite would let us adjust the + // version constraint in Update() + if vcd.client.Client.APIVCDMaxVersionIs("> 38.0") { + disabledClientTunnelParams := updatedClientTunnelParams + disabledClientTunnelParams.Enabled = false + disabledClientTunnel, err := updatedClientTunnel.Update(disabledClientTunnelParams) + check.Assert(err, IsNil) + check.Assert(disabledClientTunnel.NsxtL2VpnTunnel.Enabled, Equals, false) + } + + // There is a bug in all versions up to 10.5.0, it happens + // when a L2 VPN Tunnel is created in CLIENT mode, has at least one Org VDC + // network attached, and is updated in any way. After that, to delete the tunnel + // one needs to de-attach all the networks + // or call Delete() the amount of times the object was updated + if vcd.client.Client.APIVCDMaxVersionIs("<= 38.0") { + updatedClientTunnelParams.StretchedNetworks = nil + updatedClientTunnel, err = updatedClientTunnel.Update(updatedClientTunnelParams) + check.Assert(err, IsNil) + } + + err = updatedClientTunnel.Delete() + check.Assert(err, IsNil) + + deletedClientTunnel, err := edge.GetL2VpnTunnelById(clientTunnel.NsxtL2VpnTunnel.ID) + check.Assert(err, NotNil) + check.Assert(deletedClientTunnel, IsNil) +} diff --git a/govcd/nsxt_manager.go b/govcd/nsxt_manager.go new file mode 100644 index 000000000..722bf31fc --- /dev/null +++ b/govcd/nsxt_manager.go @@ -0,0 +1,69 @@ +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "net/http" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +type NsxtManager struct { + NsxtManager *types.NsxtManager + VCDClient *VCDClient +} + +// GetNsxtManagerByName searches for NSX-T managers available in VCD and returns the one that +// matches name +func (vcdClient *VCDClient) GetNsxtManagerByName(name string) (*NsxtManager, error) { + nsxtManagers, err := vcdClient.QueryNsxtManagerByName(name) + if err != nil { + return nil, fmt.Errorf("error retrieving NSX-T Manager by name '%s': %s", name, err) + } + + // Double check that exactly one NSX-T Manager is found and throw error otherwise + singleNsxtManager, err := oneOrError("name", name, nsxtManagers) + if err != nil { + return nil, err + } + + resp, err := vcdClient.Client.executeJsonRequest(singleNsxtManager.HREF, http.MethodGet, nil, "error retrieving NSX-T Manager: %s") + if err != nil { + return nil, err + } + + defer closeBody(resp) + + nsxtManager := NsxtManager{ + NsxtManager: &types.NsxtManager{}, + VCDClient: vcdClient, + } + + err = decodeBody(types.BodyTypeJSON, resp, nsxtManager.NsxtManager) + if err != nil { + return nil, err + } + + return &nsxtManager, nil +} + +// Urn ensures that a URN is returned insted of plain UUID because VCD returns UUID, but requires +// URN in other APIs quite often. +func (nsxtManager *NsxtManager) Urn() (string, error) { + if nsxtManager == nil || nsxtManager.NsxtManager == nil || nsxtManager.NsxtManager.ID == "" { + return "", fmt.Errorf("NSX-T manager structure is incomplete - cannot build URN without ID") + } + + if isUrn(nsxtManager.NsxtManager.ID) { + return nsxtManager.NsxtManager.ID, nil + } + + nsxtManagerUrn, err := BuildUrnWithUuid("urn:vcloud:nsxtmanager:", nsxtManager.NsxtManager.ID) + if err != nil { + return "", fmt.Errorf("error building NSX-T Manager URN from ID '%s': %s", nsxtManager.NsxtManager.ID, err) + } + return nsxtManagerUrn, nil +} diff --git a/govcd/nsxt_manager_test.go b/govcd/nsxt_manager_test.go new file mode 100644 index 000000000..8db7ed6ba --- /dev/null +++ b/govcd/nsxt_manager_test.go @@ -0,0 +1,23 @@ +//go:build network || nsxt || functional || openapi || ALL + +package govcd + +import ( + "strings" + + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_NsxtManager(check *C) { + skipNoNsxtConfiguration(vcd, check) + vcd.skipIfNotSysAdmin(check) + + nsxtManager, err := vcd.client.GetNsxtManagerByName(vcd.config.VCD.Nsxt.Manager) + check.Assert(err, IsNil) + check.Assert(nsxtManager, NotNil) + + urn, err := nsxtManager.Urn() + check.Assert(err, IsNil) + check.Assert(strings.HasPrefix(urn, "urn:vcloud:nsxtmanager:"), Equals, true) + +} diff --git a/govcd/nsxt_nat_rule.go b/govcd/nsxt_nat_rule.go new file mode 100644 index 000000000..90697940e --- /dev/null +++ b/govcd/nsxt_nat_rule.go @@ -0,0 +1,276 @@ +/* + * Copyright 2021 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" + "github.com/vmware/go-vcloud-director/v2/util" +) + +// NsxtNatRule describes a single NAT rule of 5 different Rule Types - DNAT`, `NO_DNAT`, `SNAT`, `NO_SNAT`, 'REFLEXIVE' +// 'REFLEXIVE' is only supported in API 35.2 (VCD 10.2.2+) +// +// A SNAT or a DNAT rule on an Edge Gateway in the VMware Cloud Director environment is always configured from the +// perspective of your organization VDC. +// DNAT and NO_DNAT - outside traffic going inside +// SNAT and NO_SNAT - inside traffic going outside +// More docs in https://docs.vmware.com/en/VMware-Cloud-Director/10.2/VMware-Cloud-Director-Tenant-Portal-Guide/GUID-9E43E3DC-C028-47B3-B7CA-59F0ED40E0A6.html +// +// Note. This structure and all its API calls will require at least API version 34.0, but will elevate it to 35.2 if +// possible because API 35.2 introduces support for 2 new fields FirewallMatch and Priority. +type NsxtNatRule struct { + NsxtNatRule *types.NsxtNatRule + client *Client + // edgeGatewayId is stored here so that pointer receiver functions can embed edge gateway ID into path + edgeGatewayId string +} + +// GetAllNatRules retrieves all NAT rules with an optional queryParameters filter. +func (egw *NsxtEdgeGateway) GetAllNatRules(queryParameters url.Values) ([]*NsxtNatRule, error) { + client := egw.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtNatRules + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, egw.EdgeGateway.ID)) + if err != nil { + return nil, err + } + + typeResponses := []*types.NsxtNatRule{{}} + err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParameters, &typeResponses, nil) + if err != nil { + return nil, err + } + + // Wrap all typeResponses into NsxtNatRule types with client + wrappedResponses := make([]*NsxtNatRule, len(typeResponses)) + for sliceIndex := range typeResponses { + wrappedResponses[sliceIndex] = &NsxtNatRule{ + NsxtNatRule: typeResponses[sliceIndex], + client: client, + edgeGatewayId: egw.EdgeGateway.ID, + } + } + + return wrappedResponses, nil +} + +// GetNatRuleByName finds a NAT rule by Name and returns it +// +// Note. API does not enforce name uniqueness therefore an error will be thrown if two rules with the same name exist +func (egw *NsxtEdgeGateway) GetNatRuleByName(name string) (*NsxtNatRule, error) { + // Ideally this function would use OpenAPI filters to perform server side filtering, but this endpoint does not + // support any filters - even ID. Therefore one must retrieve all items and look if there is an item with the same ID + allNatRules, err := egw.GetAllNatRules(nil) + if err != nil { + return nil, fmt.Errorf("error retriving all NSX-T NAT rules: %s", err) + } + + var allResults []*NsxtNatRule + + for _, natRule := range allNatRules { + if natRule.NsxtNatRule.Name == name { + allResults = append(allResults, natRule) + } + } + + if len(allResults) > 1 { + return nil, fmt.Errorf("error - found %d NSX-T NAT rules with name '%s'. Expected 1", len(allResults), name) + } + + if len(allResults) == 0 { + return nil, ErrorEntityNotFound + } + + return allResults[0], nil +} + +// GetNatRuleById finds a NAT rule by ID and returns it +func (egw *NsxtEdgeGateway) GetNatRuleById(id string) (*NsxtNatRule, error) { + // Ideally this function would use OpenAPI filters to perform server side filtering, but this endpoint does not + // support any filters - even ID. Therefore one must retrieve all items and look if there is an item with the same ID + allNatRules, err := egw.GetAllNatRules(nil) + if err != nil { + return nil, fmt.Errorf("error retriving all NSX-T NAT rules: %s", err) + } + + for _, natRule := range allNatRules { + if natRule.NsxtNatRule.ID == id { + return natRule, nil + } + } + + return nil, ErrorEntityNotFound +} + +// CreateNatRule creates a NAT rule and returns it. +// +// Note. API has a limitation, that it does not return ID for created rule. To work around it this function creates +// a NAT rule, fetches all rules and finds a rule with exactly the same field values and returns it (including ID) +// There is still a slight risk to retrieve wrong ID if exactly the same rule already exists. +func (egw *NsxtEdgeGateway) CreateNatRule(natRuleConfig *types.NsxtNatRule) (*NsxtNatRule, error) { + client := egw.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtNatRules + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + // Insert Edge Gateway ID into endpoint path edgeGateways/%s/nat/rules + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, egw.EdgeGateway.ID)) + if err != nil { + return nil, err + } + + // Creating NAT rule must follow different way than usual OpenAPI one because this item has an API bug and + // NAT rule ID is not returned after this object is created. The only way to find its ID afterwards is to GET all + // items, and manually match it based on rule name, etc. + task, err := client.OpenApiPostItemAsync(apiVersion, urlRef, nil, natRuleConfig) + if err != nil { + return nil, fmt.Errorf("error creating NSX-T NAT rule: %s", err) + } + + err = task.WaitTaskCompletion() + if err != nil { + return nil, fmt.Errorf("task failed while creating NSX-T NAT rule: %s", err) + } + + // queryParameters (API side filtering) are not used because pretty much nothing is accepted as filter (such fields as + // name, description, ruleType and even ID are not allowed + allNatRules, err := egw.GetAllNatRules(nil) + if err != nil { + return nil, fmt.Errorf("error fetching all NAT rules: %s", err) + } + + for index, singleRule := range allNatRules { + // Look for a matching rule + if singleRule.IsEqualTo(natRuleConfig) { + return allNatRules[index], nil + + } + } + return nil, fmt.Errorf("rule '%s' of type '%s' not found after creation", natRuleConfig.Name, natRuleConfig.RuleType) +} + +// Update allows users to update NSX-T NAT rule +func (nsxtNat *NsxtNatRule) Update(natRuleConfig *types.NsxtNatRule) (*NsxtNatRule, error) { + client := nsxtNat.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtNatRules + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + if nsxtNat.NsxtNatRule.ID == "" { + return nil, fmt.Errorf("cannot update NSX-T NAT Rule without ID") + } + + urlRef, err := nsxtNat.client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, nsxtNat.edgeGatewayId), nsxtNat.NsxtNatRule.ID) + if err != nil { + return nil, err + } + + returnObject := &NsxtNatRule{ + NsxtNatRule: &types.NsxtNatRule{}, + client: client, + edgeGatewayId: nsxtNat.edgeGatewayId, + } + + err = client.OpenApiPutItem(apiVersion, urlRef, nil, natRuleConfig, returnObject.NsxtNatRule, nil) + if err != nil { + return nil, fmt.Errorf("error updating NSX-T NAT Rule: %s", err) + } + + return returnObject, nil +} + +// Delete deletes NSX-T NAT rule +func (nsxtNat *NsxtNatRule) Delete() error { + client := nsxtNat.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtNatRules + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return err + } + + if nsxtNat.NsxtNatRule.ID == "" { + return fmt.Errorf("cannot delete NSX-T NAT rule without ID") + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, nsxtNat.edgeGatewayId), nsxtNat.NsxtNatRule.ID) + if err != nil { + return err + } + + err = client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) + + if err != nil { + return fmt.Errorf("error deleting NSX-T NAT Rule: %s", err) + } + + return nil +} + +// IsEqualTo allows to check if a rule has exactly the same fields (except ID) to the supplied rule +// This validation is very tricky because minor version changes impact how fields are return. +// This function relies on most common and stable fields: +// * Name +// * Enabled +// * Description +// * ExternalAddresses +// * InternalAddresses +// * ApplicationPortProfile.ID +func (nsxtNat *NsxtNatRule) IsEqualTo(rule *types.NsxtNatRule) bool { + return natRulesEqual(nsxtNat.NsxtNatRule, rule) +} + +// natRulesEqual is a helper to check if first and second supplied rules are exactly the same (except ID) +func natRulesEqual(first, second *types.NsxtNatRule) bool { + util.Logger.Println("comparing NAT rule:") + util.Logger.Printf("%+v\n", first) + util.Logger.Println("against:") + util.Logger.Printf("%+v\n", second) + + // Being an org user always returns logging as false - therefore cannot compare it. + // first.Logging == second.Logging + + // These fields are returned or not returned depending on version and it is impossible to be 100% sure a minor + // patch does not break such comparison + // DnatExternalPort + // SnatDestinationAddresses + // RuleType - would work up to 35.2+, but then there is another field Type + // Type only available since 35.2+. Must be explicitly used for REFLEXIVE type in API v36.0+ + // FirewallMatch - it exists only since API 35.2+ and has a default starting this version + // InternalPort - is deprecated since API V35.0+ and is replaced by DnatExternalPort + // Priority - is available only in API V35.2+ + // Version - it is something that is automatically handled by API. When creating - you must specify none, but it sets + // version to 0. When updating one must specify the last version read, and again it will automatically increment this + // value after update. (probably it is meant to avoid concurrent updates) + if first.Name == second.Name && + first.Enabled == second.Enabled && + first.Description == second.Description && + first.ExternalAddresses == second.ExternalAddresses && + first.InternalAddresses == second.InternalAddresses && + + // Match both application profiles being nil (types cannot be equal as they are pointers, not values) + ((first.ApplicationPortProfile == nil && second.ApplicationPortProfile == nil) || + // Or both being not nil and having the same IDs + (first.ApplicationPortProfile != nil && second.ApplicationPortProfile != nil && first.ApplicationPortProfile.ID == second.ApplicationPortProfile.ID) || + // Or first Application profile is nil and second is not nil, but has empty ID + (first.ApplicationPortProfile == nil && second.ApplicationPortProfile != nil && second.ApplicationPortProfile.ID == "") || + // Or first Application Profile is not nil, but has empty ID, while second application port profile is nil + (first.ApplicationPortProfile != nil && first.ApplicationPortProfile.ID == "" && second.ApplicationPortProfile == nil)) { + + return true + } + + return false +} diff --git a/govcd/nsxt_nat_rule_test.go b/govcd/nsxt_nat_rule_test.go new file mode 100644 index 000000000..4ea92c389 --- /dev/null +++ b/govcd/nsxt_nat_rule_test.go @@ -0,0 +1,338 @@ +//go:build network || nsxt || functional || openapi || ALL + +package govcd + +import ( + "fmt" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_NsxtNatDnat(check *C) { + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointFirewallGroups) + + org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + + nsxtVdc, err := org.GetVDCByName(vcd.config.VCD.Nsxt.Vdc, false) + check.Assert(err, IsNil) + + edge, err := nsxtVdc.GetNsxtEdgeGatewayByName(vcd.config.VCD.Nsxt.EdgeGateway) + check.Assert(err, IsNil) + + appPortProfiles, err := org.GetAllNsxtAppPortProfiles(nil, types.ApplicationPortProfileScopeSystem) + check.Assert(err, IsNil) + + edgeGatewayPrimaryIp := "" + if edge.EdgeGateway != nil && len(edge.EdgeGateway.EdgeGatewayUplinks) > 0 && len(edge.EdgeGateway.EdgeGatewayUplinks[0].Subnets.Values) > 0 { + edgeGatewayPrimaryIp = edge.EdgeGateway.EdgeGatewayUplinks[0].Subnets.Values[0].PrimaryIP + } + check.Assert(edgeGatewayPrimaryIp, Not(Equals), "") + + natRuleDefinition := &types.NsxtNatRule{ + Name: check.TestName() + "dnat", + Description: "description", + Enabled: true, + RuleType: types.NsxtNatRuleTypeDnat, + ExternalAddresses: edgeGatewayPrimaryIp, + InternalAddresses: "11.11.11.2", + ApplicationPortProfile: &types.OpenApiReference{ + ID: appPortProfiles[0].NsxtAppPortProfile.ID, + Name: appPortProfiles[0].NsxtAppPortProfile.Name}, + SnatDestinationAddresses: "", + Logging: vcd.client.Client.IsSysAdmin, + DnatExternalPort: "", + } + + nsxtNatRuleChecks(natRuleDefinition, edge, check, vcd) +} + +func (vcd *TestVCD) Test_NsxtNatDnatExternalPortPort(check *C) { + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointFirewallGroups) + + org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + + nsxtVdc, err := org.GetVDCByName(vcd.config.VCD.Nsxt.Vdc, false) + check.Assert(err, IsNil) + + edge, err := nsxtVdc.GetNsxtEdgeGatewayByName(vcd.config.VCD.Nsxt.EdgeGateway) + check.Assert(err, IsNil) + + appPortProfiles, err := org.GetAllNsxtAppPortProfiles(nil, types.ApplicationPortProfileScopeSystem) + check.Assert(err, IsNil) + + edgeGatewayPrimaryIp := "" + if edge.EdgeGateway != nil && len(edge.EdgeGateway.EdgeGatewayUplinks) > 0 && len(edge.EdgeGateway.EdgeGatewayUplinks[0].Subnets.Values) > 0 { + edgeGatewayPrimaryIp = edge.EdgeGateway.EdgeGatewayUplinks[0].Subnets.Values[0].PrimaryIP + } + check.Assert(edgeGatewayPrimaryIp, Not(Equals), "") + + natRuleDefinition := &types.NsxtNatRule{ + Name: check.TestName() + "dnat", + Description: "description", + Enabled: true, + RuleType: types.NsxtNatRuleTypeDnat, + ExternalAddresses: edgeGatewayPrimaryIp, + InternalAddresses: "11.11.11.2", + ApplicationPortProfile: &types.OpenApiReference{ + ID: appPortProfiles[0].NsxtAppPortProfile.ID, + Name: appPortProfiles[0].NsxtAppPortProfile.Name}, + SnatDestinationAddresses: "", + Logging: vcd.client.Client.IsSysAdmin, + DnatExternalPort: "9898", + } + + nsxtNatRuleChecks(natRuleDefinition, edge, check, vcd) +} + +func (vcd *TestVCD) Test_NsxtNatDnatFirewallMatchPriority(check *C) { + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointFirewallGroups) + + org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + + nsxtVdc, err := org.GetVDCByName(vcd.config.VCD.Nsxt.Vdc, false) + check.Assert(err, IsNil) + + edge, err := nsxtVdc.GetNsxtEdgeGatewayByName(vcd.config.VCD.Nsxt.EdgeGateway) + check.Assert(err, IsNil) + + appPortProfiles, err := org.GetAllNsxtAppPortProfiles(nil, types.ApplicationPortProfileScopeSystem) + check.Assert(err, IsNil) + + edgeGatewayPrimaryIp := "" + if edge.EdgeGateway != nil && len(edge.EdgeGateway.EdgeGatewayUplinks) > 0 && len(edge.EdgeGateway.EdgeGatewayUplinks[0].Subnets.Values) > 0 { + edgeGatewayPrimaryIp = edge.EdgeGateway.EdgeGatewayUplinks[0].Subnets.Values[0].PrimaryIP + } + check.Assert(edgeGatewayPrimaryIp, Not(Equals), "") + + natRuleDefinition := &types.NsxtNatRule{ + Name: check.TestName() + "dnat", + Description: "description", + Enabled: true, + RuleType: types.NsxtNatRuleTypeDnat, + ExternalAddresses: edgeGatewayPrimaryIp, + InternalAddresses: "11.11.11.2", + ApplicationPortProfile: &types.OpenApiReference{ + ID: appPortProfiles[0].NsxtAppPortProfile.ID, + Name: appPortProfiles[0].NsxtAppPortProfile.Name}, + SnatDestinationAddresses: "", + Logging: vcd.client.Client.IsSysAdmin, + FirewallMatch: types.NsxtNatRuleFirewallMatchExternalAddress, + Priority: addrOf(248), + } + + nsxtNatRuleChecks(natRuleDefinition, edge, check, vcd) +} + +func (vcd *TestVCD) Test_NsxtNatNoDnat(check *C) { + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointFirewallGroups) + + org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + + nsxtVdc, err := org.GetVDCByName(vcd.config.VCD.Nsxt.Vdc, false) + check.Assert(err, IsNil) + + edge, err := nsxtVdc.GetNsxtEdgeGatewayByName(vcd.config.VCD.Nsxt.EdgeGateway) + check.Assert(err, IsNil) + + edgeGatewayPrimaryIp := "" + if edge.EdgeGateway != nil && len(edge.EdgeGateway.EdgeGatewayUplinks) > 0 && len(edge.EdgeGateway.EdgeGatewayUplinks[0].Subnets.Values) > 0 { + edgeGatewayPrimaryIp = edge.EdgeGateway.EdgeGatewayUplinks[0].Subnets.Values[0].PrimaryIP + } + check.Assert(edgeGatewayPrimaryIp, Not(Equals), "") + + natRuleDefinition := &types.NsxtNatRule{ + Name: check.TestName() + "no-dnat", + Description: "description", + Enabled: true, + RuleType: types.NsxtNatRuleTypeNoDnat, + ExternalAddresses: edgeGatewayPrimaryIp, + } + + nsxtNatRuleChecks(natRuleDefinition, edge, check, vcd) +} + +func (vcd *TestVCD) Test_NsxtNatSnat(check *C) { + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointFirewallGroups) + + org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + + nsxtVdc, err := org.GetVDCByName(vcd.config.VCD.Nsxt.Vdc, false) + check.Assert(err, IsNil) + + edge, err := nsxtVdc.GetNsxtEdgeGatewayByName(vcd.config.VCD.Nsxt.EdgeGateway) + check.Assert(err, IsNil) + + appPortProfiles, err := org.GetAllNsxtAppPortProfiles(nil, types.ApplicationPortProfileScopeSystem) + check.Assert(err, IsNil) + + edgeGatewayPrimaryIp := "" + if edge.EdgeGateway != nil && len(edge.EdgeGateway.EdgeGatewayUplinks) > 0 && len(edge.EdgeGateway.EdgeGatewayUplinks[0].Subnets.Values) > 0 { + edgeGatewayPrimaryIp = edge.EdgeGateway.EdgeGatewayUplinks[0].Subnets.Values[0].PrimaryIP + } + check.Assert(edgeGatewayPrimaryIp, Not(Equals), "") + + natRuleDefinition := &types.NsxtNatRule{ + Name: check.TestName() + "snat", + Description: "description", + Enabled: true, + RuleType: types.NsxtNatRuleTypeSnat, + ExternalAddresses: edgeGatewayPrimaryIp, + InternalAddresses: "11.11.11.2", + SnatDestinationAddresses: "11.11.11.4", + ApplicationPortProfile: &types.OpenApiReference{ + ID: appPortProfiles[1].NsxtAppPortProfile.ID, + Name: appPortProfiles[1].NsxtAppPortProfile.Name}, + } + + nsxtNatRuleChecks(natRuleDefinition, edge, check, vcd) +} + +func (vcd *TestVCD) Test_NsxtNatNoSnat(check *C) { + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointFirewallGroups) + + org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + + nsxtVdc, err := org.GetVDCByName(vcd.config.VCD.Nsxt.Vdc, false) + check.Assert(err, IsNil) + + edge, err := nsxtVdc.GetNsxtEdgeGatewayByName(vcd.config.VCD.Nsxt.EdgeGateway) + check.Assert(err, IsNil) + + natRuleDefinition := &types.NsxtNatRule{ + Name: check.TestName() + "no-snat", + Description: "description", + Enabled: true, + RuleType: types.NsxtNatRuleTypeNoSnat, + ExternalAddresses: "", + InternalAddresses: "11.11.11.2", + } + + nsxtNatRuleChecks(natRuleDefinition, edge, check, vcd) +} + +func (vcd *TestVCD) Test_NsxtNatPriorityAndFirewallMatch(check *C) { + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointFirewallGroups) + + org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + + nsxtVdc, err := org.GetVDCByName(vcd.config.VCD.Nsxt.Vdc, false) + check.Assert(err, IsNil) + + edge, err := nsxtVdc.GetNsxtEdgeGatewayByName(vcd.config.VCD.Nsxt.EdgeGateway) + check.Assert(err, IsNil) + + edgeGatewayPrimaryIp := "" + if edge.EdgeGateway != nil && len(edge.EdgeGateway.EdgeGatewayUplinks) > 0 && len(edge.EdgeGateway.EdgeGatewayUplinks[0].Subnets.Values) > 0 { + edgeGatewayPrimaryIp = edge.EdgeGateway.EdgeGatewayUplinks[0].Subnets.Values[0].PrimaryIP + } + check.Assert(edgeGatewayPrimaryIp, Not(Equals), "") + + natRuleDefinition := &types.NsxtNatRule{ + Name: check.TestName() + "dnat", + Description: "description", + Enabled: true, + RuleType: types.NsxtNatRuleTypeDnat, + ExternalAddresses: edgeGatewayPrimaryIp, + InternalAddresses: "11.11.11.2", + SnatDestinationAddresses: "", + Logging: vcd.client.Client.IsSysAdmin, + DnatExternalPort: "", + Priority: addrOf(100), + FirewallMatch: types.NsxtNatRuleFirewallMatchExternalAddress, + } + + nsxtNatRuleChecks(natRuleDefinition, edge, check, vcd) +} + +// Test_NsxtNatReflexive tests out REFLEXIVE rule type. This is only available in VCD 10.3 (API V36.0) +func (vcd *TestVCD) Test_NsxtNatReflexive(check *C) { + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointFirewallGroups) + + org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + + nsxtVdc, err := org.GetVDCByName(vcd.config.VCD.Nsxt.Vdc, false) + check.Assert(err, IsNil) + + edge, err := nsxtVdc.GetNsxtEdgeGatewayByName(vcd.config.VCD.Nsxt.EdgeGateway) + check.Assert(err, IsNil) + + edgeGatewayPrimaryIp := "" + if edge.EdgeGateway != nil && len(edge.EdgeGateway.EdgeGatewayUplinks) > 0 && len(edge.EdgeGateway.EdgeGatewayUplinks[0].Subnets.Values) > 0 { + edgeGatewayPrimaryIp = edge.EdgeGateway.EdgeGatewayUplinks[0].Subnets.Values[0].PrimaryIP + } + check.Assert(edgeGatewayPrimaryIp, Not(Equals), "") + + natRuleDefinition := &types.NsxtNatRule{ + Name: check.TestName() + "reflexive", + Description: "description", + Enabled: true, + //RuleType: types.NsxtNatRuleTypeReflexive, + Type: types.NsxtNatRuleTypeReflexive, + ExternalAddresses: edgeGatewayPrimaryIp, + InternalAddresses: "11.11.11.2", + Priority: addrOf(100), + FirewallMatch: types.NsxtNatRuleFirewallMatchExternalAddress, + } + + nsxtNatRuleChecks(natRuleDefinition, edge, check, vcd) +} + +func nsxtNatRuleChecks(natRuleDefinition *types.NsxtNatRule, edge *NsxtEdgeGateway, check *C, vcd *TestVCD) { + createdNatRule, err := edge.CreateNatRule(natRuleDefinition) + check.Assert(err, IsNil) + openApiEndpoint := types.OpenApiPathVersion1_0_0 + fmt.Sprintf(types.OpenApiEndpointNsxtNatRules, edge.EdgeGateway.ID) + createdNatRule.NsxtNatRule.ID + AddToCleanupListOpenApi(createdNatRule.NsxtNatRule.Name, check.TestName(), openApiEndpoint) + + // check if created rule matches definition + check.Assert(createdNatRule.IsEqualTo(natRuleDefinition), Equals, true) + + // Validate that supplied values are the same as read values + natRuleDefinition.ID = createdNatRule.NsxtNatRule.ID // ID is always the difference + natRuleDefinition.Priority = createdNatRule.NsxtNatRule.Priority // Priority returns default value (0) for VCD 10.2.2+ + natRuleDefinition.FirewallMatch = createdNatRule.NsxtNatRule.FirewallMatch // FirewallMatch returns default value (MATCH_INTERNAL_ADDRESS) for VCD 10.2.2+ + natRuleDefinition.Version = createdNatRule.NsxtNatRule.Version // Version will always be populated afterwards + natRuleDefinition.Type = createdNatRule.NsxtNatRule.Type + + check.Assert(createdNatRule.NsxtNatRule, DeepEquals, natRuleDefinition) + + // Try to get NAT rules by name and by ID + natRuleById, err := edge.GetNatRuleById(createdNatRule.NsxtNatRule.ID) + check.Assert(err, IsNil) + natRuleByName, err := edge.GetNatRuleByName(createdNatRule.NsxtNatRule.Name) + check.Assert(err, IsNil) + + check.Assert(natRuleById.NsxtNatRule, DeepEquals, natRuleDefinition) + check.Assert(natRuleByName.NsxtNatRule, DeepEquals, natRuleDefinition) + + // Try to update value + createdNatRule.NsxtNatRule.Name = check.TestName() + "updated" + updatedNatRule, err := createdNatRule.Update(createdNatRule.NsxtNatRule) + check.Assert(err, IsNil) + + // validate that supplied values are new, but ID stays the same + check.Assert(updatedNatRule.NsxtNatRule.ID, Equals, createdNatRule.NsxtNatRule.ID) + check.Assert(updatedNatRule.NsxtNatRule.RuleType, Equals, createdNatRule.NsxtNatRule.RuleType) + + err = createdNatRule.Delete() + check.Assert(err, IsNil) + + _, err = edge.GetNatRuleById(createdNatRule.NsxtNatRule.ID) + check.Assert(ContainsNotFound(err), Equals, true) +} diff --git a/govcd/nsxt_network_context_profile.go b/govcd/nsxt_network_context_profile.go new file mode 100644 index 000000000..cf9b84927 --- /dev/null +++ b/govcd/nsxt_network_context_profile.go @@ -0,0 +1,77 @@ +package govcd + +import ( + "fmt" + "net/url" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +// GetAllNetworkContextProfiles retrieves a slice of types.NsxtNetworkContextProfile +// This function requires at least a filter value for 'context_id' which can be one of: +// * Org VDC ID - to get Network Context Profiles scoped for VDC +// * Network provider ID - to get Network Context Profiles scoped for attached NSX-T environment +// * VDC Group ID - to get Network Context Profiles scoped for attached NSX-T environment +func GetAllNetworkContextProfiles(client *Client, queryParameters url.Values) ([]*types.NsxtNetworkContextProfile, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNetworkContextProfiles + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + typeResponses := []*types.NsxtNetworkContextProfile{} + err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParameters, &typeResponses, nil) + if err != nil { + return nil, err + } + + return typeResponses, nil +} + +// GetNetworkContextProfilesByScopeAndName retrieves a single NSX-T Network Context Profile by name +// and context ID. All fields - name, scope and contextId are mandatory +// +// contextId is mandatory and can be one off: +// * Org VDC ID - to get Network Context Profiles scoped for VDC +// * Network provider ID - to get Network Context Profiles scoped for attached NSX-T environment +// * VDC Group ID - to get Network Context Profiles scoped for attached NSX-T environment +// +// scope can be one off: +// * SYSTEM +// * PROVIDER +// * TENANT +func GetNetworkContextProfilesByNameScopeAndContext(client *Client, name, scope, contextId string) (*types.NsxtNetworkContextProfile, error) { + if name == "" || contextId == "" || scope == "" { + return nil, fmt.Errorf("error - 'name', 'scope' and 'contextId' must be specified") + } + + queryParams := copyOrNewUrlValues(nil) + queryParams.Add("filter", fmt.Sprintf("name==%s", name)) + queryParams = queryParameterFilterAnd(fmt.Sprintf("_context==%s", contextId), queryParams) + queryParams = queryParameterFilterAnd(fmt.Sprintf("scope==%s", scope), queryParams) + + allProfiles, err := GetAllNetworkContextProfiles(client, queryParams) + if err != nil { + return nil, fmt.Errorf("error retrieving Network Context Profiles by name '%s', scope '%s' and context ID '%s': %s ", + name, scope, contextId, err) + } + + return returnSingleNetworkContextProfile(allProfiles) +} + +func returnSingleNetworkContextProfile(allProfiles []*types.NsxtNetworkContextProfile) (*types.NsxtNetworkContextProfile, error) { + if len(allProfiles) > 1 { + return nil, fmt.Errorf("got more than 1 NSX-T Network Context Profile %d", len(allProfiles)) + } + + if len(allProfiles) < 1 { + return nil, fmt.Errorf("%s: got 0 NSX-T Network Context Profiles", ErrorEntityNotFound) + } + + return allProfiles[0], nil +} diff --git a/govcd/nsxt_network_context_profile_test.go b/govcd/nsxt_network_context_profile_test.go new file mode 100644 index 000000000..db75b016b --- /dev/null +++ b/govcd/nsxt_network_context_profile_test.go @@ -0,0 +1,71 @@ +//go:build network || nsxt || functional || openapi || ALL + +package govcd + +import ( + "net/url" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_GetAllNetworkContextProfiles(check *C) { + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointNetworkContextProfiles) + + filteredTestGetAllNetworkContextProfiles(nil, &vcd.client.Client, check) + + // Test with SYSTEM scope + queryParams := copyOrNewUrlValues(nil) + queryParams.Add("filter", "scope==SYSTEM") + filteredTestGetAllNetworkContextProfiles(queryParams, &vcd.client.Client, check) + + // Test with PROVIDER scope + queryParams = copyOrNewUrlValues(nil) + queryParams.Add("filter", "scope==PROVIDER") + filteredTestGetAllNetworkContextProfiles(queryParams, &vcd.client.Client, check) + + // Test with TENANT scope + queryParams = copyOrNewUrlValues(nil) + queryParams.Add("filter", "scope==TENANT") + filteredTestGetAllNetworkContextProfiles(queryParams, &vcd.client.Client, check) +} + +func (vcd *TestVCD) Test_GetNetworkContextProfilesByNameScopeAndContext(check *C) { + vcd.skipIfNotSysAdmin(check) + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointNetworkContextProfiles) + + // Expect error when fields are empty + profiles, err := GetNetworkContextProfilesByNameScopeAndContext(&vcd.client.Client, "", "", "") + check.Assert(err, NotNil) + check.Assert(profiles, IsNil) + + nsxtManagers, err := vcd.client.QueryNsxtManagerByName(vcd.config.VCD.Nsxt.Manager) + check.Assert(err, IsNil) + check.Assert(len(nsxtManagers), Equals, 1) + uuid, err := GetUuidFromHref(nsxtManagers[0].HREF, true) + check.Assert(err, IsNil) + nsxtManagerUrn, err := BuildUrnWithUuid("urn:vcloud:nsxtmanager:", uuid) + check.Assert(err, IsNil) + + profiles, err = GetNetworkContextProfilesByNameScopeAndContext(&vcd.client.Client, "AMQP", "SYSTEM", nsxtManagerUrn) + check.Assert(err, IsNil) + check.Assert(profiles, NotNil) + + // VCD does not have PROVIDER Network Context Profiles by default + profiles, err = GetNetworkContextProfilesByNameScopeAndContext(&vcd.client.Client, "AMQP", "PROVIDER", nsxtManagerUrn) + check.Assert(err, NotNil) + check.Assert(profiles, IsNil) + + // VCD does not have TENANT Network Context Profiles by default + profiles, err = GetNetworkContextProfilesByNameScopeAndContext(&vcd.client.Client, "AMQP", "TENANT", nsxtManagerUrn) + check.Assert(err, NotNil) + check.Assert(profiles, IsNil) +} + +func filteredTestGetAllNetworkContextProfiles(queryParams url.Values, client *Client, check *C) { + profiles, err := GetAllNetworkContextProfiles(client, queryParams) + check.Assert(err, IsNil) + check.Assert(profiles, NotNil) +} diff --git a/govcd/nsxt_route_advertisement.go b/govcd/nsxt_route_advertisement.go new file mode 100644 index 000000000..e774a0c70 --- /dev/null +++ b/govcd/nsxt_route_advertisement.go @@ -0,0 +1,128 @@ +/* + * Copyright 2022 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +// GetNsxtRouteAdvertisementWithContext retrieves the list of subnets that will be advertised so that the Edge Gateway can route +// out to the connected external network. +func (egw *NsxtEdgeGateway) GetNsxtRouteAdvertisementWithContext(useTenantContext bool) (*types.RouteAdvertisement, error) { + err := checkSanityNsxtEdgeGatewayRouteAdvertisement(egw) + if err != nil { + return nil, err + } + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtRouteAdvertisement + + highestApiVersion, err := egw.client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := egw.client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, egw.EdgeGateway.ID)) + if err != nil { + return nil, err + } + + var tenantContextHeaders map[string]string + if useTenantContext { + tenantContext, err := egw.getTenantContext() + if err != nil { + return nil, err + } + + tenantContextHeaders = getTenantContextHeader(tenantContext) + } + + routeAdvertisement := &types.RouteAdvertisement{} + err = egw.client.OpenApiGetItem(highestApiVersion, urlRef, nil, routeAdvertisement, tenantContextHeaders) + if err != nil { + return nil, err + } + + return routeAdvertisement, nil +} + +// GetNsxtRouteAdvertisement method is the same as GetNsxtRouteAdvertisementWithContext but sending TenantContext by default +func (egw *NsxtEdgeGateway) GetNsxtRouteAdvertisement() (*types.RouteAdvertisement, error) { + return egw.GetNsxtRouteAdvertisementWithContext(true) +} + +// UpdateNsxtRouteAdvertisementWithContext updates the list of subnets that will be advertised so that the Edge Gateway can route +// out to the connected external network. +func (egw *NsxtEdgeGateway) UpdateNsxtRouteAdvertisementWithContext(enable bool, subnets []string, useTenantContext bool) (*types.RouteAdvertisement, error) { + err := checkSanityNsxtEdgeGatewayRouteAdvertisement(egw) + if err != nil { + return nil, err + } + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtRouteAdvertisement + + highestApiVersion, err := egw.client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := egw.client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, egw.EdgeGateway.ID)) + if err != nil { + return nil, err + } + + var tenantContextHeaders map[string]string + if useTenantContext { + tenantContext, err := egw.getTenantContext() + if err != nil { + return nil, err + } + + tenantContextHeaders = getTenantContextHeader(tenantContext) + } + + routeAdvertisement := &types.RouteAdvertisement{ + Enable: enable, + Subnets: subnets, + } + + err = egw.client.OpenApiPutItem(highestApiVersion, urlRef, nil, routeAdvertisement, nil, tenantContextHeaders) + if err != nil { + return nil, err + } + + return egw.GetNsxtRouteAdvertisementWithContext(useTenantContext) +} + +// UpdateNsxtRouteAdvertisement method is the same as UpdateNsxtRouteAdvertisementWithContext but sending TenantContext by default +func (egw *NsxtEdgeGateway) UpdateNsxtRouteAdvertisement(enable bool, subnets []string) (*types.RouteAdvertisement, error) { + return egw.UpdateNsxtRouteAdvertisementWithContext(enable, subnets, true) +} + +// DeleteNsxtRouteAdvertisementWithContext deletes the list of subnets that will be advertised. +func (egw *NsxtEdgeGateway) DeleteNsxtRouteAdvertisementWithContext(useTenantContext bool) error { + _, err := egw.UpdateNsxtRouteAdvertisementWithContext(false, []string{}, useTenantContext) + return err +} + +// DeleteNsxtRouteAdvertisement method is the same as DeleteNsxtRouteAdvertisementWithContext but sending TenantContext by default +func (egw *NsxtEdgeGateway) DeleteNsxtRouteAdvertisement() error { + return egw.DeleteNsxtRouteAdvertisementWithContext(true) +} + +// checkSanityNsxtEdgeGatewayRouteAdvertisement function performs some checks to *NsxtEdgeGateway parameter and returns error +// if something is wrong. It is useful with methods NsxtEdgeGateway.[Get/Update/Delete]NsxtRouteAdvertisement +func checkSanityNsxtEdgeGatewayRouteAdvertisement(egw *NsxtEdgeGateway) error { + if egw.EdgeGateway == nil { + return fmt.Errorf("the EdgeGateway pointer is nil. Please initialize it first before using this method") + } + + if egw.EdgeGateway.ID == "" { + return fmt.Errorf("the EdgeGateway ID is empty. Please initialize it first before using this method") + } + + return nil +} diff --git a/govcd/nsxt_route_advertisement_test.go b/govcd/nsxt_route_advertisement_test.go new file mode 100644 index 000000000..3bd76a1c8 --- /dev/null +++ b/govcd/nsxt_route_advertisement_test.go @@ -0,0 +1,75 @@ +//go:build network || nsxt || functional || openapi || ALL + +/* + * Copyright 2022 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_NsxtEdgeRouteAdvertisement(check *C) { + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointNsxtRouteAdvertisement) + vcd.skipIfNotSysAdmin(check) + + org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + nsxtVdc, err := org.GetVDCByName(vcd.config.VCD.Nsxt.Vdc, false) + check.Assert(err, IsNil) + edge, err := nsxtVdc.GetNsxtEdgeGatewayByName(vcd.config.VCD.Nsxt.EdgeGateway) + check.Assert(err, IsNil) + + // Make sure we are using a dedicated Tier-0 gateway (otherwise route advertisement won't be available) + edge, err = setDedicateTier0Gateway(edge, true) + check.Assert(err, IsNil) + check.Assert(edge, NotNil) + + // Make sure that things get back to normal when the test is done + defer setDedicateTier0Gateway(edge, false) + + network1 := "192.168.1.0/24" + network2 := "192.168.2.0/24" + networksToAdvertise := []string{network1, network2} // Sample networks to advertise + + // Test UpdateNsxtRouteAdvertisement + nsxtEdgeRouteAdvertisement, err := edge.UpdateNsxtRouteAdvertisement(true, networksToAdvertise) + check.Assert(err, IsNil) + check.Assert(nsxtEdgeRouteAdvertisement, NotNil) + check.Assert(nsxtEdgeRouteAdvertisement.Enable, Equals, true) + check.Assert(len(nsxtEdgeRouteAdvertisement.Subnets), Equals, 2) + check.Assert(checkNetworkInSubnetsSlice(network1, networksToAdvertise), IsNil) + check.Assert(checkNetworkInSubnetsSlice(network2, networksToAdvertise), IsNil) + + // Test DeleteNsxtRouteAdvertisement + err = edge.DeleteNsxtRouteAdvertisement() + check.Assert(err, IsNil) + nsxtEdgeRouteAdvertisement, err = edge.GetNsxtRouteAdvertisement() + check.Assert(err, IsNil) + check.Assert(nsxtEdgeRouteAdvertisement, NotNil) + check.Assert(nsxtEdgeRouteAdvertisement.Enable, Equals, false) + check.Assert(len(nsxtEdgeRouteAdvertisement.Subnets), Equals, 0) +} + +func checkNetworkInSubnetsSlice(network string, subnets []string) error { + for _, subnet := range subnets { + if subnet == network { + return nil + } + } + return fmt.Errorf("network %s is not within the slice provided", network) +} + +func setDedicateTier0Gateway(edgeGateway *NsxtEdgeGateway, dedicate bool) (*NsxtEdgeGateway, error) { + edgeGateway.EdgeGateway.EdgeGatewayUplinks[0].Dedicated = dedicate + edgeGateway, err := edgeGateway.Update(edgeGateway.EdgeGateway) + if err != nil { + return nil, err + } + + return edgeGateway, nil +} diff --git a/govcd/nsxt_segment_profile_template.go b/govcd/nsxt_segment_profile_template.go new file mode 100644 index 000000000..c45c7e6ed --- /dev/null +++ b/govcd/nsxt_segment_profile_template.go @@ -0,0 +1,102 @@ +/* + * 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" +) + +const labelNsxtSegmentProfileTemplate = "NSX-T Segment Profile Template" + +// NsxtSegmentProfileTemplate contains a structure for configuring Segment Profile Templates +type NsxtSegmentProfileTemplate struct { + NsxtSegmentProfileTemplate *types.NsxtSegmentProfileTemplate + 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 (n NsxtSegmentProfileTemplate) wrap(inner *types.NsxtSegmentProfileTemplate) *NsxtSegmentProfileTemplate { + n.NsxtSegmentProfileTemplate = inner + return &n +} + +// CreateSegmentProfileTemplate creates a Segment Profile Template that can later be assigned to +// global VCD configuration, Org VDC or Org VDC Network +func (vcdClient *VCDClient) CreateSegmentProfileTemplate(segmentProfileConfig *types.NsxtSegmentProfileTemplate) (*NsxtSegmentProfileTemplate, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtSegmentProfileTemplates, + entityLabel: labelNsxtSegmentProfileTemplate, + } + outerType := NsxtSegmentProfileTemplate{VCDClient: vcdClient} + return createOuterEntity(&vcdClient.Client, outerType, c, segmentProfileConfig) +} + +// GetAllSegmentProfileTemplates retrieves all Segment Profile Templates +func (vcdClient *VCDClient) GetAllSegmentProfileTemplates(queryFilter url.Values) ([]*NsxtSegmentProfileTemplate, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtSegmentProfileTemplates, + entityLabel: labelNsxtSegmentProfileTemplate, + queryParameters: queryFilter, + } + + outerType := NsxtSegmentProfileTemplate{VCDClient: vcdClient} + return getAllOuterEntities[NsxtSegmentProfileTemplate, types.NsxtSegmentProfileTemplate](&vcdClient.Client, outerType, c) +} + +// GetSegmentProfileTemplateById retrieves Segment Profile Template by ID +func (vcdClient *VCDClient) GetSegmentProfileTemplateById(id string) (*NsxtSegmentProfileTemplate, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtSegmentProfileTemplates, + endpointParams: []string{id}, + entityLabel: labelNsxtSegmentProfileTemplate, + } + + outerType := NsxtSegmentProfileTemplate{VCDClient: vcdClient} + return getOuterEntity[NsxtSegmentProfileTemplate, types.NsxtSegmentProfileTemplate](&vcdClient.Client, outerType, c) +} + +// GetSegmentProfileTemplateByName retrieves Segment Profile Template by ID +func (vcdClient *VCDClient) GetSegmentProfileTemplateByName(name string) (*NsxtSegmentProfileTemplate, error) { + filterByName := copyOrNewUrlValues(nil) + filterByName = queryParameterFilterAnd(fmt.Sprintf("name==%s", name), filterByName) + + allSegmentProfileTemplates, err := vcdClient.GetAllSegmentProfileTemplates(filterByName) + if err != nil { + return nil, err + } + + singleSegmentProfileTemplate, err := oneOrError("name", name, allSegmentProfileTemplates) + if err != nil { + return nil, err + } + + return singleSegmentProfileTemplate, nil +} + +// Update Segment Profile Template +func (spt *NsxtSegmentProfileTemplate) Update(nsxtSegmentProfileTemplateConfig *types.NsxtSegmentProfileTemplate) (*NsxtSegmentProfileTemplate, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtSegmentProfileTemplates, + endpointParams: []string{nsxtSegmentProfileTemplateConfig.ID}, + entityLabel: labelNsxtSegmentProfileTemplate, + } + outerType := NsxtSegmentProfileTemplate{VCDClient: spt.VCDClient} + return updateOuterEntity(&spt.VCDClient.Client, outerType, c, nsxtSegmentProfileTemplateConfig) +} + +// Delete allows deleting NSX-T Segment Profile Template +func (spt *NsxtSegmentProfileTemplate) Delete() error { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtSegmentProfileTemplates, + endpointParams: []string{spt.NsxtSegmentProfileTemplate.ID}, + entityLabel: labelNsxtSegmentProfileTemplate, + } + return deleteEntityById(&spt.VCDClient.Client, c) +} diff --git a/govcd/nsxt_segment_profile_template_test.go b/govcd/nsxt_segment_profile_template_test.go new file mode 100644 index 000000000..063405954 --- /dev/null +++ b/govcd/nsxt_segment_profile_template_test.go @@ -0,0 +1,96 @@ +//go:build network || nsxt || functional || openapi || ALL + +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_NsxtSegmentProfileTemplate(check *C) { + skipNoNsxtConfiguration(vcd, check) + vcd.skipIfNotSysAdmin(check) + + nsxtManager, err := vcd.client.GetNsxtManagerByName(vcd.config.VCD.Nsxt.Manager) + check.Assert(err, IsNil) + check.Assert(nsxtManager, NotNil) + + nsxtManagerUrn, err := nsxtManager.Urn() + check.Assert(err, IsNil) + + // Filter by NSX-T Manager + queryParams := copyOrNewUrlValues(nil) + queryParams = queryParameterFilterAnd(fmt.Sprintf("nsxTManagerRef.id==%s", nsxtManagerUrn), queryParams) + + // Lookup prerequisite profiles for Segment Profile template creation + ipDiscoveryProfile, err := vcd.client.GetIpDiscoveryProfileByName(vcd.config.VCD.Nsxt.IpDiscoveryProfile, queryParams) + check.Assert(err, IsNil) + macDiscoveryProfile, err := vcd.client.GetMacDiscoveryProfileByName(vcd.config.VCD.Nsxt.MacDiscoveryProfile, queryParams) + check.Assert(err, IsNil) + spoofGuardProfile, err := vcd.client.GetSpoofGuardProfileByName(vcd.config.VCD.Nsxt.SpoofGuardProfile, queryParams) + check.Assert(err, IsNil) + qosProfile, err := vcd.client.GetQoSProfileByName(vcd.config.VCD.Nsxt.QosProfile, queryParams) + check.Assert(err, IsNil) + segmentSecurityProfile, err := vcd.client.GetSegmentSecurityProfileByName(vcd.config.VCD.Nsxt.SegmentSecurityProfile, queryParams) + check.Assert(err, IsNil) + + config := &types.NsxtSegmentProfileTemplate{ + Name: check.TestName(), + Description: check.TestName() + "-description", + IPDiscoveryProfile: &types.Reference{ID: ipDiscoveryProfile.ID}, + MacDiscoveryProfile: &types.Reference{ID: macDiscoveryProfile.ID}, + QosProfile: &types.Reference{ID: qosProfile.ID}, + SegmentSecurityProfile: &types.Reference{ID: segmentSecurityProfile.ID}, + SpoofGuardProfile: &types.Reference{ID: spoofGuardProfile.ID}, + SourceNsxTManagerRef: &types.OpenApiReference{ID: nsxtManager.NsxtManager.ID}, + } + + createdSegmentProfileTemplate, err := vcd.client.CreateSegmentProfileTemplate(config) + check.Assert(err, IsNil) + check.Assert(createdSegmentProfileTemplate, NotNil) + + // Add to cleanup list + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtSegmentProfileTemplates + createdSegmentProfileTemplate.NsxtSegmentProfileTemplate.ID + AddToCleanupListOpenApi(config.Name, check.TestName(), openApiEndpoint) + + // Retrieve segment profile template + retrievedSpt, err := vcd.client.GetSegmentProfileTemplateById(createdSegmentProfileTemplate.NsxtSegmentProfileTemplate.ID) + check.Assert(err, IsNil) + check.Assert(retrievedSpt.NsxtSegmentProfileTemplate, DeepEquals, createdSegmentProfileTemplate.NsxtSegmentProfileTemplate) + + // Get all and look for the required one + allSpts, err := vcd.client.GetAllSegmentProfileTemplates(nil) + check.Assert(err, IsNil) + check.Assert(allSpts, NotNil) + found := false + for _, spt := range allSpts { + if spt.NsxtSegmentProfileTemplate.ID == createdSegmentProfileTemplate.NsxtSegmentProfileTemplate.ID { + found = true + break + } + } + + check.Assert(found, Equals, true) + + // Test update + createdSegmentProfileTemplate.NsxtSegmentProfileTemplate.Description = check.TestName() + "updated" + updatedSegmentProfileTemplate, err := createdSegmentProfileTemplate.Update(createdSegmentProfileTemplate.NsxtSegmentProfileTemplate) + check.Assert(err, IsNil) + check.Assert(updatedSegmentProfileTemplate.NsxtSegmentProfileTemplate.Description, Equals, createdSegmentProfileTemplate.NsxtSegmentProfileTemplate.Description) + check.Assert(updatedSegmentProfileTemplate.NsxtSegmentProfileTemplate.ID, Equals, createdSegmentProfileTemplate.NsxtSegmentProfileTemplate.ID) + + // Delete + err = createdSegmentProfileTemplate.Delete() + check.Assert(err, IsNil) + + // Check that it returns sentinel error 'ErrorEntityNotFound' when an entity is not found + notFoundSpt, err := vcd.client.GetSegmentProfileTemplateById(createdSegmentProfileTemplate.NsxtSegmentProfileTemplate.ID) + check.Assert(ContainsNotFound(err), Equals, true) + check.Assert(notFoundSpt, IsNil) +} diff --git a/govcd/nsxt_segment_profiles.go b/govcd/nsxt_segment_profiles.go new file mode 100644 index 000000000..848ebf64b --- /dev/null +++ b/govcd/nsxt_segment_profiles.go @@ -0,0 +1,129 @@ +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "net/url" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +const ( + labelIpDiscoveryProfiles = "IP Discovery Profiles" + labelMacDiscoveryProfiles = "MAC Discovery Profiles" + labelSpoofGuardProfiles = "Spoof Guard Profiles" + labelQosProfiles = "QoS Profiles" + labelSegmentSecurityProfiles = "Segment Security Profiles" +) + +// GetAllIpDiscoveryProfiles retrieves all IP Discovery Profiles configured in an NSX-T manager. +// NSX-T manager ID (nsxTManagerRef.id), Org VDC ID (orgVdcId) or VDC Group ID (vdcGroupId) must be +// supplied as a filter. Results can also be filtered by a single profile ID +// (filter=nsxTManagerRef.id==nsxTManagerUrn;id==profileId). +func (vcdClient *VCDClient) GetAllIpDiscoveryProfiles(queryParameters url.Values) ([]*types.NsxtSegmentProfileIpDiscovery, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtSegmentIpDiscoveryProfiles, + queryParameters: queryParameters, + entityLabel: labelIpDiscoveryProfiles, + } + return getAllInnerEntities[types.NsxtSegmentProfileIpDiscovery](&vcdClient.Client, c) +} + +func (vcdClient *VCDClient) GetIpDiscoveryProfileByName(name string, queryParameters url.Values) (*types.NsxtSegmentProfileIpDiscovery, error) { + apiFilteredEntities, err := vcdClient.GetAllIpDiscoveryProfiles(queryParameters) // API filtering by 'displayName' field is not supported + if err != nil { + return nil, err + } + + return localFilterOneOrError(labelIpDiscoveryProfiles, apiFilteredEntities, "DisplayName", name) +} + +// GetAllMacDiscoveryProfiles retrieves all MAC Discovery Profiles configured in an NSX-T manager. +// NSX-T manager ID (nsxTManagerRef.id), Org VDC ID (orgVdcId) or VDC Group ID (vdcGroupId) must be +// supplied as a filter. Results can also be filtered by a single profile ID +// (filter=nsxTManagerRef.id==nsxTManagerUrn;id==profileId). +func (vcdClient *VCDClient) GetAllMacDiscoveryProfiles(queryParameters url.Values) ([]*types.NsxtSegmentProfileMacDiscovery, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtSegmentMacDiscoveryProfiles, + queryParameters: queryParameters, + entityLabel: labelMacDiscoveryProfiles, + } + return getAllInnerEntities[types.NsxtSegmentProfileMacDiscovery](&vcdClient.Client, c) +} + +func (vcdClient *VCDClient) GetMacDiscoveryProfileByName(name string, queryParameters url.Values) (*types.NsxtSegmentProfileMacDiscovery, error) { + apiFilteredEntities, err := vcdClient.GetAllMacDiscoveryProfiles(queryParameters) // API filtering by 'displayName' field is not supported + if err != nil { + return nil, err + } + + return localFilterOneOrError(labelMacDiscoveryProfiles, apiFilteredEntities, "DisplayName", name) +} + +// GetAllSpoofGuardProfiles retrieves all Spoof Guard Profiles configured in an NSX-T manager. +// NSX-T manager ID (nsxTManagerRef.id), Org VDC ID (orgVdcId) or VDC Group ID (vdcGroupId) must be +// supplied as a filter. Results can also be filtered by a single profile ID +// (filter=nsxTManagerRef.id==nsxTManagerUrn;id==profileId). +func (vcdClient *VCDClient) GetAllSpoofGuardProfiles(queryParameters url.Values) ([]*types.NsxtSegmentProfileSegmentSpoofGuard, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtSegmentSpoofGuardProfiles, + queryParameters: queryParameters, + entityLabel: labelSpoofGuardProfiles, + } + return getAllInnerEntities[types.NsxtSegmentProfileSegmentSpoofGuard](&vcdClient.Client, c) +} + +func (vcdClient *VCDClient) GetSpoofGuardProfileByName(name string, queryParameters url.Values) (*types.NsxtSegmentProfileSegmentSpoofGuard, error) { + apiFilteredEntities, err := vcdClient.GetAllSpoofGuardProfiles(queryParameters) // API filtering by 'displayName' field is not supported + if err != nil { + return nil, err + } + + return localFilterOneOrError(labelSpoofGuardProfiles, apiFilteredEntities, "DisplayName", name) +} + +// GetAllQoSProfiles retrieves all QoS Profiles configured in an NSX-T manager. +// NSX-T manager ID (nsxTManagerRef.id), Org VDC ID (orgVdcId) or VDC Group ID (vdcGroupId) must be +// supplied as a filter. Results can also be filtered by a single profile ID +// (filter=nsxTManagerRef.id==nsxTManagerUrn;id==profileId). +func (vcdClient *VCDClient) GetAllQoSProfiles(queryParameters url.Values) ([]*types.NsxtSegmentProfileSegmentQosProfile, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtSegmentQosProfiles, + queryParameters: queryParameters, + entityLabel: labelQosProfiles, + } + return getAllInnerEntities[types.NsxtSegmentProfileSegmentQosProfile](&vcdClient.Client, c) +} + +func (vcdClient *VCDClient) GetQoSProfileByName(name string, queryParameters url.Values) (*types.NsxtSegmentProfileSegmentQosProfile, error) { + apiFilteredEntities, err := vcdClient.GetAllQoSProfiles(queryParameters) // API filtering by 'displayName' field is not supported + if err != nil { + return nil, err + } + + return localFilterOneOrError(labelQosProfiles, apiFilteredEntities, "DisplayName", name) +} + +// GetAllSegmentSecurityProfiles retrieves all Segment Security Profiles configured in an NSX-T manager. +// NSX-T manager ID (nsxTManagerRef.id), Org VDC ID (orgVdcId) or VDC Group ID (vdcGroupId) must be +// supplied as a filter. Results can also be filtered by a single profile ID +// (filter=nsxTManagerRef.id==nsxTManagerUrn;id==profileId). +func (vcdClient *VCDClient) GetAllSegmentSecurityProfiles(queryParameters url.Values) ([]*types.NsxtSegmentProfileSegmentSecurity, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtSegmentSecurityProfiles, + queryParameters: queryParameters, + entityLabel: labelSegmentSecurityProfiles, + } + return getAllInnerEntities[types.NsxtSegmentProfileSegmentSecurity](&vcdClient.Client, c) +} + +func (vcdClient *VCDClient) GetSegmentSecurityProfileByName(name string, queryParameters url.Values) (*types.NsxtSegmentProfileSegmentSecurity, error) { + apiFilteredEntities, err := vcdClient.GetAllSegmentSecurityProfiles(queryParameters) // API filtering by 'displayName' field is not supported + if err != nil { + return nil, err + } + + return localFilterOneOrError(labelSegmentSecurityProfiles, apiFilteredEntities, "DisplayName", name) +} diff --git a/govcd/nsxt_segment_profiles_test.go b/govcd/nsxt_segment_profiles_test.go new file mode 100644 index 000000000..cc96b259c --- /dev/null +++ b/govcd/nsxt_segment_profiles_test.go @@ -0,0 +1,162 @@ +//go:build network || nsxt || functional || openapi || ALL + +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "net/url" + + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_GetNsxtSegmentProfiles(check *C) { + skipNoNsxtConfiguration(vcd, check) + vcd.skipIfNotSysAdmin(check) + + nsxtManager, err := vcd.client.GetNsxtManagerByName(vcd.config.VCD.Nsxt.Manager) + check.Assert(err, IsNil) + check.Assert(nsxtManager, NotNil) + + // Check filtering by NSX-T Manager ID + filterByNsxtManager := copyOrNewUrlValues(nil) + filterByNsxtManager = queryParameterFilterAnd(fmt.Sprintf("nsxTManagerRef.id==%s", nsxtManager.NsxtManager.ID), filterByNsxtManager) + checkNsxtSegmentAllProfilesByFilter(vcd, check, filterByNsxtManager) + + // Check filtering by VDC ID + filterByVdc := copyOrNewUrlValues(nil) + filterByVdc = queryParameterFilterAnd(fmt.Sprintf("orgVdcId==%s", vcd.nsxtVdc.Vdc.ID), filterByVdc) + checkNsxtSegmentAllProfilesByFilter(vcd, check, filterByVdc) + + // Check filtering by VDC Group ID + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.org.Org.Name) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + vdcGroup, err := adminOrg.GetVdcGroupByName(vcd.config.VCD.Nsxt.VdcGroup) + check.Assert(err, IsNil) + check.Assert(vdcGroup, NotNil) + + filterByVdcGroup := copyOrNewUrlValues(nil) + filterByVdcGroup = queryParameterFilterAnd(fmt.Sprintf("vdcGroupId==%s", vdcGroup.VdcGroup.Id), filterByVdcGroup) + checkNsxtSegmentAllProfilesByFilter(vcd, check, filterByVdcGroup) + + // IP Discovery profile by name + ipDiscoveryProfileByNameInNsxtManager, err := vcd.client.GetIpDiscoveryProfileByName(vcd.config.VCD.Nsxt.IpDiscoveryProfile, filterByNsxtManager) + check.Assert(err, IsNil) + check.Assert(ipDiscoveryProfileByNameInNsxtManager.DisplayName, Equals, vcd.config.VCD.Nsxt.IpDiscoveryProfile) + + ipDiscoveryProfileByNameInVdc, err := vcd.client.GetIpDiscoveryProfileByName(vcd.config.VCD.Nsxt.IpDiscoveryProfile, filterByVdc) + check.Assert(err, IsNil) + check.Assert(ipDiscoveryProfileByNameInVdc.DisplayName, Equals, vcd.config.VCD.Nsxt.IpDiscoveryProfile) + + ipDiscoveryProfileByNameInVdcGroup, err := vcd.client.GetIpDiscoveryProfileByName(vcd.config.VCD.Nsxt.IpDiscoveryProfile, filterByVdcGroup) + check.Assert(err, IsNil) + check.Assert(ipDiscoveryProfileByNameInVdcGroup.DisplayName, Equals, vcd.config.VCD.Nsxt.IpDiscoveryProfile) + + // not found + notFoundipDiscoveryProfileByNameInNsxtManager, err := vcd.client.GetIpDiscoveryProfileByName("invalid-name", filterByNsxtManager) + check.Assert(ContainsNotFound(err), Equals, true) + check.Assert(notFoundipDiscoveryProfileByNameInNsxtManager, IsNil) + + // Mac Discovery Profile by name + macDiscoveryProfileByNameInNsxtManager, err := vcd.client.GetMacDiscoveryProfileByName(vcd.config.VCD.Nsxt.MacDiscoveryProfile, filterByNsxtManager) + check.Assert(err, IsNil) + check.Assert(macDiscoveryProfileByNameInNsxtManager.DisplayName, Equals, vcd.config.VCD.Nsxt.MacDiscoveryProfile) + + macDiscoveryProfileByNameInVdc, err := vcd.client.GetMacDiscoveryProfileByName(vcd.config.VCD.Nsxt.MacDiscoveryProfile, filterByVdc) + check.Assert(err, IsNil) + check.Assert(macDiscoveryProfileByNameInVdc.DisplayName, Equals, vcd.config.VCD.Nsxt.MacDiscoveryProfile) + + macDiscoveryProfileByNameInVdcGroup, err := vcd.client.GetMacDiscoveryProfileByName(vcd.config.VCD.Nsxt.MacDiscoveryProfile, filterByVdcGroup) + check.Assert(err, IsNil) + check.Assert(macDiscoveryProfileByNameInVdcGroup.DisplayName, Equals, vcd.config.VCD.Nsxt.MacDiscoveryProfile) + + // not found + notFoundmacDiscoveryProfileByNameInNsxtManager, err := vcd.client.GetMacDiscoveryProfileByName("invalid-name", filterByNsxtManager) + check.Assert(ContainsNotFound(err), Equals, true) + check.Assert(notFoundmacDiscoveryProfileByNameInNsxtManager, IsNil) + + // Spoof Guard Profile by name + spoofGuardProfileByNameInNsxtManager, err := vcd.client.GetSpoofGuardProfileByName(vcd.config.VCD.Nsxt.SpoofGuardProfile, filterByNsxtManager) + check.Assert(err, IsNil) + check.Assert(spoofGuardProfileByNameInNsxtManager.DisplayName, Equals, vcd.config.VCD.Nsxt.SpoofGuardProfile) + + spoofGuardProfileByNameInVdc, err := vcd.client.GetSpoofGuardProfileByName(vcd.config.VCD.Nsxt.SpoofGuardProfile, filterByVdc) + check.Assert(err, IsNil) + check.Assert(spoofGuardProfileByNameInVdc.DisplayName, Equals, vcd.config.VCD.Nsxt.SpoofGuardProfile) + + spoofGuardProfileByNameInVdcGroup, err := vcd.client.GetSpoofGuardProfileByName(vcd.config.VCD.Nsxt.SpoofGuardProfile, filterByVdcGroup) + check.Assert(err, IsNil) + check.Assert(spoofGuardProfileByNameInVdcGroup.DisplayName, Equals, vcd.config.VCD.Nsxt.SpoofGuardProfile) + + // not found + notFoundspoofGuardProfileByNameInVdcGroup, err := vcd.client.GetSpoofGuardProfileByName("invalid-name", filterByNsxtManager) + check.Assert(ContainsNotFound(err), Equals, true) + check.Assert(notFoundspoofGuardProfileByNameInVdcGroup, IsNil) + + // QoS Profile by name + qosProfileByNameInNsxtManager, err := vcd.client.GetQoSProfileByName(vcd.config.VCD.Nsxt.QosProfile, filterByNsxtManager) + check.Assert(err, IsNil) + check.Assert(qosProfileByNameInNsxtManager.DisplayName, Equals, vcd.config.VCD.Nsxt.QosProfile) + + qosProfileByNameInVdc, err := vcd.client.GetQoSProfileByName(vcd.config.VCD.Nsxt.QosProfile, filterByVdc) + check.Assert(err, IsNil) + check.Assert(qosProfileByNameInVdc.DisplayName, Equals, vcd.config.VCD.Nsxt.QosProfile) + + qosProfileByNameInVdcGroup, err := vcd.client.GetQoSProfileByName(vcd.config.VCD.Nsxt.QosProfile, filterByVdcGroup) + check.Assert(err, IsNil) + check.Assert(qosProfileByNameInVdcGroup.DisplayName, Equals, vcd.config.VCD.Nsxt.QosProfile) + + // not found + notFoundqosProfileByNameInNsxtManager, err := vcd.client.GetQoSProfileByName("invalid-name", filterByNsxtManager) + check.Assert(ContainsNotFound(err), Equals, true) + check.Assert(notFoundqosProfileByNameInNsxtManager, IsNil) + + // Segment Security Profile by name + segmentSecurityProfileByNameInNsxtManager, err := vcd.client.GetSegmentSecurityProfileByName(vcd.config.VCD.Nsxt.SegmentSecurityProfile, filterByNsxtManager) + check.Assert(err, IsNil) + check.Assert(segmentSecurityProfileByNameInNsxtManager.DisplayName, Equals, vcd.config.VCD.Nsxt.SegmentSecurityProfile) + + segmentSecurityProfileByNameInVdc, err := vcd.client.GetSegmentSecurityProfileByName(vcd.config.VCD.Nsxt.SegmentSecurityProfile, filterByVdc) + check.Assert(err, IsNil) + check.Assert(segmentSecurityProfileByNameInVdc.DisplayName, Equals, vcd.config.VCD.Nsxt.SegmentSecurityProfile) + + segmentSecurityProfileByNameInVdcGroup, err := vcd.client.GetSegmentSecurityProfileByName(vcd.config.VCD.Nsxt.SegmentSecurityProfile, filterByVdcGroup) + check.Assert(err, IsNil) + check.Assert(segmentSecurityProfileByNameInVdcGroup.DisplayName, Equals, vcd.config.VCD.Nsxt.SegmentSecurityProfile) + + // not found + notFoundSegmentSecurityProfileByNameInVdcGroup, err := vcd.client.GetSegmentSecurityProfileByName("invalid-name", filterByNsxtManager) + check.Assert(ContainsNotFound(err), Equals, true) + check.Assert(notFoundSegmentSecurityProfileByNameInVdcGroup, IsNil) +} + +func checkNsxtSegmentAllProfilesByFilter(vcd *TestVCD, check *C, filter url.Values) { + ipDiscoverProfiles, err := vcd.client.GetAllIpDiscoveryProfiles(filter) + check.Assert(err, IsNil) + check.Assert(ipDiscoverProfiles, NotNil) + check.Assert(len(ipDiscoverProfiles) > 1, Equals, true) + + macDiscoverProfiles, err := vcd.client.GetAllMacDiscoveryProfiles(filter) + check.Assert(err, IsNil) + check.Assert(macDiscoverProfiles, NotNil) + check.Assert(len(macDiscoverProfiles) > 1, Equals, true) + + spoofGuardDiscoverProfiles, err := vcd.client.GetAllSpoofGuardProfiles(filter) + check.Assert(err, IsNil) + check.Assert(spoofGuardDiscoverProfiles, NotNil) + check.Assert(len(spoofGuardDiscoverProfiles) > 1, Equals, true) + + qosDiscoverProfiles, err := vcd.client.GetAllQoSProfiles(filter) + check.Assert(err, IsNil) + check.Assert(qosDiscoverProfiles, NotNil) + check.Assert(len(qosDiscoverProfiles) > 1, Equals, true) + + segmentSecurityDiscoverProfiles, err := vcd.client.GetAllSegmentSecurityProfiles(filter) + check.Assert(err, IsNil) + check.Assert(segmentSecurityDiscoverProfiles, NotNil) + check.Assert(len(segmentSecurityDiscoverProfiles) > 1, Equals, true) +} diff --git a/govcd/nsxt_test.go b/govcd/nsxt_test.go index e6c77c358..6896b34e8 100644 --- a/govcd/nsxt_test.go +++ b/govcd/nsxt_test.go @@ -1,4 +1,4 @@ -// +build ALL openapi functional nsxt +//go:build ALL || openapi || functional || nsxt /* * Copyright 2020 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -13,6 +13,7 @@ import ( ) func (vcd *TestVCD) Test_QueryNsxtManagerByName(check *C) { + vcd.skipIfNotSysAdmin(check) skipNoNsxtConfiguration(vcd, check) nsxtManagers, err := vcd.client.QueryNsxtManagerByName(vcd.config.VCD.Nsxt.Manager) check.Assert(err, IsNil) diff --git a/govcd/nsxt_tier0_router.go b/govcd/nsxt_tier0_router.go index 8c6719b17..971229b90 100644 --- a/govcd/nsxt_tier0_router.go +++ b/govcd/nsxt_tier0_router.go @@ -25,7 +25,7 @@ type NsxtTier0Router struct { // Note. NSX-T manager ID is mandatory and must be in URN format (e.g. // urn:vcloud:nsxtmanager:09722307-aee0-4623-af95-7f8e577c9ebc) -func (vcdCli *VCDClient) GetImportableNsxtTier0RouterByName(name, nsxtManagerId string) (*NsxtTier0Router, error) { +func (vcdClient *VCDClient) GetImportableNsxtTier0RouterByName(name, nsxtManagerId string) (*NsxtTier0Router, error) { if nsxtManagerId == "" { return nil, fmt.Errorf("no NSX-T manager ID specified") } @@ -47,7 +47,7 @@ func (vcdCli *VCDClient) GetImportableNsxtTier0RouterByName(name, nsxtManagerId queryParameters.Add("filter", "displayName=="+name) */ - nsxtTier0Routers, err := vcdCli.GetAllImportableNsxtTier0Routers(nsxtManagerId, nil) + nsxtTier0Routers, err := vcdClient.GetAllImportableNsxtTier0Routers(nsxtManagerId, nil) if err != nil { return nil, fmt.Errorf("could not find NSX-T Tier-0 router with name '%s' for NSX-T manager with id '%s': %s", name, nsxtManagerId, err) @@ -94,18 +94,18 @@ func filterNsxtTier0RoutersInExternalNetworks(name string, allNnsxtTier0Routers // // Note. IDs of Tier-0 routers do not have a standard and may look as strings when they are created using UI or as UUIDs // when they are created using API -func (vcdCli *VCDClient) GetAllImportableNsxtTier0Routers(nsxtManagerId string, queryParameters url.Values) ([]*NsxtTier0Router, error) { +func (vcdClient *VCDClient) GetAllImportableNsxtTier0Routers(nsxtManagerId string, queryParameters url.Values) ([]*NsxtTier0Router, error) { if !isUrn(nsxtManagerId) { return nil, fmt.Errorf("NSX-T manager ID is not URN (e.g. 'urn:vcloud:nsxtmanager:09722307-aee0-4623-af95-7f8e577c9ebc)', got: %s", nsxtManagerId) } endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointImportableTier0Routers - minimumApiVersion, err := vcdCli.Client.checkOpenApiEndpointCompatibility(endpoint) + minimumApiVersion, err := vcdClient.Client.checkOpenApiEndpointCompatibility(endpoint) if err != nil { return nil, err } - urlRef, err := vcdCli.Client.OpenApiBuildEndpoint(endpoint) + urlRef, err := vcdClient.Client.OpenApiBuildEndpoint(endpoint) if err != nil { return nil, err } @@ -121,7 +121,7 @@ func (vcdCli *VCDClient) GetAllImportableNsxtTier0Routers(nsxtManagerId string, queryParams := queryParameterFilterAnd("_context=="+nsxtManagerId, queryParameters) typeResponses := []*types.NsxtTier0Router{{}} - err = vcdCli.Client.OpenApiGetAllItems(minimumApiVersion, urlRef, queryParams, &typeResponses) + err = vcdClient.Client.OpenApiGetAllItems(minimumApiVersion, urlRef, queryParams, &typeResponses, nil) if err != nil { return nil, err } @@ -130,7 +130,7 @@ func (vcdCli *VCDClient) GetAllImportableNsxtTier0Routers(nsxtManagerId string, for sliceIndex := range typeResponses { returnObjects[sliceIndex] = &NsxtTier0Router{ NsxtTier0Router: typeResponses[sliceIndex], - client: &vcdCli.Client, + client: &vcdClient.Client, } } diff --git a/govcd/nsxt_transport_zones.go b/govcd/nsxt_transport_zones.go new file mode 100644 index 000000000..76922d9d4 --- /dev/null +++ b/govcd/nsxt_transport_zones.go @@ -0,0 +1,48 @@ +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "github.com/vmware/go-vcloud-director/v2/types/v56" + "net/url" +) + +func (vcdClient *VCDClient) GetAllNsxtTransportZones(nsxtManagerId string, queryParameters url.Values) ([]*types.TransportZone, error) { + if nsxtManagerId == "" { + return nil, fmt.Errorf("empty NSX-T manager ID") + } + + if !isUrn(nsxtManagerId) { + return nil, fmt.Errorf("NSX-T manager ID is not URN (e.g. 'urn:vcloud:nsxtmanager:09722307-aee0-4623-af95-7f8e577c9ebc)', got: %s", nsxtManagerId) + } + + client := vcdClient.Client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointImportableTransportZones + apiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + queryParams := copyOrNewUrlValues(queryParameters) + filterField := "_context" + if client.APIClientVersionIs(">=38.0") { + // field "networkProviderId" does not exist prior to API 38.0, where field "_context" is deprecated + filterField = "networkProviderId" + } + queryParams = queryParameterFilterAnd(fmt.Sprintf("%s==%s", filterField, nsxtManagerId), queryParams) + var typeResponses []*types.TransportZone + err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParams, &typeResponses, nil) + if err != nil { + return nil, err + } + + return typeResponses, nil +} diff --git a/govcd/nsxv_dhcprelay_test.go b/govcd/nsxv_dhcprelay_test.go index ed55cea0a..820b531a4 100644 --- a/govcd/nsxv_dhcprelay_test.go +++ b/govcd/nsxv_dhcprelay_test.go @@ -1,4 +1,4 @@ -// +build nsxv functional ALL +//go:build nsxv || functional || ALL /* * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. diff --git a/govcd/nsxv_distributed_firewall.go b/govcd/nsxv_distributed_firewall.go new file mode 100644 index 000000000..194a5558d --- /dev/null +++ b/govcd/nsxv_distributed_firewall.go @@ -0,0 +1,442 @@ +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "net/http" + "net/url" + "regexp" + "strings" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +// NsxvDistributedFirewall defines a distributed firewall for a NSX-V VDC +type NsxvDistributedFirewall struct { + VdcId string // The ID of the VDC + Configuration *types.FirewallConfiguration // The latest firewall configuration + Etag string + enabled bool // internal flag that signifies whether the firewall is enabled + client *Client // internal usage client + + Services []types.Application // The list of services for this VDC + ServiceGroups []types.ApplicationGroup // The list of service groups for this VDC +} + +// NewNsxvDistributedFirewall creates a new NsxvDistributedFirewall +func NewNsxvDistributedFirewall(client *Client, vdcId string) *NsxvDistributedFirewall { + return &NsxvDistributedFirewall{ + client: client, + VdcId: extractUuid(vdcId), + } +} + +// GetConfiguration retrieves the configuration of a distributed firewall +func (dfw *NsxvDistributedFirewall) GetConfiguration() (*types.FirewallConfiguration, error) { + // Explicitly retrieving only the Layer 3 rules, as we don't need to deal with layer 2 + initialUrl, err := dfw.client.buildUrl("network", "firewall", "globalroot-0", "config", "layer3sections", dfw.VdcId) + if err != nil { + return nil, err + } + + requestUrl, err := url.ParseRequestURI(initialUrl) + if err != nil { + return nil, err + } + + req := dfw.client.NewRequest(nil, http.MethodGet, *requestUrl, nil) + + resp, err := checkResp(dfw.client.Http.Do(req)) + if err != nil { + return nil, err + } + var config types.FirewallConfiguration + + var firewallSection types.FirewallSection + err = decodeBody(types.BodyTypeXML, resp, &firewallSection) + if err != nil { + return nil, err + } + dfw.Etag = resp.Header.Get("etag") + // The ETag header is needed for further operations. Rules insertion and update need to have a + // header "If-Match" with the contents of the ETag from a previous read. + // The same data can be found in the "GenerationNumber" within the section to update. + // The value of the ETag changes at every GET + if dfw.Etag == "" && firewallSection.GenerationNumber != "" { + dfw.Etag = firewallSection.GenerationNumber + } + config.Layer3Sections = &types.Layer3Sections{Section: &firewallSection} + dfw.Configuration = &config + dfw.Configuration.Layer3Sections = config.Layer3Sections + dfw.enabled = true + return &config, nil +} + +// IsEnabled returns true when the distributed firewall is enabled +func (dfw *NsxvDistributedFirewall) IsEnabled() (bool, error) { + if dfw.VdcId == "" { + return false, fmt.Errorf("no VDC set for this NsxvDistributedFirewall") + } + + conf, err := dfw.GetConfiguration() + if err != nil { + return false, nil + } + if dfw.client.APIVersion == "36.0" { + return conf != nil, nil + } + return true, nil +} + +// Enable makes the distributed firewall available +// It fails with a non-NSX-V VDC +func (dfw *NsxvDistributedFirewall) Enable() error { + dfw.enabled = false + if dfw.VdcId == "" { + return fmt.Errorf("no AdminVdc set for this NsxvDistributedFirewall") + } + initialUrl, err := dfw.client.buildUrl("network", "firewall", "vdc", extractUuid(dfw.VdcId)) + if err != nil { + return err + } + + requestUrl, err := url.ParseRequestURI(initialUrl) + if err != nil { + return err + } + + req := dfw.client.NewRequest(nil, http.MethodPost, *requestUrl, nil) + + resp, err := checkResp(dfw.client.Http.Do(req)) + if err != nil { + return err + } + if resp != nil && resp.StatusCode != http.StatusCreated { + return fmt.Errorf("[enable DistributedFirewall] expected status code %d - received %d", http.StatusCreated, resp.StatusCode) + } + dfw.enabled = true + return nil +} + +// Disable removes the availability of a distributed firewall +// WARNING: it also removes all rules +func (dfw *NsxvDistributedFirewall) Disable() error { + if dfw.VdcId == "" { + return fmt.Errorf("no AdminVdc set for this NsxvDistributedFirewall") + } + initialUrl, err := dfw.client.buildUrl("network", "firewall", "vdc", extractUuid(dfw.VdcId)) + if err != nil { + return err + } + + requestUrl, err := url.ParseRequestURI(initialUrl) + if err != nil { + return err + } + + req := dfw.client.NewRequest(nil, http.MethodDelete, *requestUrl, nil) + + resp, err := checkResp(dfw.client.Http.Do(req)) + if err != nil { + // VCD 10.3.x sometimes returns an error even though the removal succeeds + if dfw.client.APIVersion == "36.0" { + conf, _ := dfw.GetConfiguration() + if conf == nil { + return nil + } + } + return fmt.Errorf("error deleting Distributed firewall: %s", err) + + } + if resp != nil && resp.StatusCode != http.StatusNoContent { + return fmt.Errorf("[disable DistributedFirewall] expected status code %d - received %d", http.StatusNoContent, resp.StatusCode) + } + dfw.Configuration = nil + dfw.Services = nil + dfw.ServiceGroups = nil + dfw.enabled = false + return nil +} + +// UpdateConfiguration will either create a new set of rules or update existing ones. +// If the firewall already contains rules, they are overwritten by the ones passed as parameters +func (dfw *NsxvDistributedFirewall) UpdateConfiguration(rules []types.NsxvDistributedFirewallRule) (*types.FirewallConfiguration, error) { + + oldConf, err := dfw.GetConfiguration() + if err != nil { + return nil, err + } + if dfw.Etag == "" { + return nil, fmt.Errorf("error getting ETag from distributed firewall") + } + initialUrl, err := dfw.client.buildUrl("network", "firewall", "globalroot-0", "config", "layer3sections", dfw.VdcId) + if err != nil { + return nil, err + } + + requestUrl, err := url.ParseRequestURI(initialUrl) + if err != nil { + return nil, err + } + + var errorList []string + for i := 0; i < len(rules); i++ { + rules[i].SectionID = oldConf.Layer3Sections.Section.ID + if rules[i].Direction == "" { + errorList = append(errorList, fmt.Sprintf("missing Direction in rule n. %d ", i+1)) + } + if rules[i].PacketType == "" { + errorList = append(errorList, fmt.Sprintf("missing Packet Type in rule n. %d ", i+1)) + } + if rules[i].Action == "" { + errorList = append(errorList, fmt.Sprintf("missing Action in rule n. %d ", i+1)) + } + } + if len(errorList) > 0 { + return nil, fmt.Errorf("missing required elements from rules: %s", strings.Join(errorList, "; ")) + } + + ruleSet := types.FirewallSection{ + ID: oldConf.Layer3Sections.Section.ID, + GenerationNumber: strings.Trim(dfw.Etag, `"`), + Name: dfw.VdcId, + Rule: rules, + } + + var newRuleset types.FirewallSection + + dfw.client.SetCustomHeader(map[string]string{ + "If-Match": strings.Trim(oldConf.Layer3Sections.Section.GenerationNumber, `"`), + }) + defer dfw.client.RemoveCustomHeader() + + contentType := fmt.Sprintf("application/*+xml;version=%s", dfw.client.APIVersion) + + resp, err := dfw.client.ExecuteRequest(requestUrl.String(), http.MethodPut, contentType, + "error updating NSX-V distributed firewall: %s", ruleSet, &newRuleset) + + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("[update DistributedFirewall] expected status code %d - received %d", http.StatusOK, resp.StatusCode) + } + return dfw.GetConfiguration() +} + +// GetServices retrieves the list of services for the current VCD +// If `refresh` = false and the services were already retrieved in a previous operation, +// then it returns the internal values instead of fetching new ones +func (dfw *NsxvDistributedFirewall) GetServices(refresh bool) ([]types.Application, error) { + if dfw.Services != nil && !refresh { + return dfw.Services, nil + } + if dfw.VdcId == "" { + return nil, fmt.Errorf("no AdminVdc set for this NsxvDistributedFirewall") + } + initialUrl, err := dfw.client.buildUrl("network", "services", "application", "scope", extractUuid(dfw.VdcId)) + if err != nil { + return nil, err + } + + requestUrl, err := url.ParseRequestURI(initialUrl) + if err != nil { + return nil, err + } + + req := dfw.client.NewRequest(nil, http.MethodGet, *requestUrl, nil) + + resp, err := checkResp(dfw.client.Http.Do(req)) + if err != nil { + return nil, err + } + if resp != nil && resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("error fetching the services: %s", resp.Status) + } + var applicationList types.ApplicationList + err = decodeBody(types.BodyTypeXML, resp, &applicationList) + if err != nil { + return nil, err + } + dfw.Services = applicationList.Application + return applicationList.Application, nil +} + +// GetServiceGroups retrieves the list of services for the current VDC +// If `refresh` = false and the services were already retrieved in a previous operation, +// then it returns the internal values instead of fetching new ones +func (dfw *NsxvDistributedFirewall) GetServiceGroups(refresh bool) ([]types.ApplicationGroup, error) { + if dfw.ServiceGroups != nil && !refresh { + return dfw.ServiceGroups, nil + } + if dfw.VdcId == "" { + return nil, fmt.Errorf("no AdminVdc set for this NsxvDistributedFirewall") + } + initialUrl, err := dfw.client.buildUrl("network", "services", "applicationgroup", "scope", extractUuid(dfw.VdcId)) + if err != nil { + return nil, err + } + + requestUrl, err := url.ParseRequestURI(initialUrl) + if err != nil { + return nil, err + } + + req := dfw.client.NewRequest(nil, http.MethodGet, *requestUrl, nil) + + resp, err := checkResp(dfw.client.Http.Do(req)) + if err != nil { + return nil, err + } + if resp != nil && resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("error fetching the service groups: %s", resp.Status) + } + var applicationGroupList types.ApplicationGroupList + err = decodeBody(types.BodyTypeXML, resp, &applicationGroupList) + if err != nil { + return nil, err + } + dfw.ServiceGroups = applicationGroupList.ApplicationGroup + return applicationGroupList.ApplicationGroup, nil +} + +// Refresh retrieves fresh values for the distributed firewall rules, services, and service groups +func (dfw *NsxvDistributedFirewall) Refresh() error { + if dfw.VdcId == "" { + return fmt.Errorf("no AdminVdc set for this NsxvDistributedFirewall") + } + + _, err := dfw.GetServices(true) + if err != nil { + return err + } + + _, err = dfw.GetServiceGroups(true) + if err != nil { + return err + } + + _, err = dfw.GetConfiguration() + return err +} + +// GetServiceById retrieves a single service, identified by its ID, for the current VDC +// If the list of services was already retrieved, it uses it, otherwise fetches new ones. +// Returns ErrorEntityNotFound when the requested services was not found +func (dfw *NsxvDistributedFirewall) GetServiceById(serviceId string) (*types.Application, error) { + services, err := dfw.GetServices(false) + if err != nil { + return nil, err + } + for _, app := range services { + if app.ObjectID == serviceId { + return &app, nil + } + } + return nil, ErrorEntityNotFound +} + +// GetServiceByName retrieves a single service, identified by its name, for the current VDC +// If the list of services was already retrieved, it uses it, otherwise fetches new ones. +// Returns ErrorEntityNotFound when the requested service was not found +func (dfw *NsxvDistributedFirewall) GetServiceByName(serviceName string) (*types.Application, error) { + services, err := dfw.GetServices(false) + if err != nil { + return nil, err + } + var foundService types.Application + for _, app := range services { + if app.Name == serviceName { + if foundService.ObjectID != "" { + return nil, fmt.Errorf("more than one service found with name '%s'", serviceName) + } + foundService = app + } + } + if foundService.ObjectID == "" { + return nil, ErrorEntityNotFound + } + return &foundService, nil +} + +// GetServicesByRegex returns a list of services with their names matching the given regular expression +// It may return an empty list (without error) +func (dfw *NsxvDistributedFirewall) GetServicesByRegex(expression string) ([]types.Application, error) { + services, err := dfw.GetServices(false) + if err != nil { + return nil, err + } + searchRegex, err := regexp.Compile(expression) + if err != nil { + return nil, fmt.Errorf("[GetServicesByRegex] error validating regular expression '%s': %s", expression, err) + } + var found []types.Application + for _, app := range services { + if searchRegex.MatchString(app.Name) { + found = append(found, app) + } + } + return found, nil +} + +// GetServiceGroupById retrieves a single service group, identified by its ID, for the current VDC +// If the list of service groups was already retrieved, it uses it, otherwise fetches new ones. +// Returns ErrorEntityNotFound when the requested service group was not found +func (dfw *NsxvDistributedFirewall) GetServiceGroupById(serviceGroupId string) (*types.ApplicationGroup, error) { + serviceGroups, err := dfw.GetServiceGroups(false) + if err != nil { + return nil, err + } + for _, appGroup := range serviceGroups { + if appGroup.ObjectID == serviceGroupId { + return &appGroup, nil + } + } + return nil, ErrorEntityNotFound +} + +// GetServiceGroupByName retrieves a single service group, identified by its name, for the current VDC +// If the list of service groups was already retrieved, it uses it, otherwise fetches new ones. +// Returns ErrorEntityNotFound when the requested service group was not found +func (dfw *NsxvDistributedFirewall) GetServiceGroupByName(serviceGroupName string) (*types.ApplicationGroup, error) { + serviceGroups, err := dfw.GetServiceGroups(false) + if err != nil { + return nil, err + } + var foundAppGroup types.ApplicationGroup + for _, appGroup := range serviceGroups { + if appGroup.Name == serviceGroupName { + if foundAppGroup.ObjectID != "" { + return nil, fmt.Errorf("more than one service group found with name %s", serviceGroupName) + } + foundAppGroup = appGroup + } + } + if foundAppGroup.ObjectID == "" { + return nil, ErrorEntityNotFound + } + return &foundAppGroup, nil +} + +// GetServiceGroupsByRegex returns a list of services with their names matching the given regular expression +// It may return an empty list (without error) +func (dfw *NsxvDistributedFirewall) GetServiceGroupsByRegex(expression string) ([]types.ApplicationGroup, error) { + serviceGroups, err := dfw.GetServiceGroups(false) + if err != nil { + return nil, err + } + searchRegex, err := regexp.Compile(expression) + if err != nil { + return nil, fmt.Errorf("[GetServiceGroupsByRegex] error validating regular expression '%s': %s", expression, err) + } + var found []types.ApplicationGroup + for _, appGroup := range serviceGroups { + if searchRegex.MatchString(appGroup.Name) { + found = append(found, appGroup) + } + } + return found, nil +} diff --git a/govcd/nsxv_distributed_firewall_test.go b/govcd/nsxv_distributed_firewall_test.go new file mode 100644 index 000000000..ec0b86ce3 --- /dev/null +++ b/govcd/nsxv_distributed_firewall_test.go @@ -0,0 +1,321 @@ +//go:build functional || network || ALL + +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ +package govcd + +import ( + "fmt" + "strings" + + "github.com/kr/pretty" + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_NsxvDistributedFirewall(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + + org, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(org, NotNil) + + if vcd.config.VCD.Nsxt.Vdc != "" { + if testVerbose { + fmt.Println("Testing attempted access to NSX-T VDC") + } + // Retrieve a NSX-T VDC + nsxtVdc, err := org.GetAdminVDCByName(vcd.config.VCD.Nsxt.Vdc, false) + check.Assert(err, IsNil) + + notWorkingDfw := NewNsxvDistributedFirewall(nsxtVdc.client, nsxtVdc.AdminVdc.ID) + check.Assert(notWorkingDfw, NotNil) + + isEnabled, err := notWorkingDfw.IsEnabled() + check.Assert(err, IsNil) + check.Assert(isEnabled, Equals, false) + + err = notWorkingDfw.Enable() + // NSX-T VDCs don't support NSX-V distributed firewalls. We expect an error here. + check.Assert(err, NotNil) + check.Assert(strings.Contains(err.Error(), "Forbidden"), Equals, true) + if testVerbose { + fmt.Printf("notWorkingDfw: %s\n", err) + } + config, err := notWorkingDfw.GetConfiguration() + // Also this operation should fail + check.Assert(err, NotNil) + check.Assert(config, IsNil) + } + + // Retrieve a NSX-V VDC + vdc, err := org.GetAdminVDCByName(vcd.config.VCD.Vdc, false) + check.Assert(err, IsNil) + check.Assert(vdc, NotNil) + + dfw := NewNsxvDistributedFirewall(vdc.client, vdc.AdminVdc.ID) + check.Assert(dfw, NotNil) + + // dfw.Enable is an idempotent operation. It can be repeated on an already enabled DFW + // without errors. + err = dfw.Enable() + check.Assert(err, IsNil) + + enabled, err := dfw.IsEnabled() + check.Assert(err, IsNil) + check.Assert(enabled, Equals, true) + + config, err := dfw.GetConfiguration() + check.Assert(err, IsNil) + check.Assert(config, NotNil) + if testVerbose { + fmt.Printf("%# v\n", pretty.Formatter(config)) + } + + // Repeat enable operation + err = dfw.Enable() + check.Assert(err, IsNil) + + enabled, err = dfw.IsEnabled() + check.Assert(err, IsNil) + check.Assert(enabled, Equals, true) + + err = dfw.Disable() + check.Assert(err, IsNil) + enabled, err = dfw.IsEnabled() + check.Assert(err, IsNil) + check.Assert(enabled, Equals, false) + + // Also dfw.Disable is idempotent + err = dfw.Disable() + check.Assert(err, IsNil) + + enabled, err = dfw.IsEnabled() + check.Assert(err, IsNil) + check.Assert(enabled, Equals, false) +} + +func (vcd *TestVCD) Test_NsxvDistributedFirewallUpdate(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + + org, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(org, NotNil) + + // Retrieve a NSX-V VDC + adminVdc, err := org.GetAdminVDCByName(vcd.config.VCD.Vdc, false) + check.Assert(err, IsNil) + check.Assert(adminVdc, NotNil) + vdc, err := org.GetVDCByName(vcd.config.VCD.Vdc, false) + check.Assert(err, IsNil) + + dfw := NewNsxvDistributedFirewall(adminVdc.client, adminVdc.AdminVdc.ID) + check.Assert(dfw, NotNil) + enabled, err := dfw.IsEnabled() + check.Assert(err, IsNil) + // + if enabled { + check.Skip(fmt.Sprintf("VDC %s already contains a distributed firewall - skipping", vcd.config.VCD.Vdc)) + } + + vms, err := vdc.QueryVmList(types.VmQueryFilterOnlyDeployed) + check.Assert(err, IsNil) + + sampleDestination := &types.Destinations{} + if len(vms) > 0 { + sampleDestination.Destination = []types.Destination{ + { + Name: vms[0].Name, + Value: extractUuid(vms[0].HREF), + Type: types.DFWElementVirtualMachine, + IsValid: true, + }, + } + } + err = dfw.Enable() + check.Assert(err, IsNil) + + dnsService, err := dfw.GetServiceByName("DNS") + check.Assert(err, IsNil) + integrationServiceGroup, err := dfw.GetServiceGroupByName("MSSQL Integration Services") + check.Assert(err, IsNil) + + network, err := vdc.GetOrgVdcNetworkByName(vcd.config.VCD.Network.Net1, false) + check.Assert(err, IsNil) + AddToCleanupList(vcd.config.VCD.Vdc, "nsxv_dfw", vcd.config.VCD.Org, check.TestName()) + rules := []types.NsxvDistributedFirewallRule{ + { + Name: "first", + Action: types.DFWActionDeny, + AppliedToList: &types.AppliedToList{ + AppliedTo: []types.AppliedTo{ + { + Name: adminVdc.AdminVdc.Name, + Value: adminVdc.AdminVdc.ID, + Type: "VDC", + IsValid: true, + }, + }, + }, + Direction: types.DFWDirectionInout, + PacketType: types.DFWPacketAny, + }, + { + Name: "second", + AppliedToList: &types.AppliedToList{}, + SectionID: nil, + Sources: nil, + Destinations: nil, + Services: nil, + Direction: types.DFWDirectionIn, + PacketType: types.DFWPacketAny, + Action: types.DFWActionAllow, + }, + { + Name: "third", + Action: types.DFWActionAllow, + AppliedToList: &types.AppliedToList{}, + Sources: &types.Sources{ + Source: []types.Source{ + // Anonymous source + { + Name: "10.10.10.1", + Value: "10.10.10.1", + Type: types.DFWElementIpv4, + }, + // Named source + { + Name: network.OrgVDCNetwork.Name, + Value: extractUuid(network.OrgVDCNetwork.ID), + Type: types.DFWElementNetwork, + IsValid: true, + }, + }, + }, + Destinations: sampleDestination, + Services: &types.Services{ + Service: []types.Service{ + // Anonymous service + { + IsValid: true, + SourcePort: addrOf("1000"), + DestinationPort: addrOf("1200"), + Protocol: addrOf(types.NsxvProtocolCodes[types.DFWProtocolTcp]), + ProtocolName: addrOf(types.DFWProtocolTcp), + }, + // Named service + { + IsValid: true, + Name: dnsService.Name, + Value: dnsService.ObjectID, + Type: types.DFWServiceTypeApplication, + }, + // Named service group + { + IsValid: true, + Name: integrationServiceGroup.Name, + Value: integrationServiceGroup.ObjectID, + Type: types.DFWServiceTypeApplicationGroup, + }, + }, + }, + Direction: types.DFWDirectionIn, + PacketType: types.DFWPacketIpv4, + }, + } + + updatedRules, err := dfw.UpdateConfiguration(rules) + check.Assert(err, IsNil) + check.Assert(updatedRules, NotNil) + + err = dfw.Disable() + check.Assert(err, IsNil) + +} + +func (vcd *TestVCD) Test_NsxvServices(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + + org, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(org, NotNil) + + // Retrieve a NSX-V VDC + vdc, err := org.GetAdminVDCByName(vcd.config.VCD.Vdc, false) + check.Assert(err, IsNil) + check.Assert(vdc, NotNil) + + dfw := NewNsxvDistributedFirewall(vdc.client, vdc.AdminVdc.ID) + check.Assert(dfw, NotNil) + + services, err := dfw.GetServices(false) + check.Assert(err, IsNil) + check.Assert(services, NotNil) + check.Assert(len(services) > 0, Equals, true) + + if testVerbose { + fmt.Printf("services: %d\n", len(services)) + fmt.Printf("%# v\n", pretty.Formatter(services[0])) + } + + serviceName := "PostgreSQL" + serviceByName, err := dfw.GetServiceByName(serviceName) + + check.Assert(err, IsNil) + check.Assert(serviceByName, NotNil) + check.Assert(serviceByName.Name, Equals, serviceName) + + serviceById, err := dfw.GetServiceById(serviceByName.ObjectID) + check.Assert(err, IsNil) + check.Assert(serviceById.Name, Equals, serviceName) + + searchRegex := "M.SQL" // Finds, among others, names containing "MySQL" or "MSSQL" + servicesByRegex, err := dfw.GetServicesByRegex(searchRegex) + check.Assert(err, IsNil) + check.Assert(len(servicesByRegex) > 1, Equals, true) + + searchRegex = "." // Finds all services + servicesByRegex, err = dfw.GetServicesByRegex(searchRegex) + check.Assert(err, IsNil) + check.Assert(len(servicesByRegex), Equals, len(services)) + + searchRegex = "^####--no-such-service--####$" // Finds no services + servicesByRegex, err = dfw.GetServicesByRegex(searchRegex) + check.Assert(err, IsNil) + check.Assert(len(servicesByRegex), Equals, 0) + + serviceGroups, err := dfw.GetServiceGroups(false) + check.Assert(err, IsNil) + check.Assert(serviceGroups, NotNil) + check.Assert(len(serviceGroups) > 0, Equals, true) + if testVerbose { + fmt.Printf("service groups: %d\n", len(serviceGroups)) + fmt.Printf("%# v\n", pretty.Formatter(serviceGroups[0])) + } + serviceGroupName := "Orchestrator" + serviceGroupByName, err := dfw.GetServiceGroupByName(serviceGroupName) + check.Assert(err, IsNil) + check.Assert(serviceGroupByName, NotNil) + check.Assert(serviceGroupByName.Name, Equals, serviceGroupName) + + serviceGroupById, err := dfw.GetServiceGroupById(serviceGroupByName.ObjectID) + check.Assert(err, IsNil) + check.Assert(serviceGroupById, NotNil) + check.Assert(serviceGroupById.Name, Equals, serviceGroupName) + + searchRegex = "Oracle" + serviceGroupsByRegex, err := dfw.GetServiceGroupsByRegex(searchRegex) + check.Assert(err, IsNil) + check.Assert(len(serviceGroupsByRegex) > 1, Equals, true) + + searchRegex = "." + serviceGroupsByRegex, err = dfw.GetServiceGroupsByRegex(searchRegex) + check.Assert(err, IsNil) + check.Assert(len(serviceGroupsByRegex), Equals, len(serviceGroups)) + + searchRegex = "^####--no-such-service-group--####$" + serviceGroupsByRegex, err = dfw.GetServiceGroupsByRegex(searchRegex) + check.Assert(err, IsNil) + check.Assert(len(serviceGroupsByRegex), Equals, 0) +} diff --git a/govcd/nsxv_firewall.go b/govcd/nsxv_firewall.go index 89b7cd551..0c31d383d 100644 --- a/govcd/nsxv_firewall.go +++ b/govcd/nsxv_firewall.go @@ -26,7 +26,7 @@ type responseEdgeFirewallRules struct { EdgeFirewallRules requestEdgeFirewallRules `xml:"firewallRules"` } -// CreateNsxvFirewallRule creates firewall rule using proxied NSX-V API. It is a synchronuous operation. +// CreateNsxvFirewallRule creates firewall rule using proxied NSX-V API. It is a synchronous operation. // It returns an object with all fields populated (including ID) // If aboveRuleId is not empty, it will send a query parameter aboveRuleId= which instructs NSX to // place this rule above the specified rule ID diff --git a/govcd/nsxv_firewall_test.go b/govcd/nsxv_firewall_test.go index c22e4b084..c8dd511cf 100644 --- a/govcd/nsxv_firewall_test.go +++ b/govcd/nsxv_firewall_test.go @@ -1,4 +1,4 @@ -// +build nsxv edge firewall functional ALL +//go:build nsxv || edge || firewall || functional || ALL /* * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -98,13 +98,13 @@ func (vcd *TestVCD) Test_NsxvFirewallRuleIpSets(check *C) { ipSetConfig1 := &types.EdgeIpSet{ Name: "test-ipset-1", IPAddresses: "10.10.10.1", - InheritanceAllowed: takeBoolPointer(true), // Must be true to allow using it in firewall rule + InheritanceAllowed: addrOf(true), // Must be true to allow using it in firewall rule } ipSetConfig2 := &types.EdgeIpSet{ Name: "test-ipset-2", IPAddresses: "192.168.1.1-192.168.1.200", - InheritanceAllowed: takeBoolPointer(true), // Must be true to allow using it in firewall rule + InheritanceAllowed: addrOf(true), // Must be true to allow using it in firewall rule } // Set parent entity and create two IP sets for usage in firewall rule diff --git a/govcd/nsxv_ipset_test.go b/govcd/nsxv_ipset_test.go index d711e0986..4491f4ebb 100644 --- a/govcd/nsxv_ipset_test.go +++ b/govcd/nsxv_ipset_test.go @@ -1,4 +1,4 @@ -// +build nsxv functional ALL +//go:build nsxv || functional || ALL /* * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -46,7 +46,7 @@ func (vcd *TestVCD) Test_NsxvIpSet(check *C) { // returned, next time it shuffles it again so one can not rely on order of list returned. // IPAddresses: "192.168.200.1-192.168.200.24,192.168.200.1,192.168.200.1/24", IPAddresses: "192.168.200.1/24", - InheritanceAllowed: takeBoolPointer(false), + InheritanceAllowed: addrOf(false), } createdIpSet, err := vdc.CreateNsxvIpSet(ipSetConfig) @@ -86,7 +86,7 @@ func (vcd *TestVCD) Test_NsxvIpSet(check *C) { check.Assert(ipSetByName, DeepEquals, ipSetById2) // 5. Update IP set field - createdIpSet.InheritanceAllowed = takeBoolPointer(true) + createdIpSet.InheritanceAllowed = addrOf(true) updatedIpSet, err := vcd.vdc.UpdateNsxvIpSet(createdIpSet) check.Assert(err, IsNil) @@ -122,7 +122,7 @@ func testCreateIpSet(name string, vdc *Vdc) (*types.EdgeIpSet, error) { Name: name, Description: "test-ipset-description", IPAddresses: "192.168.200.1/24", - InheritanceAllowed: takeBoolPointer(true), + InheritanceAllowed: addrOf(true), } return vdc.CreateNsxvIpSet(ipSetConfig) diff --git a/govcd/nsxv_nat_test.go b/govcd/nsxv_nat_test.go index 51d1d4558..be7d9f9ff 100644 --- a/govcd/nsxv_nat_test.go +++ b/govcd/nsxv_nat_test.go @@ -1,4 +1,4 @@ -// +build edge nat nsxv functional ALL +//go:build edge || nat || nsxv || functional || ALL /* * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. diff --git a/govcd/openapi.go b/govcd/openapi.go index 0c218f16e..7664f8e9d 100644 --- a/govcd/openapi.go +++ b/govcd/openapi.go @@ -1,7 +1,7 @@ package govcd /* - * Copyright 2020 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + * Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. */ import ( @@ -9,10 +9,10 @@ import ( "encoding/json" "fmt" "io" - "io/ioutil" "net/http" "net/url" "reflect" + "strconv" "strings" "github.com/peterhellberg/link" @@ -72,7 +72,7 @@ func (client *Client) OpenApiBuildEndpoint(endpoint ...string) (*url.URL, error) // must be a slice of object (e.g. []*types.OpenAPIEdgeGateway) because this response contains slice of structs. // // Note. Query parameter 'pageSize' is defaulted to 128 (maximum supported) unless it is specified in queryParams -func (client *Client) OpenApiGetAllItems(apiVersion string, urlRef *url.URL, queryParams url.Values, outType interface{}) error { +func (client *Client) OpenApiGetAllItems(apiVersion string, urlRef *url.URL, queryParams url.Values, outType interface{}, additionalHeader map[string]string) error { // copy passed in URL ref so that it is not mutated urlRefCopy := copyUrlRef(urlRef) @@ -90,7 +90,7 @@ func (client *Client) OpenApiGetAllItems(apiVersion string, urlRef *url.URL, que // Perform API call to initial endpoint. The function call recursively follows pages using Link headers "nextPage" // until it crawls all results - responses, err := client.openApiGetAllPages(apiVersion, urlRefCopy, newQueryParams, outType, nil) + responses, err := client.openApiGetAllPages(apiVersion, urlRefCopy, newQueryParams, outType, nil, additionalHeader) if err != nil { return fmt.Errorf("error getting all pages for endpoint %s: %s", urlRefCopy.String(), err) } @@ -118,8 +118,18 @@ func (client *Client) OpenApiGetAllItems(apiVersion string, urlRef *url.URL, que // The urlRef must point to ID of exact item (e.g. '/1.0.0/edgeGateways/{EDGE_ID}') // It responds with HTTP 403: Forbidden - If the user is not authorized or the entity does not exist. When HTTP 403 is // returned this function returns "ErrorEntityNotFound: API_ERROR" so that one can use ContainsNotFound(err) to -// differentiate when an objects was not found from any other error. -func (client *Client) OpenApiGetItem(apiVersion string, urlRef *url.URL, params url.Values, outType interface{}) error { +// differentiate when an object was not found from any other error. +func (client *Client) OpenApiGetItem(apiVersion string, urlRef *url.URL, params url.Values, outType interface{}, additionalHeader map[string]string) error { + _, err := client.OpenApiGetItemAndHeaders(apiVersion, urlRef, params, outType, additionalHeader) + return err +} + +// OpenApiGetItemAndHeaders is a low level OpenAPI client function to perform GET request for any item and return all the headers. +// The urlRef must point to ID of exact item (e.g. '/1.0.0/edgeGateways/{EDGE_ID}') +// It responds with HTTP 403: Forbidden - If the user is not authorized or the entity does not exist. When HTTP 403 is +// returned this function returns "ErrorEntityNotFound: API_ERROR" so that one can use ContainsNotFound(err) to +// differentiate when an object was not found from any other error. +func (client *Client) OpenApiGetItemAndHeaders(apiVersion string, urlRef *url.URL, params url.Values, outType interface{}, additionalHeader map[string]string) (http.Header, error) { // copy passed in URL ref so that it is not mutated urlRefCopy := copyUrlRef(urlRef) @@ -127,13 +137,13 @@ func (client *Client) OpenApiGetItem(apiVersion string, urlRef *url.URL, params urlRefCopy.String(), reflect.TypeOf(outType)) if !client.OpenApiIsSupported() { - return fmt.Errorf("OpenAPI is not supported on this VCD version") + return nil, fmt.Errorf("OpenAPI is not supported on this VCD version") } - req := client.newOpenApiRequest(apiVersion, params, http.MethodGet, urlRefCopy, nil) + req := client.newOpenApiRequest(apiVersion, params, http.MethodGet, urlRefCopy, nil, additionalHeader) resp, err := client.Http.Do(req) if err != nil { - return fmt.Errorf("error performing GET request to %s: %s", urlRefCopy.String(), err) + return nil, fmt.Errorf("error performing GET request to %s: %s", urlRefCopy.String(), err) } // Bypassing the regular path using function checkRespWithErrType and returning parsed error directly @@ -141,7 +151,7 @@ func (client *Client) OpenApiGetItem(apiVersion string, urlRef *url.URL, params if resp.StatusCode == http.StatusForbidden { err := ParseErr(types.BodyTypeJSON, resp, &types.OpenApiError{}) closeErr := resp.Body.Close() - return fmt.Errorf("%s: %s [body close error: %s]", ErrorEntityNotFound, err, closeErr) + return nil, fmt.Errorf("%s: %s [body close error: %s]", ErrorEntityNotFound, err, closeErr) } // resp is ignored below because it is the same as above @@ -149,24 +159,24 @@ func (client *Client) OpenApiGetItem(apiVersion string, urlRef *url.URL, params // Any other error occurred if err != nil { - return fmt.Errorf("error in HTTP GET request: %s", err) + return nil, fmt.Errorf("error in HTTP GET request: %s", err) } if err = decodeBody(types.BodyTypeJSON, resp, outType); err != nil { - return fmt.Errorf("error decoding JSON response after GET: %s", err) + return nil, fmt.Errorf("error decoding JSON response after GET: %s", err) } err = resp.Body.Close() if err != nil { - return fmt.Errorf("error closing response body: %s", err) + return nil, fmt.Errorf("error closing response body: %s", err) } - return nil + return resp.Header, nil } // OpenApiPostItemSync is a low level OpenAPI client function to perform POST request for items that support synchronous // requests. The urlRef must point to POST endpoint (e.g. '/1.0.0/edgeGateways') that supports synchronous requests. It -// will return an error when endpoint does not support synchronous requests (HTTP response status code is not 201). +// will return an error when endpoint does not support synchronous requests (HTTP response status code is not 200 or 201). // Response will be unmarshalled into outType. // // Note. Even though it may return error if the item does not support synchronous request - the object may still be @@ -182,13 +192,14 @@ func (client *Client) OpenApiPostItemSync(apiVersion string, urlRef *url.URL, pa return fmt.Errorf("OpenAPI is not supported on this VCD version") } - resp, err := client.openApiPerformPostPut(http.MethodPost, apiVersion, urlRefCopy, params, payload) + resp, err := client.openApiPerformPostPut(http.MethodPost, apiVersion, urlRefCopy, params, payload, nil) if err != nil { return err } - if resp.StatusCode != http.StatusCreated { - util.Logger.Printf("[TRACE] Synchronous task expected (HTTP status code 201). Got %d", resp.StatusCode) + if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK { + util.Logger.Printf("[TRACE] Synchronous task expected (HTTP status code 200 or 201). Got %d", resp.StatusCode) + return fmt.Errorf("POST request expected sync task (HTTP response 200 or 201), got %d", resp.StatusCode) } @@ -211,6 +222,16 @@ func (client *Client) OpenApiPostItemSync(apiVersion string, urlRef *url.URL, pa // Note. Even though it may return error if the item does not support asynchronous request - the object may still be // created. OpenApiPostItem would handle both cases and always return created item. func (client *Client) OpenApiPostItemAsync(apiVersion string, urlRef *url.URL, params url.Values, payload interface{}) (Task, error) { + return client.OpenApiPostItemAsyncWithHeaders(apiVersion, urlRef, params, payload, nil) +} + +// OpenApiPostItemAsyncWithHeaders is a low level OpenAPI client function to perform POST request for items that support +// asynchronous requests. The urlRef must point to POST endpoint (e.g. '/1.0.0/edgeGateways') that supports asynchronous +// requests. It will return an error if item does not support asynchronous request (does not respond with HTTP 202). +// +// Note. Even though it may return error if the item does not support asynchronous request - the object may still be +// created. OpenApiPostItem would handle both cases and always return created item. +func (client *Client) OpenApiPostItemAsyncWithHeaders(apiVersion string, urlRef *url.URL, params url.Values, payload interface{}, additionalHeader map[string]string) (Task, error) { // copy passed in URL ref so that it is not mutated urlRefCopy := copyUrlRef(urlRef) @@ -221,7 +242,7 @@ func (client *Client) OpenApiPostItemAsync(apiVersion string, urlRef *url.URL, p return Task{}, fmt.Errorf("OpenAPI is not supported on this VCD version") } - resp, err := client.openApiPerformPostPut(http.MethodPost, apiVersion, urlRefCopy, params, payload) + resp, err := client.openApiPerformPostPut(http.MethodPost, apiVersion, urlRefCopy, params, payload, additionalHeader) if err != nil { return Task{}, err } @@ -230,6 +251,10 @@ func (client *Client) OpenApiPostItemAsync(apiVersion string, urlRef *url.URL, p return Task{}, fmt.Errorf("POST request expected async task (HTTP response 202), got %d", resp.StatusCode) } + // The response shouldn't have payload as the main information should be in "Location" header + util.ProcessResponseOutput(util.FuncNameCallStack(), resp, "") + debugShowResponse(resp, []byte("SKIPPED RESPONSE")) + err = resp.Body.Close() if err != nil { return Task{}, fmt.Errorf("error closing response body: %s", err) @@ -249,7 +274,15 @@ func (client *Client) OpenApiPostItemAsync(apiVersion string, urlRef *url.URL, p // OpenApiPostItem is a low level OpenAPI client function to perform POST request for item supporting synchronous or // asynchronous requests. The urlRef must point to POST endpoint (e.g. '/1.0.0/edgeGateways'). When a task is // synchronous - it will track task until it is finished and pick reference to marshal outType. -func (client *Client) OpenApiPostItem(apiVersion string, urlRef *url.URL, params url.Values, payload, outType interface{}) error { +func (client *Client) OpenApiPostItem(apiVersion string, urlRef *url.URL, params url.Values, payload, outType interface{}, additionalHeader map[string]string) error { + _, err := client.OpenApiPostItemAndGetHeaders(apiVersion, urlRef, params, payload, outType, additionalHeader) + return err +} + +// OpenApiPostItemAndGetHeaders is a low level OpenAPI client function to perform POST request for item supporting synchronous or +// asynchronous requests, that returns also the response headers. The urlRef must point to POST endpoint (e.g. '/1.0.0/edgeGateways'). When a task is +// synchronous - it will track task until it is finished and pick reference to marshal outType. +func (client *Client) OpenApiPostItemAndGetHeaders(apiVersion string, urlRef *url.URL, params url.Values, payload, outType interface{}, additionalHeader map[string]string) (http.Header, error) { // copy passed in URL ref so that it is not mutated urlRefCopy := copyUrlRef(urlRef) @@ -257,15 +290,15 @@ func (client *Client) OpenApiPostItem(apiVersion string, urlRef *url.URL, params reflect.TypeOf(payload), urlRefCopy.String(), reflect.TypeOf(outType)) if !client.OpenApiIsSupported() { - return fmt.Errorf("OpenAPI is not supported on this VCD version") + return nil, fmt.Errorf("OpenAPI is not supported on this VCD version") } - resp, err := client.openApiPerformPostPut(http.MethodPost, apiVersion, urlRefCopy, params, payload) + resp, err := client.openApiPerformPostPut(http.MethodPost, apiVersion, urlRefCopy, params, payload, additionalHeader) if err != nil { - return err + return nil, err } - // Handle two cases of API behaviour - synchronous (response status code is 201) and asynchronous (response status + // Handle two cases of API behaviour - synchronous (response status code is 200 or 201) and asynchronous (response status // code 202) switch resp.StatusCode { // Asynchronous case - must track task and get item HREF from there @@ -276,27 +309,77 @@ func (client *Client) OpenApiPostItem(apiVersion string, urlRef *url.URL, params task.Task.HREF = taskUrl err = task.WaitTaskCompletion() if err != nil { - return fmt.Errorf("error waiting completion of task (%s): %s", taskUrl, err) + return nil, fmt.Errorf("error waiting completion of task (%s): %s", taskUrl, err) } // Here we have to find the resource once more to return it populated. // Task Owner ID is the ID of created object. ID must be used (although HREF exists in task) because HREF points to // old XML API and here we need to pull data from OpenAPI. - newObjectUrl, _ := url.ParseRequestURI(urlRefCopy.String() + task.Task.Owner.ID) - err = client.OpenApiGetItem(apiVersion, newObjectUrl, nil, outType) + newObjectUrl := urlParseRequestURI(urlRefCopy.String() + task.Task.Owner.ID) + err = client.OpenApiGetItem(apiVersion, newObjectUrl, nil, outType, additionalHeader) if err != nil { - return fmt.Errorf("error retrieving item after creation: %s", err) + return nil, fmt.Errorf("error retrieving item after creation: %s", err) } // Synchronous task - new item body is returned in response of HTTP POST request - case http.StatusCreated: - util.Logger.Printf("[TRACE] Synchronous task detected, marshalling outType '%s'", reflect.TypeOf(outType)) + case http.StatusCreated, http.StatusOK: + util.Logger.Printf("[TRACE] Synchronous task detected (HTTP Status %d), marshalling outType '%s'", resp.StatusCode, reflect.TypeOf(outType)) if err = decodeBody(types.BodyTypeJSON, resp, outType); err != nil { - return fmt.Errorf("error decoding JSON response after POST: %s", err) + return nil, fmt.Errorf("error decoding JSON response after POST: %s", err) } } + err = resp.Body.Close() + if err != nil { + return nil, fmt.Errorf("error closing response body: %s", err) + } + + return resp.Header, nil +} + +// OpenApiPostUrlEncoded is a non-standard function used to send a POST request with `x-www-form-urlencoded` format. +// Accepts a map in format of key:value, marshals the response body in JSON format to outType. +// If additionalHeader contains a "Content-Type" header, it will be overwritten to "x-www-form-urlencoded" +func (client *Client) OpenApiPostUrlEncoded(apiVersion string, urlRef *url.URL, params url.Values, payloadMap map[string]string, outType interface{}, additionalHeaders map[string]string) error { + urlRefCopy := copyUrlRef(urlRef) + + util.Logger.Printf("[TRACE] Sending a POST request with 'Content-Type: x-www-form-urlencoded' header to endpoint %s with expected response of type %s", urlRefCopy.String(), reflect.TypeOf(outType)) + + // Add all values of the payloadMap to the actual payload + urlValues := url.Values{} + for key, value := range payloadMap { + urlValues.Add(key, value) + } + body := strings.NewReader(urlValues.Encode()) + + // Create the header map if it's nil + if additionalHeaders == nil { + additionalHeaders = make(map[string]string) + } + // Overwrite the Content-Type header as this is a method only usable for x-www-form-urlencoded + additionalHeaders["Content-Type"] = "application/x-www-form-urlencoded" + + req := client.newOpenApiRequest(apiVersion, params, http.MethodPost, urlRef, body, additionalHeaders) + resp, err := client.Http.Do(req) + if err != nil { + return err + } + + // resp is ignored below because it is the same the one above + _, err = checkRespWithErrType(types.BodyTypeJSON, resp, err, &types.OpenApiError{}) + if err != nil { + return fmt.Errorf("error in HTTP %s request: %s", http.MethodPost, err) + } + + if resp.StatusCode != http.StatusOK { + util.Logger.Printf("[TRACE] HTTP status code 200 expected. Got %d", resp.StatusCode) + } + + if err = decodeBody(types.BodyTypeJSON, resp, outType); err != nil { + return fmt.Errorf("error decoding JSON response after POST: %s", err) + } + err = resp.Body.Close() if err != nil { return fmt.Errorf("error closing response body: %s", err) @@ -312,7 +395,7 @@ func (client *Client) OpenApiPostItem(apiVersion string, urlRef *url.URL, params // // Note. Even though it may return error if the item does not support synchronous request - the object may still be // updated. OpenApiPutItem would handle both cases and always return updated item. -func (client *Client) OpenApiPutItemSync(apiVersion string, urlRef *url.URL, params url.Values, payload, outType interface{}) error { +func (client *Client) OpenApiPutItemSync(apiVersion string, urlRef *url.URL, params url.Values, payload, outType interface{}, additionalHeader map[string]string) error { // copy passed in URL ref so that it is not mutated urlRefCopy := copyUrlRef(urlRef) @@ -323,14 +406,13 @@ func (client *Client) OpenApiPutItemSync(apiVersion string, urlRef *url.URL, par return fmt.Errorf("OpenAPI is not supported on this VCD version") } - resp, err := client.openApiPerformPostPut(http.MethodPut, apiVersion, urlRefCopy, params, payload) + resp, err := client.openApiPerformPostPut(http.MethodPut, apiVersion, urlRefCopy, params, payload, additionalHeader) if err != nil { return err } if resp.StatusCode != http.StatusCreated { util.Logger.Printf("[TRACE] Synchronous task expected (HTTP status code 201). Got %d", resp.StatusCode) - } if err = decodeBody(types.BodyTypeJSON, resp, outType); err != nil { @@ -351,7 +433,7 @@ func (client *Client) OpenApiPutItemSync(apiVersion string, urlRef *url.URL, par // // Note. Even though it may return error if the item does not support asynchronous request - the object may still be // created. OpenApiPutItem would handle both cases and always return created item. -func (client *Client) OpenApiPutItemAsync(apiVersion string, urlRef *url.URL, params url.Values, payload interface{}) (Task, error) { +func (client *Client) OpenApiPutItemAsync(apiVersion string, urlRef *url.URL, params url.Values, payload interface{}, additionalHeader map[string]string) (Task, error) { // copy passed in URL ref so that it is not mutated urlRefCopy := copyUrlRef(urlRef) @@ -361,7 +443,7 @@ func (client *Client) OpenApiPutItemAsync(apiVersion string, urlRef *url.URL, pa if !client.OpenApiIsSupported() { return Task{}, fmt.Errorf("OpenAPI is not supported on this VCD version") } - resp, err := client.openApiPerformPostPut(http.MethodPut, apiVersion, urlRefCopy, params, payload) + resp, err := client.openApiPerformPostPut(http.MethodPut, apiVersion, urlRefCopy, params, payload, additionalHeader) if err != nil { return Task{}, err } @@ -389,7 +471,15 @@ func (client *Client) OpenApiPutItemAsync(apiVersion string, urlRef *url.URL, pa // OpenApiPutItem is a low level OpenAPI client function to perform PUT request for any item. // The urlRef must point to ID of exact item (e.g. '/1.0.0/edgeGateways/{EDGE_ID}') // It handles synchronous and asynchronous tasks. When a task is synchronous - it will block until it is finished. -func (client *Client) OpenApiPutItem(apiVersion string, urlRef *url.URL, params url.Values, payload, outType interface{}) error { +func (client *Client) OpenApiPutItem(apiVersion string, urlRef *url.URL, params url.Values, payload, outType interface{}, additionalHeader map[string]string) error { + _, err := client.OpenApiPutItemAndGetHeaders(apiVersion, urlRef, params, payload, outType, additionalHeader) + return err +} + +// OpenApiPutItemAndGetHeaders is a low level OpenAPI client function to perform PUT request for any item and return the response headers. +// The urlRef must point to ID of exact item (e.g. '/1.0.0/edgeGateways/{EDGE_ID}') +// It handles synchronous and asynchronous tasks. When a task is synchronous - it will block until it is finished. +func (client *Client) OpenApiPutItemAndGetHeaders(apiVersion string, urlRef *url.URL, params url.Values, payload, outType interface{}, additionalHeader map[string]string) (http.Header, error) { // copy passed in URL ref so that it is not mutated urlRefCopy := copyUrlRef(urlRef) @@ -397,12 +487,12 @@ func (client *Client) OpenApiPutItem(apiVersion string, urlRef *url.URL, params reflect.TypeOf(payload), urlRefCopy.String(), reflect.TypeOf(outType)) if !client.OpenApiIsSupported() { - return fmt.Errorf("OpenAPI is not supported on this VCD version") + return nil, fmt.Errorf("OpenAPI is not supported on this VCD version") } - resp, err := client.openApiPerformPostPut(http.MethodPut, apiVersion, urlRefCopy, params, payload) + resp, err := client.openApiPerformPostPut(http.MethodPut, apiVersion, urlRefCopy, params, payload, additionalHeader) if err != nil { - return err + return nil, err } // Handle two cases of API behaviour - synchronous (response status code is 201) and asynchronous (response status @@ -416,35 +506,35 @@ func (client *Client) OpenApiPutItem(apiVersion string, urlRef *url.URL, params task.Task.HREF = taskUrl err = task.WaitTaskCompletion() if err != nil { - return fmt.Errorf("error waiting completion of task (%s): %s", taskUrl, err) + return nil, fmt.Errorf("error waiting completion of task (%s): %s", taskUrl, err) } // Here we have to find the resource once more to return it populated. Provided params ir ignored for retrieval. - err = client.OpenApiGetItem(apiVersion, urlRefCopy, nil, outType) + err = client.OpenApiGetItem(apiVersion, urlRefCopy, nil, outType, additionalHeader) if err != nil { - return fmt.Errorf("error retrieving item after updating: %s", err) + return nil, fmt.Errorf("error retrieving item after updating: %s", err) } // Synchronous task - new item body is returned in response of HTTP PUT request case http.StatusOK: util.Logger.Printf("[TRACE] Synchronous task detected, marshalling outType '%s'", reflect.TypeOf(outType)) if err = decodeBody(types.BodyTypeJSON, resp, outType); err != nil { - return fmt.Errorf("error decoding JSON response after PUT: %s", err) + return nil, fmt.Errorf("error decoding JSON response after PUT: %s", err) } } err = resp.Body.Close() if err != nil { - return fmt.Errorf("error closing HTTP PUT response body: %s", err) + return nil, fmt.Errorf("error closing HTTP PUT response body: %s", err) } - return nil + return resp.Header, nil } // OpenApiDeleteItem is a low level OpenAPI client function to perform DELETE request for any item. // The urlRef must point to ID of exact item (e.g. '/1.0.0/edgeGateways/{EDGE_ID}') // It handles synchronous and asynchronous tasks. When a task is synchronous - it will block until it is finished. -func (client *Client) OpenApiDeleteItem(apiVersion string, urlRef *url.URL, params url.Values) error { +func (client *Client) OpenApiDeleteItem(apiVersion string, urlRef *url.URL, params url.Values, additionalHeader map[string]string) error { // copy passed in URL ref so that it is not mutated urlRefCopy := copyUrlRef(urlRef) @@ -455,13 +545,20 @@ func (client *Client) OpenApiDeleteItem(apiVersion string, urlRef *url.URL, para } // Perform request - req := client.newOpenApiRequest(apiVersion, params, http.MethodDelete, urlRefCopy, nil) + req := client.newOpenApiRequest(apiVersion, params, http.MethodDelete, urlRefCopy, nil, additionalHeader) resp, err := client.Http.Do(req) if err != nil { return err } + bodyBytes, err := rewrapRespBodyNoopCloser(resp) + if err != nil { + return err + } + util.ProcessResponseOutput(util.FuncNameCallStack(), resp, string(bodyBytes)) + debugShowResponse(resp, bodyBytes) + // resp is ignored below because it would be the same as above _, err = checkRespWithErrType(types.BodyTypeJSON, resp, err, &types.OpenApiError{}) if err != nil { @@ -491,9 +588,9 @@ func (client *Client) OpenApiDeleteItem(apiVersion string, urlRef *url.URL, para // openApiPerformPostPut is a shared function for all public PUT and POST function parts - OpenApiPostItemSync, // OpenApiPostItemAsync, OpenApiPostItem, OpenApiPutItemSync, OpenApiPutItemAsync, OpenApiPutItem -func (client *Client) openApiPerformPostPut(httpMethod string, apiVersion string, urlRef *url.URL, params url.Values, payload interface{}) (*http.Response, error) { +func (client *Client) openApiPerformPostPut(httpMethod string, apiVersion string, urlRef *url.URL, params url.Values, payload interface{}, additionalHeader map[string]string) (*http.Response, error) { // Marshal payload if we have one - var body *bytes.Buffer + body := new(bytes.Buffer) if payload != nil { marshaledJson, err := json.MarshalIndent(payload, "", " ") if err != nil { @@ -502,7 +599,7 @@ func (client *Client) openApiPerformPostPut(httpMethod string, apiVersion string body = bytes.NewBuffer(marshaledJson) } - req := client.newOpenApiRequest(apiVersion, params, httpMethod, urlRef, body) + req := client.newOpenApiRequest(apiVersion, params, httpMethod, urlRef, body, additionalHeader) resp, err := client.Http.Do(req) if err != nil { return nil, err @@ -520,7 +617,19 @@ func (client *Client) openApiPerformPostPut(httpMethod string, apiVersion string // works by at first crawling pages and accumulating all responses into []json.RawMessage (as strings). Because there is // no intermediate unmarshalling to exact `outType` for every page it can unmarshal into direct `outType` supplied. // outType must be a slice of object (e.g. []*types.OpenApiRole) because accumulated responses are in JSON list -func (client *Client) openApiGetAllPages(apiVersion string, urlRef *url.URL, queryParams url.Values, outType interface{}, responses []json.RawMessage) ([]json.RawMessage, error) { +// +// It follows pages in two ways: +// * Finds a 'nextPage' link and uses it to recursively crawl all pages (default for all, except for API bug) +// * Uses fields 'resultTotal', 'page', and 'pageSize' to calculate if it should crawl further on. It is only done +// because there is a BUG in API and in some endpoints it does not return 'nextPage' link as well as null 'pageCount' +// +// In general 'nextPage' header is preferred because some endpoints +// (like cloudapi/1.0.0/nsxTResources/importableTier0Routers) do not contain pagination details and nextPage header +// contains a base64 encoded data chunk via a supplied `cursor` field +// (e.g. ...importableTier0Routers?filter=_context==urn:vcloud:nsxtmanager:85aa2514-6a6f-4a32-8904-9695dc0f0298& +// cursor=eyJORVRXT1JLSU5HX0NVUlNPUl9PRkZTRVQiOiIwIiwicGFnZVNpemUiOjEsIk5FVFdPUktJTkdfQ1VSU09SIjoiMDAwMTMifQ==) +// The 'cursor' in example contains such values {"NETWORKING_CURSOR_OFFSET":"0","pageSize":1,"NETWORKING_CURSOR":"00013"} +func (client *Client) openApiGetAllPages(apiVersion string, urlRef *url.URL, queryParams url.Values, outType interface{}, responses []json.RawMessage, additionalHeader map[string]string) ([]json.RawMessage, error) { // copy passed in URL ref so that it is not mutated urlRefCopy := copyUrlRef(urlRef) @@ -529,7 +638,7 @@ func (client *Client) openApiGetAllPages(apiVersion string, urlRef *url.URL, que } // Perform request - req := client.newOpenApiRequest(apiVersion, queryParams, http.MethodGet, urlRefCopy, nil) + req := client.newOpenApiRequest(apiVersion, queryParams, http.MethodGet, urlRefCopy, nil, additionalHeader) resp, err := client.Http.Do(req) if err != nil { @@ -570,18 +679,48 @@ func (client *Client) openApiGetAllPages(apiVersion string, urlRef *url.URL, que } if nextPageUrlRef != nil { - responses, err = client.openApiGetAllPages(apiVersion, nextPageUrlRef, url.Values{}, outType, responses) + responses, err = client.openApiGetAllPages(apiVersion, nextPageUrlRef, url.Values{}, outType, responses, additionalHeader) if err != nil { return nil, fmt.Errorf("got error on page %d: %s", pages.Page, err) } } + // If nextPage header was not found, but we are not at the last page - the query URL should be forged manually to + // overcome OpenAPI BUG when it does not return 'nextPage' header + // Some API calls do not return `OpenApiPages` results at all (just values) + // In some endpoints the page field is returned as `null` and this code block cannot handle it. + if nextPageUrlRef == nil && pages.PageSize != 0 && pages.Page != 0 { + // Next URL page ref was not found therefore one must double-check if it is not an API BUG. There are endpoints which + // return only Total results and pageSize (not 'pageCount' and not 'nextPage' header) + pageCount := pages.ResultTotal / pages.PageSize // This division returns number of "full pages" (containing 'pageSize' amount of results) + if pages.ResultTotal%pages.PageSize > 0 { // Check if is an incomplete page (containing less than 'pageSize' results) + pageCount++ // Total pageCount is "number of complete pages + 1 incomplete" if it exists) + } + if pages.Page < pageCount { + // Clone all originally supplied query parameters to avoid overwriting them + urlQueryString := queryParams.Encode() + urlQuery, err := url.ParseQuery(urlQueryString) + if err != nil { + return nil, fmt.Errorf("error cloning queryParams: %s", err) + } + + // Increase page query by one to fetch "next" page + urlQuery.Set("page", strconv.Itoa(pages.Page+1)) + + responses, err = client.openApiGetAllPages(apiVersion, urlRefCopy, urlQuery, outType, responses, additionalHeader) + if err != nil { + return nil, fmt.Errorf("got error on page %d: %s", pages.Page, err) + } + } + + } + return responses, nil } // newOpenApiRequest is a low level function used in upstream OpenAPI functions which handles logging and // authentication for each API request -func (client *Client) newOpenApiRequest(apiVersion string, params url.Values, method string, reqUrl *url.URL, body io.Reader) *http.Request { +func (client *Client) newOpenApiRequest(apiVersion string, params url.Values, method string, reqUrl *url.URL, body io.Reader, additionalHeader map[string]string) *http.Request { // copy passed in URL ref so that it is not mutated reqUrlCopy := copyUrlRef(reqUrl) @@ -591,15 +730,19 @@ func (client *Client) newOpenApiRequest(apiVersion string, params url.Values, me // If the body contains data - try to read all contents for logging and re-create another // io.Reader with all contents to use it down the line var readBody []byte + var err error if body != nil { - readBody, _ = ioutil.ReadAll(body) + readBody, err = io.ReadAll(body) + if err != nil { + util.Logger.Printf("[DEBUG - newOpenApiRequest] error reading body: %s", err) + } body = bytes.NewReader(readBody) } - // Build the request, no point in checking for errors here as we're just - // passing a string version of an url.URL struct and http.NewRequest returns - // error only if can't process an url.ParseRequestURI(). - req, _ := http.NewRequest(method, reqUrlCopy.String(), body) + req, err := http.NewRequest(method, reqUrlCopy.String(), body) + if err != nil { + util.Logger.Printf("[DEBUG - newOpenApiRequest] error getting new request: %s", err) + } if client.VCDAuthHeader != "" && client.VCDToken != "" { // Add the authorization header @@ -615,10 +758,28 @@ func (client *Client) newOpenApiRequest(apiVersion string, params url.Values, me req.Header.Add("Accept", acceptMime) } - // Inject JSON mime type - req.Header.Add("Content-Type", types.JSONMime) + for k, v := range client.customHeader { + for _, v1 := range v { + req.Header.Set(k, v1) + } + } + for k, v := range additionalHeader { + if strings.Contains(v, "{{MEDIA_TYPE}}") || strings.Contains(v, "{{API_VERSION}}") { + v = strings.Replace(v, "{{MEDIA_TYPE}}", types.JSONMime, 1) + v = strings.Replace(v, "{{API_VERSION}}", apiVersion, 1) + req.Header.Set(k, v) + } else { + req.Header.Add(k, v) + } + } + + // Inject JSON mime type if there are no overwrites + if req.Header.Get("Content-Type") == "" { + req.Header.Add("Content-Type", types.JSONMime) + } setHttpUserAgent(client.UserAgent, req) + setVcloudClientRequestId(client.RequestIdFunc, req) // Avoids passing data if the logging of requests is disabled if util.LogHttpRequest { @@ -724,6 +885,30 @@ func defaultPageSize(queryParams url.Values, defaultPageSize string) url.Values // copyUrlRef creates a copy of URL reference by re-parsing it func copyUrlRef(in *url.URL) *url.URL { // error is ignored because we expect to have correct URL supplied and this greatly simplifies code inside. - newUrlRef, _ := url.Parse(in.String()) + newUrlRef, err := url.Parse(in.String()) + if err != nil { + util.Logger.Printf("[DEBUG - copyUrlRef] error parsing URL: %s", err) + } return newUrlRef } + +// shouldDoSlowSearch returns true and nil url.Values if the filter value contains commas, semicolons or asterisks, +// as the encoding is rejected by VCD with an error: QueryParseException: Cannot parse the supplied filter, so +// the caller knows that it needs to run a brute force search and NOT use filtering in any case. +// Also, url.QueryEscape as well as url.Values.Encode() both encode the space as a + character, so in this case +// it returns true and nil to specify a brute force search too. Reference to issue: +// https://github.com/golang/go/issues/4013 +// https://github.com/czos/goamz/pull/11/files +// When this function returns false, it returns the url.Values that are not encoded, so make sure that the +// client encodes them before sending them. +func shouldDoSlowSearch(filterKey, filterValue string) (bool, url.Values) { + if strings.Contains(filterValue, ",") || strings.Contains(filterValue, ";") || + strings.Contains(filterValue, " ") || strings.Contains(filterValue, "+") || strings.Contains(filterValue, "*") { + return true, nil + } else { + params := url.Values{} + params.Set("filter", fmt.Sprintf(filterKey+"==%s", filterValue)) + params.Set("filterEncoded", "true") + return false, params + } +} diff --git a/govcd/openapi_endpoints.go b/govcd/openapi_endpoints.go index b6f34a615..8f2ada320 100644 --- a/govcd/openapi_endpoints.go +++ b/govcd/openapi_endpoints.go @@ -1,40 +1,241 @@ package govcd /* - * Copyright 2020 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + * Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. */ import ( "fmt" + "sort" + "strings" + + "github.com/vmware/go-vcloud-director/v2/util" + + "github.com/hashicorp/go-version" "github.com/vmware/go-vcloud-director/v2/types/v56" ) // endpointMinApiVersions holds mapping of OpenAPI endpoints and API versions they were introduced in. var endpointMinApiVersions = map[string]string{ - types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRoles: "31.0", - types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAuditTrail: "33.0", - types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointImportableTier0Routers: "32.0", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRights: "31.0", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRightsBundles: "31.0", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRightsCategories: "31.0", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRoles: "31.0", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointGlobalRoles: "31.0", + 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") types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointExternalNetworks: "33.0", types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcComputePolicies: "32.0", types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcAssignedComputePolicies: "33.0", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointSessionCurrent: "34.0", types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeClusters: "34.0", // VCD 10.1+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointQosProfiles: "36.2", // VCD 10.3.2+ (NSX-T only) + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayQos: "36.2", // VCD 10.3.2+ (NSX-T only) + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayDhcpForwarder: "36.1", // VCD 10.3.1+ (NSX-T only) + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayDns: "37.0", // VCD 10.4.0+ (NSX-T only) + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayL2VpnTunnel: "35.0", // VCD 10.2.0+ (NSX-T only) + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewaySlaacProfile: "35.0", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayStaticRoutes: "37.0", // VCD 10.4.0+ (NSX-T only) types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGateways: "34.0", - types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointOrgVdcNetworks: "32.0", // VCD 9.7+ for NSX-V, 10.1+ for NSX-T - types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointOrgVdcNetworksDhcp: "32.0", // VCD 9.7+ for NSX-V, 10.1+ for NSX-T - types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcCapabilities: "32.0", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayUsedIpAddresses: "34.0", + + // Static security groups and IP sets in VCD 10.2, Dynamic security groups in VCD 10.3+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointFirewallGroups: "34.0", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtNatRules: "34.0", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtFirewallRules: "34.0", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointOrgVdcNetworks: "32.0", // VCD 9.7+ for NSX-V, 10.1+ for NSX-T + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointOrgVdcNetworkSegmentProfiles: "37.0", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointOrgVdcNetworksDhcp: "32.0", // VCD 9.7+ for NSX-V, 10.1+ for NSX-T + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointOrgVdcNetworksDhcpBindings: "36.1", // VCD 10.3.1+ (NSX-T only) + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcCapabilities: "32.0", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAppPortProfiles: "34.0", // VCD 10.1+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSecVpnTunnel: "34.0", // VCD 10.1+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSecVpnTunnelConnectionProperties: "34.0", // VCD 10.1+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSecVpnTunnelStatus: "34.0", // VCD 10.1+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroups: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroupsCandidateVdcs: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroupsDfwPolicies: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroupsDfwDefaultPolicies: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointSecurityTags: "36.0", // VCD 10.3+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtRouteAdvertisement: "34.0", // VCD 10.1+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointLogicalVmGroups: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeInterfaces: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeInterfaceBehaviors: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntityTypes: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeTypeBehaviors: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeTypeBehaviorAccessControls: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntities: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntityAccessControls: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntitiesTypes: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntitiesResolve: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntitiesBehaviorsInvocations: "35.0", // VCD 10.2+ + + // IP Spaces + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaces: "37.1", // VCD 10.4.1+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceSummaries: "37.1", // VCD 10.4.1+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceUplinks: "37.1", // VCD 10.4.1+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceUplinksAllocate: "37.1", // VCD 10.4.1+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceIpAllocations: "37.1", // VCD 10.4.1+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceOrgAssignments: "37.1", // VCD 10.4.1+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceFloatingIpSuggestions: "37.1", // VCD 10.4.1+ + + // NSX-T ALB (Advanced/AVI Load Balancer) support was introduced in 10.2 + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbController: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbImportableClouds: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbImportableServiceEngineGroups: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbCloud: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbServiceEngineGroups: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbEdgeGateway: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbServiceEngineGroupAssignments: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbPools: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbPoolSummaries: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbVirtualServices: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbVirtualServiceSummaries: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointSSLCertificateLibrary: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointSSLCertificateLibraryOld: "35.0", // VCD 10.2+ and deprecated from 10.3 + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroupsDfwRules: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNetworkContextProfiles: "35.0", // VCD 10.2+ + + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeBgpNeighbor: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeBgpConfigPrefixLists: "35.0", // VCD 10.2+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeBgpConfig: "35.0", // VCD 10.2+ + + types.OpenApiPathVersion2_0_0 + types.OpenApiEndpointVdcAssignedComputePolicies: "35.0", + types.OpenApiPathVersion2_0_0 + types.OpenApiEndpointVdcComputePolicies: "35.0", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcNetworkProfile: "36.0", // VCD 10.3+ + + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVirtualCenters: "36.0", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointResourcePools: "36.0", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointResourcePoolsBrowseAll: "36.2", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointResourcePoolHardware: "36.0", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNetworkPools: "36.0", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNetworkPoolSummaries: "36.0", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointStorageProfiles: "33.0", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtSegmentProfileTemplates: "36.2", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtGlobalDefaultSegmentProfileTemplates: "36.2", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtSegmentIpDiscoveryProfiles: "36.2", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtSegmentMacDiscoveryProfiles: "36.2", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtSegmentSpoofGuardProfiles: "36.2", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtSegmentQosProfiles: "36.2", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtSegmentSecurityProfiles: "36.2", + + // Extensions API endpoints. These are not versioned + types.OpenApiEndpointExtensionsUi: "35.0", // VCD 10.2+ + types.OpenApiEndpointExtensionsUiPlugin: "35.0", // VCD 10.2+ + types.OpenApiEndpointExtensionsUiTenants: "35.0", // VCD 10.2+ + types.OpenApiEndpointExtensionsUiTenantsPublishAll: "35.0", // VCD 10.2+ + types.OpenApiEndpointExtensionsUiTenantsPublish: "35.0", // VCD 10.2+ + types.OpenApiEndpointExtensionsUiTenantsUnpublishAll: "35.0", // VCD 10.2+ + types.OpenApiEndpointExtensionsUiTenantsUnpublish: "35.0", // VCD 10.2+ + + // Endpoints for managing tokens and service accounts + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointTokens: "36.1", // VCD 10.3.1+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointServiceAccounts: "37.0", // VCD 10.4.0+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointServiceAccountGrant: "37.0", // VCD 10.4.0+ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointImportableTransportZones: "33.0", + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVCenterDistributedSwitch: "33.0", + + // Endpoint for managing vGPU profiles + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVgpuProfile: "36.2", + + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointOrgs: "37.0", +} + +// endpointElevatedApiVersions endpoint elevated API versions +var endpointElevatedApiVersions = map[string][]string{ + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtNatRules: { + //"34.0", // Basic minimum required version + "35.2", // Introduces support for new fields FirewallMatch and Priority + "36.0", // Adds support for new NAT Rule Type - REFLEXIVE (field Type must be used instead of RuleType) + }, + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointExternalNetworks: { + //"33.0", // Basic minimum required version + "35.0", // Deprecates field BackingType in favor of BackingTypeValue + "36.0", // Adds support new type of BackingTypeValue - IMPORTED_T_LOGICAL_SWITCH (backed by NSX-T segment) + "37.1", // Adds support for IP Spaces with new fields - UsingIpSpace, DedicatedOrg + "38.1", // Adds support for NAT, Firewall and Route Advertisement intention configuration + }, + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroupsDfwRules: { + //"35.0", // Basic minimum required version + "35.2", // Deprecates Action field in favor of ActionValue + "36.2", // Adds 3 new fields - Comments, SourceGroupsExcluded, and DestinationGroupsExcluded + }, + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointOrgVdcNetworksDhcp: { + //"32.0", // Basic minimum required version + "36.1", // Adds support for dnsServers + }, + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointFirewallGroups: { + //"34.0", // Basic minimum required version + "36.0", // Adds support for Dynamic Security Groups by deprecating `Type` field in favor of `TypeValue` + }, + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbController: { + //"35.0", // Basic minimum required version + "37.0", // Deprecates LicenseType in favor of SupportedFeatureSet + }, + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbServiceEngineGroups: { + //"35.0", // Basic minimum required version + "37.0", // Adds SupportedFeatureSet + }, + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbEdgeGateway: { + //"35.0", // Basic minimum required version + "37.0", // Deprecates LicenseType in favor of SupportedFeatureSet. Adds IPv6 service network definition support + "37.1", // Adds support for Transparent Mode + }, + // + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbVirtualServices: { + //"35.0", // Basic minimum required version + "37.0", // Adds IPv6 Virtual Service Support + "37.1", // Adds support for Transparent Mode + }, + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointAlbVirtualServiceSummaries: { + //"35.0", // Basic minimum required version + "37.0", // Adds IPv6 Virtual Service Support + "37.1", // Adds support for Transparent Mode + }, + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcNetworkProfile: { + //"36.0", // Introduced support + "36.2", // 2 additional fields vappNetworkSegmentProfileTemplateRef and vdcNetworkSegmentProfileTemplateRef added + }, + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntityTypes: { + //"35.0", // Introduced support + "37.1", // Added MaxImplicitRight property in DefinedEntityType + }, + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRdeEntities: { + //"35.0", // Introduced support + "37.0", // Added metadata support + }, + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGateways: { + //"35.0", // Introduced support + "37.1", // Exposes computed field `UsingIpSpace` in `types.EdgeGatewayUplinks` + }, + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayDns: { + "37.0", // Introduced support + "38.0", // New field SnatRuleExternalIpAddress + }, + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaceUplinksAllocate: { + //"37.1", // Introduced support + "37.2", // Adds 'value' field + }, + types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointIpSpaces: { + //"37.1", // Introduced support + "38.0", // Adds 'DefaultGatewayServiceConfig' structure for firewall and NAT rule creation + }, } // checkOpenApiEndpointCompatibility checks if VCD version (to which the client is connected) is sufficient to work with -// specified OpenAPI endpoint and returns either error, either Api version to use for calling that endpoint. This Api +// specified OpenAPI endpoint and returns either an error or the Api version to use for calling that endpoint. This Api // version can then be supplied to low level OpenAPI client functions. // If the system default API version is higher than endpoint introduction version - default system one is used. func (client *Client) checkOpenApiEndpointCompatibility(endpoint string) (string, error) { minimumApiVersion, ok := endpointMinApiVersions[endpoint] if !ok { - return "", fmt.Errorf("minimum API version for endopoint '%s' is not defined", endpoint) + return "", fmt.Errorf("minimum API version for endpoint '%s' is not defined", endpoint) } if client.APIVCDMaxVersionIs("< " + minimumApiVersion) { @@ -53,3 +254,77 @@ func (client *Client) checkOpenApiEndpointCompatibility(endpoint string) (string return minimumApiVersion, nil } + +// getOpenApiHighestElevatedVersion returns the highest supported API version for particular endpoint +// These API versions must be defined in endpointElevatedApiVersions. If none are there - it will return minimum +// supported API versions just like client.checkOpenApiEndpointCompatibility(). +// +// The advantage of this function is that it provides a controlled API elevation instead of just picking the highest +// which could be risky and untested (especially if new API version is released after release of package consuming this +// SDK) +func (client *Client) getOpenApiHighestElevatedVersion(endpoint string) (string, error) { + util.Logger.Printf("[DEBUG] Checking if elevated API versions are defined for endpoint '%s'", endpoint) + + // At first get minimum API version and check if it can be supported + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return "", fmt.Errorf("error getting minimum required API version: %s", err) + } + + // If no elevated versions are defined - return minimumApiVersion + elevatedVersionSlice, elevatedVersionsDefined := endpointElevatedApiVersions[endpoint] + if !elevatedVersionsDefined { + util.Logger.Printf("[DEBUG] No elevated API versions are defined for endpoint '%s'. Using minimum '%s'", + endpoint, minimumApiVersion) + return minimumApiVersion, nil + } + + util.Logger.Printf("[DEBUG] Found '%d' (%s) elevated API versions for endpoint '%s' ", + len(elevatedVersionSlice), strings.Join(elevatedVersionSlice, ", "), endpoint) + + // Reverse sort (highest to lowest) slice of elevated API versions so that we can start by highest supported and go down + versionsRaw := elevatedVersionSlice + versions := make([]*version.Version, len(versionsRaw)) + for i, raw := range versionsRaw { + v, err := version.NewVersion(raw) + if err != nil { + return "", fmt.Errorf("error evaluating version %s: %s", raw, err) + } + versions[i] = v + } + sort.Sort(sort.Reverse(version.Collection(versions))) + + var supportedElevatedVersion string + // Loop highest to the lowest elevated versions and try to find highest from the list of supported ones + for _, elevatedVersion := range versions { + + util.Logger.Printf("[DEBUG] Checking if elevated version '%s' is supported by VCD instance for endpoint '%s'", + elevatedVersion.Original(), endpoint) + // Check if maximum VCD API version supported is greater or equal to elevated version + + if client.APIVCDMaxVersionIs(fmt.Sprintf(">= %s", elevatedVersion.Original())) && + !client.APIClientVersionIs(fmt.Sprintf("> %s", elevatedVersion.Original())) { + util.Logger.Printf("[DEBUG] Elevated version '%s' is supported by VCD instance for endpoint '%s'", + elevatedVersion.Original(), endpoint) + // highest version found - store it and abort the loop + supportedElevatedVersion = elevatedVersion.Original() + break + } else { + util.Logger.Printf("[DEBUG] Skipped Elevated version '%s' for endpoint '%s', Default minimum version '%s'", + elevatedVersion.Original(), endpoint, client.APIVersion) + } + + util.Logger.Printf("[DEBUG] API version '%s' is not supported by VCD instance for endpoint '%s'", + elevatedVersion.Original(), endpoint) + } + + if supportedElevatedVersion == "" { + util.Logger.Printf("[DEBUG] No elevated API versions are supported for endpoint '%s'. Will use minimum "+ + "required version '%s'", endpoint, minimumApiVersion) + return minimumApiVersion, nil + } + + util.Logger.Printf("[DEBUG] Will use elevated version '%s for endpoint '%s'", + supportedElevatedVersion, endpoint) + return supportedElevatedVersion, nil +} diff --git a/govcd/openapi_endpoints_unit_test.go b/govcd/openapi_endpoints_unit_test.go new file mode 100644 index 000000000..121e78b99 --- /dev/null +++ b/govcd/openapi_endpoints_unit_test.go @@ -0,0 +1,200 @@ +//go:build unit || ALL + +package govcd + +import ( + "fmt" + "testing" + + semver "github.com/hashicorp/go-version" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +// TestClient_getOpenApiHighestElevatedVersion aims to test out capabilities of getOpenApiHighestElevatedVersion +// It consists of: +// * A few manually defined tests for known endpoints +// * Automatically generated tests for each entry in endpointMinApiVersions to ensure it returns correct version +// * 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) + if err != nil { + t.Fatalf("error parsing 'minVcdApiVersion': %s", err) + } + + type testCase struct { + name string + supportedVersions SupportedVersions + endpoint string + wantVersion string + wantErr bool + // overrideClientMinApiVersion is an option to override default expected version that is + // defined in global variable`minVcdApiVersion` + overrideClientMinApiVersion string + } + + // Start with some statically defined tests for known endpoints + tests := []testCase{ + { + name: "VCD_does_not_support_minimum_required_version", + supportedVersions: renderSupportedVersions([]string{"27.0", "28.0", "29.0", "30.0", "31.0", "32.0", "33.0"}), + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtNatRules, + wantVersion: "", + wantErr: true, // NAT requires at least version 34.0 + }, + { + name: "VCD_minimum_required_version_only", + supportedVersions: renderSupportedVersions([]string{"28.0", "29.0", "30.0", "31.0", "32.0", "33.0", "34.0"}), + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtNatRules, + wantVersion: "34.0", + wantErr: false, // NAT minimum requirement is version 34.0 + overrideClientMinApiVersion: "34.0", + }, + { + name: "VCD_minimum_required_version_only_unordered", + // Explicitly pass in unordered VCD API supported versions to ensure that ordering and matching works well + supportedVersions: renderSupportedVersions([]string{"33.0", "34.0", "30.0", "31.0", "32.0", "28.0", "29.0"}), + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtNatRules, + wantVersion: "34.0", + wantErr: false, // NAT minimum requirement is version 34.0 + overrideClientMinApiVersion: "34.0", + }, + { + 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, + wantErr: false, + }, + } + + // Generate unit tests for each defined endpoint in endpointMinApiVersions which does not have an elevated + // version entry in endpointElevatedApiVersions. + // Expect to get minimal supported version returned without error + for endpointName, minimumRequiredVersion := range endpointMinApiVersions { + // Do not create a test case for those endpoints which explicitly have elevated versions defined in + // endpointElevatedApiVersions + if _, hasEntry := endpointElevatedApiVersions[endpointName]; hasEntry { + continue + } + + wantVersion := minimumRequiredVersion + semverWantVersion, err := semver.NewSemver(wantVersion) + if err != nil { + t.Fatalf("error parsing 'singleElevatedApiVersion': %s", err) + } + + if semverWantVersion.LessThan(semverMinVcdApiVersion) { + semverMinVcdApiVersionSegments := semverMinVcdApiVersion.Segments() + wantVersion = fmt.Sprintf("%d.%d", semverMinVcdApiVersionSegments[0], semverMinVcdApiVersionSegments[1]) + if testVerbose { + fmt.Printf("# Overriding wanted version to %s for endpoint %s\n", wantVersion, endpointName) + } + } + + tCase := testCase{ + name: fmt.Sprintf("%s_minimum_version_%s", minimumRequiredVersion, endpointName), + // Put a list of versions which always satisfied minimum requirement + supportedVersions: renderSupportedVersions([]string{ + "27.0", "28.0", "29.0", "30.0", "31.0", "32.0", "33.0", "34.0", "35.0", "35.1", "35.2", "36.0", "37.0", "38.0", + }), + endpoint: endpointName, + wantVersion: wantVersion, + wantErr: false, + } + tests = append(tests, tCase) + } + + // Generate unit tests for each defined endpoint in endpointMinApiVersions which does not have supported version at all + // Always expect an error and empty version + for endpointName, minimumRequiredVersion := range endpointMinApiVersions { + tCase := testCase{ + name: fmt.Sprintf("%s_unsatisfied_minimum_version_%s", minimumRequiredVersion, endpointName), + supportedVersions: renderSupportedVersions([]string{ + "25.0", + }), + endpoint: endpointName, + wantVersion: "", + wantErr: true, + } + tests = append(tests, tCase) + } + + // Generate tests for each elevated API version in endpoints which do have elevated rights defined + // Expect to get either that version or minimum supported version + for endpointName := range endpointMinApiVersions { + // Do not create a test case for those endpoints which do not have endpointElevatedApiVersions specified + if _, hasEntry := endpointElevatedApiVersions[endpointName]; !hasEntry { + continue + } + + // generate tests for all elevated rights and expect to get + for _, singleElevatedApiVersion := range endpointElevatedApiVersions[endpointName] { + wantVersion := singleElevatedApiVersion + + semverWantVersion, err := semver.NewSemver(wantVersion) + if err != nil { + t.Fatalf("error parsing 'singleElevatedApiVersion': %s", err) + } + + if semverWantVersion.LessThan(semverMinVcdApiVersion) { + semverMinVcdApiVersionSegments := semverMinVcdApiVersion.Segments() + wantVersion = fmt.Sprintf("%d.%d", semverMinVcdApiVersionSegments[0], semverMinVcdApiVersionSegments[1]) + if testVerbose { + fmt.Printf("# Overriding wanted version to %s for endpoint %s\n", wantVersion, endpointName) + } + } + + tCase := testCase{ + name: fmt.Sprintf("elevated_up_to_%s_for_%s", singleElevatedApiVersion, endpointName), + // Insert some older versions and make it so that the highest elevated API version matches + supportedVersions: renderSupportedVersions([]string{ + "27.0", singleElevatedApiVersion, "23.0", "30.0", + }), + endpoint: endpointName, + wantVersion: wantVersion, + wantErr: false, + } + tests = append(tests, tCase) + } + } + + // Run all defined tests + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client := &Client{ + supportedVersions: tt.supportedVersions, + APIVersion: minVcdApiVersion, + } + + if tt.overrideClientMinApiVersion != "" { + client.APIVersion = tt.overrideClientMinApiVersion + } + + got, err := client.getOpenApiHighestElevatedVersion(tt.endpoint) + if (err != nil) != tt.wantErr { + t.Errorf("getOpenApiHighestElevatedVersion() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.wantVersion { + t.Errorf("getOpenApiHighestElevatedVersion() got = %v, wantVersion %v", got, tt.wantVersion) + } + }) + } +} + +// renderSupportedVersions is a helper to create fake `SupportedVersions` type out of given VCD API version list +func renderSupportedVersions(versions []string) SupportedVersions { + supportedVersions := SupportedVersions{} + + for _, version := range versions { + supportedVersions.VersionInfos = append(supportedVersions.VersionInfos, + VersionInfo{ + Version: version, + LoginUrl: "https://fake-host/api/sessions", + Deprecated: false, + }) + } + return supportedVersions +} diff --git a/govcd/openapi_generic_inner_entities.go b/govcd/openapi_generic_inner_entities.go new file mode 100644 index 000000000..81aeb02df --- /dev/null +++ b/govcd/openapi_generic_inner_entities.go @@ -0,0 +1,288 @@ +package govcd + +import ( + "fmt" + "net/http" + "net/url" + "strings" +) + +// crudConfig contains configuration that must be supplied when invoking generic functions that are defined +// in `openapi_generic_inner_entities.go` and `openapi_generic_outer_entities.go` +type crudConfig struct { + // Mandatory parameters + + // entityLabel contains friendly entity name that is used for logging meaningful errors + entityLabel string + + // endpoint in the usual format (e.g. types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtSegmentIpDiscoveryProfiles) + endpoint string + + // Optional parameters + + // endpointParams contains a slice of strings that will be used to construct the request URL. It will + // initially replace '%s' placeholders in the `endpoint` (if any) and will add them as suffix + // afterwards + endpointParams []string + + // queryParameters will be passed as GET queries to the URL. Usually they are used for API filtering parameters + queryParameters url.Values + // additionalHeader can be used to pass additional headers for API calls. One of the common purposes is to pass + // tenant context + additionalHeader map[string]string +} + +// validate should catch errors in consuming generic CRUD functions and should never produce false +// positives. +func (c crudConfig) validate() error { + // crudConfig misconfiguration - we can panic so that developer catches the problem during + // development of this SDK + if c.entityLabel == "" { + panic("'entityName' must always be specified when initializing crudConfig") + } + + if c.endpoint == "" { + panic("'endpoint' must always be specified when initializing crudConfig") + } + + // softer validations that consumers of this SDK can manipulate + + // If `endpointParams` is specified in `crudConfig` (it is not nil), then it must contain at + // least a single non empty string parameter + // Such validation should prevent cases where some ID is not speficied upon function call. + // E.g.: endpointParams: []string{vdcId}, <--- vdcId comes from consumer of the SDK + // If the user specified empty `vdcId` - we'd validate this + for _, paramValue := range c.endpointParams { + if paramValue == "" { + return fmt.Errorf(`endpointParams were specified but they contain empty value "" for %s. %#v`, + c.entityLabel, c.endpointParams) + } + } + + return nil +} + +// createInnerEntity implements a common pattern for creating an entity throughout codebase +// Parameters: +// * `client` is a *Client +// * `c` holds settings for performing API call +// * `innerConfig` is the new entity type +func createInnerEntity[I any](client *Client, c crudConfig, innerConfig *I) (*I, error) { + if err := c.validate(); err != nil { + return nil, err + } + + apiVersion, err := client.getOpenApiHighestElevatedVersion(c.endpoint) + if err != nil { + return nil, fmt.Errorf("error getting API version for creating entity '%s': %s", c.entityLabel, err) + } + + exactEndpoint, err := urlFromEndpoint(c.endpoint, c.endpointParams) + if err != nil { + return nil, fmt.Errorf("error building endpoint '%s' with given params '%s' for entity '%s': %s", c.endpoint, strings.Join(c.endpointParams, ","), c.entityLabel, err) + } + + urlRef, err := client.OpenApiBuildEndpoint(exactEndpoint) + if err != nil { + return nil, fmt.Errorf("error building API endpoint for entity '%s' creation: %s", c.entityLabel, err) + } + + createdInnerEntityConfig := new(I) + err = client.OpenApiPostItem(apiVersion, urlRef, c.queryParameters, innerConfig, createdInnerEntityConfig, c.additionalHeader) + if err != nil { + return nil, fmt.Errorf("error creating entity of type '%s': %s", c.entityLabel, err) + } + + return createdInnerEntityConfig, nil +} + +// updateInnerEntity implements a common pattern for updating entity throughout codebase +// Parameters: +// * `client` is a *Client +// * `c` holds settings for performing API call +// * `innerConfig` is the new entity type +func updateInnerEntity[I any](client *Client, c crudConfig, innerConfig *I) (*I, error) { + // Discarding returned headers to better match return signature for most common cases + updatedInnerEntity, _, err := updateInnerEntityWithHeaders(client, c, innerConfig) + return updatedInnerEntity, err +} + +// updateInnerEntityWithHeaders implements a common pattern for updating entity throughout codebase +// Parameters: +// * `client` is a *Client +// * `c` holds settings for performing API call +// * `innerConfig` is the new entity type +func updateInnerEntityWithHeaders[I any](client *Client, c crudConfig, innerConfig *I) (*I, http.Header, error) { + if err := c.validate(); err != nil { + return nil, nil, err + } + + apiVersion, err := client.getOpenApiHighestElevatedVersion(c.endpoint) + if err != nil { + return nil, nil, fmt.Errorf("error getting API version for updating entity '%s': %s", c.entityLabel, err) + } + + exactEndpoint, err := urlFromEndpoint(c.endpoint, c.endpointParams) + if err != nil { + return nil, nil, fmt.Errorf("error building endpoint '%s' with given params '%s' for entity '%s': %s", c.endpoint, strings.Join(c.endpointParams, ","), c.entityLabel, err) + } + + urlRef, err := client.OpenApiBuildEndpoint(exactEndpoint) + if err != nil { + return nil, nil, fmt.Errorf("error building API endpoint for entity '%s' update: %s", c.entityLabel, err) + } + + updatedInnerEntityConfig := new(I) + headers, err := client.OpenApiPutItemAndGetHeaders(apiVersion, urlRef, c.queryParameters, innerConfig, updatedInnerEntityConfig, c.additionalHeader) + if err != nil { + return nil, nil, fmt.Errorf("error updating entity of type '%s': %s", c.entityLabel, err) + } + + return updatedInnerEntityConfig, headers, nil +} + +// getInnerEntity is an implementation for a common pattern in our code where we have to retrieve +// outer entity (usually *types.XXXX) and does not need to be wrapped in an inner container entity. +// Parameters: +// * `client` is a *Client +// * `c` holds settings for performing API call +func getInnerEntity[I any](client *Client, c crudConfig) (*I, error) { + // Discarding returned headers to better match return signature for most common cases + innerEntity, _, err := getInnerEntityWithHeaders[I](client, c) + return innerEntity, err +} + +// getInnerEntityWithHeaders is an implementation for a common pattern in our code where we have to retrieve +// outer entity (usually *types.XXXX) and does not need to be wrapped in an inner container entity. +// Parameters: +// * `client` is a *Client +// * `c` holds settings for performing API call +func getInnerEntityWithHeaders[I any](client *Client, c crudConfig) (*I, http.Header, error) { + if err := c.validate(); err != nil { + return nil, nil, err + } + + apiVersion, err := client.getOpenApiHighestElevatedVersion(c.endpoint) + if err != nil { + return nil, nil, fmt.Errorf("error getting API version for entity '%s': %s", c.entityLabel, err) + } + + exactEndpoint, err := urlFromEndpoint(c.endpoint, c.endpointParams) + if err != nil { + return nil, nil, fmt.Errorf("error building endpoint '%s' with given params '%s' for entity '%s': %s", c.endpoint, strings.Join(c.endpointParams, ","), c.entityLabel, err) + } + + urlRef, err := client.OpenApiBuildEndpoint(exactEndpoint) + if err != nil { + return nil, nil, fmt.Errorf("error building API endpoint for entity '%s': %s", c.entityLabel, err) + } + + typeResponse := new(I) + headers, err := client.OpenApiGetItemAndHeaders(apiVersion, urlRef, c.queryParameters, typeResponse, c.additionalHeader) + if err != nil { + return nil, nil, fmt.Errorf("error retrieving entity of type '%s': %s", c.entityLabel, err) + } + + return typeResponse, headers, nil +} + +// getAllInnerEntities can be used to retrieve a slice of any inner entities in the OpenAPI +// endpoints that are not nested in outer types +// +// Parameters: +// * `client` is a *Client +// * `c` holds settings for performing API call +func getAllInnerEntities[I any](client *Client, c crudConfig) ([]*I, error) { + if err := c.validate(); err != nil { + return nil, err + } + + apiVersion, err := client.getOpenApiHighestElevatedVersion(c.endpoint) + if err != nil { + return nil, fmt.Errorf("error getting API version for entity '%s': %s", c.entityLabel, err) + } + + exactEndpoint, err := urlFromEndpoint(c.endpoint, c.endpointParams) + if err != nil { + return nil, fmt.Errorf("error building endpoint '%s' with given params '%s' for entity '%s': %s", c.endpoint, strings.Join(c.endpointParams, ","), c.entityLabel, err) + } + + urlRef, err := client.OpenApiBuildEndpoint(exactEndpoint) + if err != nil { + return nil, fmt.Errorf("error building API endpoint for entity '%s': %s", c.entityLabel, err) + } + + typeResponses := make([]*I, 0) + err = client.OpenApiGetAllItems(apiVersion, urlRef, c.queryParameters, &typeResponses, c.additionalHeader) + if err != nil { + return nil, fmt.Errorf("error retrieving all entities of type '%s': %s", c.entityLabel, err) + } + + return typeResponses, nil +} + +// deleteEntityById performs a common operation for OpenAPI endpoints that calls DELETE method for a +// given endpoint. +// Note. It does not use generics for the operation, but is held in this file with other CRUD entries +// Parameters: +// * `client` is a *Client +// * `c` holds settings for performing API call +func deleteEntityById(client *Client, c crudConfig) error { + if err := c.validate(); err != nil { + return err + } + + apiVersion, err := client.getOpenApiHighestElevatedVersion(c.endpoint) + if err != nil { + return err + } + + exactEndpoint, err := urlFromEndpoint(c.endpoint, c.endpointParams) + if err != nil { + return fmt.Errorf("error building endpoint '%s' with given params '%s' for entity '%s': %s", c.endpoint, strings.Join(c.endpointParams, ","), c.entityLabel, err) + } + + urlRef, err := client.OpenApiBuildEndpoint(exactEndpoint) + if err != nil { + return err + } + + err = client.OpenApiDeleteItem(apiVersion, urlRef, c.queryParameters, c.additionalHeader) + + if err != nil { + return fmt.Errorf("error deleting %s: %s", c.entityLabel, err) + } + + return nil +} + +func urlFromEndpoint(endpoint string, endpointParams []string) (string, error) { + // Count how many '%s' placeholders exist in the 'endpoint' + placeholderCount := strings.Count(endpoint, "%s") + + // Validation. At the very minimum all placeholders must have their replacements - otherwise it + // is an error as we never want to query an endpoint that still has placeholders '%s' + if len(endpointParams) < placeholderCount { + return "", fmt.Errorf("endpoint '%s' has unpopulated placeholders", endpoint) + } + + // if there are no 'endpointParams' - exit with the same endpoint + if len(endpointParams) == 0 { + return endpoint, nil + } + + // Loop over given endpointParams and replace placeholders at first. Afterwards - amend any + // additional parameters to the end of endpoint + for _, v := range endpointParams { + // If there are placeholders '%s' to replace in the endpoint itself - do it + if placeholderCount > 0 { + endpoint = strings.Replace(endpoint, "%s", v, 1) + placeholderCount = placeholderCount - 1 + continue + } + + endpoint = endpoint + v + } + + return endpoint, nil +} diff --git a/govcd/openapi_generic_outer_entities.go b/govcd/openapi_generic_outer_entities.go new file mode 100644 index 000000000..d7a77bf0e --- /dev/null +++ b/govcd/openapi_generic_outer_entities.go @@ -0,0 +1,83 @@ +package govcd + +import ( + "fmt" + "net/http" +) + +// Generic type explanations +// Common generic parameter names seen in this code +// O - Outer type that is in the `govcd` package. (e.g. 'IpSpace') +// I - Inner type the type that is being marshalled/unmarshalled (usually in `types` package. E.g. `types.IpSpace`) + +// outerEntityWrapper is a type constraint that outer entities must implement in order to +// use generic CRUD functions defined in this file +type outerEntityWrapper[O any, I any] interface { + // wrap is a value receiver function that must implement one thing for a concrete type - wrap + // pointer to innter entity I and return pointer to outer entity O + wrap(inner *I) *O +} + +// createOuterEntity creates an outer entity with given inner entity config +func createOuterEntity[O outerEntityWrapper[O, I], I any](client *Client, outerEntity O, c crudConfig, innerEntityConfig *I) (*O, error) { + if innerEntityConfig == nil { + return nil, fmt.Errorf("entity config '%s' cannot be empty for create operation", c.entityLabel) + } + + createdInnerEntity, err := createInnerEntity(client, c, innerEntityConfig) + if err != nil { + return nil, err + } + + return outerEntity.wrap(createdInnerEntity), nil +} + +// updateOuterEntity updates an outer entity with given inner entity config +func updateOuterEntity[O outerEntityWrapper[O, I], I any](client *Client, outerEntity O, c crudConfig, innerEntityConfig *I) (*O, error) { + if innerEntityConfig == nil { + return nil, fmt.Errorf("entity config '%s' cannot be empty for update operation", c.entityLabel) + } + + updatedInnerEntity, err := updateInnerEntity(client, c, innerEntityConfig) + if err != nil { + return nil, err + } + + return outerEntity.wrap(updatedInnerEntity), nil +} + +// getOuterEntity retrieves a single outer entity +func getOuterEntity[O outerEntityWrapper[O, I], I any](client *Client, outerEntity O, c crudConfig) (*O, error) { + retrievedInnerEntity, err := getInnerEntity[I](client, c) + if err != nil { + return nil, err + } + + return outerEntity.wrap(retrievedInnerEntity), nil +} + +// getOuterEntity retrieves a single outer entity +func getOuterEntityWithHeaders[O outerEntityWrapper[O, I], I any](client *Client, outerEntity O, c crudConfig) (*O, http.Header, error) { + retrievedInnerEntity, headers, err := getInnerEntityWithHeaders[I](client, c) + if err != nil { + return nil, nil, err + } + + return outerEntity.wrap(retrievedInnerEntity), headers, nil +} + +// getAllOuterEntities retrieves all outer entities +func getAllOuterEntities[O outerEntityWrapper[O, I], I any](client *Client, outerEntity O, c crudConfig) ([]*O, error) { + retrievedAllInnerEntities, err := getAllInnerEntities[I](client, c) + if err != nil { + return nil, err + } + + wrappedOuterEntities := make([]*O, len(retrievedAllInnerEntities)) + for index, singleInnerEntity := range retrievedAllInnerEntities { + // outerEntity.wrap() is a value receiver, therefore it creates a shallow copy for each call + wrappedOuterEntities[index] = outerEntity.wrap(singleInnerEntity) + } + + return wrappedOuterEntities, nil +} diff --git a/govcd/openapi_generic_unit_test.go b/govcd/openapi_generic_unit_test.go new file mode 100644 index 000000000..c4602dba2 --- /dev/null +++ b/govcd/openapi_generic_unit_test.go @@ -0,0 +1,75 @@ +//go:build unit || ALL + +package govcd + +import ( + "testing" +) + +func Test_urlFromEndpoint(t *testing.T) { + type args struct { + endpoint string + endpointParams []string + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "PlaceholderAndSuffix", + args: args{endpoint: "1.0.0/edgeGateways/%s/ipsec/tunnels/", endpointParams: []string{"edgeGatewayId", "suffix"}}, + want: "1.0.0/edgeGateways/edgeGatewayId/ipsec/tunnels/suffix", + wantErr: false, + }, + { + name: "PlaceholderUnsatisfiedEmptySlice", + args: args{endpoint: "1.0.0/edgeGateways/%s/ipsec/tunnels/", endpointParams: []string{}}, + want: "", + wantErr: true, + }, + { + name: "PlaceholderUnsatisfiedNilSlice", + args: args{endpoint: "1.0.0/edgeGateways/%s/ipsec/tunnels/", endpointParams: nil}, + want: "", + wantErr: true, + }, + { + name: "NoPlaceholderNilSlice", + args: args{endpoint: "1.0.0/edgeGateways/ipsec/tunnels/", endpointParams: nil}, + want: "1.0.0/edgeGateways/ipsec/tunnels/", + wantErr: false, + }, + { + name: "NoPlaceholderEmptySlice", + args: args{endpoint: "1.0.0/edgeGateways/ipsec/tunnels/", endpointParams: []string{}}, + want: "1.0.0/edgeGateways/ipsec/tunnels/", + wantErr: false, + }, + { + name: "InsufficientPlaceholderReplacements", + args: args{endpoint: "1.0.0/edgeGateways/%s/ipsec/%s/tunnels/", endpointParams: []string{"replacement-one"}}, + want: "", + wantErr: true, + }, + { + name: "MultipleSuffixes", + args: args{endpoint: "1.0.0/edgeGateways/%s/ipsec/%s/tunnels/", endpointParams: []string{"replacement-one", "replacement-two", "suffix-one", "/", "suffix-two"}}, + want: "1.0.0/edgeGateways/replacement-one/ipsec/replacement-two/tunnels/suffix-one/suffix-two", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := urlFromEndpoint(tt.args.endpoint, tt.args.endpointParams) + if (err != nil) != tt.wantErr { + t.Errorf("urlFromEndpoint() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("urlFromEndpoint() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/govcd/openapi_org_network.go b/govcd/openapi_org_network.go index c95e701e2..7580f3e73 100644 --- a/govcd/openapi_org_network.go +++ b/govcd/openapi_org_network.go @@ -12,6 +12,8 @@ import ( "github.com/vmware/go-vcloud-director/v2/types/v56" ) +const labelOrgVdcNetworkSegmentProfile = "Org VDC Network Segment Profile" + // OpenApiOrgVdcNetwork uses OpenAPI endpoint to operate both - NSX-T and NSX-V Org VDC networks type OpenApiOrgVdcNetwork struct { OpenApiOrgVdcNetwork *types.OpenApiOrgVdcNetwork @@ -26,12 +28,37 @@ func (org *Org) GetOpenApiOrgVdcNetworkById(id string) (*OpenApiOrgVdcNetwork, e return getOpenApiOrgVdcNetworkById(org.client, id, filterParams) } +// GetOpenApiOrgVdcNetworkByNameAndOwnerId allows to retrieve both - NSX-T and NSX-V Org VDC networks +// by network name and Owner (VDC or VDC Group) ID +func (org *Org) GetOpenApiOrgVdcNetworkByNameAndOwnerId(name, ownerId string) (*OpenApiOrgVdcNetwork, error) { + // Inject Org ID filter to perform filtering on server side + queryParameters := url.Values{} + queryParameters = queryParameterFilterAnd(fmt.Sprintf("name==%s;ownerRef.id==%s", name, ownerId), queryParameters) + + allEdges, err := getAllOpenApiOrgVdcNetworks(org.client, queryParameters, nil) + if err != nil { + return nil, fmt.Errorf("unable to retrieve Org VDC network by name '%s' in Owner '%s': %s", name, ownerId, err) + } + + return returnSingleOpenApiOrgVdcNetwork(name, allEdges) +} + // GetOpenApiOrgVdcNetworkById allows to retrieve both - NSX-T and NSX-V Org VDC networks func (vdc *Vdc) GetOpenApiOrgVdcNetworkById(id string) (*OpenApiOrgVdcNetwork, error) { + return getOrgVdcNetworkById(vdc.client, id, vdc.Vdc.ID) +} + +// GetOpenApiOrgVdcNetworkById allows to retrieve both - NSX-T and NSX-V Org VDC Group networks +func (vdcGroup *VdcGroup) GetOpenApiOrgVdcNetworkById(id string) (*OpenApiOrgVdcNetwork, error) { + return getOrgVdcNetworkById(vdcGroup.client, id, vdcGroup.VdcGroup.Id) +} + +// getOrgVdcNetworkById allows to retrieve both - NSX-T and NSX-V Org VDC Group networks +func getOrgVdcNetworkById(client *Client, id, ownerId string) (*OpenApiOrgVdcNetwork, error) { // Inject Vdc ID filter to perform filtering on server side params := url.Values{} - filterParams := queryParameterFilterAnd("orgVdc.id=="+vdc.Vdc.ID, params) - egw, err := getOpenApiOrgVdcNetworkById(vdc.client, id, filterParams) + filterParams := queryParameterFilterAnd("ownerRef.id=="+ownerId, params) + egw, err := getOpenApiOrgVdcNetworkById(client, id, filterParams) if err != nil { return nil, err } @@ -42,7 +69,7 @@ func (vdc *Vdc) GetOpenApiOrgVdcNetworkById(id string) (*OpenApiOrgVdcNetwork, e // GetOpenApiOrgVdcNetworkByName allows to retrieve both - NSX-T and NSX-V Org VDC networks func (vdc *Vdc) GetOpenApiOrgVdcNetworkByName(name string) (*OpenApiOrgVdcNetwork, error) { queryParameters := url.Values{} - queryParameters.Add("filter", "name=="+name) + queryParameters.Add("filter", fmt.Sprintf("name==%s", name)) allEdges, err := vdc.GetAllOpenApiOrgVdcNetworks(queryParameters) if err != nil { @@ -52,39 +79,67 @@ func (vdc *Vdc) GetOpenApiOrgVdcNetworkByName(name string) (*OpenApiOrgVdcNetwor return returnSingleOpenApiOrgVdcNetwork(name, allEdges) } -// GetAllOpenApiOrgVdcNetworks allows to retrieve all NSX-T or NSX-V Org VDC networks +// GetOpenApiOrgVdcNetworkByName allows to retrieve both - NSX-T and NSX-V Org VDC networks +func (vdcGroup *VdcGroup) GetOpenApiOrgVdcNetworkByName(name string) (*OpenApiOrgVdcNetwork, error) { + queryParameters := url.Values{} + queryParameters.Add("filter", fmt.Sprintf("name==%s", name)) + + allEdges, err := vdcGroup.GetAllOpenApiOrgVdcNetworks(queryParameters) + if err != nil { + return nil, fmt.Errorf("unable to retrieve Org VDC network by name '%s': %s", name, err) + } + + return returnSingleOpenApiOrgVdcNetwork(name, allEdges) +} + +// GetAllOpenApiOrgVdcNetworks allows to retrieve all NSX-T or NSX-V Org VDC networks in Org +// +// Note. If pageSize > 32 it will be limited to maximum of 32 in this function because API validation does not allow for +// higher number +func (org *Org) GetAllOpenApiOrgVdcNetworks(queryParameters url.Values) ([]*OpenApiOrgVdcNetwork, error) { + filteredQueryParams := queryParameterFilterAnd("orgRef.id=="+org.Org.ID, queryParameters) + return getAllOpenApiOrgVdcNetworks(org.client, filteredQueryParams, nil) +} + +// GetAllOpenApiOrgVdcNetworks allows to retrieve all NSX-T or NSX-V Org VDC networks in Vdc // // Note. If pageSize > 32 it will be limited to maximum of 32 in this function because API validation does not allow for // higher number func (vdc *Vdc) GetAllOpenApiOrgVdcNetworks(queryParameters url.Values) ([]*OpenApiOrgVdcNetwork, error) { - filteredQueryParams := queryParameterFilterAnd("orgVdc.id=="+vdc.Vdc.ID, queryParameters) - return getAllOpenApiOrgVdcNetworks(vdc.client, filteredQueryParams) + filteredQueryParams := queryParameterFilterAnd("ownerRef.id=="+vdc.Vdc.ID, queryParameters) + return getAllOpenApiOrgVdcNetworks(vdc.client, filteredQueryParams, nil) +} + +// GetAllOpenApiOrgVdcNetworks allows to retrieve all NSX-T or NSX-V Org VDC networks in Vdc +// +// Note. If pageSize > 32 it will be limited to maximum of 32 in this function because API validation does not allow for +// higher number +func (vdcGroup *VdcGroup) GetAllOpenApiOrgVdcNetworks(queryParameters url.Values) ([]*OpenApiOrgVdcNetwork, error) { + filteredQueryParams := queryParameterFilterAnd("ownerRef.id=="+vdcGroup.VdcGroup.Id, queryParameters) + return getAllOpenApiOrgVdcNetworks(vdcGroup.client, filteredQueryParams, nil) } // CreateOpenApiOrgVdcNetwork allows to create NSX-T or NSX-V Org VDC network -func (vdc *Vdc) CreateOpenApiOrgVdcNetwork(OrgVdcNetworkConfig *types.OpenApiOrgVdcNetwork) (*OpenApiOrgVdcNetwork, error) { - endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointOrgVdcNetworks - minimumApiVersion, err := vdc.client.checkOpenApiEndpointCompatibility(endpoint) - if err != nil { - return nil, err - } +func (org *Org) CreateOpenApiOrgVdcNetwork(orgVdcNetworkConfig *types.OpenApiOrgVdcNetwork) (*OpenApiOrgVdcNetwork, error) { + return createOpenApiOrgVdcNetwork(org.client, orgVdcNetworkConfig) +} - urlRef, err := vdc.client.OpenApiBuildEndpoint(endpoint) - if err != nil { - return nil, err - } +// CreateOpenApiOrgVdcNetwork allows to create NSX-T or NSX-V Org VDC network +func (vdc *Vdc) CreateOpenApiOrgVdcNetwork(orgVdcNetworkConfig *types.OpenApiOrgVdcNetwork) (*OpenApiOrgVdcNetwork, error) { + return createOpenApiOrgVdcNetwork(vdc.client, orgVdcNetworkConfig) +} - returnEgw := &OpenApiOrgVdcNetwork{ - OpenApiOrgVdcNetwork: &types.OpenApiOrgVdcNetwork{}, - client: vdc.client, - } +// CreateOpenApiOrgVdcNetwork allows to create NSX-T or NSX-V Org VDC network +func (vdcGroup *VdcGroup) CreateOpenApiOrgVdcNetwork(orgVdcNetworkConfig *types.OpenApiOrgVdcNetwork) (*OpenApiOrgVdcNetwork, error) { + return createOpenApiOrgVdcNetwork(vdcGroup.client, orgVdcNetworkConfig) +} - err = vdc.client.OpenApiPostItem(minimumApiVersion, urlRef, nil, OrgVdcNetworkConfig, returnEgw.OpenApiOrgVdcNetwork) - if err != nil { - return nil, fmt.Errorf("error creating Org VDC network: %s", err) +// UpdateDhcp updates DHCP configuration for specific Org VDC network +func (orgVdcNet *OpenApiOrgVdcNetwork) UpdateDhcp(orgVdcNetworkDhcpConfig *types.OpenApiOrgVdcNetworkDhcp) (*OpenApiOrgVdcNetworkDhcp, error) { + if orgVdcNet.client == nil || orgVdcNet.OpenApiOrgVdcNetwork == nil || orgVdcNet.OpenApiOrgVdcNetwork.ID == "" { + return nil, fmt.Errorf("error - Org VDC network structure must be set and have ID field available") } - - return returnEgw, nil + return updateOrgNetworkDhcp(orgVdcNet.client, orgVdcNet.OpenApiOrgVdcNetwork.ID, orgVdcNetworkDhcpConfig) } // Update allows to update Org VDC network @@ -109,7 +164,7 @@ func (orgVdcNet *OpenApiOrgVdcNetwork) Update(OrgVdcNetworkConfig *types.OpenApi client: orgVdcNet.client, } - err = orgVdcNet.client.OpenApiPutItem(minimumApiVersion, urlRef, nil, OrgVdcNetworkConfig, returnEgw.OpenApiOrgVdcNetwork) + err = orgVdcNet.client.OpenApiPutItem(minimumApiVersion, urlRef, nil, OrgVdcNetworkConfig, returnEgw.OpenApiOrgVdcNetwork, nil) if err != nil { return nil, fmt.Errorf("error updating Org VDC network: %s", err) } @@ -134,7 +189,7 @@ func (orgVdcNet *OpenApiOrgVdcNetwork) Delete() error { return err } - err = orgVdcNet.client.OpenApiDeleteItem(minimumApiVersion, urlRef, nil) + err = orgVdcNet.client.OpenApiDeleteItem(minimumApiVersion, urlRef, nil, nil) if err != nil { return fmt.Errorf("error deleting Org VDC network: %s", err) @@ -168,6 +223,34 @@ func (orgVdcNet *OpenApiOrgVdcNetwork) IsDirect() bool { return orgVdcNet.GetType() == types.OrgVdcNetworkTypeDirect } +// IsNsxt returns true if the network is backed by NSX-T +func (orgVdcNet *OpenApiOrgVdcNetwork) IsNsxt() bool { + + // orgVdcNet.OpenApiOrgVdcNetwork.OrgVdcIsNsxTBacked returns `true` only if network is a member + // of VDC (not VDC Group) therefore an additional check for `BackingNetworkType` is required + + return orgVdcNet.OpenApiOrgVdcNetwork.OrgVdcIsNsxTBacked || + orgVdcNet.OpenApiOrgVdcNetwork.BackingNetworkType == types.OpenApiOrgVdcNetworkBackingTypeNsxt +} + +// IsDhcpEnabled returns true if DHCP is enabled for NSX-T Org VDC network, false otherwise +func (orgVdcNet *OpenApiOrgVdcNetwork) IsDhcpEnabled() bool { + if !orgVdcNet.IsNsxt() { + return false + } + + dhcpConfig, err := orgVdcNet.GetOpenApiOrgVdcNetworkDhcp() + if err != nil { + return false + } + + if dhcpConfig == nil || dhcpConfig.OpenApiOrgVdcNetworkDhcp == nil || dhcpConfig.OpenApiOrgVdcNetworkDhcp.Enabled == nil || !*dhcpConfig.OpenApiOrgVdcNetworkDhcp.Enabled { + return false + } + + return true +} + // getOpenApiOrgVdcNetworkById is a private parent for wrapped functions: // func (org *Org) GetOpenApiOrgVdcNetworkById(id string) (*OpenApiOrgVdcNetwork, error) // func (vdc *Vdc) GetOpenApiOrgVdcNetworkById(id string) (*OpenApiOrgVdcNetwork, error) @@ -192,7 +275,7 @@ func getOpenApiOrgVdcNetworkById(client *Client, id string, queryParameters url. client: client, } - err = client.OpenApiGetItem(minimumApiVersion, urlRef, queryParameters, egw.OpenApiOrgVdcNetwork) + err = client.OpenApiGetItem(minimumApiVersion, urlRef, queryParameters, egw.OpenApiOrgVdcNetwork, nil) if err != nil { return nil, err } @@ -219,7 +302,7 @@ func returnSingleOpenApiOrgVdcNetwork(name string, allEdges []*OpenApiOrgVdcNetw // // Note. If pageSize > 32 it will be limited to maximum of 32 in this function because API validation does not allow // higher number -func getAllOpenApiOrgVdcNetworks(client *Client, queryParameters url.Values) ([]*OpenApiOrgVdcNetwork, error) { +func getAllOpenApiOrgVdcNetworks(client *Client, queryParameters url.Values, additionalHeader map[string]string) ([]*OpenApiOrgVdcNetwork, error) { // Enforce maximum pageSize to be 32 as API endpoint throws error if it is > 32 pageSizeString := queryParameters.Get("pageSize") @@ -252,7 +335,7 @@ func getAllOpenApiOrgVdcNetworks(client *Client, queryParameters url.Values) ([] } typeResponses := []*types.OpenApiOrgVdcNetwork{{}} - err = client.OpenApiGetAllItems(minimumApiVersion, urlRef, queryParameters, &typeResponses) + err = client.OpenApiGetAllItems(minimumApiVersion, urlRef, queryParameters, &typeResponses, additionalHeader) if err != nil { return nil, err } @@ -268,3 +351,63 @@ func getAllOpenApiOrgVdcNetworks(client *Client, queryParameters url.Values) ([] return wrappedResponses, nil } + +// createOpenApiOrgVdcNetwork is wrapped by public CreateOpenApiOrgVdcNetwork methods +func createOpenApiOrgVdcNetwork(client *Client, OrgVdcNetworkConfig *types.OpenApiOrgVdcNetwork) (*OpenApiOrgVdcNetwork, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointOrgVdcNetworks + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + returnEgw := &OpenApiOrgVdcNetwork{ + OpenApiOrgVdcNetwork: &types.OpenApiOrgVdcNetwork{}, + client: client, + } + + err = client.OpenApiPostItem(minimumApiVersion, urlRef, nil, OrgVdcNetworkConfig, returnEgw.OpenApiOrgVdcNetwork, nil) + if err != nil { + return nil, fmt.Errorf("error creating Org VDC network: %s", err) + } + + return returnEgw, nil +} + +// GetSegmentProfile retrieves Segment Profile configuration for a single Org VDC Network +func (orgVdcNet *OpenApiOrgVdcNetwork) GetSegmentProfile() (*types.OrgVdcNetworkSegmentProfiles, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointOrgVdcNetworkSegmentProfiles, + endpointParams: []string{orgVdcNet.OpenApiOrgVdcNetwork.ID}, + entityLabel: labelOrgVdcNetworkSegmentProfile, + } + return getInnerEntity[types.OrgVdcNetworkSegmentProfiles](orgVdcNet.client, c) +} + +// UpdateSegmentProfile updates a Segment Profile with a given configuration +func (orgVdcNet *OpenApiOrgVdcNetwork) UpdateSegmentProfile(entityConfig *types.OrgVdcNetworkSegmentProfiles) (*types.OrgVdcNetworkSegmentProfiles, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointOrgVdcNetworkSegmentProfiles, + endpointParams: []string{orgVdcNet.OpenApiOrgVdcNetwork.ID}, + entityLabel: labelOrgVdcNetworkSegmentProfile, + } + return updateInnerEntity(orgVdcNet.client, c, entityConfig) +} + +// GetAllOpenApiOrgVdcNetworks checks all Org VDC networks available to the current user +// When 'multiSite' is set, it will also check the networks available from associated organizations +func (adminOrg *AdminOrg) GetAllOpenApiOrgVdcNetworks(queryParameters url.Values, multiSite bool) ([]*OpenApiOrgVdcNetwork, error) { + var additionalHeader map[string]string + if multiSite { + additionalHeader = map[string]string{"Accept": "{{MEDIA_TYPE}};version={{API_VERSION}};multisite=global"} + } + if queryParameters == nil { + queryParameters = make(url.Values) + } + result, err := getAllOpenApiOrgVdcNetworks(adminOrg.client, queryParameters, additionalHeader) + return result, err +} diff --git a/govcd/openapi_org_network_dhcp.go b/govcd/openapi_org_network_dhcp.go index 025965bf8..85ba1c1b4 100644 --- a/govcd/openapi_org_network_dhcp.go +++ b/govcd/openapi_org_network_dhcp.go @@ -17,6 +17,41 @@ type OpenApiOrgVdcNetworkDhcp struct { client *Client } +// GetOpenApiOrgVdcNetworkDhcp allows to retrieve DHCP configuration for specific Org VDC network +func (orgVdcNet *OpenApiOrgVdcNetwork) GetOpenApiOrgVdcNetworkDhcp() (*OpenApiOrgVdcNetworkDhcp, error) { + if orgVdcNet == nil || orgVdcNet.client == nil { + return nil, fmt.Errorf("error - Org VDC network and client cannot be nil") + } + + client := orgVdcNet.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointOrgVdcNetworksDhcp + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + if orgVdcNet.OpenApiOrgVdcNetwork.ID == "" { + return nil, fmt.Errorf("empty Org VDC network ID") + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, orgVdcNet.OpenApiOrgVdcNetwork.ID)) + if err != nil { + return nil, err + } + + orgNetDhcp := &OpenApiOrgVdcNetworkDhcp{ + OpenApiOrgVdcNetworkDhcp: &types.OpenApiOrgVdcNetworkDhcp{}, + client: client, + } + + err = client.OpenApiGetItem(apiVersion, urlRef, nil, orgNetDhcp.OpenApiOrgVdcNetworkDhcp, nil) + if err != nil { + return nil, err + } + + return orgNetDhcp, nil +} + // GetOpenApiOrgVdcNetworkDhcp allows to retrieve DHCP configuration for specific Org VDC network // ID specified as orgNetworkId using OpenAPI func (vdc *Vdc) GetOpenApiOrgVdcNetworkDhcp(orgNetworkId string) (*OpenApiOrgVdcNetworkDhcp, error) { @@ -27,7 +62,7 @@ func (vdc *Vdc) GetOpenApiOrgVdcNetworkDhcp(orgNetworkId string) (*OpenApiOrgVdc queryParameters := queryParameterFilterAnd("orgVdc.id=="+vdc.Vdc.ID, params) endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointOrgVdcNetworksDhcp - minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) if err != nil { return nil, err } @@ -46,7 +81,7 @@ func (vdc *Vdc) GetOpenApiOrgVdcNetworkDhcp(orgNetworkId string) (*OpenApiOrgVdc client: client, } - err = client.OpenApiGetItem(minimumApiVersion, urlRef, queryParameters, orgNetDhcp.OpenApiOrgVdcNetworkDhcp) + err = client.OpenApiGetItem(apiVersion, urlRef, queryParameters, orgNetDhcp.OpenApiOrgVdcNetworkDhcp, nil) if err != nil { return nil, err } @@ -57,52 +92,54 @@ func (vdc *Vdc) GetOpenApiOrgVdcNetworkDhcp(orgNetworkId string) (*OpenApiOrgVdc // UpdateOpenApiOrgVdcNetworkDhcp allows to update DHCP configuration for specific Org VDC network // ID specified as orgNetworkId using OpenAPI func (vdc *Vdc) UpdateOpenApiOrgVdcNetworkDhcp(orgNetworkId string, orgVdcNetworkDhcpConfig *types.OpenApiOrgVdcNetworkDhcp) (*OpenApiOrgVdcNetworkDhcp, error) { + return updateOrgNetworkDhcp(vdc.client, orgNetworkId, orgVdcNetworkDhcpConfig) +} + +// DeleteOpenApiOrgVdcNetworkDhcp allows to perform HTTP DELETE request on DHCP pool configuration for specified Org VDC +// Network ID +func (vdc *Vdc) DeleteOpenApiOrgVdcNetworkDhcp(orgNetworkId string) error { endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointOrgVdcNetworksDhcp - minimumApiVersion, err := vdc.client.checkOpenApiEndpointCompatibility(endpoint) + apiVersion, err := vdc.client.getOpenApiHighestElevatedVersion(endpoint) if err != nil { - return nil, err + return err + } + + if orgNetworkId == "" { + return fmt.Errorf("cannot delete Org VDC network DHCP configuration without ID") } urlRef, err := vdc.client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, orgNetworkId)) if err != nil { - return nil, err + return err } - orgNetDhcpResponse := &OpenApiOrgVdcNetworkDhcp{ - OpenApiOrgVdcNetworkDhcp: &types.OpenApiOrgVdcNetworkDhcp{}, - client: vdc.client, - } + err = vdc.client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) - err = vdc.client.OpenApiPutItem(minimumApiVersion, urlRef, nil, orgVdcNetworkDhcpConfig, orgNetDhcpResponse.OpenApiOrgVdcNetworkDhcp) if err != nil { - return nil, fmt.Errorf("error updating Org VDC network DHCP configuration: %s", err) + return fmt.Errorf("error deleting Org VDC network DHCP configuration: %s", err) } - return orgNetDhcpResponse, nil + return nil } -// DeleteOpenApiOrgVdcNetworkDhcp allows to perform HTTP DELETE request on DHCP pool configuration for specified Org VDC -// Network ID -// -// Note. VCD Versions before 10.2 do not allow to perform "DELETE" on DHCP pool and will return error. The way to -// remove DHCP configuration is to recreate Org VDC network itself. -func (vdc *Vdc) DeleteOpenApiOrgVdcNetworkDhcp(orgNetworkId string) error { +// DeletNetworkDhcp allows to perform HTTP DELETE request on DHCP pool configuration for Org network +func (orgVdcNet *OpenApiOrgVdcNetwork) DeletNetworkDhcp() error { endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointOrgVdcNetworksDhcp - minimumApiVersion, err := vdc.client.checkOpenApiEndpointCompatibility(endpoint) + apiVersion, err := orgVdcNet.client.getOpenApiHighestElevatedVersion(endpoint) if err != nil { return err } - if orgNetworkId == "" { + if orgVdcNet.OpenApiOrgVdcNetwork.ID == "" { return fmt.Errorf("cannot delete Org VDC network DHCP configuration without ID") } - urlRef, err := vdc.client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, orgNetworkId)) + urlRef, err := orgVdcNet.client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, orgVdcNet.OpenApiOrgVdcNetwork.ID)) if err != nil { return err } - err = vdc.client.OpenApiDeleteItem(minimumApiVersion, urlRef, nil) + err = orgVdcNet.client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) if err != nil { return fmt.Errorf("error deleting Org VDC network DHCP configuration: %s", err) @@ -110,3 +147,40 @@ func (vdc *Vdc) DeleteOpenApiOrgVdcNetworkDhcp(orgNetworkId string) error { return nil } + +func updateOrgNetworkDhcp(client *Client, orgNetworkId string, orgVdcNetworkDhcpConfig *types.OpenApiOrgVdcNetworkDhcp) (*OpenApiOrgVdcNetworkDhcp, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointOrgVdcNetworksDhcp + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, orgNetworkId)) + if err != nil { + return nil, err + } + + orgNetDhcpResponse := &OpenApiOrgVdcNetworkDhcp{ + OpenApiOrgVdcNetworkDhcp: &types.OpenApiOrgVdcNetworkDhcp{}, + client: client, + } + + // From v35.0 onwards, if orgVdcNetworkDhcpConfig.LeaseTime or orgVdcNetworkDhcpConfig.Mode are not explicitly + // passed, the API doesn't use any defaults returning an error. Previous API versions were setting + // LeaseTime to 86400 seconds and Mode to EDGE if these values were not supplied. These two conditional + // address the situation. + if orgVdcNetworkDhcpConfig.LeaseTime == nil { + orgVdcNetworkDhcpConfig.LeaseTime = addrOf(86400) + } + + if len(orgVdcNetworkDhcpConfig.Mode) == 0 { + orgVdcNetworkDhcpConfig.Mode = "EDGE" + } + + err = client.OpenApiPutItem(apiVersion, urlRef, nil, orgVdcNetworkDhcpConfig, orgNetDhcpResponse.OpenApiOrgVdcNetworkDhcp, nil) + if err != nil { + return nil, fmt.Errorf("error updating Org VDC network DHCP configuration: %s", err) + } + + return orgNetDhcpResponse, nil +} diff --git a/govcd/openapi_org_network_dhcp_binding.go b/govcd/openapi_org_network_dhcp_binding.go new file mode 100644 index 000000000..144545dfb --- /dev/null +++ b/govcd/openapi_org_network_dhcp_binding.go @@ -0,0 +1,277 @@ +/* + * 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" +) + +// OpenApiOrgVdcNetworkDhcpBinding handles IPv4 and IPv6 DHCP bindings for NSX-T Org VDC networks. +// Note. To create DHCP bindings, DHCP must be enabled on the network first (see +// `OpenApiOrgVdcNetworkDhcp`) +type OpenApiOrgVdcNetworkDhcpBinding struct { + OpenApiOrgVdcNetworkDhcpBinding *types.OpenApiOrgVdcNetworkDhcpBinding + client *Client + // ParentOrgVdcNetworkId is used to construct the URL for the DHCP binding as it contains Org + // VDC network ID in the path + ParentOrgVdcNetworkId string +} + +// CreateOpenApiOrgVdcNetworkDhcpBinding allows to create DHCP binding for specific Org VDC network +func (orgVdcNet *OpenApiOrgVdcNetwork) CreateOpenApiOrgVdcNetworkDhcpBinding(dhcpBindingConfig *types.OpenApiOrgVdcNetworkDhcpBinding) (*OpenApiOrgVdcNetworkDhcpBinding, error) { + client := orgVdcNet.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointOrgVdcNetworksDhcpBindings + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, orgVdcNet.OpenApiOrgVdcNetwork.ID)) + if err != nil { + return nil, err + } + + // DHCP Binding endpoint returns ID of newly created object in `Details` field of the task, + // which is not standard, therefore it must be explicitly handled in the code here + task, err := client.OpenApiPostItemAsync(apiVersion, urlRef, nil, dhcpBindingConfig) + if err != nil { + return nil, fmt.Errorf("error creating Org VDC Network DHCP Binding: %s", err) + } + + err = task.WaitTaskCompletion() + if err != nil { + return nil, fmt.Errorf("error waiting for Org VDC Network DHCP Binding to be created: %s", err) + } + + dhcpBindingId := task.Task.Details + if dhcpBindingId == "" { + return nil, fmt.Errorf("could not retrieve ID of newly created DHCP binding for Org VDC network with IP address '%s' and MAC address '%s'", + dhcpBindingConfig.IpAddress, dhcpBindingConfig.MacAddress) + } + + // Get the DHCP binding by ID and return it + createdBinding, err := orgVdcNet.GetOpenApiOrgVdcNetworkDhcpBindingById(dhcpBindingId) + if err != nil { + return nil, fmt.Errorf("error retrieving DHCP binding for Org VDC network after creation: %s", err) + } + + return createdBinding, nil +} + +// GetAllOpenApiOrgVdcNetworkDhcpBindings allows to retrieve all DHCP binding configurations for +// specific Org VDC network +func (orgVdcNet *OpenApiOrgVdcNetwork) GetAllOpenApiOrgVdcNetworkDhcpBindings(queryParameters url.Values) ([]*OpenApiOrgVdcNetworkDhcpBinding, error) { + if orgVdcNet == nil || orgVdcNet.client == nil { + return nil, fmt.Errorf("error - Org VDC network and client cannot be nil") + } + + if orgVdcNet.OpenApiOrgVdcNetwork == nil || orgVdcNet.OpenApiOrgVdcNetwork.ID == "" { + return nil, fmt.Errorf("empty Org VDC network ID") + } + + client := orgVdcNet.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointOrgVdcNetworksDhcpBindings + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, orgVdcNet.OpenApiOrgVdcNetwork.ID)) + if err != nil { + return nil, err + } + + typeResponses := []*types.OpenApiOrgVdcNetworkDhcpBinding{{}} + err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParameters, &typeResponses, nil) + if err != nil { + return nil, err + } + + // Wrap all typeResponses into OpenApiOrgVdcNetworkDhcpBinding types with client + wrappedResponses := make([]*OpenApiOrgVdcNetworkDhcpBinding, len(typeResponses)) + for sliceIndex := range typeResponses { + wrappedResponses[sliceIndex] = &OpenApiOrgVdcNetworkDhcpBinding{ + OpenApiOrgVdcNetworkDhcpBinding: typeResponses[sliceIndex], + client: client, + ParentOrgVdcNetworkId: orgVdcNet.OpenApiOrgVdcNetwork.ID, + } + } + + return wrappedResponses, nil +} + +// GetOpenApiOrgVdcNetworkDhcpBindingById allows to retrieve DHCP binding configuration +func (orgVdcNet *OpenApiOrgVdcNetwork) GetOpenApiOrgVdcNetworkDhcpBindingById(id string) (*OpenApiOrgVdcNetworkDhcpBinding, error) { + if orgVdcNet == nil || orgVdcNet.client == nil { + return nil, fmt.Errorf("error - Org VDC network and client cannot be nil") + } + + if id == "" { + return nil, fmt.Errorf("empty DHCP binding ID") + } + + if orgVdcNet.OpenApiOrgVdcNetwork == nil || orgVdcNet.OpenApiOrgVdcNetwork.ID == "" { + return nil, fmt.Errorf("empty Org VDC network ID") + } + + client := orgVdcNet.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointOrgVdcNetworksDhcpBindings + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, orgVdcNet.OpenApiOrgVdcNetwork.ID), id) + if err != nil { + return nil, err + } + + orgVdcNetDhcpBinding := &OpenApiOrgVdcNetworkDhcpBinding{ + OpenApiOrgVdcNetworkDhcpBinding: &types.OpenApiOrgVdcNetworkDhcpBinding{}, + client: client, + ParentOrgVdcNetworkId: orgVdcNet.OpenApiOrgVdcNetwork.ID, + } + + err = client.OpenApiGetItem(apiVersion, urlRef, nil, orgVdcNetDhcpBinding.OpenApiOrgVdcNetworkDhcpBinding, nil) + if err != nil { + return nil, err + } + + return orgVdcNetDhcpBinding, nil +} + +// GetOpenApiOrgVdcNetworkDhcpBindingByName allows to retrieve DHCP binding configuration by name +func (orgVdcNet *OpenApiOrgVdcNetwork) GetOpenApiOrgVdcNetworkDhcpBindingByName(name string) (*OpenApiOrgVdcNetworkDhcpBinding, error) { + // TODO: uncomment when filtering by name is supported in VCD API (It was not supported up to + // VCD10.4.1) + // Perform filtering by name in VCD API + // queryParameters := url.Values{} + // queryParameters.Add("filter", fmt.Sprintf("name==%s", name)) + + // allBindings, err := orgVdcNet.GetAllOpenApiOrgVdcNetworkDhcpBindings(queryParameters) + + allDhcpBindings, err := orgVdcNet.GetAllOpenApiOrgVdcNetworkDhcpBindings(nil) + if err != nil { + return nil, fmt.Errorf("unable to retrieve Org VDC network by name '%s': %s", name, err) + } + + // Bindings do not support name filtering, so we need to filter them manually + var foundBinding *OpenApiOrgVdcNetworkDhcpBinding + for _, binding := range allDhcpBindings { + if binding.OpenApiOrgVdcNetworkDhcpBinding.Name == name { + foundBinding = binding + break + } + } + + if foundBinding == nil { + return nil, fmt.Errorf("%s: could not find NSX-T Org Network Binding by Name %s", ErrorEntityNotFound, name) + } + + return foundBinding, nil +} + +// Update allows to update DHCP configuration +// +// Note. This API requires `Version` field to be sent in the request and this function does it +// automatically +func (dhcpBinding *OpenApiOrgVdcNetworkDhcpBinding) Update(orgVdcNetworkDhcpConfig *types.OpenApiOrgVdcNetworkDhcpBinding) (*OpenApiOrgVdcNetworkDhcpBinding, error) { + client := dhcpBinding.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointOrgVdcNetworksDhcpBindings + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + if orgVdcNetworkDhcpConfig.ID == "" { + return nil, fmt.Errorf("empty Org VDC network DHCP binding ID") + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, dhcpBinding.ParentOrgVdcNetworkId), orgVdcNetworkDhcpConfig.ID) + if err != nil { + return nil, err + } + + result := &OpenApiOrgVdcNetworkDhcpBinding{ + OpenApiOrgVdcNetworkDhcpBinding: &types.OpenApiOrgVdcNetworkDhcpBinding{ID: orgVdcNetworkDhcpConfig.ID}, + client: client, + ParentOrgVdcNetworkId: dhcpBinding.ParentOrgVdcNetworkId, + } + + // load latest binding information to fetch Version value which is required for updates + err = result.Refresh() + if err != nil { + return nil, fmt.Errorf("error refreshing Org VDC network DHCP binding configuration with ID '%s': %s", orgVdcNetworkDhcpConfig.ID, err) + } + orgVdcNetworkDhcpConfig.Version = result.OpenApiOrgVdcNetworkDhcpBinding.Version + + err = client.OpenApiPutItem(apiVersion, urlRef, nil, orgVdcNetworkDhcpConfig, result.OpenApiOrgVdcNetworkDhcpBinding, nil) + if err != nil { + return nil, fmt.Errorf("error updating Org VDC network DHCP configuration with ID '%s': %s", orgVdcNetworkDhcpConfig.ID, err) + } + + return result, nil +} + +// Refresh DHCP binding configuration. Mainly useful for retrieving latest `Version` field` of DHCP +// binding before performing update +func (dhcpBinding *OpenApiOrgVdcNetworkDhcpBinding) Refresh() error { + if dhcpBinding.ParentOrgVdcNetworkId == "" { + return fmt.Errorf("empty parent Org VDC network ID") + } + + if dhcpBinding.OpenApiOrgVdcNetworkDhcpBinding.ID == "" { + return fmt.Errorf("empty DHCP binding ID") + } + + client := dhcpBinding.client + orgVdcNet, err := getOpenApiOrgVdcNetworkById(client, dhcpBinding.ParentOrgVdcNetworkId, nil) + if err != nil { + return fmt.Errorf("error refreshing Org VDC network DHCP binding configuration with ID '%s': %s", dhcpBinding.OpenApiOrgVdcNetworkDhcpBinding.ID, err) + } + + newDhcpBinding, err := orgVdcNet.GetOpenApiOrgVdcNetworkDhcpBindingById(dhcpBinding.OpenApiOrgVdcNetworkDhcpBinding.ID) + if err != nil { + return fmt.Errorf("error refreshing Org VDC network DHCP binding configuration with ID '%s': %s", dhcpBinding.OpenApiOrgVdcNetworkDhcpBinding.ID, err) + } + + // Explicitly reassign the body + dhcpBinding.OpenApiOrgVdcNetworkDhcpBinding = newDhcpBinding.OpenApiOrgVdcNetworkDhcpBinding + + return nil +} + +// Delete removes DHCP binding by performing HTTP DELETE request on DHCP binding +func (dhcpBinding *OpenApiOrgVdcNetworkDhcpBinding) Delete() error { + if dhcpBinding.ParentOrgVdcNetworkId == "" { + return fmt.Errorf("empty parent Org VDC network ID") + } + + if dhcpBinding.OpenApiOrgVdcNetworkDhcpBinding.ID == "" { + return fmt.Errorf("empty DHCP binding ID") + } + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointOrgVdcNetworksDhcpBindings + apiVersion, err := dhcpBinding.client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return err + } + + urlRef, err := dhcpBinding.client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, dhcpBinding.ParentOrgVdcNetworkId), dhcpBinding.OpenApiOrgVdcNetworkDhcpBinding.ID) + if err != nil { + return err + } + + err = dhcpBinding.client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) + + if err != nil { + return fmt.Errorf("error deleting Org VDC network DHCP configuration: %s", err) + } + + return nil +} diff --git a/govcd/openapi_org_network_test.go b/govcd/openapi_org_network_test.go index f5efcfa87..3e97c87b7 100644 --- a/govcd/openapi_org_network_test.go +++ b/govcd/openapi_org_network_test.go @@ -1,4 +1,4 @@ -// +build network nsxt functional openapi ALL +//go:build network || nsxt || functional || openapi || ALL /* * Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -16,12 +16,12 @@ import ( func (vcd *TestVCD) Test_NsxtOrgVdcNetworkIsolated(check *C) { skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointOrgVdcNetworks) skipNoNsxtConfiguration(vcd, check) + vcd.skipIfNotSysAdmin(check) // this test uses GetNsxtEdgeClusterByName, which requires system administrator privileges orgVdcNetworkConfig := &types.OpenApiOrgVdcNetwork{ Name: check.TestName(), Description: check.TestName() + "-description", - OrgVdc: &types.OpenApiReference{ID: vcd.nsxtVdc.Vdc.ID}, - + OwnerRef: &types.OpenApiReference{ID: vcd.nsxtVdc.Vdc.ID}, NetworkType: types.OrgVdcNetworkTypeIsolated, Subnets: types.OrgVdcNetworkSubnets{ Values: []types.OrgVdcNetworkSubnetValues{ @@ -51,12 +51,16 @@ func (vcd *TestVCD) Test_NsxtOrgVdcNetworkIsolated(check *C) { }, } - runOpenApiOrgVdcNetworkTest(check, vcd.nsxtVdc, orgVdcNetworkConfig, types.OrgVdcNetworkTypeIsolated, nil) + runOpenApiOrgVdcNetworkTestWithSegmentProfileTemplateEndpoint(check, vcd, vcd.nsxtVdc, orgVdcNetworkConfig, types.OrgVdcNetworkTypeIsolated) + runOpenApiOrgVdcNetworkTestWithSegmentProfileTemplate(check, vcd, vcd.nsxtVdc, orgVdcNetworkConfig, types.OrgVdcNetworkTypeIsolated) + runOpenApiOrgVdcNetworkTest(check, vcd, vcd.nsxtVdc, orgVdcNetworkConfig, types.OrgVdcNetworkTypeIsolated, []dhcpConfigFunc{nsxtDhcpConfigNetworkMode}) + runOpenApiOrgVdcNetworkWithVdcGroupTest(check, vcd, orgVdcNetworkConfig, types.OrgVdcNetworkTypeIsolated, []dhcpConfigFunc{nsxtDhcpConfigNetworkMode}) } func (vcd *TestVCD) Test_NsxtOrgVdcNetworkRouted(check *C) { skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointOrgVdcNetworks) skipNoNsxtConfiguration(vcd, check) + vcd.skipIfNotSysAdmin(check) // this test uses GetNsxtEdgeClusterByName, which requires system administrator privileges egw, err := vcd.org.GetNsxtEdgeGatewayByName(vcd.config.VCD.Nsxt.EdgeGateway) check.Assert(err, IsNil) @@ -64,8 +68,7 @@ func (vcd *TestVCD) Test_NsxtOrgVdcNetworkRouted(check *C) { orgVdcNetworkConfig := &types.OpenApiOrgVdcNetwork{ Name: check.TestName(), Description: check.TestName() + "-description", - OrgVdc: &types.OpenApiReference{ID: vcd.nsxtVdc.Vdc.ID}, - + OwnerRef: &types.OpenApiReference{ID: vcd.nsxtVdc.Vdc.ID}, NetworkType: types.OrgVdcNetworkTypeRouted, // Connection is used for "routed" network @@ -109,10 +112,13 @@ func (vcd *TestVCD) Test_NsxtOrgVdcNetworkRouted(check *C) { }, } - runOpenApiOrgVdcNetworkTest(check, vcd.nsxtVdc, orgVdcNetworkConfig, types.OrgVdcNetworkTypeRouted, nsxtRoutedDhcpConfig) + runOpenApiOrgVdcNetworkTestWithSegmentProfileTemplateEndpoint(check, vcd, vcd.nsxtVdc, orgVdcNetworkConfig, types.OrgVdcNetworkTypeRouted) + runOpenApiOrgVdcNetworkTestWithSegmentProfileTemplate(check, vcd, vcd.nsxtVdc, orgVdcNetworkConfig, types.OrgVdcNetworkTypeRouted) + runOpenApiOrgVdcNetworkTest(check, vcd, vcd.nsxtVdc, orgVdcNetworkConfig, types.OrgVdcNetworkTypeRouted, []dhcpConfigFunc{nsxtRoutedDhcpConfigEdgeMode, nsxtDhcpConfigNetworkMode}) + 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())) } @@ -129,7 +135,9 @@ func (vcd *TestVCD) Test_NsxtOrgVdcNetworkImported(check *C) { orgVdcNetworkConfig := &types.OpenApiOrgVdcNetwork{ Name: check.TestName(), Description: check.TestName() + "-description", - OrgVdc: &types.OpenApiReference{ID: vcd.nsxtVdc.Vdc.ID}, + + // On v35.0 orgVdc is not supported anymore. Using ownerRef instead. + OwnerRef: &types.OpenApiReference{ID: vcd.nsxtVdc.Vdc.ID}, NetworkType: types.OrgVdcNetworkTypeOpaque, // BackingNetworkId contains NSX-T logical switch ID for Imported networks @@ -159,8 +167,63 @@ func (vcd *TestVCD) Test_NsxtOrgVdcNetworkImported(check *C) { }, } - runOpenApiOrgVdcNetworkTest(check, vcd.nsxtVdc, orgVdcNetworkConfig, types.OrgVdcNetworkTypeOpaque, nil) + runOpenApiOrgVdcNetworkTestWithSegmentProfileTemplateEndpoint(check, vcd, vcd.nsxtVdc, orgVdcNetworkConfig, types.OrgVdcNetworkTypeOpaque) + runOpenApiOrgVdcNetworkTestWithSegmentProfileTemplate(check, vcd, vcd.nsxtVdc, orgVdcNetworkConfig, types.OrgVdcNetworkTypeOpaque) + runOpenApiOrgVdcNetworkTest(check, vcd, vcd.nsxtVdc, orgVdcNetworkConfig, types.OrgVdcNetworkTypeOpaque, nil) + 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) { @@ -169,7 +232,9 @@ func (vcd *TestVCD) Test_NsxvOrgVdcNetworkIsolated(check *C) { orgVdcNetworkConfig := &types.OpenApiOrgVdcNetwork{ Name: check.TestName(), Description: check.TestName() + "-description", - OrgVdc: &types.OpenApiReference{ID: vcd.vdc.Vdc.ID}, + + // On v35.0 orgVdc is not supported anymore. Using ownerRef instead. + OwnerRef: &types.OpenApiReference{ID: vcd.vdc.Vdc.ID}, NetworkType: types.OrgVdcNetworkTypeIsolated, Subnets: types.OrgVdcNetworkSubnets{ @@ -200,7 +265,7 @@ func (vcd *TestVCD) Test_NsxvOrgVdcNetworkIsolated(check *C) { }, } - runOpenApiOrgVdcNetworkTest(check, vcd.vdc, orgVdcNetworkConfig, types.OrgVdcNetworkTypeIsolated, nil) + runOpenApiOrgVdcNetworkTest(check, vcd, vcd.vdc, orgVdcNetworkConfig, types.OrgVdcNetworkTypeIsolated, nil) } func (vcd *TestVCD) Test_NsxvOrgVdcNetworkRouted(check *C) { @@ -212,7 +277,9 @@ func (vcd *TestVCD) Test_NsxvOrgVdcNetworkRouted(check *C) { orgVdcNetworkConfig := &types.OpenApiOrgVdcNetwork{ Name: check.TestName(), Description: check.TestName() + "-description", - OrgVdc: &types.OpenApiReference{ID: vcd.vdc.Vdc.ID}, + + // On v35.0 orgVdc is not supported anymore. Using ownerRef instead. + OwnerRef: &types.OpenApiReference{ID: vcd.vdc.Vdc.ID}, NetworkType: types.OrgVdcNetworkTypeRouted, @@ -257,7 +324,7 @@ func (vcd *TestVCD) Test_NsxvOrgVdcNetworkRouted(check *C) { }, } - runOpenApiOrgVdcNetworkTest(check, vcd.vdc, orgVdcNetworkConfig, types.OrgVdcNetworkTypeRouted, nil) + runOpenApiOrgVdcNetworkTest(check, vcd, vcd.vdc, orgVdcNetworkConfig, types.OrgVdcNetworkTypeRouted, nil) } func (vcd *TestVCD) Test_NsxvOrgVdcNetworkDirect(check *C) { @@ -266,15 +333,16 @@ func (vcd *TestVCD) Test_NsxvOrgVdcNetworkDirect(check *C) { check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) } - // Using legacy API lookup function because GetExternalNetworkV2ByName does not support VCD 9.7 - externalNetwork, err := vcd.client.GetExternalNetworkByName(vcd.config.VCD.ExternalNetwork) - + externalNetwork, err := GetExternalNetworkV2ByName(vcd.client, vcd.config.VCD.ExternalNetwork) check.Assert(err, IsNil) + check.Assert(externalNetwork, NotNil) orgVdcNetworkConfig := &types.OpenApiOrgVdcNetwork{ Name: check.TestName(), Description: check.TestName() + "-description", - OrgVdc: &types.OpenApiReference{ID: vcd.vdc.Vdc.ID}, + + // On v35.0 orgVdc is not supported anymore. Using ownerRef instead. + OwnerRef: &types.OpenApiReference{ID: vcd.vdc.Vdc.ID}, NetworkType: types.OrgVdcNetworkTypeDirect, ParentNetwork: &types.OpenApiReference{ID: externalNetwork.ExternalNetwork.ID}, @@ -313,10 +381,10 @@ func (vcd *TestVCD) Test_NsxvOrgVdcNetworkDirect(check *C) { }, } - runOpenApiOrgVdcNetworkTest(check, vcd.vdc, orgVdcNetworkConfig, types.OrgVdcNetworkTypeDirect, nil) + runOpenApiOrgVdcNetworkTest(check, vcd, vcd.vdc, orgVdcNetworkConfig, types.OrgVdcNetworkTypeDirect, nil) } -func runOpenApiOrgVdcNetworkTest(check *C, vdc *Vdc, orgVdcNetworkConfig *types.OpenApiOrgVdcNetwork, extpectNetworkType string, dhcpFunc dhcpConfigFunc) { +func runOpenApiOrgVdcNetworkTest(check *C, vcd *TestVCD, vdc *Vdc, orgVdcNetworkConfig *types.OpenApiOrgVdcNetwork, expectNetworkType string, dhcpFunc []dhcpConfigFunc) { orgVdcNet, err := vdc.CreateOpenApiOrgVdcNetwork(orgVdcNetworkConfig) check.Assert(err, IsNil) @@ -324,7 +392,7 @@ func runOpenApiOrgVdcNetworkTest(check *C, vdc *Vdc, orgVdcNetworkConfig *types. openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointOrgVdcNetworks + orgVdcNet.OpenApiOrgVdcNetwork.ID AddToCleanupListOpenApi(orgVdcNet.OpenApiOrgVdcNetwork.Name, check.TestName(), openApiEndpoint) - check.Assert(orgVdcNet.GetType(), Equals, extpectNetworkType) + check.Assert(orgVdcNet.GetType(), Equals, expectNetworkType) // Check it can be found orgVdcNetByIdInVdc, err := vdc.GetOpenApiOrgVdcNetworkById(orgVdcNet.OpenApiOrgVdcNetwork.ID) @@ -356,8 +424,8 @@ func runOpenApiOrgVdcNetworkTest(check *C, vdc *Vdc, orgVdcNetworkConfig *types. check.Assert(updatedOrgVdcNet.OpenApiOrgVdcNetwork.Description, Equals, orgVdcNet.OpenApiOrgVdcNetwork.Description) // Configure DHCP if specified - if dhcpFunc != nil { - dhcpFunc(check, vdc, updatedOrgVdcNet.OpenApiOrgVdcNetwork.ID) + for i := range dhcpFunc { + dhcpFunc[i](check, vcd, vdc, updatedOrgVdcNet.OpenApiOrgVdcNetwork.ID) } // Delete err = orgVdcNet.Delete() @@ -371,30 +439,520 @@ func runOpenApiOrgVdcNetworkTest(check *C, vdc *Vdc, orgVdcNetworkConfig *types. check.Assert(ContainsNotFound(err), Equals, true) } -type dhcpConfigFunc func(check *C, vdc *Vdc, orgNetId string) +type dhcpConfigFunc func(check *C, vcd *TestVCD, vdc *Vdc, orgNetId string) + +func nsxtRoutedDhcpConfigEdgeMode(check *C, vcd *TestVCD, vdc *Vdc, orgNetId string) { + printVerbose("## Testing DHCP in EDGE mode\n") + dhcpDefinition := &types.OpenApiOrgVdcNetworkDhcp{ + Enabled: addrOf(true), + DhcpPools: []types.OpenApiOrgVdcNetworkDhcpPools{ + { + Enabled: addrOf(true), + IPRange: types.OpenApiOrgVdcNetworkDhcpIpRange{ + StartAddress: "2.1.1.200", + EndAddress: "2.1.1.201", + }, + }, + }, + DnsServers: []string{ + "8.8.8.8", + "8.8.4.4", + }, + } + + // In API versions lower than 36.1, dnsServers list does not exist + if vdc.client.APIVCDMaxVersionIs("< 36.1") { + dhcpDefinition.DnsServers = nil + } + + orgVdcNetwork, err := vcd.org.GetOpenApiOrgVdcNetworkById(orgNetId) + check.Assert(err, IsNil) + check.Assert(orgVdcNetwork, NotNil) + + // Check that DHCP is not enabled + check.Assert(orgVdcNetwork.IsDhcpEnabled(), Equals, false) + + updatedDhcp, err := vdc.UpdateOpenApiOrgVdcNetworkDhcp(orgNetId, dhcpDefinition) + check.Assert(err, IsNil) + + // Check that DHCP is enabled + check.Assert(orgVdcNetwork.IsDhcpEnabled(), Equals, true) + check.Assert(dhcpDefinition, DeepEquals, updatedDhcp.OpenApiOrgVdcNetworkDhcp) + + if orgVdcNetwork.client.APIVCDMaxVersionIs(">= 36.1") { + printVerbose("### Testing DHCP Bindings - only supported in 10.3.1+\n") + testNsxtDhcpBinding(check, vcd, orgVdcNetwork) + } + + err = vdc.DeleteOpenApiOrgVdcNetworkDhcp(orgNetId) + check.Assert(err, IsNil) + + updatedDhcp2, err := orgVdcNetwork.UpdateDhcp(dhcpDefinition) + check.Assert(err, IsNil) + check.Assert(updatedDhcp2, NotNil) + check.Assert(dhcpDefinition, DeepEquals, updatedDhcp2.OpenApiOrgVdcNetworkDhcp) + + err = orgVdcNetwork.DeletNetworkDhcp() + check.Assert(err, IsNil) + + deletedDhcp, err := orgVdcNetwork.GetOpenApiOrgVdcNetworkDhcp() + check.Assert(err, IsNil) + check.Assert(len(deletedDhcp.OpenApiOrgVdcNetworkDhcp.DhcpPools), Equals, 0) + check.Assert(len(deletedDhcp.OpenApiOrgVdcNetworkDhcp.DnsServers), Equals, 0) + + // Check that DHCP is not enabled + check.Assert(orgVdcNetwork.IsDhcpEnabled(), Equals, false) +} + +// nsxtDhcpConfigNetworkMode checks DHCP functionality in NETWORK mode. +// It requires that Edge Cluster is set at VDC level therefore this function does it for the +// duration of this test and restores it back +func nsxtDhcpConfigNetworkMode(check *C, vcd *TestVCD, vdc *Vdc, orgNetId string) { + // Only supported in 10.3.1+ + if vdc.client.APIVCDMaxVersionIs("< 36.1") { + return + } + + printVerbose("## Testing DHCP in NETWORK mode\n") + + // DHCP in NETWORK mode requires Edge Cluster to be set for VDC and cleaned up afterward + edgeCluster, err := vdc.GetNsxtEdgeClusterByName(vcd.config.VCD.Nsxt.NsxtEdgeCluster) + check.Assert(err, IsNil) + vdcNetworkProfile := &types.VdcNetworkProfile{ + ServicesEdgeCluster: &types.VdcNetworkProfileServicesEdgeCluster{ + BackingID: edgeCluster.NsxtEdgeCluster.ID, + }, + } + _, err = vdc.UpdateVdcNetworkProfile(vdcNetworkProfile) + check.Assert(err, IsNil) + defer func() { + err := vdc.DeleteVdcNetworkProfile() + if err != nil { + check.Errorf("error cleaning up VDC Network Profile: %s", err) + } + }() -func nsxtRoutedDhcpConfig(check *C, vdc *Vdc, orgNetId string) { dhcpDefinition := &types.OpenApiOrgVdcNetworkDhcp{ - Enabled: takeBoolPointer(true), + Enabled: addrOf(true), + Mode: "NETWORK", + IPAddress: "2.1.1.252", DhcpPools: []types.OpenApiOrgVdcNetworkDhcpPools{ { - Enabled: takeBoolPointer(true), + Enabled: addrOf(true), IPRange: types.OpenApiOrgVdcNetworkDhcpIpRange{ StartAddress: "2.1.1.200", EndAddress: "2.1.1.201", }, }, }, + DnsServers: []string{ + "8.8.8.8", + "8.8.4.4", + }, } + + orgVdcNetwork, err := vcd.org.GetOpenApiOrgVdcNetworkById(orgNetId) + check.Assert(err, IsNil) + check.Assert(orgVdcNetwork, NotNil) + + // Check that DHCP is not enabled + check.Assert(orgVdcNetwork.IsDhcpEnabled(), Equals, false) + updatedDhcp, err := vdc.UpdateOpenApiOrgVdcNetworkDhcp(orgNetId, dhcpDefinition) check.Assert(err, IsNil) + // Check that DHCP is enabled + check.Assert(orgVdcNetwork.IsDhcpEnabled(), Equals, true) check.Assert(dhcpDefinition, DeepEquals, updatedDhcp.OpenApiOrgVdcNetworkDhcp) - // VCD Versions before 10.2 do not allow to perform "DELETE" on DHCP pool - // To remove DHCP configuration one must remove Org VDC network itself. - if vdc.client.APIVCDMaxVersionIs(">= 35.0") { - err = vdc.DeleteOpenApiOrgVdcNetworkDhcp(orgNetId) - check.Assert(err, IsNil) + if orgVdcNetwork.client.APIVCDMaxVersionIs(">= 36.1") { + printVerbose("### Testing DHCP Bindings - only supported in 10.3.1+\n") + testNsxtDhcpBinding(check, vcd, orgVdcNetwork) + } + + err = vdc.DeleteOpenApiOrgVdcNetworkDhcp(orgNetId) + check.Assert(err, IsNil) + + updatedDhcp2, err := orgVdcNetwork.UpdateDhcp(dhcpDefinition) + check.Assert(err, IsNil) + check.Assert(updatedDhcp2, NotNil) + + check.Assert(dhcpDefinition, DeepEquals, updatedDhcp2.OpenApiOrgVdcNetworkDhcp) + + err = orgVdcNetwork.DeletNetworkDhcp() + check.Assert(err, IsNil) + + deletedDhcp, err := orgVdcNetwork.GetOpenApiOrgVdcNetworkDhcp() + check.Assert(err, IsNil) + check.Assert(len(deletedDhcp.OpenApiOrgVdcNetworkDhcp.DhcpPools), Equals, 0) + check.Assert(len(deletedDhcp.OpenApiOrgVdcNetworkDhcp.DnsServers), Equals, 0) + + // Check that DHCP is not enabled + check.Assert(orgVdcNetwork.IsDhcpEnabled(), Equals, false) +} + +func runOpenApiOrgVdcNetworkWithVdcGroupTest(check *C, vcd *TestVCD, orgVdcNetworkConfig *types.OpenApiOrgVdcNetwork, expectNetworkType string, dhcpFunc []dhcpConfigFunc) { + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + + nsxtExternalNetwork, err := GetExternalNetworkV2ByName(vcd.client, vcd.config.VCD.Nsxt.ExternalNetwork) + check.Assert(err, IsNil) + check.Assert(nsxtExternalNetwork, NotNil) + + vdc, vdcGroup := test_CreateVdcGroup(check, adminOrg, vcd) + egwDefinition := &types.OpenAPIEdgeGateway{ + Name: "nsx-for-org-network-edge", + Description: "nsx-for-org-network-edge-description", + OwnerRef: &types.OpenApiReference{ + ID: vdc.Vdc.ID, + }, + EdgeGatewayUplinks: []types.EdgeGatewayUplinks{{ + UplinkID: nsxtExternalNetwork.ExternalNetwork.ID, + Subnets: types.OpenAPIEdgeGatewaySubnets{Values: []types.OpenAPIEdgeGatewaySubnetValue{{ + Gateway: "10.10.10.10", + PrefixLength: 24, + Enabled: true, + }}}, + Connected: true, + Dedicated: false, + }}, + } + + // Create Edge Gateway in VDC Group + createdEdge, err := adminOrg.CreateNsxtEdgeGateway(egwDefinition) + check.Assert(err, IsNil) + check.Assert(createdEdge.EdgeGateway.OwnerRef.ID, Matches, `^urn:vcloud:vdc:.*`) + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGateways + createdEdge.EdgeGateway.ID + PrependToCleanupListOpenApi(createdEdge.EdgeGateway.Name, check.TestName(), openApiEndpoint) + + check.Assert(createdEdge.EdgeGateway.Name, Equals, egwDefinition.Name) + check.Assert(createdEdge.EdgeGateway.OwnerRef.ID, Equals, egwDefinition.OwnerRef.ID) + + movedGateway, err := createdEdge.MoveToVdcOrVdcGroup(vdcGroup.VdcGroup.Id) + check.Assert(err, IsNil) + check.Assert(movedGateway.EdgeGateway.OwnerRef.ID, Equals, vdcGroup.VdcGroup.Id) + check.Assert(movedGateway.EdgeGateway.OwnerRef.ID, Matches, `^urn:vcloud:vdcGroup:.*`) + + orgVdcNetworkConfig.OwnerRef.ID = vdcGroup.VdcGroup.Id + if orgVdcNetworkConfig.Connection != nil { + orgVdcNetworkConfig.Connection.RouterRef.ID = movedGateway.EdgeGateway.ID + } + orgVdcNet, err := vdcGroup.CreateOpenApiOrgVdcNetwork(orgVdcNetworkConfig) + check.Assert(err, IsNil) + + // Use generic "OpenApiEntity" resource cleanup type + openApiEndpoint = types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointOrgVdcNetworks + orgVdcNet.OpenApiOrgVdcNetwork.ID + AddToCleanupListOpenApi(orgVdcNet.OpenApiOrgVdcNetwork.Name, check.TestName(), openApiEndpoint) + + check.Assert(orgVdcNet.GetType(), Equals, expectNetworkType) + + // Check it can be found + orgVdcNetByIdInVdc, err := vdcGroup.GetOpenApiOrgVdcNetworkById(orgVdcNet.OpenApiOrgVdcNetwork.ID) + check.Assert(err, IsNil) + check.Assert(orgVdcNetByIdInVdc, NotNil) + orgVdcNetByName, err := vdcGroup.GetOpenApiOrgVdcNetworkByName(orgVdcNet.OpenApiOrgVdcNetwork.Name) + check.Assert(err, IsNil) + check.Assert(orgVdcNetByName, NotNil) + + check.Assert(orgVdcNetByIdInVdc.OpenApiOrgVdcNetwork.ID, Equals, orgVdcNet.OpenApiOrgVdcNetwork.ID) + check.Assert(orgVdcNetByName.OpenApiOrgVdcNetwork.ID, Equals, orgVdcNet.OpenApiOrgVdcNetwork.ID) + + // Retrieve all networks in VDC and expect newly created network to be there + var foundNetInVdc bool + allOrgVdcNets, err := vdcGroup.GetAllOpenApiOrgVdcNetworks(nil) + check.Assert(err, IsNil) + for _, net := range allOrgVdcNets { + if net.OpenApiOrgVdcNetwork.ID == orgVdcNet.OpenApiOrgVdcNetwork.ID { + foundNetInVdc = true + } + } + check.Assert(foundNetInVdc, Equals, true) + + // Update + orgVdcNet.OpenApiOrgVdcNetwork.Description = check.TestName() + "updated description" + updatedOrgVdcNet, err := orgVdcNet.Update(orgVdcNet.OpenApiOrgVdcNetwork) + check.Assert(err, IsNil) + + check.Assert(updatedOrgVdcNet.OpenApiOrgVdcNetwork.Name, Equals, orgVdcNet.OpenApiOrgVdcNetwork.Name) + check.Assert(updatedOrgVdcNet.OpenApiOrgVdcNetwork.ID, Equals, orgVdcNet.OpenApiOrgVdcNetwork.ID) + check.Assert(updatedOrgVdcNet.OpenApiOrgVdcNetwork.Description, Equals, orgVdcNet.OpenApiOrgVdcNetwork.Description) + + // Configure DHCP if specified + for i := range dhcpFunc { + dhcpFunc[i](check, vcd, vdc, updatedOrgVdcNet.OpenApiOrgVdcNetwork.ID) + } + // Delete + err = orgVdcNet.Delete() + check.Assert(err, IsNil) + + // Test again if it was deleted and expect it to contain ErrorEntityNotFound + _, err = vdcGroup.GetOpenApiOrgVdcNetworkByName(orgVdcNet.OpenApiOrgVdcNetwork.Name) + check.Assert(ContainsNotFound(err), Equals, true) + + _, err = vdcGroup.GetOpenApiOrgVdcNetworkById(orgVdcNet.OpenApiOrgVdcNetwork.ID) + check.Assert(ContainsNotFound(err), Equals, true) + + //cleanup + err = movedGateway.Delete() + check.Assert(err, IsNil) + + // Remove VDC group and VDC + err = vdcGroup.Delete() + check.Assert(err, IsNil) + task, err := vdc.Delete(true, true) + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) +} + +func testNsxtDhcpBinding(check *C, vcd *TestVCD, orgNet *OpenApiOrgVdcNetwork) { + // define DHCP binding configuration + dhcpBindingConfig := &types.OpenApiOrgVdcNetworkDhcpBinding{ + Name: check.TestName() + "-dhcp-binding", + Description: "dhcp binding description", + IpAddress: "2.1.1.231", + MacAddress: "00:11:22:33:44:55", + BindingType: types.NsxtDhcpBindingTypeIpv4, + DhcpV4BindingConfig: &types.DhcpV4BindingConfig{ + HostName: "dhcp-binding-hostname", + GatewayIPAddress: "2.1.1.244", + }, + } + + // create DHCP binding + createdDhcpBinding, err := orgNet.CreateOpenApiOrgVdcNetworkDhcpBinding(dhcpBindingConfig) + check.Assert(err, IsNil) + check.Assert(createdDhcpBinding, NotNil) + + // Add binding to cleanup list + openApiEndpoint := fmt.Sprintf(types.OpenApiPathVersion1_0_0+types.OpenApiEndpointOrgVdcNetworksDhcpBindings+"%s", + orgNet.OpenApiOrgVdcNetwork.ID, createdDhcpBinding.OpenApiOrgVdcNetworkDhcpBinding.ID) + PrependToCleanupListOpenApi(createdDhcpBinding.OpenApiOrgVdcNetworkDhcpBinding.Name, check.TestName(), openApiEndpoint) + + // Validate DHCP binding fields + check.Assert(createdDhcpBinding.OpenApiOrgVdcNetworkDhcpBinding.Name, Equals, dhcpBindingConfig.Name) + check.Assert(createdDhcpBinding.OpenApiOrgVdcNetworkDhcpBinding.Description, Equals, dhcpBindingConfig.Description) + check.Assert(createdDhcpBinding.OpenApiOrgVdcNetworkDhcpBinding.IpAddress, Equals, dhcpBindingConfig.IpAddress) + check.Assert(createdDhcpBinding.OpenApiOrgVdcNetworkDhcpBinding.MacAddress, Equals, dhcpBindingConfig.MacAddress) + check.Assert(createdDhcpBinding.OpenApiOrgVdcNetworkDhcpBinding.BindingType, Equals, dhcpBindingConfig.BindingType) + + // Get DHCP binding by ID + getDhcpBinding, err := orgNet.GetOpenApiOrgVdcNetworkDhcpBindingById(createdDhcpBinding.OpenApiOrgVdcNetworkDhcpBinding.ID) + check.Assert(err, IsNil) + check.Assert(getDhcpBinding, NotNil) + check.Assert(getDhcpBinding.OpenApiOrgVdcNetworkDhcpBinding.Name, Equals, dhcpBindingConfig.Name) + + // Get DHCP binding by Name + getDhcpBindingByName, err := orgNet.GetOpenApiOrgVdcNetworkDhcpBindingByName(createdDhcpBinding.OpenApiOrgVdcNetworkDhcpBinding.Name) + check.Assert(err, IsNil) + check.Assert(getDhcpBindingByName, NotNil) + check.Assert(getDhcpBindingByName.OpenApiOrgVdcNetworkDhcpBinding.ID, Equals, createdDhcpBinding.OpenApiOrgVdcNetworkDhcpBinding.ID) + + // Get all DHCP bindings + allDhcpBindings, err := orgNet.GetAllOpenApiOrgVdcNetworkDhcpBindings(nil) + check.Assert(err, IsNil) + check.Assert(allDhcpBindings, NotNil) + check.Assert(len(allDhcpBindings), Equals, 1) + check.Assert(allDhcpBindings[0].OpenApiOrgVdcNetworkDhcpBinding.ID, Equals, createdDhcpBinding.OpenApiOrgVdcNetworkDhcpBinding.ID) + + // Update DHCP binding + dhcpBindingConfig.Description = "updated description" + dhcpBindingConfig.IpAddress = "2.1.1.232" + dhcpBindingConfig.MacAddress = "00:11:22:33:33:33" + dhcpBindingConfig.ID = createdDhcpBinding.OpenApiOrgVdcNetworkDhcpBinding.ID + + updatedDhcpBinding, err := createdDhcpBinding.Update(dhcpBindingConfig) + check.Assert(err, IsNil) + check.Assert(updatedDhcpBinding, NotNil) + check.Assert(updatedDhcpBinding.OpenApiOrgVdcNetworkDhcpBinding.Description, Equals, dhcpBindingConfig.Description) + check.Assert(updatedDhcpBinding.OpenApiOrgVdcNetworkDhcpBinding.IpAddress, Equals, dhcpBindingConfig.IpAddress) + check.Assert(updatedDhcpBinding.OpenApiOrgVdcNetworkDhcpBinding.MacAddress, Equals, dhcpBindingConfig.MacAddress) + + // Attempt to refresh originally created binding and see if it got these new updates values as well + err = createdDhcpBinding.Refresh() + check.Assert(err, IsNil) + check.Assert(createdDhcpBinding.OpenApiOrgVdcNetworkDhcpBinding.Description, Equals, dhcpBindingConfig.Description) + check.Assert(createdDhcpBinding.OpenApiOrgVdcNetworkDhcpBinding.IpAddress, Equals, dhcpBindingConfig.IpAddress) + check.Assert(createdDhcpBinding.OpenApiOrgVdcNetworkDhcpBinding.MacAddress, Equals, dhcpBindingConfig.MacAddress) + + // Delete DHCP binding + err = createdDhcpBinding.Delete() + check.Assert(err, IsNil) + + // Ensure the binding is removed + bindingShouldBeNil, err := orgNet.GetOpenApiOrgVdcNetworkDhcpBindingById(createdDhcpBinding.OpenApiOrgVdcNetworkDhcpBinding.ID) + check.Assert(err, NotNil) + check.Assert(bindingShouldBeNil, IsNil) +} + +func runOpenApiOrgVdcNetworkTestWithSegmentProfileTemplateEndpoint(check *C, vcd *TestVCD, vdc *Vdc, orgVdcNetworkConfig *types.OpenApiOrgVdcNetwork, expectNetworkType string) { + printVerbose("## Testing Segment Profile assignment in explicit Segment Profile endpoint\n") + + nsxtManager, err := vcd.client.GetNsxtManagerByName(vcd.config.VCD.Nsxt.Manager) + check.Assert(err, IsNil) + check.Assert(nsxtManager, NotNil) + nsxtManagerUrn, err := nsxtManager.Urn() + check.Assert(err, IsNil) + + // Filter by NSX-T Manager + queryParams := copyOrNewUrlValues(nil) + queryParams = queryParameterFilterAnd(fmt.Sprintf("nsxTManagerRef.id==%s", nsxtManagerUrn), queryParams) + + // Lookup prerequisite profiles for Segment Profile template creation + ipDiscoveryProfile, err := vcd.client.GetIpDiscoveryProfileByName(vcd.config.VCD.Nsxt.IpDiscoveryProfile, queryParams) + check.Assert(err, IsNil) + macDiscoveryProfile, err := vcd.client.GetMacDiscoveryProfileByName(vcd.config.VCD.Nsxt.MacDiscoveryProfile, queryParams) + check.Assert(err, IsNil) + spoofGuardProfile, err := vcd.client.GetSpoofGuardProfileByName(vcd.config.VCD.Nsxt.SpoofGuardProfile, queryParams) + check.Assert(err, IsNil) + qosProfile, err := vcd.client.GetQoSProfileByName(vcd.config.VCD.Nsxt.QosProfile, queryParams) + check.Assert(err, IsNil) + segmentSecurityProfile, err := vcd.client.GetSegmentSecurityProfileByName(vcd.config.VCD.Nsxt.SegmentSecurityProfile, queryParams) + check.Assert(err, IsNil) + + orgVdcNet, err := vdc.CreateOpenApiOrgVdcNetwork(orgVdcNetworkConfig) + check.Assert(err, IsNil) + + // Use generic "OpenApiEntity" resource cleanup type + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointOrgVdcNetworks + orgVdcNet.OpenApiOrgVdcNetwork.ID + AddToCleanupListOpenApi(orgVdcNet.OpenApiOrgVdcNetwork.Name, check.TestName(), openApiEndpoint) + + // Set segment profiles explicitly without using templates + entitySegmentProfileCfg := &types.OrgVdcNetworkSegmentProfiles{ + IPDiscoveryProfile: &types.Reference{ID: ipDiscoveryProfile.ID}, + MacDiscoveryProfile: &types.Reference{ID: macDiscoveryProfile.ID}, + SpoofGuardProfile: &types.Reference{ID: spoofGuardProfile.ID}, + QosProfile: &types.Reference{ID: qosProfile.ID}, + SegmentSecurityProfile: &types.Reference{ID: segmentSecurityProfile.ID}, } + + updatedSegmentProfiles, err := orgVdcNet.UpdateSegmentProfile(entitySegmentProfileCfg) + check.Assert(err, IsNil) + check.Assert(updatedSegmentProfiles, NotNil) + + check.Assert(updatedSegmentProfiles.IPDiscoveryProfile.ID, Equals, ipDiscoveryProfile.ID) + check.Assert(updatedSegmentProfiles.MacDiscoveryProfile.ID, Equals, macDiscoveryProfile.ID) + check.Assert(updatedSegmentProfiles.SpoofGuardProfile.ID, Equals, spoofGuardProfile.ID) + check.Assert(updatedSegmentProfiles.QosProfile.ID, Equals, qosProfile.ID) + check.Assert(updatedSegmentProfiles.SegmentSecurityProfile.ID, Equals, segmentSecurityProfile.ID) + + retrievedSegmentProfile, err := orgVdcNet.GetSegmentProfile() + check.Assert(err, IsNil) + check.Assert(retrievedSegmentProfile, NotNil) + + check.Assert(retrievedSegmentProfile.IPDiscoveryProfile.ID, Equals, ipDiscoveryProfile.ID) + check.Assert(retrievedSegmentProfile.MacDiscoveryProfile.ID, Equals, macDiscoveryProfile.ID) + check.Assert(retrievedSegmentProfile.SpoofGuardProfile.ID, Equals, spoofGuardProfile.ID) + check.Assert(retrievedSegmentProfile.QosProfile.ID, Equals, qosProfile.ID) + check.Assert(retrievedSegmentProfile.SegmentSecurityProfile.ID, Equals, segmentSecurityProfile.ID) + + // Delete + err = orgVdcNet.Delete() + check.Assert(err, IsNil) +} + +func runOpenApiOrgVdcNetworkTestWithSegmentProfileTemplate(check *C, vcd *TestVCD, vdc *Vdc, orgVdcNetworkConfig *types.OpenApiOrgVdcNetwork, expectNetworkType string) { + printVerbose("## Testing Segment Profile Template assignment in Org VDC Network Structure\n") + // Precreate two segment profile templates + spt1 := preCreateSegmentProfileTemplate(vcd, check, "1") + spt2 := preCreateSegmentProfileTemplate(vcd, check, "2") + orgVdcNetworkConfig.SegmentProfileTemplate = &types.OpenApiReference{ID: spt1.NsxtSegmentProfileTemplate.ID} + defer func() { // Cleanup Segment Profile Template configuration to prevent altering other tests + orgVdcNetworkConfig.SegmentProfileTemplate = nil + }() + + orgVdcNet, err := vdc.CreateOpenApiOrgVdcNetwork(orgVdcNetworkConfig) + check.Assert(err, IsNil) + + // Use generic "OpenApiEntity" resource cleanup type + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointOrgVdcNetworks + orgVdcNet.OpenApiOrgVdcNetwork.ID + AddToCleanupListOpenApi(orgVdcNet.OpenApiOrgVdcNetwork.Name, check.TestName(), openApiEndpoint) + + // Segment Profile Templates are not returned in GET by API definition which is not convenient, + // but this check will work as a fuse to detect if anything changes in that regard in future + check.Assert(orgVdcNet.OpenApiOrgVdcNetwork.SegmentProfileTemplate, IsNil) + + // Retrieve Segment Profile Template using its dedicated endpoint + segmentProfileConfig, err := orgVdcNet.GetSegmentProfile() + check.Assert(err, IsNil) + check.Assert(segmentProfileConfig, NotNil) + check.Assert(segmentProfileConfig.SegmentProfileTemplate.TemplateRef.ID, Equals, spt1.NsxtSegmentProfileTemplate.ID) + + // Update Segment Profile Template + orgVdcNet.OpenApiOrgVdcNetwork.SegmentProfileTemplate = &types.OpenApiReference{ID: spt2.NsxtSegmentProfileTemplate.ID} + + updatedOrgVdcNet, err := orgVdcNet.Update(orgVdcNet.OpenApiOrgVdcNetwork) + check.Assert(err, IsNil) + + check.Assert(updatedOrgVdcNet.OpenApiOrgVdcNetwork.Name, Equals, orgVdcNet.OpenApiOrgVdcNetwork.Name) + check.Assert(updatedOrgVdcNet.OpenApiOrgVdcNetwork.ID, Equals, orgVdcNet.OpenApiOrgVdcNetwork.ID) + check.Assert(updatedOrgVdcNet.OpenApiOrgVdcNetwork.Description, Equals, orgVdcNet.OpenApiOrgVdcNetwork.Description) + + // Segment Profile Templates are not returned in GET by API definition which is not convenient, + // but this check will work as a fuse to detect if anything changes in that regard in future + check.Assert(updatedOrgVdcNet.OpenApiOrgVdcNetwork.SegmentProfileTemplate, IsNil) + // Retrieve Segment Profile Template using its dedicated endpoint + segmentProfileConfig, err = orgVdcNet.GetSegmentProfile() + check.Assert(err, IsNil) + check.Assert(segmentProfileConfig, NotNil) + check.Assert(segmentProfileConfig.SegmentProfileTemplate.TemplateRef.ID, Equals, spt2.NsxtSegmentProfileTemplate.ID) + + // Delete + err = orgVdcNet.Delete() + check.Assert(err, IsNil) + + // Delete Segment Profile Templates + + err = spt1.Delete() + check.Assert(err, IsNil) + err = spt2.Delete() + check.Assert(err, IsNil) +} + +func preCreateSegmentProfileTemplate(vcd *TestVCD, check *C, sptNameSuffix string) *NsxtSegmentProfileTemplate { + skipNoNsxtConfiguration(vcd, check) + vcd.skipIfNotSysAdmin(check) + + nsxtManager, err := vcd.client.GetNsxtManagerByName(vcd.config.VCD.Nsxt.Manager) + check.Assert(err, IsNil) + check.Assert(nsxtManager, NotNil) + nsxtManagerUrn, err := nsxtManager.Urn() + check.Assert(err, IsNil) + + // Filter by NSX-T Manager + queryParams := copyOrNewUrlValues(nil) + queryParams = queryParameterFilterAnd(fmt.Sprintf("nsxTManagerRef.id==%s", nsxtManagerUrn), queryParams) + + // Lookup prerequisite profiles for Segment Profile template creation + ipDiscoveryProfile, err := vcd.client.GetIpDiscoveryProfileByName(vcd.config.VCD.Nsxt.IpDiscoveryProfile, queryParams) + check.Assert(err, IsNil) + macDiscoveryProfile, err := vcd.client.GetMacDiscoveryProfileByName(vcd.config.VCD.Nsxt.MacDiscoveryProfile, queryParams) + check.Assert(err, IsNil) + spoofGuardProfile, err := vcd.client.GetSpoofGuardProfileByName(vcd.config.VCD.Nsxt.SpoofGuardProfile, queryParams) + check.Assert(err, IsNil) + qosProfile, err := vcd.client.GetQoSProfileByName(vcd.config.VCD.Nsxt.QosProfile, queryParams) + check.Assert(err, IsNil) + segmentSecurityProfile, err := vcd.client.GetSegmentSecurityProfileByName(vcd.config.VCD.Nsxt.SegmentSecurityProfile, queryParams) + check.Assert(err, IsNil) + + config := &types.NsxtSegmentProfileTemplate{ + Name: check.TestName() + "-" + sptNameSuffix, + Description: check.TestName() + "-description", + IPDiscoveryProfile: &types.Reference{ID: ipDiscoveryProfile.ID}, + MacDiscoveryProfile: &types.Reference{ID: macDiscoveryProfile.ID}, + QosProfile: &types.Reference{ID: qosProfile.ID}, + SegmentSecurityProfile: &types.Reference{ID: segmentSecurityProfile.ID}, + SpoofGuardProfile: &types.Reference{ID: spoofGuardProfile.ID}, + SourceNsxTManagerRef: &types.OpenApiReference{ID: nsxtManager.NsxtManager.ID}, + } + + createdSegmentProfileTemplate, err := vcd.client.CreateSegmentProfileTemplate(config) + check.Assert(err, IsNil) + check.Assert(createdSegmentProfileTemplate, NotNil) + + // Add to cleanup list + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtSegmentProfileTemplates + createdSegmentProfileTemplate.NsxtSegmentProfileTemplate.ID + AddToCleanupListOpenApi(config.Name, check.TestName(), openApiEndpoint) + + return createdSegmentProfileTemplate } diff --git a/govcd/openapi_test.go b/govcd/openapi_test.go index 7f548101c..a0b4a665d 100644 --- a/govcd/openapi_test.go +++ b/govcd/openapi_test.go @@ -1,4 +1,4 @@ -// +build functional openapi ALL +//go:build functional || openapi || ALL /* * Copyright 2020 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -8,6 +8,7 @@ package govcd import ( "encoding/json" + "fmt" "net/http" "net/url" "regexp" @@ -42,7 +43,7 @@ func (vcd *TestVCD) Test_OpenApiRawJsonAuditTrail(check *C) { queryParams.Add("sortDesc", "timestamp") allResponses := []json.RawMessage{{}} - err = vcd.vdc.client.OpenApiGetAllItems(apiVersion, urlRef, queryParams, &allResponses) + err = vcd.vdc.client.OpenApiGetAllItems(apiVersion, urlRef, queryParams, &allResponses, nil) check.Assert(err, IsNil) check.Assert(len(allResponses) > 1, Equals, true) @@ -106,7 +107,7 @@ func (vcd *TestVCD) Test_OpenApiInlineStructAuditTrail(check *C) { filterTime := time.Now().Add(-6 * time.Hour).Format(types.FiqlQueryTimestampFormat) queryParams.Add("filter", "timestamp=gt="+filterTime) - err = vcd.vdc.client.OpenApiGetAllItems(apiVersion, urlRef, queryParams, &allResponses) + err = vcd.vdc.client.OpenApiGetAllItems(apiVersion, urlRef, queryParams, &allResponses, nil) check.Assert(err, IsNil) check.Assert(len(allResponses) > 1, Equals, true) @@ -132,6 +133,7 @@ func (vcd *TestVCD) Test_OpenApiInlineStructAuditTrail(check *C) { // 5. Deletes created role // 6. Tests read for deleted item // 7. Create role once more using "Sync" version of POST function +// 7.1 Queries TestConnection endpoint using "Sync" version of POST function to see that it handles 200OK accordingly // 8. Update role once more using "Sync" version of PUT function // 9. Delete role once again func (vcd *TestVCD) Test_OpenApiInlineStructCRUDRoles(check *C) { @@ -153,7 +155,7 @@ func (vcd *TestVCD) Test_OpenApiInlineStructCRUDRoles(check *C) { } allExistingRoles := []*InlineRoles{{}} - err = vcd.vdc.client.OpenApiGetAllItems(apiVersion, urlRef, nil, &allExistingRoles) + err = vcd.vdc.client.OpenApiGetAllItems(apiVersion, urlRef, nil, &allExistingRoles, nil) check.Assert(err, IsNil) // Step 2 - Get all roles using query filters @@ -167,7 +169,7 @@ func (vcd *TestVCD) Test_OpenApiInlineStructCRUDRoles(check *C) { expectOneRoleResultById := []*InlineRoles{{}} - err = vcd.vdc.client.OpenApiGetAllItems(apiVersion, urlRef2, queryParams, &expectOneRoleResultById) + err = vcd.vdc.client.OpenApiGetAllItems(apiVersion, urlRef2, queryParams, &expectOneRoleResultById, nil) check.Assert(err, IsNil) check.Assert(len(expectOneRoleResultById) == 1, Equals, true) @@ -176,7 +178,7 @@ func (vcd *TestVCD) Test_OpenApiInlineStructCRUDRoles(check *C) { check.Assert(err, IsNil) oneRole := &InlineRoles{} - err = vcd.vdc.client.OpenApiGetItem(apiVersion, singleRef, nil, oneRole) + err = vcd.vdc.client.OpenApiGetItem(apiVersion, singleRef, nil, oneRole, nil) check.Assert(err, IsNil) check.Assert(oneRole, NotNil) @@ -193,11 +195,11 @@ func (vcd *TestVCD) Test_OpenApiInlineStructCRUDRoles(check *C) { Name: check.TestName(), Description: "Role created by test", // This BundleKey is being set by VCD even if it is not sent - BundleKey: "com.vmware.vcloud.undefined.key", + BundleKey: types.VcloudUndefinedKey, ReadOnly: false, } newRoleResponse := &InlineRoles{} - err = vcd.client.Client.OpenApiPostItem(apiVersion, createUrl, nil, newRole, newRoleResponse) + err = vcd.client.Client.OpenApiPostItem(apiVersion, createUrl, nil, newRole, newRoleResponse, nil) check.Assert(err, IsNil) // Ensure supplied and created structs differ only by ID @@ -210,7 +212,7 @@ func (vcd *TestVCD) Test_OpenApiInlineStructCRUDRoles(check *C) { check.Assert(err, IsNil) updatedRoleResponse := &InlineRoles{} - err = vcd.client.Client.OpenApiPutItem(apiVersion, updateUrl, nil, newRoleResponse, updatedRoleResponse) + err = vcd.client.Client.OpenApiPutItem(apiVersion, updateUrl, nil, newRoleResponse, updatedRoleResponse, nil) check.Assert(err, IsNil) // Ensure supplied and response objects are identical (update worked) @@ -220,14 +222,14 @@ func (vcd *TestVCD) Test_OpenApiInlineStructCRUDRoles(check *C) { deleteUrlRef, err := vcd.client.Client.OpenApiBuildEndpoint(endpoint, newRoleResponse.ID) check.Assert(err, IsNil) - err = vcd.client.Client.OpenApiDeleteItem(apiVersion, deleteUrlRef, nil) + err = vcd.client.Client.OpenApiDeleteItem(apiVersion, deleteUrlRef, nil, nil) check.Assert(err, IsNil) // Step 6 - try to read deleted role and expect error to contain 'ErrorEntityNotFound' // Read is tricky - it throws an error ACCESS_TO_RESOURCE_IS_FORBIDDEN when the resource with ID does not // exist therefore one cannot know what kind of error occurred. lostRole := &InlineRoles{} - err = vcd.client.Client.OpenApiGetItem(apiVersion, deleteUrlRef, nil, lostRole) + err = vcd.client.Client.OpenApiGetItem(apiVersion, deleteUrlRef, nil, lostRole, nil) check.Assert(ContainsNotFound(err), Equals, true) // Step 7 - test synchronous POST and PUT functions (because Roles is a synchronous OpenAPI endpoint) @@ -239,13 +241,32 @@ func (vcd *TestVCD) Test_OpenApiInlineStructCRUDRoles(check *C) { newRole.ID = newRoleResponse.ID check.Assert(newRoleResponse, DeepEquals, newRole) + // Step 7.1 test synchronous POST with return code 200 OK works accordingly - This is checked because OpenAPI endpoint TestConnection returns 200 instead of 201 when success + var testConnectionResult types.TestConnectionResult + testConnectionPayload := types.TestConnection{ + Host: vcd.client.Client.VCDHREF.Host, + Port: 443, + Secure: addrOf(true), + Timeout: 10, + } + + testConnectionEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointTestConnection + apiVersionTestConnection, err := vcd.client.Client.checkOpenApiEndpointCompatibility(testConnectionEndpoint) + check.Assert(err, IsNil) + + urlRefTestConnection, err := vcd.client.Client.OpenApiBuildEndpoint(testConnectionEndpoint) + check.Assert(err, IsNil) + + err = vcd.client.Client.OpenApiPostItemSync(apiVersionTestConnection, urlRefTestConnection, nil, testConnectionPayload, &testConnectionResult) // This call will get a 200 OK, which is what is being tested here + check.Assert(err, IsNil) + // Step 8 - update role using synchronous PUT function newRoleResponse.Description = "Updated description created by sync test" updateUrl2, err := vcd.client.Client.OpenApiBuildEndpoint(endpoint, newRoleResponse.ID) check.Assert(err, IsNil) updatedRoleResponse2 := &InlineRoles{} - err = vcd.client.Client.OpenApiPutItem(apiVersion, updateUrl2, nil, newRoleResponse, updatedRoleResponse2) + err = vcd.client.Client.OpenApiPutItem(apiVersion, updateUrl2, nil, newRoleResponse, updatedRoleResponse2, nil) check.Assert(err, IsNil) // Ensure supplied and response objects are identical (update worked) @@ -255,11 +276,55 @@ func (vcd *TestVCD) Test_OpenApiInlineStructCRUDRoles(check *C) { deleteUrlRef2, err := vcd.client.Client.OpenApiBuildEndpoint(endpoint, newRoleResponse.ID) check.Assert(err, IsNil) - err = vcd.client.Client.OpenApiDeleteItem(apiVersion, deleteUrlRef2, nil) + err = vcd.client.Client.OpenApiDeleteItem(apiVersion, deleteUrlRef2, nil, nil) check.Assert(err, IsNil) } +func (vcd *TestVCD) Test_OpenApiTestConnection(check *C) { + // TestConnection is going to be used against the same VCD instance as the client is connected + urlTest1 := vcd.client.Client.VCDHREF + urlTest1.Path = "vcsp/lib/a0c959b4-a6dd-4a68-8042-5025f42d845e" + urlTest2 := vcd.client.Client.VCDHREF + urlTest2.Scheme = "http" + urlTest3 := vcd.client.Client.VCDHREF + urlTest3.Host = "imadethisup.io" + urlTest4 := vcd.client.Client.VCDHREF + urlTest4.Host = fmt.Sprintf("%s:666", urlTest4.Hostname()) // For testing custom port feature + tests := []struct { + SubscriptionURL string + WantedConnection bool + WantedError bool + }{ + { + SubscriptionURL: urlTest1.String(), + WantedConnection: true, // it connects and it does SSL connection + WantedError: false, // + }, + { + SubscriptionURL: urlTest2.String(), + WantedConnection: true, // it connects but it does not do SSL connection + WantedError: true, + }, + { + SubscriptionURL: urlTest3.String(), // it doesn't do neither connection nor SSL + WantedConnection: false, + WantedError: true, + }, + { + SubscriptionURL: urlTest4.String(), // it doesn't do neither connection nor SSL but tests custom port + WantedConnection: false, + WantedError: true, + }, + } + + for _, test := range tests { + result, err := vcd.client.Client.TestConnectionWithDefaults(test.SubscriptionURL) + check.Assert(err == nil, Equals, !test.WantedError) + check.Assert(result, Equals, test.WantedConnection) + } +} + // getAuditTrailTimestampWithElements helps to pick good timestamp filter so that it doesn't take long time to retrieve // too many items func getAuditTrailTimestampWithElements(elementCount int, check *C, vcd *TestVCD, apiVersion string, urlRef *url.URL) string { @@ -267,7 +332,7 @@ func getAuditTrailTimestampWithElements(elementCount int, check *C, vcd *TestVCD qp := url.Values{} qp.Add("pageSize", "128") qp.Add("sortDesc", "timestamp") // Need to get the newest - req := client.newOpenApiRequest(apiVersion, qp, http.MethodGet, urlRef, nil) + req := client.newOpenApiRequest(apiVersion, qp, http.MethodGet, urlRef, nil, nil) resp, err := client.Http.Do(req) check.Assert(err, IsNil) diff --git a/govcd/openapi_unit_test.go b/govcd/openapi_unit_test.go index fefd7ffba..f28ddc0d6 100644 --- a/govcd/openapi_unit_test.go +++ b/govcd/openapi_unit_test.go @@ -1,4 +1,4 @@ -// +build unit ALL +//go:build unit || ALL package govcd diff --git a/govcd/org.go b/govcd/org.go index 91f3d75c9..37cacd2e4 100644 --- a/govcd/org.go +++ b/govcd/org.go @@ -1,5 +1,5 @@ /* - * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + * Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. */ package govcd @@ -9,14 +9,16 @@ import ( "fmt" "net/http" "net/url" + "strings" "github.com/vmware/go-vcloud-director/v2/types/v56" "github.com/vmware/go-vcloud-director/v2/util" ) type Org struct { - Org *types.Org - client *Client + Org *types.Org + client *Client + TenantContext *TenantContext } func NewOrg(client *Client) *Org { @@ -80,6 +82,7 @@ func (org *Org) GetVdcByName(vdcname string) (Vdc, error) { for _, link := range org.Org.Link { if link.Name == vdcname { vdc := NewVdc(org.client) + vdc.parent = org _, err := org.client.ExecuteRequest(link.HREF, http.MethodGet, "", "error retrieving vdc: %s", nil, vdc.Vdc) @@ -127,8 +130,15 @@ func CreateCatalogWithStorageProfile(client *Client, links types.LinkList, Name, catalog := NewAdminCatalog(client) _, err := client.ExecuteRequest(createOrgLink.HREF, http.MethodPost, "application/vnd.vmware.admin.catalog+xml", "error creating catalog: %s", vcomp, catalog.AdminCatalog) + if err != nil { + return nil, err + } - return catalog, err + err = catalog.WaitForTasks() + if err != nil { + return nil, err + } + return catalog, nil } // CreateCatalog creates a catalog with given name and description under @@ -140,6 +150,17 @@ func (org *Org) CreateCatalog(name, description string) (Catalog, error) { if err != nil { return Catalog{}, err } + catalog.parent = org + + err = catalog.Refresh() + if err != nil { + return Catalog{}, err + } + // Make sure that the creation task is finished + err = catalog.WaitForTasks() + if err != nil { + return Catalog{}, err + } return *catalog, nil } @@ -151,6 +172,7 @@ func (org *Org) CreateCatalogWithStorageProfile(name, description string, storag return nil, err } catalog.Catalog = &adminCatalog.AdminCatalog.Catalog + catalog.parent = org return catalog, nil } @@ -209,54 +231,56 @@ func (org *Org) GetCatalogByHref(catalogHref string) (*Catalog, error) { return nil, err } // The request was successful + cat.parent = org return cat, nil } // GetCatalogByName finds a Catalog by Name // On success, returns a pointer to the Catalog structure and a nil error // On failure, returns a nil pointer and an error +// +// refresh has no effect here, but is kept to preserve signature func (org *Org) GetCatalogByName(catalogName string, refresh bool) (*Catalog, error) { - if refresh { - err := org.Refresh() - if err != nil { - return nil, err - } + vdcQuery, err := org.queryCatalogByName(catalogName) + if ContainsNotFound(err) { + return nil, ErrorEntityNotFound } - for _, catalog := range org.Org.Link { - // Get Catalog HREF - if catalog.Name == catalogName && catalog.Type == types.MimeCatalog { - return org.GetCatalogByHref(catalog.HREF) - } + if err != nil { + return nil, fmt.Errorf("error querying Catalog: %s", err) } - return nil, ErrorEntityNotFound + // This is not an AdminOrg and admin HREF must be removed if it exists + href := strings.Replace(vdcQuery.HREF, "/api/admin", "/api", 1) + return org.GetCatalogByHref(href) } // GetCatalogById finds a Catalog by ID // On success, returns a pointer to the Catalog structure and a nil error // On failure, returns a nil pointer and an error func (org *Org) GetCatalogById(catalogId string, refresh bool) (*Catalog, error) { - if refresh { - err := org.Refresh() - if err != nil { - return nil, err - } + vdcQuery, err := org.queryCatalogById(catalogId) + if ContainsNotFound(err) { + return nil, ErrorEntityNotFound } - for _, catalog := range org.Org.Link { - // Get Catalog HREF - if equalIds(catalogId, catalog.ID, catalog.HREF) { - return org.GetCatalogByHref(catalog.HREF) - } + if err != nil { + return nil, fmt.Errorf("error querying Catalog: %s", err) } - return nil, ErrorEntityNotFound + + // This is not an AdminOrg and admin HREF must be removed if it exists + href := strings.Replace(vdcQuery.HREF, "/api/admin", "/api", 1) + return org.GetCatalogByHref(href) } // GetCatalogByNameOrId finds a Catalog by name or ID // On success, returns a pointer to the Catalog structure and a nil error // On failure, returns a nil pointer and an error func (org *Org) GetCatalogByNameOrId(identifier string, refresh bool) (*Catalog, error) { - getByName := func(name string, refresh bool) (interface{}, error) { return org.GetCatalogByName(name, refresh) } - getById := func(id string, refresh bool) (interface{}, error) { return org.GetCatalogById(id, refresh) } - entity, err := getEntityByNameOrId(getByName, getById, identifier, refresh) + getByName := func(name string, refresh bool) (interface{}, error) { + return org.GetCatalogByName(name, refresh) + } + getById := func(id string, refresh bool) (interface{}, error) { + return org.GetCatalogById(id, refresh) + } + entity, err := getEntityByNameOrIdSkipNonId(getByName, getById, identifier, refresh) if entity == nil { return nil, err } @@ -267,59 +291,78 @@ func (org *Org) GetCatalogByNameOrId(identifier string, refresh bool) (*Catalog, // On success, returns a pointer to the VDC structure and a nil error // On failure, returns a nil pointer and an error func (org *Org) GetVDCByHref(vdcHref string) (*Vdc, error) { - vdc := NewVdc(org.client) - _, err := org.client.ExecuteRequest(vdcHref, http.MethodGet, - "", "error retrieving VDC: %s", nil, vdc.Vdc) + vdc, err := getVDCByHref(org.client, vdcHref) if err != nil { return nil, err } // The request was successful + result := NewVdc(org.client) + result.Vdc = vdc + result.parent = org + return result, nil +} + +// getVDCByHref gets a plain VDC object from its HREF. +func getVDCByHref(client *Client, vdcHref string) (*types.Vdc, error) { + vdc := &types.Vdc{} + _, err := client.ExecuteRequest(vdcHref, http.MethodGet, + "", "error retrieving VDC: %s", nil, vdc) + if err != nil { + return nil, err + } return vdc, nil } // GetVDCByName finds a VDC by Name // On success, returns a pointer to the VDC structure and a nil error // On failure, returns a nil pointer and an error +// +// refresh has no effect and is kept to preserve signature func (org *Org) GetVDCByName(vdcName string, refresh bool) (*Vdc, error) { - if refresh { - err := org.Refresh() - if err != nil { - return nil, err - } + vdcQuery, err := org.queryOrgVdcByName(vdcName) + if ContainsNotFound(err) { + return nil, ErrorEntityNotFound } - for _, link := range org.Org.Link { - if link.Name == vdcName && link.Type == types.MimeVDC { - return org.GetVDCByHref(link.HREF) - } + if err != nil { + return nil, fmt.Errorf("error querying VDC: %s", err) } - return nil, ErrorEntityNotFound + // This is not an AdminOrg and admin HREF must be removed if it exists + href := strings.Replace(vdcQuery.HREF, "/api/admin", "/api", 1) + return org.GetVDCByHref(href) } // GetVDCById finds a VDC by ID // On success, returns a pointer to the VDC structure and a nil error // On failure, returns a nil pointer and an error +// +// refresh has no effect and is kept to preserve signature func (org *Org) GetVDCById(vdcId string, refresh bool) (*Vdc, error) { - if refresh { - err := org.Refresh() - if err != nil { - return nil, err - } + vdcQuery, err := org.queryOrgVdcById(vdcId) + if ContainsNotFound(err) { + return nil, ErrorEntityNotFound } - for _, link := range org.Org.Link { - if equalIds(vdcId, link.ID, link.HREF) { - return org.GetVDCByHref(link.HREF) - } + if err != nil { + return nil, fmt.Errorf("error querying VDC: %s", err) } - return nil, ErrorEntityNotFound + + // This is not an AdminOrg and admin HREF must be removed if it exists + href := strings.Replace(vdcQuery.HREF, "/api/admin", "/api", 1) + return org.GetVDCByHref(href) } // GetVDCByNameOrId finds a VDC by name or ID // On success, returns a pointer to the VDC structure and a nil error // On failure, returns a nil pointer and an error +// +// refresh has no effect and is kept to preserve signature func (org *Org) GetVDCByNameOrId(identifier string, refresh bool) (*Vdc, error) { - getByName := func(name string, refresh bool) (interface{}, error) { return org.GetVDCByName(name, refresh) } - getById := func(id string, refresh bool) (interface{}, error) { return org.GetVDCById(id, refresh) } - entity, err := getEntityByNameOrId(getByName, getById, identifier, refresh) + getByName := func(name string, refresh bool) (interface{}, error) { + return org.GetVDCByName(name, refresh) + } + getById := func(id string, refresh bool) (interface{}, error) { + return org.GetVDCById(id, refresh) + } + entity, err := getEntityByNameOrIdSkipNonId(getByName, getById, identifier, refresh) if entity == nil { return nil, err } @@ -328,26 +371,11 @@ func (org *Org) GetVDCByNameOrId(identifier string, refresh bool) (*Vdc, error) // QueryCatalogList returns a list of catalogs for this organization func (org *Org) QueryCatalogList() ([]*types.CatalogRecord, error) { - util.Logger.Printf("[DEBUG] QueryCatalogList with org name %s", org.Org.Name) - queryType := org.client.GetQueryType(types.QtCatalog) - results, err := org.client.cumulativeQuery(queryType, nil, map[string]string{ - "type": queryType, - "filter": fmt.Sprintf("orgName==%s", url.QueryEscape(org.Org.Name)), - "filterEncoded": "true", - }) - if err != nil { - return nil, err - } - - var catalogs []*types.CatalogRecord - - if org.client.IsSysAdmin { - catalogs = results.Results.AdminCatalogRecord - } else { - catalogs = results.Results.CatalogRecord + util.Logger.Printf("[DEBUG] QueryCatalogList with Org HREF %s", org.Org.HREF) + filter := map[string]string{ + "org": org.Org.HREF, } - util.Logger.Printf("[DEBUG] QueryCatalogList returned with : %#v and error: %s", catalogs, err) - return catalogs, nil + return queryCatalogList(org.client, filter) } // GetTaskList returns Tasks for Organization and error. @@ -370,3 +398,323 @@ func (org *Org) GetTaskList() (*types.TasksList, error) { return nil, fmt.Errorf("link not found") } + +// QueryAllOrgs returns all Orgs using query endpoint +func (vcdclient *VCDClient) QueryAllOrgs() ([]*types.QueryResultOrgRecordType, error) { + return vcdclient.Client.queryOrgList(nil) +} + +// queryOrgList performs an `orgVdc` or `adminOrgVdc` (for System user) and optionally applies filterFields +func (client *Client) queryOrgList(filterFields map[string]string) ([]*types.QueryResultOrgRecordType, error) { + util.Logger.Printf("[DEBUG] queryOrgList with filter %#v", filterFields) + queryType := client.GetQueryType(types.QtOrg) + + filter := map[string]string{ + "type": queryType, + } + + // When a map of filters with non empty keys and values is supplied - apply it + if filterFields != nil { + filterSlice := make([]string, 0) + + for filterFieldName, filterFieldValue := range filterFields { + // Do not inject 'org' filter for System user as API returns an error + if !client.IsSysAdmin && filterFieldName == "org" { + continue + } + + if filterFieldName != "" && filterFieldValue != "" { + filterText := fmt.Sprintf("%s==%s", filterFieldName, url.QueryEscape(filterFieldValue)) + filterSlice = append(filterSlice, filterText) + } + } + + if len(filterSlice) > 0 { + filter["filter"] = strings.Join(filterSlice, ";") + filter["filterEncoded"] = "true" + } + } + + results, err := client.cumulativeQuery(queryType, nil, filter) + if err != nil { + return nil, fmt.Errorf("error querying Orgs %s", err) + } + + return results.Results.OrgRecord, nil +} + +// QueryOrgByName retrieves an Org +func (vcdclient *VCDClient) QueryOrgByName(name string) (*types.QueryResultOrgRecordType, error) { + return vcdclient.Client.queryOrgByName(name) +} + +// queryOrgByName returns a single QueryResultOrgRecordType +func (client *Client) queryOrgByName(orgName string) (*types.QueryResultOrgRecordType, error) { + filterMap := map[string]string{ + "name": orgName, + } + allOrgs, err := client.queryOrgList(filterMap) + if err != nil { + return nil, err + } + + if allOrgs == nil || len(allOrgs) < 1 { + return nil, ErrorEntityNotFound + } + + if len(allOrgs) > 1 { + return nil, fmt.Errorf("found more than 1 Org with Name '%s'", orgName) + } + + return allOrgs[0], nil +} + +// QueryOrgByID retrieves an Org +func (vcdclient *VCDClient) QueryOrgByID(id string) (*types.QueryResultOrgRecordType, error) { + return vcdclient.Client.queryOrgByID(id) +} + +// queryOrgByID returns a single QueryResultOrgRecordType +func (client *Client) queryOrgByID(orgId string) (*types.QueryResultOrgRecordType, error) { + filterMap := map[string]string{ + "id": orgId, + } + allOrgs, err := client.queryOrgList(filterMap) + + if err != nil { + return nil, err + } + + if len(allOrgs) < 1 { + return nil, ErrorEntityNotFound + } + + return allOrgs[0], nil +} + +// queryOrgVdcByName returns a single QueryResultOrgVdcRecordType +func (org *Org) queryOrgVdcByName(vdcName string) (*types.QueryResultOrgVdcRecordType, error) { + filterFields := map[string]string{ + "org": org.Org.HREF, + "orgName": org.Org.Name, + "name": vdcName, + } + allVdcs, err := queryOrgVdcList(org.client, filterFields) + if err != nil { + return nil, err + } + + if allVdcs == nil || len(allVdcs) < 1 { + return nil, ErrorEntityNotFound + } + + if len(allVdcs) > 1 { + return nil, fmt.Errorf("found more than 1 VDC with Name '%s'", vdcName) + } + + return allVdcs[0], nil +} + +// queryOrgVdcById returns a single QueryResultOrgVdcRecordType +func (org *Org) queryOrgVdcById(vdcId string) (*types.QueryResultOrgVdcRecordType, error) { + filterMap := map[string]string{ + "org": org.Org.HREF, + "orgName": org.Org.Name, + "id": vdcId, + } + allVdcs, err := queryOrgVdcList(org.client, filterMap) + + if err != nil { + return nil, err + } + + if len(allVdcs) < 1 { + return nil, ErrorEntityNotFound + } + + return allVdcs[0], nil +} + +// queryCatalogByName returns a single CatalogRecord +func (org *Org) queryCatalogByName(catalogName string) (*types.CatalogRecord, error) { + filterMap := map[string]string{ + // Not injecting `org` or `orgName` here because shared catalogs may also appear here and they would have different + // parent Org + // "org": org.Org.HREF, + // "orgName": org.Org.Name, + "name": catalogName, + } + allCatalogs, err := queryCatalogList(org.client, filterMap) + if err != nil { + return nil, err + } + + if allCatalogs == nil || len(allCatalogs) < 1 { + return nil, ErrorEntityNotFound + } + + // To conform with this API standard it would be best to return an error if more than 1 item is found, but because + // previous method of getting Catalog by Name returned the first result we are doing the same here + // if len(allCatalogs) > 1 { + // return nil, fmt.Errorf("found more than 1 Catalog with Name '%s'", catalogName) + // } + + var localCatalog *types.CatalogRecord + // if multiple results are found - return the one defined in `org` (local) + if len(allCatalogs) > 1 { + util.Logger.Printf("[DEBUG] org.queryCatalogByName found %d Catalogs by name '%s'", len(allCatalogs), catalogName) + for _, catalog := range allCatalogs { + util.Logger.Printf("[DEBUG] org.queryCatalogByName found a Catalog by name '%s' in Org '%s'", catalogName, catalog.OrgName) + if catalog.OrgName == org.Org.Name { + util.Logger.Printf("[DEBUG] org.queryCatalogByName Catalog '%s' is local for Org '%s'. Prioritising it", + catalogName, org.Org.Name) + // Not interrupting the loop here to still dump all results to logs + localCatalog = catalog + } + } + } + + // local catalog was found - return it + if localCatalog != nil { + return localCatalog, nil + } + + // If only one catalog is found or multiple catalogs with no local ones - return the first one + return allCatalogs[0], nil +} + +// queryCatalogById returns a single QueryResultOrgVdcRecordType +func (org *Org) queryCatalogById(catalogId string) (*types.CatalogRecord, error) { + filterMap := map[string]string{ + // Not injecting `org` or `orgName` here because shared catalogs may also appear here and they would have different + // parent Org + // "org": org.Org.HREF, + // "orgName": org.Org.Name, + "id": catalogId, + } + allCatalogs, err := queryCatalogList(org.client, filterMap) + + if err != nil { + return nil, err + } + + if len(allCatalogs) < 1 { + return nil, ErrorEntityNotFound + } + + return allCatalogs[0], nil +} + +// QueryOrgVdcList returns all Org VDCs using query endpoint +// +// Note. Being a 'System' user it will not return any VDC +func (org *Org) QueryOrgVdcList() ([]*types.QueryResultOrgVdcRecordType, error) { + filter := map[string]string{ + "org": org.Org.HREF, + } + + return queryOrgVdcList(org.client, filter) +} + +// queryOrgVdcList performs an `orgVdc` or `adminOrgVdc` (for System user) and optionally applies filterFields +func queryOrgVdcList(client *Client, filterFields map[string]string) ([]*types.QueryResultOrgVdcRecordType, error) { + util.Logger.Printf("[DEBUG] queryOrgVdcList with filter %#v", filterFields) + queryType := client.GetQueryType(types.QtOrgVdc) + + filter := map[string]string{ + "type": queryType, + } + + // When a map of filters with non empty keys and values is supplied - apply it + if filterFields != nil { + filterSlice := make([]string, 0) + + for filterFieldName, filterFieldValue := range filterFields { + // Do not inject 'org' filter for System user as API returns an error + if !client.IsSysAdmin && filterFieldName == "org" { + continue + } + + if filterFieldName != "" && filterFieldValue != "" { + filterText := fmt.Sprintf("%s==%s", filterFieldName, url.QueryEscape(filterFieldValue)) + filterSlice = append(filterSlice, filterText) + } + } + + if len(filterSlice) > 0 { + filter["filter"] = strings.Join(filterSlice, ";") + filter["filterEncoded"] = "true" + } + } + + results, err := client.cumulativeQuery(queryType, nil, filter) + if err != nil { + return nil, fmt.Errorf("error querying Org VDCs %s", err) + } + + if client.IsSysAdmin { + return results.Results.OrgVdcAdminRecord, nil + } else { + return results.Results.OrgVdcRecord, nil + } +} + +func queryCatalogList(client *Client, filterFields map[string]string) ([]*types.CatalogRecord, error) { + util.Logger.Printf("[DEBUG] queryCatalogList with filter %#v", filterFields) + queryType := client.GetQueryType(types.QtCatalog) + + filter := map[string]string{ + "type": queryType, + } + + // When a map of filters with non empty keys and values is supplied - apply it + if filterFields != nil { + filterSlice := make([]string, 0) + + for filterFieldName, filterFieldValue := range filterFields { + // Do not inject 'org' filter for System user as API returns an error + if !client.IsSysAdmin && filterFieldName == "org" { + continue + } + + if filterFieldName != "" && filterFieldValue != "" { + filterText := fmt.Sprintf("%s==%s", filterFieldName, url.QueryEscape(filterFieldValue)) + filterSlice = append(filterSlice, filterText) + } + } + + if len(filterSlice) > 0 { + filter["filter"] = strings.Join(filterSlice, ";") + filter["filterEncoded"] = "true" + } + } + + results, err := client.cumulativeQuery(queryType, nil, filter) + if err != nil { + return nil, err + } + + var catalogs []*types.CatalogRecord + + if client.IsSysAdmin { + catalogs = results.Results.AdminCatalogRecord + } else { + catalogs = results.Results.CatalogRecord + } + util.Logger.Printf("[DEBUG] QueryCatalogList returned with : %#v and error: %s", catalogs, err) + return catalogs, nil +} + +// GetVappByHref returns a vApp reference by running a VCD API call +// If no valid vApp is found, it returns a nil VApp reference and an error +func (org *Org) GetVAppByHref(vappHref string) (*VApp, error) { + newVapp := NewVApp(org.client) + + _, err := org.client.ExecuteRequest(vappHref, http.MethodGet, + "", "error retrieving vApp: %s", nil, newVapp.VApp) + + if err != nil { + return nil, err + } + return newVapp, nil +} diff --git a/govcd/org_oidc.go b/govcd/org_oidc.go new file mode 100644 index 000000000..e581c81a6 --- /dev/null +++ b/govcd/org_oidc.go @@ -0,0 +1,286 @@ +/* + * Copyright 2024 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "bytes" + "cmp" + "encoding/xml" + "fmt" + "github.com/vmware/go-vcloud-director/v2/types/v56" + "io" + "net/http" + "net/url" + "strconv" + "strings" +) + +// GetOpenIdConnectSettings retrieves the current OpenID Connect settings for a given Organization +func (adminOrg *AdminOrg) GetOpenIdConnectSettings() (*types.OrgOAuthSettings, error) { + return oidcExecuteRequest(adminOrg, http.MethodGet, nil) +} + +// SetOpenIdConnectSettings sets the OpenID Connect configuration for a given Organization. If the well-known configuration +// endpoint is provided, the configuration is automatically retrieved from that URL. +// If other fields have been set in the input structure, the corresponding values retrieved from the well-known endpoint are overridden. +// If there are no fields set, the configuration retrieved from the well-known configuration endpoint is applied as-is. +// ClientId and ClientSecret properties are always mandatory, with and without well-known endpoint. +// This method returns an error if the settings can't be saved in VCD for any reason or if the provided settings are wrong. +func (adminOrg *AdminOrg) SetOpenIdConnectSettings(settings types.OrgOAuthSettings) (*types.OrgOAuthSettings, error) { + if settings.ClientId == "" { + return nil, fmt.Errorf("the Client ID is mandatory to configure OpenID Connect") + } + if settings.ClientSecret == "" { + return nil, fmt.Errorf("the Client Secret is mandatory to configure OpenID Connect") + } + if settings.WellKnownEndpoint != "" { + err := oidcValidateConnection(adminOrg.client, settings.WellKnownEndpoint) + if err != nil { + return nil, err + } + wellKnownSettings, err := oidcConfigureWithEndpoint(adminOrg.client, adminOrg.AdminOrg.HREF, settings.WellKnownEndpoint) + if err != nil { + return nil, err + } + + // The following statements allow users to override the well-known automatic configuration values with their own, + // mimicking what users can do in UI. + // If an attribute was not set in the input settings, the well-known endpoint value will be chosen. + settings.AccessTokenEndpoint = cmp.Or(settings.AccessTokenEndpoint, wellKnownSettings.AccessTokenEndpoint) + settings.IssuerId = cmp.Or(settings.IssuerId, wellKnownSettings.IssuerId) + settings.JwksUri = cmp.Or(settings.JwksUri, wellKnownSettings.JwksUri) + settings.UserInfoEndpoint = cmp.Or(settings.UserInfoEndpoint, wellKnownSettings.UserInfoEndpoint) + settings.UserAuthorizationEndpoint = cmp.Or(settings.UserAuthorizationEndpoint, wellKnownSettings.UserAuthorizationEndpoint) + settings.ScimEndpoint = cmp.Or(settings.ScimEndpoint, wellKnownSettings.ScimEndpoint) + + if settings.Scope == nil || len(settings.Scope) == 0 { + settings.Scope = wellKnownSettings.Scope + } + + if settings.OIDCAttributeMapping == nil { + // The whole mapping is missing, we take the whole struct from well-known endpoint + settings.OIDCAttributeMapping = wellKnownSettings.OIDCAttributeMapping + } else { + // Some mappings are present, others are missing. We take the missing ones from well-known endpoint + settings.OIDCAttributeMapping.EmailAttributeName = cmp.Or(settings.OIDCAttributeMapping.EmailAttributeName, wellKnownSettings.OIDCAttributeMapping.EmailAttributeName) + settings.OIDCAttributeMapping.SubjectAttributeName = cmp.Or(settings.OIDCAttributeMapping.SubjectAttributeName, wellKnownSettings.OIDCAttributeMapping.SubjectAttributeName) + settings.OIDCAttributeMapping.LastNameAttributeName = cmp.Or(settings.OIDCAttributeMapping.LastNameAttributeName, wellKnownSettings.OIDCAttributeMapping.LastNameAttributeName) + settings.OIDCAttributeMapping.RolesAttributeName = cmp.Or(settings.OIDCAttributeMapping.RolesAttributeName, wellKnownSettings.OIDCAttributeMapping.RolesAttributeName) + settings.OIDCAttributeMapping.FullNameAttributeName = cmp.Or(settings.OIDCAttributeMapping.FullNameAttributeName, wellKnownSettings.OIDCAttributeMapping.FullNameAttributeName) + settings.OIDCAttributeMapping.GroupsAttributeName = cmp.Or(settings.OIDCAttributeMapping.GroupsAttributeName, wellKnownSettings.OIDCAttributeMapping.GroupsAttributeName) + settings.OIDCAttributeMapping.FirstNameAttributeName = cmp.Or(settings.OIDCAttributeMapping.FirstNameAttributeName, wellKnownSettings.OIDCAttributeMapping.FirstNameAttributeName) + } + + if settings.OAuthKeyConfigurations == nil { + settings.OAuthKeyConfigurations = wellKnownSettings.OAuthKeyConfigurations + } + } + // Perform early validations. These are required in UI before sending the payload. + if settings.UserAuthorizationEndpoint == "" { + return nil, fmt.Errorf("the User Authorization Endpoint is mandatory to configure OpenID Connect") + } + if settings.AccessTokenEndpoint == "" { + return nil, fmt.Errorf("the Access Token Endpoint is mandatory to configure OpenID Connect") + } + if settings.UserInfoEndpoint == "" { + return nil, fmt.Errorf("the User Info Endpoint is mandatory to configure OpenID Connect") + } + if settings.MaxClockSkew < 0 { + return nil, fmt.Errorf("the Max Clock Skew must be positive to correctly configure OpenID Connect") + } + if settings.OIDCAttributeMapping == nil || settings.OIDCAttributeMapping.SubjectAttributeName == "" || + settings.OIDCAttributeMapping.EmailAttributeName == "" || settings.OIDCAttributeMapping.FullNameAttributeName == "" || + settings.OIDCAttributeMapping.FirstNameAttributeName == "" || settings.OIDCAttributeMapping.LastNameAttributeName == "" { + return nil, fmt.Errorf("the Subject, Email, Full name, First Name and Last name are mandatory OIDC Attribute (Claims) Mappings, to configure OpenID Connect") + } + if settings.OAuthKeyConfigurations == nil || len(settings.OAuthKeyConfigurations.OAuthKeyConfiguration) == 0 { + return nil, fmt.Errorf("the OIDC Key Configuration is mandatory to configure OpenID Connect") + } + + // Perform connectivity validations + err := oidcValidateConnection(adminOrg.client, settings.UserAuthorizationEndpoint) + if err != nil { + return nil, err + } + err = oidcValidateConnection(adminOrg.client, settings.AccessTokenEndpoint) + if err != nil { + return nil, err + } + err = oidcValidateConnection(adminOrg.client, settings.UserInfoEndpoint) + if err != nil { + return nil, err + } + + // The namespace must be set for all structures, otherwise the API call fails + settings.Xmlns = types.XMLNamespaceVCloud + settings.OAuthKeyConfigurations.Xmlns = types.XMLNamespaceVCloud + for i := range settings.OAuthKeyConfigurations.OAuthKeyConfiguration { + settings.OAuthKeyConfigurations.OAuthKeyConfiguration[i].Xmlns = types.XMLNamespaceVCloud + } + settings.OIDCAttributeMapping.Xmlns = types.XMLNamespaceVCloud + + result, err := oidcExecuteRequest(adminOrg, http.MethodPut, &settings) + if err != nil { + return nil, err + } + + return result, nil +} + +// DeleteOpenIdConnectSettings deletes the current OpenID Connect settings from a given Organization +func (adminOrg *AdminOrg) DeleteOpenIdConnectSettings() error { + _, err := oidcExecuteRequest(adminOrg, http.MethodDelete, nil) + if err != nil { + return err + } + return nil +} + +// oidcExecuteRequest executes a request to the OIDC endpoint with the given payload and HTTP method +func oidcExecuteRequest(adminOrg *AdminOrg, method string, payload *types.OrgOAuthSettings) (*types.OrgOAuthSettings, error) { + if adminOrg.AdminOrg.HREF == "" { + return nil, fmt.Errorf("the HREF of the Organization is required to use OpenID Connect") + } + endpoint, err := url.Parse(adminOrg.AdminOrg.HREF + "/settings/oauth") + if err != nil { + return nil, fmt.Errorf("error parsing Organization '%s' OpenID Connect URL: %s", adminOrg.AdminOrg.Name, err) + } + if endpoint == nil { + return nil, fmt.Errorf("error parsing Organization '%s' OpenID Connect URL: it is nil", adminOrg.AdminOrg.Name) + } + if method == http.MethodPut && payload == nil { + return nil, fmt.Errorf("the OIDC settings cannot be nil when performing a PUT call") + } + + // Set Organization "tenant context" headers + headers := make(http.Header) + headers.Set("Content-Type", types.MimeOAuthSettingsXml) + for k, v := range getTenantContextHeader(&TenantContext{ + OrgId: adminOrg.AdminOrg.ID, + OrgName: adminOrg.AdminOrg.Name, + }) { + headers.Add(k, v) + } + + // If the call is a PUT, we prepare the body with the input settings + var body io.Reader + if method == http.MethodPut { + text := bytes.Buffer{} + encoder := xml.NewEncoder(&text) + err = encoder.Encode(*payload) + if err != nil { + return nil, err + } + body = strings.NewReader(text.String()) + } + + // Perform the HTTP call with the custom headers and obtained API version + req := adminOrg.client.newRequest(nil, nil, method, *endpoint, body, getHighestOidcApiVersion(adminOrg.client), headers) + resp, err := checkResp(adminOrg.client.Http.Do(req)) + + // Check the errors and get the response + switch method { + case http.MethodDelete: + if err != nil { + return nil, fmt.Errorf("error deleting Organization OpenID Connect settings: %s", err) + } + if resp != nil && resp.StatusCode != http.StatusNoContent { + return nil, fmt.Errorf("error deleting Organization OpenID Connect settings, expected status code %d - received %d", http.StatusNoContent, resp.StatusCode) + } + return nil, nil + case http.MethodGet: + if err != nil { + return nil, fmt.Errorf("error getting Organization OpenID Connect settings: %s", err) + } + var result types.OrgOAuthSettings + err = decodeBody(types.BodyTypeXML, resp, &result) + if err != nil { + return nil, fmt.Errorf("error decoding Organization OpenID Connect settings: %s", err) + } + return &result, nil + case http.MethodPut: + if err != nil { + return nil, fmt.Errorf("error setting Organization OpenID Connect settings: %s", err) + } + // Note: This branch of the switch should be exactly the same as the GET operation, however there is a bug found in VCD 10.5.1.1: + // the PUT call returns a wrong redirect URL. + // For that reason, we ignore the response body and call GetOpenIdConnectSettings() to return the correct response body to the caller. + if resp != nil && resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("error saving Organization OpenID Connect settings, expected status code %d - received %d", http.StatusOK, resp.StatusCode) + } + return adminOrg.GetOpenIdConnectSettings() + default: + return nil, fmt.Errorf("not supported HTTP method %s", method) + } +} + +// oidcValidateConnection executes a test probe against the given endpoint to validate that the client +// can establish a connection. +func oidcValidateConnection(client *Client, endpoint string) error { + uri, err := url.Parse(endpoint) + if err != nil { + return err + } + isSecure := strings.ToLower(uri.Scheme) == "https" + + rawPort := uri.Port() + if rawPort == "" { + rawPort = "80" + if isSecure { + rawPort = "443" + } + } + port, err := strconv.Atoi(rawPort) + if err != nil { + return err + } + + result, err := client.TestConnection(types.TestConnection{ + Host: uri.Hostname(), + Port: port, + Secure: &isSecure, + }) + if err != nil { + return err + } + + if result.TargetProbe == nil || !result.TargetProbe.CanConnect || (isSecure && !result.TargetProbe.SSLHandshake) { + return fmt.Errorf("could not establish a connection to %s://%s", uri.Scheme, uri.Host) + } + return nil +} + +// oidcConfigureWithEndpoint uses the given endpoint to retrieve an OpenID Connect configuration +func oidcConfigureWithEndpoint(client *Client, orgHref, endpoint string) (types.OrgOAuthSettings, error) { + payload := types.OpenIdProviderInfo{ + Xmlns: types.XMLNamespaceVCloud, + OpenIdProviderConfigurationEndpoint: endpoint, + } + var result types.OpenIdProviderConfiguration + + _, err := client.ExecuteRequestWithApiVersion(orgHref+"/settings/oauth/openIdProviderConfig", http.MethodPost, + types.MimeOpenIdProviderInfoXml, "error getting OpenID Connect settings from endpoint: %s", payload, &result, + getHighestOidcApiVersion(client)) + if err != nil { + return types.OrgOAuthSettings{}, err + } + + return result.OrgOAuthSettings, nil +} + +// getHighestOidcApiVersion tries to get the highest possible version for the OpenID Connect endpoint +func getHighestOidcApiVersion(client *Client) string { + // v38.1 adds CustomUiButtonLabel + targetVersion := client.GetSpecificApiVersionOnCondition(">= 38.1", "38.1") + if targetVersion != "38.1" { + // v38.0 adds SendClientCredentialsAsAuthorizationHeader, UsePKCE, + targetVersion = client.GetSpecificApiVersionOnCondition(">= 38.0", "38.0") + if targetVersion != "38.0" { + // v37.1 adds EnableIdTokenClaims + targetVersion = client.GetSpecificApiVersionOnCondition(">= 37.1", "37.1") + } + } // Otherwise we get the default API version + return targetVersion +} diff --git a/govcd/org_oidc_test.go b/govcd/org_oidc_test.go new file mode 100644 index 000000000..1faf3a6ee --- /dev/null +++ b/govcd/org_oidc_test.go @@ -0,0 +1,642 @@ +//go:build org || functional || ALL + +/* + * Copyright 2024 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + _ "embed" + "fmt" + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" + "net/url" + "strings" + "time" +) + +// Test_OrgOidcSettingsSystemAdminCreateWithWellKnownEndpoint configures OIDC +// with a wellknown endpoint. +func (vcd *TestVCD) Test_OrgOidcSettingsSystemAdminCreateWithWellKnownEndpoint(check *C) { + if !vcd.client.Client.IsSysAdmin { + check.Skip("test requires system administrator privileges") + } + oidcServerUrl := validateAndGetOidcServerUrl(check, vcd) + + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + + settings, err := adminOrg.GetOpenIdConnectSettings() + check.Assert(err, IsNil) + check.Assert(settings, NotNil) + check.Assert(settings.Enabled, Equals, false) + check.Assert(settings.AccessTokenEndpoint, Equals, "") + check.Assert(settings.UserInfoEndpoint, Equals, "") + check.Assert(settings.UserAuthorizationEndpoint, Equals, "") + check.Assert(true, Equals, strings.HasSuffix(settings.OrgRedirectUri, vcd.config.VCD.Org)) + + settings, err = setOIDCSettings(adminOrg, types.OrgOAuthSettings{ + ClientId: "clientId", + ClientSecret: "clientSecret", + Enabled: true, + MaxClockSkew: 60, + WellKnownEndpoint: oidcServerUrl.String(), + }) + check.Assert(err, IsNil) + defer func() { + deleteOIDCSettings(check, adminOrg) + }() + + check.Assert(settings, NotNil) + check.Assert(settings.Xmlns, Equals, "http://www.vmware.com/vcloud/v1.5") + check.Assert(settings.Href, Equals, adminOrg.AdminOrg.HREF+"/settings/oauth") + check.Assert(settings.Type, Equals, "application/vnd.vmware.admin.organizationOAuthSettings+xml") + check.Assert(true, Equals, strings.HasSuffix(settings.OrgRedirectUri, vcd.config.VCD.Org)) + check.Assert(settings.IssuerId, Not(Equals), "") + check.Assert(settings.Enabled, Equals, true) + check.Assert(settings.ClientId, Equals, "clientId") + check.Assert(settings.ClientSecret, Equals, "clientSecret") + check.Assert(settings.UserAuthorizationEndpoint, Not(Equals), "") + check.Assert(settings.AccessTokenEndpoint, Not(Equals), "") + check.Assert(settings.UserInfoEndpoint, Not(Equals), "") + check.Assert(settings.ScimEndpoint, Equals, "") + check.Assert(len(settings.Scope), Not(Equals), 0) + check.Assert(settings.MaxClockSkew, Equals, 60) + check.Assert(settings.WellKnownEndpoint, Not(Equals), "") + check.Assert(settings.OIDCAttributeMapping, NotNil) + check.Assert(settings.OAuthKeyConfigurations, NotNil) + check.Assert(len(settings.OAuthKeyConfigurations.OAuthKeyConfiguration), Not(Equals), 0) +} + +// Test_OrgOidcSettingsSystemAdminCreateWithWellKnownEndpointAndOverridingOptions configures OIDC +// with a wellknown endpoint, but overrides the obtained values with custom ones. +func (vcd *TestVCD) Test_OrgOidcSettingsSystemAdminCreateWithWellKnownEndpointAndOverridingOptions(check *C) { + if !vcd.client.Client.IsSysAdmin { + check.Skip("test requires system administrator privileges") + } + oidcServerUrl := validateAndGetOidcServerUrl(check, vcd) + + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + + settings, err := adminOrg.GetOpenIdConnectSettings() + check.Assert(err, IsNil) + check.Assert(settings, NotNil) + check.Assert(true, Equals, strings.HasSuffix(settings.OrgRedirectUri, vcd.config.VCD.Org)) + + accessTokenEndpoint := fmt.Sprintf("%s://%s/foo", oidcServerUrl.Scheme, oidcServerUrl.Host) + userAuthorizationEndpoint := fmt.Sprintf("%s://%s/foo2", oidcServerUrl.Scheme, oidcServerUrl.Host) + + settings, err = setOIDCSettings(adminOrg, types.OrgOAuthSettings{ + ClientId: "clientId", + ClientSecret: "clientSecret", + Enabled: true, + MaxClockSkew: 60, + AccessTokenEndpoint: accessTokenEndpoint, + UserAuthorizationEndpoint: userAuthorizationEndpoint, + WellKnownEndpoint: oidcServerUrl.String(), + }) + check.Assert(err, IsNil) + defer func() { + deleteOIDCSettings(check, adminOrg) + }() + + check.Assert(settings, NotNil) + check.Assert(settings.AccessTokenEndpoint, Equals, accessTokenEndpoint) + check.Assert(settings.UserAuthorizationEndpoint, Equals, userAuthorizationEndpoint) + check.Assert(settings.Xmlns, Equals, "http://www.vmware.com/vcloud/v1.5") + check.Assert(settings.Href, Equals, adminOrg.AdminOrg.HREF+"/settings/oauth") + check.Assert(settings.Type, Equals, "application/vnd.vmware.admin.organizationOAuthSettings+xml") + check.Assert(true, Equals, strings.HasSuffix(settings.OrgRedirectUri, vcd.config.VCD.Org)) + check.Assert(settings.IssuerId, Not(Equals), "") + check.Assert(settings.Enabled, Equals, true) + check.Assert(settings.ClientId, Equals, "clientId") + check.Assert(settings.ClientSecret, Equals, "clientSecret") + check.Assert(settings.UserInfoEndpoint, Not(Equals), "") + check.Assert(settings.ScimEndpoint, Equals, "") + check.Assert(len(settings.Scope), Not(Equals), 0) + check.Assert(settings.MaxClockSkew, Equals, 60) + check.Assert(settings.WellKnownEndpoint, Not(Equals), "") + check.Assert(settings.OIDCAttributeMapping, NotNil) + check.Assert(settings.OAuthKeyConfigurations, NotNil) + check.Assert(len(settings.OAuthKeyConfigurations.OAuthKeyConfiguration), Not(Equals), 0) +} + +// Test_OrgOidcSettingsSystemAdminCreateWithCustomValues configures OIDC +// without the wellknown endpoint, by hand. +func (vcd *TestVCD) Test_OrgOidcSettingsSystemAdminCreateWithCustomValues(check *C) { + if !vcd.client.Client.IsSysAdmin { + check.Skip("test requires system administrator privileges") + } + + oidcServerUrl := validateAndGetOidcServerUrl(check, vcd) + + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + + accessTokenEndpoint := fmt.Sprintf("%s://%s/accessToken", oidcServerUrl.Scheme, oidcServerUrl.Host) + userAuthorizationEndpoint := fmt.Sprintf("%s://%s/userAuth", oidcServerUrl.Scheme, oidcServerUrl.Host) + issuerId := fmt.Sprintf("%s://%s/issuerId", oidcServerUrl.Scheme, oidcServerUrl.Host) + userInfoEndpoint := fmt.Sprintf("%s://%s/userInfo", oidcServerUrl.Scheme, oidcServerUrl.Host) + + expirationDate := "2123-12-31T01:59:59.000Z" + dummyKey := "-----BEGIN PUBLIC KEY-----\n" + + "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC9gXitSASYbVS56gBkQ3UOCS7F\n" + + "8SnFABs44sxXykt8DW4y1mxdyCcM0X/lVPf+DNfXbIISmPk/mqoRS9uZSuQIUtC2\n" + + "4iaGkWyUALvrq8FJcR8Krf5EtDt1W9AkLEREDJ7VkpJx/VoCd9ZNe8NFstAvbQ6+\n" + + "bM0Jg9lJJdr+VPNvywIDAQAB\n" + + "-----END PUBLIC KEY-----" + + settings, err := setOIDCSettings(adminOrg, types.OrgOAuthSettings{ + ClientId: "clientId", + ClientSecret: "clientSecret", + Enabled: true, + UserAuthorizationEndpoint: userAuthorizationEndpoint, + AccessTokenEndpoint: accessTokenEndpoint, + IssuerId: issuerId, + UserInfoEndpoint: userInfoEndpoint, + MaxClockSkew: 60, + Scope: []string{"foo", "bar"}, + OIDCAttributeMapping: &types.OIDCAttributeMapping{ + SubjectAttributeName: "subject", + EmailAttributeName: "email", + FullNameAttributeName: "fullname", + FirstNameAttributeName: "first", + LastNameAttributeName: "last", + GroupsAttributeName: "groups", + RolesAttributeName: "roles", + }, + OAuthKeyConfigurations: &types.OAuthKeyConfigurationsList{ + OAuthKeyConfiguration: []types.OAuthKeyConfiguration{ + { + KeyId: "rsa1", + Algorithm: "RSA", + Key: dummyKey, + ExpirationDate: expirationDate, + }, + }, + }, + }) + check.Assert(err, IsNil) + defer func() { + deleteOIDCSettings(check, adminOrg) + }() + + check.Assert(settings, NotNil) + check.Assert(settings.Xmlns, Equals, "http://www.vmware.com/vcloud/v1.5") + check.Assert(settings.Href, Equals, adminOrg.AdminOrg.HREF+"/settings/oauth") + check.Assert(settings.Type, Equals, "application/vnd.vmware.admin.organizationOAuthSettings+xml") + check.Assert(true, Equals, strings.HasSuffix(settings.OrgRedirectUri, vcd.config.VCD.Org)) + check.Assert(settings.Enabled, Equals, true) + check.Assert(settings.ClientId, Equals, "clientId") + check.Assert(settings.ClientSecret, Equals, "clientSecret") + check.Assert(settings.IssuerId, Equals, issuerId) + check.Assert(settings.UserAuthorizationEndpoint, Equals, userAuthorizationEndpoint) + check.Assert(settings.AccessTokenEndpoint, Equals, accessTokenEndpoint) + check.Assert(settings.UserInfoEndpoint, Equals, userInfoEndpoint) + check.Assert(settings.ScimEndpoint, Equals, "") + check.Assert(len(settings.Scope), Equals, 2) + check.Assert(settings.MaxClockSkew, Equals, 60) + check.Assert(settings.WellKnownEndpoint, Equals, "") + check.Assert(settings.OIDCAttributeMapping, NotNil) + check.Assert(settings.OIDCAttributeMapping.EmailAttributeName, Equals, "email") + check.Assert(settings.OIDCAttributeMapping.LastNameAttributeName, Equals, "last") + check.Assert(settings.OIDCAttributeMapping.FirstNameAttributeName, Equals, "first") + check.Assert(settings.OIDCAttributeMapping.SubjectAttributeName, Equals, "subject") + check.Assert(settings.OIDCAttributeMapping.GroupsAttributeName, Equals, "groups") + check.Assert(settings.OIDCAttributeMapping.FullNameAttributeName, Equals, "fullname") + check.Assert(settings.OIDCAttributeMapping.RolesAttributeName, Equals, "roles") + check.Assert(settings.OAuthKeyConfigurations, NotNil) + check.Assert(len(settings.OAuthKeyConfigurations.OAuthKeyConfiguration), Equals, 1) + check.Assert(settings.OAuthKeyConfigurations.OAuthKeyConfiguration[0].KeyId, Equals, "rsa1") + check.Assert(settings.OAuthKeyConfigurations.OAuthKeyConfiguration[0].Algorithm, Equals, "RSA") + check.Assert(settings.OAuthKeyConfigurations.OAuthKeyConfiguration[0].Key, Equals, dummyKey) + check.Assert(settings.OAuthKeyConfigurations.OAuthKeyConfiguration[0].ExpirationDate, Equals, expirationDate) +} + +// Test_OrgOidcSettingsSystemAdminUpdate configures OIDC settings with a wellknown endpoint, then updates some values. +func (vcd *TestVCD) Test_OrgOidcSettingsSystemAdminUpdate(check *C) { + if !vcd.client.Client.IsSysAdmin { + check.Skip("test requires system administrator privileges") + } + + oidcServerUrl := validateAndGetOidcServerUrl(check, vcd) + + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + + settings, err := adminOrg.GetOpenIdConnectSettings() + check.Assert(err, IsNil) + check.Assert(settings, NotNil) + check.Assert(settings.Enabled, Equals, false) + check.Assert(settings.AccessTokenEndpoint, Equals, "") + check.Assert(settings.UserInfoEndpoint, Equals, "") + check.Assert(settings.UserAuthorizationEndpoint, Equals, "") + check.Assert(true, Equals, strings.HasSuffix(settings.OrgRedirectUri, vcd.config.VCD.Org)) + + settings, err = setOIDCSettings(adminOrg, types.OrgOAuthSettings{ + ClientId: "clientId", + ClientSecret: "clientSecret", + Enabled: true, + MaxClockSkew: 60, + WellKnownEndpoint: oidcServerUrl.String(), + }) + check.Assert(err, IsNil) + defer func() { + deleteOIDCSettings(check, adminOrg) + }() + check.Assert(settings, NotNil) + + updatedSettings, err := setOIDCSettings(adminOrg, types.OrgOAuthSettings{ + ClientId: "clientId2", + ClientSecret: "clientSecret2", + Enabled: false, + MaxClockSkew: 120, + OIDCAttributeMapping: &types.OIDCAttributeMapping{ + SubjectAttributeName: "subject2", + EmailAttributeName: "email2", + FullNameAttributeName: "fullname2", + FirstNameAttributeName: "first2", + LastNameAttributeName: "last2", + GroupsAttributeName: "groups2", + RolesAttributeName: "roles2", + }, + WellKnownEndpoint: oidcServerUrl.String(), + }) + check.Assert(err, IsNil) + check.Assert(updatedSettings, NotNil) + + check.Assert(updatedSettings.Enabled, Equals, false) + check.Assert(updatedSettings.ClientId, Equals, "clientId2") + check.Assert(updatedSettings.ClientSecret, Equals, "clientSecret2") + check.Assert(updatedSettings.MaxClockSkew, Equals, 120) + check.Assert(updatedSettings.OIDCAttributeMapping, NotNil) + check.Assert(updatedSettings.OIDCAttributeMapping.EmailAttributeName, Equals, "email2") + check.Assert(updatedSettings.OIDCAttributeMapping.LastNameAttributeName, Equals, "last2") + check.Assert(updatedSettings.OIDCAttributeMapping.FirstNameAttributeName, Equals, "first2") + check.Assert(updatedSettings.OIDCAttributeMapping.SubjectAttributeName, Equals, "subject2") + check.Assert(updatedSettings.OIDCAttributeMapping.GroupsAttributeName, Equals, "groups2") + check.Assert(updatedSettings.OIDCAttributeMapping.FullNameAttributeName, Equals, "fullname2") + check.Assert(updatedSettings.OIDCAttributeMapping.RolesAttributeName, Equals, "roles2") +} + +// Test_OrgOidcSettingsWithTenantUser configures OIDC settings with a tenant user instead of System administrator. +func (vcd *TestVCD) Test_OrgOidcSettingsWithTenantUser(check *C) { + if len(vcd.config.Tenants) == 0 { + check.Skip(check.TestName() + " requires at least one tenant in the configuration") + } + + oidcServerUrl := validateAndGetOidcServerUrl(check, vcd) + + orgName := vcd.config.Tenants[0].SysOrg + userName := vcd.config.Tenants[0].User + password := vcd.config.Tenants[0].Password + + vcdClient := NewVCDClient(vcd.client.Client.VCDHREF, true) + err := vcdClient.Authenticate(userName, password, orgName) + check.Assert(err, IsNil) + + adminOrg, err := vcd.client.GetAdminOrgByName(orgName) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + + settings, err := setOIDCSettings(adminOrg, types.OrgOAuthSettings{ + ClientId: "clientId", + ClientSecret: "clientSecret", + Enabled: true, + MaxClockSkew: 60, + WellKnownEndpoint: oidcServerUrl.String(), + }) + check.Assert(err, IsNil) + defer func() { + deleteOIDCSettings(check, adminOrg) + }() + check.Assert(settings, NotNil) + check.Assert(settings.Xmlns, Equals, "http://www.vmware.com/vcloud/v1.5") + check.Assert(settings.Href, Equals, adminOrg.AdminOrg.HREF+"/settings/oauth") + check.Assert(settings.Type, Equals, "application/vnd.vmware.admin.organizationOAuthSettings+xml") + check.Assert(true, Equals, strings.HasSuffix(settings.OrgRedirectUri, orgName)) + check.Assert(settings.IssuerId, Not(Equals), "") + check.Assert(settings.Enabled, Equals, true) + check.Assert(settings.ClientId, Equals, "clientId") + check.Assert(settings.ClientSecret, Equals, "clientSecret") + check.Assert(settings.UserAuthorizationEndpoint, Not(Equals), "") + check.Assert(settings.AccessTokenEndpoint, Not(Equals), "") + check.Assert(settings.UserInfoEndpoint, Not(Equals), "") + check.Assert(settings.ScimEndpoint, Equals, "") + check.Assert(len(settings.Scope), Not(Equals), 0) + check.Assert(settings.MaxClockSkew, Equals, 60) + check.Assert(settings.WellKnownEndpoint, Not(Equals), "") + check.Assert(settings.OIDCAttributeMapping, NotNil) + check.Assert(settings.OAuthKeyConfigurations, NotNil) + check.Assert(len(settings.OAuthKeyConfigurations.OAuthKeyConfiguration), Not(Equals), 0) + + settings2, err := adminOrg.GetOpenIdConnectSettings() + check.Assert(err, IsNil) + check.Assert(settings2, DeepEquals, settings) +} + +// Test_OrgOidcSettingsDifferentVersions tests the parameters that are only available in certain +// VCD versions, like the UI button label. This test only makes sense when it is run in several +// VCD versions. +func (vcd *TestVCD) Test_OrgOidcSettingsDifferentVersions(check *C) { + if !vcd.client.Client.IsSysAdmin { + check.Skip("test requires system administrator privileges") + } + + oidcServerUrl := validateAndGetOidcServerUrl(check, vcd) + + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + + settings, err := adminOrg.GetOpenIdConnectSettings() + check.Assert(err, IsNil) + check.Assert(settings, NotNil) + check.Assert(settings.Enabled, Equals, false) + check.Assert(settings.AccessTokenEndpoint, Equals, "") + check.Assert(settings.UserInfoEndpoint, Equals, "") + check.Assert(settings.UserAuthorizationEndpoint, Equals, "") + check.Assert(true, Equals, strings.HasSuffix(settings.OrgRedirectUri, vcd.config.VCD.Org)) + + s := types.OrgOAuthSettings{ + ClientId: "clientId", + ClientSecret: "clientSecret", + Enabled: true, + MaxClockSkew: 60, + WellKnownEndpoint: oidcServerUrl.String(), + } + if vcd.client.Client.APIVCDMaxVersionIs(">= 37.1") { + s.EnableIdTokenClaims = addrOf(true) + } + if vcd.client.Client.APIVCDMaxVersionIs(">= 38.0") { + s.SendClientCredentialsAsAuthorizationHeader = addrOf(true) + s.UsePKCE = addrOf(true) + } + if vcd.client.Client.APIVCDMaxVersionIs(">= 38.1") { + s.CustomUiButtonLabel = addrOf("this is a test") + } + + settings, err = setOIDCSettings(adminOrg, s) + check.Assert(err, IsNil) + defer func() { + deleteOIDCSettings(check, adminOrg) + }() + + check.Assert(settings, NotNil) + if vcd.client.Client.APIVCDMaxVersionIs(">= 37.1") { + check.Assert(settings.EnableIdTokenClaims, NotNil) + check.Assert(*settings.EnableIdTokenClaims, Equals, true) + } else { + check.Assert(settings.EnableIdTokenClaims, IsNil) + } + if vcd.client.Client.APIVCDMaxVersionIs(">= 38.0") { + check.Assert(settings.SendClientCredentialsAsAuthorizationHeader, NotNil) + check.Assert(settings.UsePKCE, NotNil) + check.Assert(*settings.SendClientCredentialsAsAuthorizationHeader, Equals, true) + check.Assert(*settings.UsePKCE, Equals, true) + } else { + check.Assert(settings.SendClientCredentialsAsAuthorizationHeader, IsNil) + check.Assert(settings.UsePKCE, IsNil) + } + if vcd.client.Client.APIVCDMaxVersionIs(">= 38.1") { + check.Assert(settings.CustomUiButtonLabel, NotNil) + check.Assert(*settings.CustomUiButtonLabel, Equals, "this is a test") + } else { + check.Assert(settings.CustomUiButtonLabel, IsNil) + } +} + +// Test_OrgOidcSettingsValidationErrors tests the validation rules when setting OpenID Connect Settings with AdminOrg.SetOpenIdConnectSettings +func (vcd *TestVCD) Test_OrgOidcSettingsValidationErrors(check *C) { + if !vcd.client.Client.IsSysAdmin { + check.Skip("test requires system administrator privileges") + } + + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + + tests := []struct { + wrongConfig types.OrgOAuthSettings + errorMsg string + }{ + { + wrongConfig: types.OrgOAuthSettings{}, + errorMsg: "the Client ID is mandatory to configure OpenID Connect", + }, + { + wrongConfig: types.OrgOAuthSettings{ + ClientId: "clientId", + }, + errorMsg: "the Client Secret is mandatory to configure OpenID Connect", + }, + { + wrongConfig: types.OrgOAuthSettings{ + ClientId: "clientId", + ClientSecret: "clientSecret", + }, + errorMsg: "the User Authorization Endpoint is mandatory to configure OpenID Connect", + }, + { + wrongConfig: types.OrgOAuthSettings{ + ClientId: "clientId", + ClientSecret: "clientSecret", + UserAuthorizationEndpoint: "https://dummy.url/authorize", + }, + errorMsg: "the Access Token Endpoint is mandatory to configure OpenID Connect", + }, + { + wrongConfig: types.OrgOAuthSettings{ + ClientId: "clientId", + ClientSecret: "clientSecret", + UserAuthorizationEndpoint: "https://dummy.url/authorize", + AccessTokenEndpoint: "https://dummy.url/token", + }, + errorMsg: "the User Info Endpoint is mandatory to configure OpenID Connect", + }, + { + wrongConfig: types.OrgOAuthSettings{ + ClientId: "clientId", + ClientSecret: "clientSecret", + UserAuthorizationEndpoint: "https://dummy.url/authorize", + AccessTokenEndpoint: "https://dummy.url/token", + UserInfoEndpoint: "https://dummy.url/userinfo", + MaxClockSkew: -1, + }, + errorMsg: "the Max Clock Skew must be positive to correctly configure OpenID Connect", + }, + { + wrongConfig: types.OrgOAuthSettings{ + ClientId: "clientId", + ClientSecret: "clientSecret", + UserAuthorizationEndpoint: "https://dummy.url/authorize", + AccessTokenEndpoint: "https://dummy.url/token", + UserInfoEndpoint: "https://dummy.url/userinfo", + MaxClockSkew: 60, + OIDCAttributeMapping: &types.OIDCAttributeMapping{}, + }, + errorMsg: "the Subject, Email, Full name, First Name and Last name are mandatory OIDC Attribute (Claims) Mappings, to configure OpenID Connect", + }, + { + wrongConfig: types.OrgOAuthSettings{ + ClientId: "clientId", + ClientSecret: "clientSecret", + UserAuthorizationEndpoint: "https://dummy.url/authorize", + AccessTokenEndpoint: "https://dummy.url/token", + UserInfoEndpoint: "https://dummy.url/userinfo", + MaxClockSkew: 60, + OIDCAttributeMapping: &types.OIDCAttributeMapping{ + SubjectAttributeName: "a", + }, + }, + errorMsg: "the Subject, Email, Full name, First Name and Last name are mandatory OIDC Attribute (Claims) Mappings, to configure OpenID Connect", + }, + { + wrongConfig: types.OrgOAuthSettings{ + ClientId: "clientId", + ClientSecret: "clientSecret", + UserAuthorizationEndpoint: "https://dummy.url/authorize", + AccessTokenEndpoint: "https://dummy.url/token", + UserInfoEndpoint: "https://dummy.url/userinfo", + MaxClockSkew: 60, + OIDCAttributeMapping: &types.OIDCAttributeMapping{ + SubjectAttributeName: "a", + EmailAttributeName: "b", + }, + }, + errorMsg: "the Subject, Email, Full name, First Name and Last name are mandatory OIDC Attribute (Claims) Mappings, to configure OpenID Connect", + }, + { + wrongConfig: types.OrgOAuthSettings{ + ClientId: "clientId", + ClientSecret: "clientSecret", + UserAuthorizationEndpoint: "https://dummy.url/authorize", + AccessTokenEndpoint: "https://dummy.url/token", + UserInfoEndpoint: "https://dummy.url/userinfo", + MaxClockSkew: 60, + OIDCAttributeMapping: &types.OIDCAttributeMapping{ + SubjectAttributeName: "a", + EmailAttributeName: "b", + FullNameAttributeName: "c", + }, + }, + errorMsg: "the Subject, Email, Full name, First Name and Last name are mandatory OIDC Attribute (Claims) Mappings, to configure OpenID Connect", + }, + { + wrongConfig: types.OrgOAuthSettings{ + ClientId: "clientId", + ClientSecret: "clientSecret", + UserAuthorizationEndpoint: "https://dummy.url/authorize", + AccessTokenEndpoint: "https://dummy.url/token", + UserInfoEndpoint: "https://dummy.url/userinfo", + MaxClockSkew: 60, + OIDCAttributeMapping: &types.OIDCAttributeMapping{ + SubjectAttributeName: "a", + EmailAttributeName: "b", + FullNameAttributeName: "c", + FirstNameAttributeName: "d", + }, + }, + errorMsg: "the Subject, Email, Full name, First Name and Last name are mandatory OIDC Attribute (Claims) Mappings, to configure OpenID Connect", + }, + { + wrongConfig: types.OrgOAuthSettings{ + ClientId: "clientId", + ClientSecret: "clientSecret", + + UserAuthorizationEndpoint: "https://dummy.url/authorize", + AccessTokenEndpoint: "https://dummy.url/token", + UserInfoEndpoint: "https://dummy.url/userinfo", + MaxClockSkew: 60, + OIDCAttributeMapping: &types.OIDCAttributeMapping{ + SubjectAttributeName: "a", + EmailAttributeName: "b", + FullNameAttributeName: "c", + FirstNameAttributeName: "d", + LastNameAttributeName: "e", + }, + }, + errorMsg: "the OIDC Key Configuration is mandatory to configure OpenID Connect", + }, + { + wrongConfig: types.OrgOAuthSettings{ + ClientId: "clientId", + ClientSecret: "clientSecret", + UserAuthorizationEndpoint: "https://dummy.url/authorize", + AccessTokenEndpoint: "https://dummy.url/token", + UserInfoEndpoint: "https://dummy.url/userinfo", + MaxClockSkew: 60, + OIDCAttributeMapping: &types.OIDCAttributeMapping{ + SubjectAttributeName: "a", + EmailAttributeName: "b", + FullNameAttributeName: "c", + FirstNameAttributeName: "d", + LastNameAttributeName: "e", + }, + OAuthKeyConfigurations: &types.OAuthKeyConfigurationsList{}, + }, + errorMsg: "the OIDC Key Configuration is mandatory to configure OpenID Connect", + }, + } + + for _, test := range tests { + _, err := adminOrg.SetOpenIdConnectSettings(test.wrongConfig) + check.Assert(err, NotNil) + check.Assert(true, Equals, strings.Contains(err.Error(), test.errorMsg)) + } +} + +// setOIDCSettings sets the given OIDC settings for the given Organization. It does this operation +// with some tries to avoid test failures due to network glitches. +func setOIDCSettings(adminOrg *AdminOrg, settings types.OrgOAuthSettings) (*types.OrgOAuthSettings, error) { + tries := 0 + var newSettings *types.OrgOAuthSettings + var err error + for tries < 5 { + tries++ + newSettings, err = adminOrg.SetOpenIdConnectSettings(settings) + if err == nil { + break + } + if strings.Contains(err.Error(), "could not establish a connection") || strings.Contains(err.Error(), "connect timed out") { + time.Sleep(10 * time.Second) + } + } + if err != nil { + return nil, err + } + return newSettings, nil +} + +// deleteOIDCSettings deletes the current OIDC settings for the given Organization +func deleteOIDCSettings(check *C, adminOrg *AdminOrg) { + err := adminOrg.DeleteOpenIdConnectSettings() + check.Assert(err, IsNil) + + settings, err := adminOrg.GetOpenIdConnectSettings() + check.Assert(err, IsNil) + check.Assert(settings, NotNil) + check.Assert(settings.Enabled, Equals, false) + check.Assert(settings.AccessTokenEndpoint, Equals, "") + check.Assert(settings.UserInfoEndpoint, Equals, "") + check.Assert(settings.UserAuthorizationEndpoint, Equals, "") + check.Assert(settings.OrgRedirectUri, Not(Equals), "") +} + +func validateAndGetOidcServerUrl(check *C, vcd *TestVCD) *url.URL { + if vcd.config.VCD.OidcServer.Url == "" || vcd.config.VCD.OidcServer.WellKnownEndpoint == "" { + check.Skip("test requires OIDC configuration") + } + + oidcServer, err := url.Parse(vcd.config.VCD.OidcServer.Url) + if err != nil { + check.Skip(check.TestName() + " requires OIDC Server URL and its well-known endpoint") + } + return oidcServer.JoinPath(vcd.config.VCD.OidcServer.WellKnownEndpoint) +} diff --git a/govcd/org_saml.go b/govcd/org_saml.go new file mode 100644 index 000000000..4658986c5 --- /dev/null +++ b/govcd/org_saml.go @@ -0,0 +1,323 @@ +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "bytes" + "encoding/json" + "encoding/xml" + "fmt" + "github.com/vmware/go-vcloud-director/v2/types/v56" + "github.com/vmware/go-vcloud-director/v2/util" + "io" + "net/http" + "net/url" + "regexp" + "strings" +) + +// GetFederationSettings retrieves the current federation (SAML) settings for a given organization +func (adminOrg *AdminOrg) GetFederationSettings() (*types.OrgFederationSettings, error) { + var settings types.OrgFederationSettings + + if adminOrg.AdminOrg.OrgSettings == nil || adminOrg.AdminOrg.OrgSettings.Link == nil { + return nil, fmt.Errorf("no Org settings links found in Org %s", adminOrg.AdminOrg.Name) + } + fsUrl := getUrlFromLink(adminOrg.AdminOrg.OrgSettings.Link, "down", types.MimeFederationSettingsXml) + if fsUrl == "" { + return nil, fmt.Errorf("no link found for federation settings (SAML: %s) in Org %s", types.MimeFederationSettingsXml, adminOrg.AdminOrg.Name) + } + + resp, err := adminOrg.client.ExecuteRequest(fsUrl, http.MethodGet, types.MimeFederationSettingsXml, + "error fetching federation settings: %s", nil, &settings) + + if err != nil { + return nil, err + } + + _, err = checkResp(resp, err) + if err != nil { + return nil, err + } + + return &settings, nil +} + +// SetFederationSettings creates or replaces federation (SAML) settings for a given organization +func (adminOrg *AdminOrg) SetFederationSettings(settings *types.OrgFederationSettings) (*types.OrgFederationSettings, error) { + + if adminOrg.AdminOrg.OrgSettings == nil || adminOrg.AdminOrg.OrgSettings.Link == nil { + return nil, fmt.Errorf("no Org settings links found in Org %s", adminOrg.AdminOrg.Name) + } + fsUrl := getUrlFromLink(adminOrg.AdminOrg.OrgSettings.Link, "down", types.MimeFederationSettingsJson) + if fsUrl == "" { + return nil, fmt.Errorf("no URL found for federation settings (SAML) in Org %s", adminOrg.AdminOrg.Name) + } + + setUrl, err := url.Parse(fsUrl) + if err != nil { + return nil, err + } + + text := bytes.Buffer{} + encoder := json.NewEncoder(&text) + encoder.SetEscapeHTML(false) + err = encoder.Encode(settings) + if err != nil { + return nil, err + } + body := strings.NewReader(text.String()) + apiVersion := adminOrg.client.APIVersion + headAccept := http.Header{} + // NOTE: given that the UI uses JSON based API to run SAML settings, it seemed the safest way to + // imitate it and use JSON payload and results for this operation + headAccept.Set("Accept", types.JSONMime) + headAccept.Set("Content-Type", types.MimeFederationSettingsJson) + request := adminOrg.client.newRequest(nil, nil, http.MethodPut, *setUrl, body, apiVersion, headAccept) + request.Header.Set("Accept", fmt.Sprintf("application/*+json;version=%s", apiVersion)) + request.Header.Set("Content-Type", types.MimeFederationSettingsJson) + + resp, err := adminOrg.client.Http.Do(request) + if err != nil { + return nil, err + } + + if !isSuccessStatus(resp.StatusCode) { + body, _ := io.ReadAll(resp.Body) + var jsonError types.OpenApiError + err = json.Unmarshal(body, &jsonError) + // By default, we return the whole response body as error message. This may also contain the stack trace + message := string(body) + // if the body contains a valid JSON representation of the error, we return a more agile message, using the + // exposed fields, and hiding the stack trace from view + if err == nil { + message = fmt.Sprintf("%s - %s", jsonError.MinorErrorCode, jsonError.Message) + } + return nil, fmt.Errorf("error setting SAML for org %s: %s (%d) - %s", adminOrg.AdminOrg.Name, resp.Status, resp.StatusCode, message) + } + + _, err = checkResp(resp, err) + if err != nil { + return nil, err + } + + return adminOrg.GetFederationSettings() +} + +// UnsetFederationSettings removes federation (SAML) settings for a given organization +func (adminOrg *AdminOrg) UnsetFederationSettings() error { + settings, err := adminOrg.GetFederationSettings() + if err != nil { + return fmt.Errorf("[UnsetFederationSettings] error getting SAML settings for Org %s: %s", adminOrg.AdminOrg.Name, err) + } + + settings.SAMLMetadata = "" + settings.Enabled = false + _, err = adminOrg.SetFederationSettings(settings) + return err +} + +// GetServiceProviderSamlMetadata retrieves the service provider SAML metadata of the given Org +func (adminOrg *AdminOrg) GetServiceProviderSamlMetadata() (*types.VcdSamlMetadata, error) { + + metadataText, err := adminOrg.RetrieveServiceProviderSamlMetadata() + if err != nil { + return nil, err + } + var metadata types.VcdSamlMetadata + + err = xml.Unmarshal([]byte(metadataText), &metadata) + if err != nil { + return nil, fmt.Errorf("[GetSamlMetadata] error decoding metadata retrieved from %s: %s", adminOrg.AdminOrg.Name, err) + } + + return &metadata, nil +} + +// RetrieveServiceProviderSamlMetadata retrieves the SAML metadata of the given Org +func (adminOrg *AdminOrg) RetrieveServiceProviderSamlMetadata() (string, error) { + + settings, err := adminOrg.GetFederationSettings() + if err != nil { + return "", err + } + metadataUrl := getUrlFromLink(settings.Link, "down", types.MimeSamlMetadata) + if metadataUrl == "" { + return "", fmt.Errorf("[RetrieveRemoteDocument] no URL found for metadata retrieval (%s) in org %s", types.MimeSamlMetadata, adminOrg.AdminOrg.Name) + } + + metadataText, err := adminOrg.client.RetrieveRemoteDocument(metadataUrl) + if err != nil { + return "", fmt.Errorf("[RetrieveRemoteDocument] error retrieving SAML metadata from %s: %s", metadataUrl, err) + } + return string(metadataText), nil +} + +func getUrlFromLink(linkList types.LinkList, wantRel, wantType string) string { + for _, link := range linkList { + if link.Rel == wantRel && link.Type == wantType { + return link.HREF + } + } + return "" +} + +var ( + // samlMetadataItems contains name space identifiers and corresponding tags + // that should be found in VCD SAML service provider metadata + samlMetadataItems = map[string][]string{ + "ds": { + "KeyInfo", + "X509Certificate", + "X509Data", + }, + "md": { + "AssertionConsumerService", + "EntityDescriptor", + "KeyDescriptor", + "NameIDFormat", + "SPSSODescriptor", + "SingleLogoutService", + }, + "hoksso": { + "ProtocolBinding", + }, + } +) + +// RetrieveRemoteDocument gets the contents of a given URL +func (client *Client) RetrieveRemoteDocument(metadataUrl string) ([]byte, error) { + + retrieveUrl, err := url.Parse(metadataUrl) + if err != nil { + return nil, err + } + request := client.newRequest(nil, nil, http.MethodGet, *retrieveUrl, nil, client.APIVersion, nil) + + resp, err := client.Http.Do(request) + + if err != nil { + return nil, fmt.Errorf("[RetrieveRemoteDocument] error retrieving metadata from %s: %s", metadataUrl, err) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("[RetrieveRemoteDocument] error reading response body from metadata retrieved from %s: %s", metadataUrl, err) + } + + util.ProcessResponseOutput("[RetrieveRemoteDocument]", resp, string(body)) + return body, nil +} + +// normalizeServiceProviderSamlMetadata takes a string containing the XML code with Metadata definition +// and makes sure it has all the expected elements +func normalizeServiceProviderSamlMetadata(in string) (string, error) { + var metadata types.VcdSamlMetadata + + // Phase 1: Decode the XML, to find possible encoding errors + err := xml.Unmarshal([]byte(in), &metadata) + if err != nil { + return "", fmt.Errorf("[normalizeSamlMetadata] error decoding SAML metadata definition from XML: %s", err) + } + + // Phase 2: Add the namespace definition elements, required to recognize the structure as a valid SAML definition + metadata.Md = types.SamlNamespaceMd + metadata.SPSSODescriptor.Ds = types.SamlNamespaceDs + for i := 0; i < len(metadata.SPSSODescriptor.AssertionConsumerService); i++ { + metadata.SPSSODescriptor.AssertionConsumerService[i].Hoksso = types.SamlNamespaceHoksso + } + + // Phase 3: Convert the data structure to text again. The text now includes the needed namespace definition elements + out, err := xml.Marshal(metadata) + if err != nil { + return "", fmt.Errorf("[normalizeSamlMetadata] error encoding SAML metadata text: %s", err) + } + + // Phase 4: Add the namespace elements to the XML text + metadataText := string(out) + for ns, fields := range samlMetadataItems { + if !strings.Contains(metadataText, ns) { + return metadataText, fmt.Errorf("[normalizeSamlMetadata] namespace '%s' not found in SAML metadata", ns) + } + for _, fieldName := range fields { + fullName := fmt.Sprintf("%s:%s", ns, fieldName) + // If we find just "FieldName", but not "namespace:FieldName", then we replace the bare FieldName with the full identifier + if strings.Contains(metadataText, fieldName) && !strings.Contains(metadataText, fullName) { + metadataText = strings.Replace(metadataText, fieldName, fullName, -1) + } + } + } + + return metadataText, nil +} + +// validateNamespaceDefinition checks that a metadata XML text contains the expected namespace definition +func validateNamespaceDefinition(metadataText string, namespace string) bool { + reEmptyDefinition := regexp.MustCompile(`xmlns:` + namespace + `\s*=\s*""`) + reFilledDefinition := regexp.MustCompile(`xmlns:` + namespace + `\s*=\s*"\S+"`) + // Check that the namespace is mentioned at all in the metadata text + if !strings.Contains(metadataText, namespace) { + return false + } + // Check that an empty namespace definition is NOT found in the metadata text + // (for example: xmlns:md="") + if reEmptyDefinition.FindString(metadataText) != "" { + return false + } + // Check that a filled namespace definition is found in the metadata text + // (for example: xmlns:md="something") + found := reFilledDefinition.FindString(metadataText) + return found != "" +} + +// ValidateSamlServiceProviderMetadata tells whether a given string contains valid XML that defines SAML service provider metadata +// Returns nil on valid data, and an array of errors for invalid data +func ValidateSamlServiceProviderMetadata(metadataText string) []error { + var metadata types.VcdSamlMetadata + var errors []error + + // Check n. 1: encode the string into XML, thus establishing that it is valid syntax + err := xml.Unmarshal([]byte(metadataText), &metadata) + if err != nil { + errors = append(errors, fmt.Errorf("[ValidateSamlMetadata] error decoding XML into SAML metadata structure: %s", err)) + } + + reNameSpace, err := regexp.Compile(`<(\w+):(\w+)`) + + if err != nil { + errors = append(errors, fmt.Errorf("error compiling regular expression: %s", err)) + return errors + } + + nsInfoList := reNameSpace.FindAllStringSubmatch(metadataText, -1) + processed := map[string]bool{} + + // Check n. 2: make sure that each namespace used in the metadata text has a corresponding definition + for _, nsInfo := range nsInfoList { + seen, ok := processed[nsInfo[0]] + if ok && seen { + continue + } + ns := nsInfo[1] + if !validateNamespaceDefinition(metadataText, ns) { + errors = append(errors, fmt.Errorf("[ValidateSamlMetadata] namespace '%s' undefined in SAML metadata", ns)) + } + processed[nsInfo[0]] = true + } + + if len(errors) == 0 { + return nil + } + return errors +} + +// GetErrorMessageFromErrorSlice returns a single error message from a list of error +func GetErrorMessageFromErrorSlice(errors []error) string { + result := "" + for i, err := range errors { + result = fmt.Sprintf("%s\n%2d %s", result, i, err) + } + return result +} diff --git a/govcd/org_saml_test.go b/govcd/org_saml_test.go new file mode 100644 index 000000000..e5f7a9833 --- /dev/null +++ b/govcd/org_saml_test.go @@ -0,0 +1,136 @@ +//go:build org || functional || ALL + +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + _ "embed" + "encoding/xml" + "fmt" + "github.com/kr/pretty" + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +//go:embed test-resources/saml-test-idp.xml +var externalMetadata string + +func (vcd *TestVCD) Test_OrgSamlSettingsCRUD(check *C) { + + if !vcd.client.Client.IsSysAdmin { + check.Skip("test requires system administrator privileges") + } + + orgName := check.TestName() + + task, err := CreateOrg(vcd.client, orgName, orgName, orgName, &types.OrgSettings{}, true) + check.Assert(err, IsNil) + check.Assert(task, NotNil) + AddToCleanupList(orgName, "org", "", check.TestName()) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) + + adminOrg, err := vcd.client.GetAdminOrgByName(orgName) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + + settings, err := adminOrg.GetFederationSettings() + check.Assert(err, IsNil) + check.Assert(settings, NotNil) + + if testVerbose { + fmt.Printf("# 1 %# v\n", pretty.Formatter(settings)) + } + + metadata, err := adminOrg.GetServiceProviderSamlMetadata() + check.Assert(err, IsNil) + check.Assert(metadata, NotNil) + if testVerbose { + fmt.Printf("# 2 %# v\n", pretty.Formatter(metadata)) + } + + metadataText, err := xml.Marshal(metadata) + check.Assert(err, IsNil) + settings.SAMLMetadata = string(metadataText) + settings.SamlSPEntityID = "dummyId" + settings.Enabled = true + settings.SamlAttributeMapping.EmailAttributeName = "email" + settings.SamlAttributeMapping.UserNameAttributeName = "uname" + settings.SamlAttributeMapping.FirstNameAttributeName = "fname" + settings.SamlAttributeMapping.SurnameAttributeName = "lname" + settings.SamlAttributeMapping.FullNameAttributeName = "fullname" + settings.SamlAttributeMapping.RoleAttributeName = "role" + settings.SamlAttributeMapping.GroupAttributeName = "group" + // Use a service provider metadata, without proper namespace settings: expecting an error + newSetting, err := adminOrg.SetFederationSettings(settings) + check.Assert(err, NotNil) + check.Assert(err.Error(), Matches, "(?i).*bad request.*is not a valid SAML 2.0 metadata document.*") + check.Assert(newSetting, IsNil) + + // Add namespace definitions to the metadata, and this time it will pass + newMetadataText, err := normalizeServiceProviderSamlMetadata(string(metadataText)) + check.Assert(err, IsNil) + settings.SAMLMetadata = newMetadataText + newSetting, err = adminOrg.SetFederationSettings(settings) + check.Assert(err, IsNil) + check.Assert(newSetting, NotNil) + + check.Assert(err, IsNil) + settings.SAMLMetadata = externalMetadata + newSetting, err = adminOrg.SetFederationSettings(settings) + check.Assert(err, IsNil) + check.Assert(newSetting, NotNil) + check.Assert(newSetting.SamlSPEntityID, Equals, "dummyId") + check.Assert(newSetting.Enabled, Equals, true) + check.Assert(newSetting.SamlAttributeMapping.EmailAttributeName, Equals, "email") + check.Assert(newSetting.SamlAttributeMapping.FirstNameAttributeName, Equals, "fname") + check.Assert(newSetting.SamlAttributeMapping.SurnameAttributeName, Equals, "lname") + check.Assert(newSetting.SamlAttributeMapping.FullNameAttributeName, Equals, "fullname") + check.Assert(newSetting.SamlAttributeMapping.UserNameAttributeName, Equals, "uname") + check.Assert(newSetting.SamlAttributeMapping.RoleAttributeName, Equals, "role") + check.Assert(newSetting.SamlAttributeMapping.GroupAttributeName, Equals, "group") + check.Assert(newSetting, NotNil) + + err = adminOrg.UnsetFederationSettings() + check.Assert(err, IsNil) + err = adminOrg.Refresh() + check.Assert(err, IsNil) + newSettings, err := adminOrg.GetFederationSettings() + check.Assert(err, IsNil) + check.Assert(newSettings.SamlSPEntityID, Equals, "dummyId") + check.Assert(newSettings.Enabled, Equals, false) + + err = adminOrg.Disable() + check.Assert(err, IsNil) + err = adminOrg.Delete(true, true) + check.Assert(err, IsNil) +} + +func (vcd *TestVCD) TestClient_RetrieveRemoteDoc(check *C) { + // samltest.id is a well known test site for SAML services + metadataUrl := "https://samltest.id/saml/idp" + metadata, err := vcd.client.Client.RetrieveRemoteDocument(metadataUrl) + if err != nil { + check.Skip("samltest.id is not responding") + } + check.Assert(err, IsNil) + check.Assert(metadata, NotNil) + errors := ValidateSamlServiceProviderMetadata(string(metadata)) + check.Assert(errors, IsNil) +} + +func (vcd *TestVCD) TestClient_RetrieveRemoteSamlMetadata(check *C) { + if vcd.config.VCD.Org == "" { + check.Skip("No organization found") + } + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + metadataText, err := adminOrg.RetrieveServiceProviderSamlMetadata() + check.Assert(err, IsNil) + errors := ValidateSamlServiceProviderMetadata(metadataText) + check.Assert(errors, IsNil) +} diff --git a/govcd/org_saml_unit_test.go b/govcd/org_saml_unit_test.go new file mode 100644 index 000000000..fda67d7f7 --- /dev/null +++ b/govcd/org_saml_unit_test.go @@ -0,0 +1,70 @@ +//go:build unit || ALL + +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + _ "embed" + "fmt" + "testing" +) + +//go:embed test-resources/saml-test-sp.xml +var md1 string + +//go:embed test-resources/saml-test-sp-invalid.xml +var md2 string + +const md3 = ` + + + + +` + +func TestNormalizeSamlMetadata(t *testing.T) { + + type mdSample struct { + name string + data string + wantErr bool + } + var samples = []mdSample{ + {"correct", md1, false}, + {"no-tags", md2, false}, + {"empty-SPSSODescriptor", md3, true}, + } + + for i, sample := range samples { + t.Run(fmt.Sprintf("%02d-%s", i, sample.name), func(t *testing.T) { + result, err := normalizeServiceProviderSamlMetadata(sample.data) + if err != nil { + if !sample.wantErr { + t.Fatalf("unwanted error: %s ", err) + } + t.Logf("expected error found: %s\n", err) + } else { + if sample.wantErr { + t.Logf("%s\n", result) + t.Fatalf("expected an error but returned success") + } + } + if len(result) == 0 { + t.Fatalf("unexpected 0 length for result\n") + } + + errors := ValidateSamlServiceProviderMetadata(result) + + if errors != nil { + message := GetErrorMessageFromErrorSlice(errors) + t.Logf("%s\n", message) + if !sample.wantErr { + t.Fatalf("validation errors found\n") + } + } + }) + } +} diff --git a/govcd/org_test.go b/govcd/org_test.go index 201438588..69dec6bf1 100644 --- a/govcd/org_test.go +++ b/govcd/org_test.go @@ -1,4 +1,4 @@ -// +build org functional ALL +//go:build org || functional || ALL /* * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -104,32 +104,46 @@ func (vcd *TestVCD) Test_UpdateOrg(check *C) { check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) } type updateSet struct { - orgName string - enabled bool - canPublishCatalogs bool + orgName string + enabled bool + canPublishCatalogs bool + canPublishExternally bool + canSubscribe bool } // Tests a combination of enabled and canPublishCatalogs to see // whether they are updated correctly var updateOrgs = []updateSet{ - {TestUpdateOrg + "1", true, false}, - {TestUpdateOrg + "2", false, false}, - {TestUpdateOrg + "3", true, true}, - {TestUpdateOrg + "4", false, true}, + {TestUpdateOrg + "1", true, false, false, false}, + {TestUpdateOrg + "2", false, false, false, false}, + {TestUpdateOrg + "3", true, true, true, false}, + {TestUpdateOrg + "4", false, true, false, true}, } for _, uo := range updateOrgs { - + if vcd.client.Client.APIVCDMaxVersionIs("= 37.2") && !uo.enabled { + // TODO revisit once bug is fixed in VCD + fmt.Println("[INFO] VCD 10.4.2 has a bug that prevents creating a disabled Org - Changing 'enabled' parameter to 'true'") + uo.enabled = true + } fmt.Printf("Org %s - enabled %v - catalogs %v\n", uo.orgName, uo.enabled, uo.canPublishCatalogs) task, err := CreateOrg(vcd.client, uo.orgName, uo.orgName, uo.orgName, &types.OrgSettings{ - OrgGeneralSettings: &types.OrgGeneralSettings{CanPublishCatalogs: uo.canPublishCatalogs}, - OrgLdapSettings: &types.OrgLdapSettingsType{OrgLdapMode: "NONE"}, + OrgGeneralSettings: &types.OrgGeneralSettings{ + CanPublishCatalogs: uo.canPublishCatalogs, + CanPublishExternally: uo.canPublishExternally, + CanSubscribe: uo.canSubscribe, + }, + OrgLdapSettings: &types.OrgLdapSettingsType{OrgLdapMode: "NONE"}, }, uo.enabled) + check.Assert(err, IsNil) check.Assert(task, Not(Equals), Task{}) + err = task.WaitTaskCompletion() check.Assert(err, IsNil) + AddToCleanupList(uo.orgName, "org", "", "TestUpdateOrg") + // fetch newly created org adminOrg, err := vcd.client.GetAdminOrgByName(uo.orgName) check.Assert(err, IsNil) @@ -143,6 +157,9 @@ func (vcd *TestVCD) Test_UpdateOrg(check *C) { adminOrg.AdminOrg.Description = updatedDescription adminOrg.AdminOrg.FullName = updatedFullName adminOrg.AdminOrg.OrgSettings.OrgGeneralSettings.CanPublishCatalogs = !uo.canPublishCatalogs + adminOrg.AdminOrg.OrgSettings.OrgGeneralSettings.CanPublishExternally = !uo.canPublishExternally + adminOrg.AdminOrg.OrgSettings.OrgGeneralSettings.CanSubscribe = !uo.canSubscribe + adminOrg.AdminOrg.IsEnabled = !uo.enabled task, err = adminOrg.Update() @@ -159,6 +176,8 @@ func (vcd *TestVCD) Test_UpdateOrg(check *C) { check.Assert(updatedAdminOrg.AdminOrg.IsEnabled, Equals, !uo.enabled) check.Assert(updatedAdminOrg.AdminOrg.OrgSettings.OrgGeneralSettings.CanPublishCatalogs, Equals, !uo.canPublishCatalogs) + check.Assert(updatedAdminOrg.AdminOrg.OrgSettings.OrgGeneralSettings.CanPublishExternally, Equals, !uo.canPublishExternally) + check.Assert(updatedAdminOrg.AdminOrg.OrgSettings.OrgGeneralSettings.CanSubscribe, Equals, !uo.canSubscribe) if testVerbose { fmt.Printf("[updated] Org %s - enabled %v (expected %v) - catalogs %v (expected %v)\n", updatedAdminOrg.AdminOrg.Name, @@ -249,15 +268,8 @@ func (vcd *TestVCD) Test_CreateVdc(check *C) { } providerVdcHref := results.Results.VMWProviderVdcRecord[0].HREF - results, err = vcd.client.QueryWithNotEncodedParams(nil, map[string]string{ - "type": "providerVdcStorageProfile", - "filter": fmt.Sprintf("name==%s", vcd.config.VCD.ProviderVdc.StorageProfile), - }) + storageProfile, err := vcd.client.QueryProviderVdcStorageProfileByName(vcd.config.VCD.ProviderVdc.StorageProfile, providerVdcHref) check.Assert(err, IsNil) - if len(results.Results.ProviderVdcStorageProfileRecord) == 0 { - check.Skip(fmt.Sprintf("No storage profile found with name '%s'", vcd.config.VCD.ProviderVdc.StorageProfile)) - } - providerVdcStorageProfileHref := results.Results.ProviderVdcStorageProfileRecord[0].HREF results, err = vcd.client.QueryWithNotEncodedParams(nil, map[string]string{ "type": "networkPool", @@ -288,12 +300,12 @@ func (vcd *TestVCD) Test_CreateVdc(check *C) { }, }, VdcStorageProfile: []*types.VdcStorageProfileConfiguration{&types.VdcStorageProfileConfiguration{ - Enabled: true, + Enabled: addrOf(true), Units: "MB", Limit: 1024, Default: true, ProviderVdcStorageProfile: &types.Reference{ - HREF: providerVdcStorageProfileHref, + HREF: storageProfile.HREF, }, }, }, @@ -407,10 +419,9 @@ func (vcd *TestVCD) Test_AdminOrgCreateCatalog(check *C) { AddToCleanupList(TestCreateCatalog, "catalog", vcd.org.Org.Name, "Test_CreateCatalog") check.Assert(adminCatalog.AdminCatalog.Name, Equals, TestCreateCatalog) check.Assert(adminCatalog.AdminCatalog.Description, Equals, TestCreateCatalogDesc) - task := NewTask(&vcd.client.Client) - task.Task = adminCatalog.AdminCatalog.Tasks.Task[0] - err = task.WaitTaskCompletion() - check.Assert(err, IsNil) + // Immediately after the catalog creation, the creation task should be already complete + check.Assert(ResourceComplete(adminCatalog.AdminCatalog.Tasks), Equals, true) + adminOrg, err = vcd.client.GetAdminOrgByName(vcd.org.Org.Name) check.Assert(err, IsNil) copyAdminCatalog, err := adminOrg.GetAdminCatalogByName(TestCreateCatalog, false) @@ -445,9 +456,8 @@ func (vcd *TestVCD) Test_AdminOrgCreateCatalogWithStorageProfile(check *C) { AddToCleanupList(check.TestName(), "catalog", vcd.org.Org.Name, check.TestName()) check.Assert(adminCatalog.AdminCatalog.Name, Equals, check.TestName()) check.Assert(adminCatalog.AdminCatalog.Description, Equals, TestCreateCatalogDesc) - task := NewTask(&vcd.client.Client) - task.Task = adminCatalog.AdminCatalog.Tasks.Task[0] - err = task.WaitTaskCompletion() + // Accessing the task directly with `adminCatalog.AdminCatalog.Tasks.Task[0]` is not safe for Org user + err = adminCatalog.WaitForTasks() check.Assert(err, IsNil) adminOrg, err = vcd.client.GetAdminOrgByName(vcd.org.Org.Name) check.Assert(err, IsNil) @@ -492,10 +502,8 @@ func (vcd *TestVCD) Test_OrgCreateCatalog(check *C) { AddToCleanupList(TestCreateCatalog, "catalog", vcd.org.Org.Name, "Test_CreateCatalog") check.Assert(catalog.Catalog.Name, Equals, TestCreateCatalog) check.Assert(catalog.Catalog.Description, Equals, TestCreateCatalogDesc) - task := NewTask(&vcd.client.Client) - task.Task = catalog.Catalog.Tasks.Task[0] - err = task.WaitTaskCompletion() - check.Assert(err, IsNil) + // Immediately after the catalog creation, the creation task should be already complete + check.Assert(ResourceComplete(catalog.Catalog.Tasks), Equals, true) org, err = vcd.client.GetOrgByName(vcd.org.Org.Name) check.Assert(err, IsNil) copyCatalog, err := org.GetCatalogByName(TestCreateCatalog, false) @@ -528,9 +536,7 @@ func (vcd *TestVCD) Test_OrgCreateCatalogWithStorageProfile(check *C) { AddToCleanupList(check.TestName(), "catalog", vcd.org.Org.Name, check.TestName()) check.Assert(catalog.Catalog.Name, Equals, check.TestName()) check.Assert(catalog.Catalog.Description, Equals, TestCreateCatalogDesc) - task := NewTask(&vcd.client.Client) - task.Task = catalog.Catalog.Tasks.Task[0] - err = task.WaitTaskCompletion() + err = catalog.WaitForTasks() check.Assert(err, IsNil) org, err = vcd.client.GetOrgByName(vcd.org.Org.Name) check.Assert(err, IsNil) @@ -626,15 +632,10 @@ func setupVdc(vcd *TestVCD, check *C, allocationModel string) (AdminOrg, *types. check.Skip(fmt.Sprintf("No Provider VDC found with name '%s'", vcd.config.VCD.ProviderVdc.Name)) } providerVdcHref := results.Results.VMWProviderVdcRecord[0].HREF - results, err = vcd.client.QueryWithNotEncodedParams(nil, map[string]string{ - "type": "providerVdcStorageProfile", - "filter": fmt.Sprintf("name==%s", vcd.config.VCD.ProviderVdc.StorageProfile), - }) + storageProfile, err := vcd.client.QueryProviderVdcStorageProfileByName(vcd.config.VCD.ProviderVdc.StorageProfile, providerVdcHref) check.Assert(err, IsNil) - if len(results.Results.ProviderVdcStorageProfileRecord) == 0 { - check.Skip(fmt.Sprintf("No storage profile found with name '%s'", vcd.config.VCD.ProviderVdc.StorageProfile)) - } - providerVdcStorageProfileHref := results.Results.ProviderVdcStorageProfileRecord[0].HREF + + check.Assert(storageProfile.HREF, Not(Equals), "") results, err = vcd.client.QueryWithNotEncodedParams(nil, map[string]string{ "type": "networkPool", "filter": fmt.Sprintf("name==%s", vcd.config.VCD.ProviderVdc.NetworkPool), @@ -645,7 +646,7 @@ func setupVdc(vcd *TestVCD, check *C, allocationModel string) (AdminOrg, *types. } networkPoolHref := results.Results.NetworkPoolRecord[0].HREF vdcConfiguration := &types.VdcConfiguration{ - Name: TestCreateOrgVdc + "ForRefresh", + Name: check.TestName(), AllocationModel: allocationModel, ComputeCapacity: []*types.ComputeCapacity{ &types.ComputeCapacity{ @@ -662,12 +663,12 @@ func setupVdc(vcd *TestVCD, check *C, allocationModel string) (AdminOrg, *types. }, }, VdcStorageProfile: []*types.VdcStorageProfileConfiguration{&types.VdcStorageProfileConfiguration{ - Enabled: true, + Enabled: addrOf(true), Units: "MB", Limit: 1024, Default: true, ProviderVdcStorageProfile: &types.Reference{ - HREF: providerVdcStorageProfileHref, + HREF: storageProfile.HREF, }, }, }, @@ -686,6 +687,7 @@ func setupVdc(vcd *TestVCD, check *C, allocationModel string) (AdminOrg, *types. if allocationModel == "Flex" { vdcConfiguration.IsElastic = &falseValue vdcConfiguration.IncludeMemoryOverhead = &trueValue + vdcConfiguration.ResourceGuaranteedMemory = addrOf(1.00) } vdc, _ := adminOrg.GetVDCByName(vdcConfiguration.Name, false) @@ -699,6 +701,209 @@ func setupVdc(vcd *TestVCD, check *C, allocationModel string) (AdminOrg, *types. return *adminOrg, vdcConfiguration, err } +func (vcd *TestVCD) Test_QueryStorageProfiles(check *C) { + + // retrieve Org and VDC + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + adminVdc, err := adminOrg.GetAdminVDCByName(vcd.config.VCD.Vdc, false) + check.Assert(err, IsNil) + + if adminVdc.AdminVdc.ProviderVdcReference == nil { + check.Skip(fmt.Sprintf("test %s requires system administrator privileges", check.TestName())) + } + // Gets the Provider VDC from the AdminVdc structure + providerVdcName := adminVdc.AdminVdc.ProviderVdcReference.Name + check.Assert(providerVdcName, Not(Equals), "") + providerVdcHref := adminVdc.AdminVdc.ProviderVdcReference.HREF + check.Assert(providerVdcHref, Not(Equals), "") + + // Gets the full list of storage profilers + rawSpList, err := vcd.client.Client.QueryAllProviderVdcStorageProfiles() + check.Assert(err, IsNil) + + // Manually select the storage profiles that belong to the current provider VDC + var spList []*types.QueryResultProviderVdcStorageProfileRecordType + var duplicateNames = make(map[string]bool) + var notLocalStorageProfile string + var used = make(map[string]bool) + for _, sp := range rawSpList { + if sp.ProviderVdcHREF == providerVdcHref { + spList = append(spList, sp) + } + _, seen := used[sp.Name] + if seen { + duplicateNames[sp.Name] = true + } + used[sp.Name] = true + } + // Find a storage profile from a different provider VDC + for _, sp := range rawSpList { + if sp.ProviderVdcHREF != providerVdcHref { + _, isDuplicate := duplicateNames[sp.Name] + if !isDuplicate { + notLocalStorageProfile = sp.Name + } + } + } + + // Get the list of local storage profiles (belonging to the Provider VDC that the adminVdc depends on) + localSpList, err := vcd.client.Client.QueryProviderVdcStorageProfiles(providerVdcHref) + check.Assert(err, IsNil) + // Make sure the automated list and the manual list match + check.Assert(spList, DeepEquals, localSpList) + + // Get the same list using the AdminVdc method and check that the result matches + compatibleSpList, err := adminVdc.QueryCompatibleStorageProfiles() + check.Assert(err, IsNil) + check.Assert(compatibleSpList, DeepEquals, localSpList) + + for _, sp := range compatibleSpList { + fullSp, err := vcd.client.QueryProviderVdcStorageProfileByName(sp.Name, providerVdcHref) + check.Assert(err, IsNil) + check.Assert(sp.HREF, Equals, fullSp.HREF) + check.Assert(fullSp.ProviderVdcHREF, Equals, providerVdcHref) + } + + // When we have duplicate names, we also check the effectiveness of the retrieval function with Provider VDC filter + for name := range duplicateNames { + // Duplicate name with specific provider VDC HREF will succeed + fullSp, err := vcd.client.QueryProviderVdcStorageProfileByName(name, providerVdcHref) + check.Assert(err, IsNil) + check.Assert(fullSp.ProviderVdcHREF, Equals, providerVdcHref) + // Duplicate name with empty provider VDC HREF will fail + faultySp, err := vcd.client.QueryProviderVdcStorageProfileByName(name, "") + check.Assert(err, NotNil) + check.Assert(faultySp, IsNil) + } + + // Search explicitly for a storage profile not present in current provider VDC + if notLocalStorageProfile != "" { + fullSp, err := vcd.client.QueryProviderVdcStorageProfileByName(notLocalStorageProfile, providerVdcHref) + check.Assert(err, NotNil) + check.Assert(fullSp, IsNil) + } +} + +func (vcd *TestVCD) Test_AddRemoveVdcStorageProfiles(check *C) { + vcd.skipIfNotSysAdmin(check) + if vcd.config.VCD.ProviderVdc.Name == "" { + check.Skip("No provider VDC found in configuration") + } + providerVDCs, err := QueryProviderVdcByName(vcd.client, vcd.config.VCD.ProviderVdc.Name) + check.Assert(err, IsNil) + check.Assert(len(providerVDCs), Equals, 1) + + rawSpList, err := vcd.client.Client.QueryAllProviderVdcStorageProfiles() + check.Assert(err, IsNil) + var spList []*types.QueryResultProviderVdcStorageProfileRecordType + for _, sp := range rawSpList { + if sp.ProviderVdcHREF == providerVDCs[0].HREF { + spList = append(spList, sp) + } + } + + localSpList, err := vcd.client.Client.QueryProviderVdcStorageProfiles(providerVDCs[0].HREF) + check.Assert(err, IsNil) + check.Assert(spList, DeepEquals, localSpList) + + const minSp = 2 + if len(spList) < minSp { + check.Skip(fmt.Sprintf("At least %d storage profiles are needed for this test", minSp)) + } + var defaultSp *types.QueryResultProviderVdcStorageProfileRecordType + var sp2 *types.QueryResultProviderVdcStorageProfileRecordType + + for i := 0; i < minSp; i++ { + if spList[i].Name == vcd.config.VCD.ProviderVdc.StorageProfile { + if defaultSp == nil { + defaultSp = spList[i] + } + } else { + if sp2 == nil { + sp2 = spList[i] + } + } + } + + check.Assert(defaultSp, NotNil) + check.Assert(sp2, NotNil) + + // Create the VDC + adminOrg, vdcConfiguration, err := setupVdc(vcd, check, "AllocationPool") + check.Assert(err, IsNil) + + adminVdc, err := adminOrg.GetAdminVDCByName(vdcConfiguration.Name, true) + check.Assert(err, IsNil) + + // Add another storage profile + err = adminVdc.AddStorageProfileWait(&types.VdcStorageProfileConfiguration{ + Enabled: addrOf(true), + Units: "MB", + Limit: 1024, + Default: false, + ProviderVdcStorageProfile: &types.Reference{ + HREF: sp2.HREF, + Name: sp2.Name, + }, + }, "new sp 2") + check.Assert(err, IsNil) + check.Assert(len(adminVdc.AdminVdc.VdcStorageProfiles.VdcStorageProfile), Equals, 2) + + // Find the default storage profile and makes sure it matches with the one we know to be the default + defaultSpRef, err := adminVdc.GetDefaultStorageProfileReference() + check.Assert(err, IsNil) + check.Assert(defaultSp.Name, Equals, defaultSpRef.Name) + + // Remove the second storage profile + err = adminVdc.RemoveStorageProfileWait(sp2.Name) + check.Assert(err, IsNil) + check.Assert(len(adminVdc.AdminVdc.VdcStorageProfiles.VdcStorageProfile), Equals, 1) + + // Add the second storage profile again + err = adminVdc.AddStorageProfileWait(&types.VdcStorageProfileConfiguration{ + Enabled: addrOf(true), + Units: "MB", + Limit: 1024, + Default: false, + ProviderVdcStorageProfile: &types.Reference{ + HREF: sp2.HREF, + Name: sp2.Name, + }, + }, "new sp 2") + + check.Assert(err, IsNil) + check.Assert(len(adminVdc.AdminVdc.VdcStorageProfiles.VdcStorageProfile), Equals, 2) + + // Change default storage profile from the original one to the second one + err = adminVdc.SetDefaultStorageProfile(sp2.Name) + check.Assert(err, IsNil) + + // Check that the default storage profile was changed + defaultSpRef, err = adminVdc.GetDefaultStorageProfileReference() + check.Assert(err, IsNil) + check.Assert(defaultSpRef.Name, Equals, sp2.Name) + + // Set the default storage profile again to the same item. + // This proves that SetDefaultStorageProfile is idempotent + err = adminVdc.SetDefaultStorageProfile(sp2.Name) + check.Assert(err, IsNil) + defaultSpRef, err = adminVdc.GetDefaultStorageProfileReference() + check.Assert(err, IsNil) + check.Assert(defaultSpRef.Name, Equals, sp2.Name) + + // Remove the former default storage profile + err = adminVdc.RemoveStorageProfileWait(defaultSp.Name) + check.Assert(err, IsNil) + check.Assert(len(adminVdc.AdminVdc.VdcStorageProfiles.VdcStorageProfile), Equals, 1) + + // Delete the VDC + vdc, err := adminOrg.GetVDCByName(adminVdc.AdminVdc.Name, false) + check.Assert(err, IsNil) + err = vdc.DeleteWait(true, true) + check.Assert(err, IsNil) +} + // Tests VDC by updating it and then asserting if the // variable is updated. func (vcd *TestVCD) Test_UpdateVdc(check *C) { @@ -762,6 +967,12 @@ func (vcd *TestVCD) Test_UpdateVdc(check *C) { check.Assert(*updatedVdc.AdminVdc.UsesFastProvisioning, Equals, false) check.Assert(math.Abs(*updatedVdc.AdminVdc.ResourceGuaranteedCpu-guaranteed) < 0.001, Equals, true) check.Assert(math.Abs(*updatedVdc.AdminVdc.ResourceGuaranteedMemory-guaranteed) < 0.001, Equals, true) + vdc, err := adminOrg.GetVDCByName(updatedVdc.AdminVdc.Name, true) + check.Assert(err, IsNil) + task, err := vdc.Delete(true, true) + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) } // Tests org function GetAdminVdcByName with the vdc specified @@ -997,3 +1208,126 @@ func (vcd *TestVCD) Test_GetTaskList(check *C) { check.Assert(taskList.Task[0].Status, Not(Equals), "") check.Assert(taskList.Task[0].Progress, FitsTypeOf, 0) } + +func (vcd *TestVCD) TestQueryOrgVdcList(check *C) { + if !vcd.client.Client.IsSysAdmin { + check.Skip("TestQueryOrgVdcList: requires admin user") + return + } + if vcd.config.VCD.Org == "" { + check.Skip("TestQueryOrgVdcList: Org name not given.") + return + } + + if testVerbose { + fmt.Println("# Setting up 2 additional Orgs and 1 additional VDC") + } + + // Pre-create two more Orgs and one VDC to test that filtering behaves correctly + newOrgName1 := spawnTestOrg(vcd, check, "org1") + newOrgName2 := spawnTestOrg(vcd, check, "org2") + vdc := spawnTestVdc(vcd, check, newOrgName1) + + // Dump structure + if testVerbose { + fmt.Println("# Org and VDC structure layout") + queryOrgList := []string{"System", vcd.config.VCD.Org, newOrgName1, newOrgName2} + for _, orgName := range queryOrgList { + org, err := vcd.client.GetOrgByName(orgName) + check.Assert(err, IsNil) + check.Assert(org, NotNil) + + vdcs, err := org.QueryOrgVdcList() + check.Assert(err, IsNil) + if testVerbose { + fmt.Printf("VDCs for Org '%s'\n", orgName) + for i, vdc := range vdcs { + fmt.Printf("%d %s -> %s\n", i+1, vdc.OrgName, vdc.Name) + } + fmt.Println() + } + } + fmt.Println("") + } + + // expectedVdcCountInSystem = 1 NSX-V VDC + expectedVdcCountInSystem := 1 + // If an NSX-T VDC exists - then expected count of VDCs is at least 2 + if vcd.config.VCD.Nsxt.Vdc != "" { + expectedVdcCountInSystem++ + } + + // System Org does not directly report any child VDCs + validateQueryOrgVdcResults(vcd, check, "Org should have no VDCs", "System", addrOf(0), nil) + validateQueryOrgVdcResults(vcd, check, fmt.Sprintf("Should have 1 VDC %s", vdc.Vdc.Name), newOrgName1, addrOf(1), nil) + validateQueryOrgVdcResults(vcd, check, "Should have 0 VDCs", newOrgName2, addrOf(0), nil) + // Main Org 'vcd.config.VCD.Org' is expected to have at least (expectedVdcCountInSystem). Might be more if there are + // more VDCs created manually + validateQueryOrgVdcResults(vcd, check, fmt.Sprintf("Should have %d VDCs or more", expectedVdcCountInSystem), vcd.config.VCD.Org, nil, &expectedVdcCountInSystem) + + task, err := vdc.Delete(true, true) + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) + org1, err := vcd.client.GetAdminOrgByName(newOrgName1) + check.Assert(err, IsNil) + err = org1.Delete(true, true) + check.Assert(err, IsNil) + org2, err := vcd.client.GetAdminOrgByName(newOrgName2) + check.Assert(err, IsNil) + err = org2.Delete(true, true) + check.Assert(err, IsNil) +} + +func validateQueryOrgVdcResults(vcd *TestVCD, check *C, name, orgName string, expectedVdcCount, expectedVdcCountOrMore *int) { + if testVerbose { + fmt.Printf("# Checking VDCs in Org '%s' (%s):\n", orgName, name) + } + + org, err := vcd.client.GetOrgByName(orgName) + check.Assert(err, IsNil) + orgList, err := org.QueryOrgVdcList() + check.Assert(err, IsNil) + + // Number of components should be equal to the one returned by 'adminOrg.GetAllVDCs' which looks up VDCs in + // structure + adminOrg, err := vcd.client.GetAdminOrgByName(orgName) + check.Assert(err, IsNil) + allVdcs, err := adminOrg.GetAllVDCs(true) + check.Assert(err, IsNil) + check.Assert(len(orgList), Equals, len(allVdcs)) + + // Ensure the expected count of VDCs is found + if expectedVdcCount != nil { + check.Assert(len(orgList), Equals, *expectedVdcCount) + } + // Ensure that no less than 'expectedVdcCountOrMore' VDCs found in object. This validation allows to have more than + if expectedVdcCountOrMore != nil { + check.Assert(len(orgList) >= *expectedVdcCountOrMore, Equals, true) + } + + if testVerbose { + if expectedVdcCount != nil { + fmt.Printf("Got %d VDCs in Org '%s'. Expected (%d)\n", len(orgList), orgName, *expectedVdcCount) + } + + if expectedVdcCountOrMore != nil { + fmt.Printf("Got %d VDCs in Org '%s'. Expected (%d) or more\n", len(orgList), orgName, *expectedVdcCountOrMore) + } + } + + // Ensure that all VDCs have the same parent (or different if a query was performed for 'System') + for index := range orgList { + if orgName == "System" { + check.Assert(orgList[index].OrgName, Not(Equals), orgName) + } else { + check.Assert(orgList[index].OrgName, Equals, orgName) + + } + + } + if testVerbose { + fmt.Printf("%d VDC(s) in Org '%s' have correct parent set\n", len(orgList), orgName) + fmt.Println() + } +} diff --git a/govcd/orgs.go b/govcd/orgs.go new file mode 100644 index 000000000..fa573c01e --- /dev/null +++ b/govcd/orgs.go @@ -0,0 +1,41 @@ +/* + * Copyright 2024 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "github.com/vmware/go-vcloud-director/v2/types/v56" + "net/url" +) + +type OpenApiOrg struct { + Org *types.OpenApiOrg + vcdClient *VCDClient +} + +const LabelOrgs = "Organization" + +// 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 (org OpenApiOrg) wrap(inner *types.OpenApiOrg) *OpenApiOrg { + org.Org = inner + return &org +} + +// GetAllOrgs retrieve all organizations visible to the user +// When 'multiSite' is set, it will also check the organizations available from associated sites +func (vcdClient *VCDClient) GetAllOrgs(queryParameters url.Values, multiSite bool) ([]*OpenApiOrg, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointOrgs, + entityLabel: LabelOrgs, + queryParameters: queryParameters, + } + if multiSite { + c.additionalHeader = map[string]string{"Accept": "{{MEDIA_TYPE}};version={{API_VERSION}};multisite=global"} + } + + outerType := OpenApiOrg{vcdClient: vcdClient} + return getAllOuterEntities[OpenApiOrg, types.OpenApiOrg](&vcdClient.Client, outerType, c) +} diff --git a/govcd/orgs_test.go b/govcd/orgs_test.go new file mode 100644 index 000000000..4f0d8470b --- /dev/null +++ b/govcd/orgs_test.go @@ -0,0 +1,31 @@ +//go:build org || functional || ALL + +/* + * Copyright 2024 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "github.com/kr/pretty" + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_GetOrgs(check *C) { + + if !vcd.client.Client.APIClientVersionIs(">= 37.0") { + check.Skip(fmt.Sprintf("Minimum API version required for this test is 37.0. Found: %s", vcd.client.Client.APIVersion)) + } + orgs, err := vcd.client.GetAllOrgs(nil, true) + check.Assert(err, IsNil) + for i, org := range orgs { + fmt.Printf("%d %# v\n", i, pretty.Formatter(org.Org)) + } + fmt.Println() + orgs2, err := vcd.client.GetAllOrgs(nil, false) + check.Assert(err, IsNil) + for i, org := range orgs2 { + fmt.Printf("%d %# v\n", i, pretty.Formatter(org.Org)) + } +} diff --git a/govcd/orgvdcnetwork.go b/govcd/orgvdcnetwork.go index a2e366303..222d69d58 100644 --- a/govcd/orgvdcnetwork.go +++ b/govcd/orgvdcnetwork.go @@ -60,7 +60,7 @@ func (orgVdcNet *OrgVDCNetwork) Delete() (Task, error) { return Task{}, fmt.Errorf("error refreshing network: %s", err) } pathArr := strings.Split(orgVdcNet.OrgVDCNetwork.HREF, "/") - apiEndpoint, _ := url.ParseRequestURI(orgVdcNet.OrgVDCNetwork.HREF) + apiEndpoint := urlParseRequestURI(orgVdcNet.OrgVDCNetwork.HREF) apiEndpoint.Path = "/api/admin/network/" + pathArr[len(pathArr)-1] var resp *http.Response @@ -199,8 +199,8 @@ func (vdc *Vdc) CreateOrgVDCNetwork(networkConfig *types.OrgVDCNetwork) (Task, e // GetNetworkList returns a list of networks for the VDC func (vdc *Vdc) GetNetworkList() ([]*types.QueryResultOrgVdcNetworkRecordType, error) { // Find the list of networks with the wanted name - result, err := vdc.client.QueryWithNotEncodedParams(nil, map[string]string{ - "type": "orgVdcNetwork", + result, err := vdc.client.cumulativeQuery(types.QtOrgVdcNetwork, nil, map[string]string{ + "type": types.QtOrgVdcNetwork, "filter": fmt.Sprintf("vdc==%s", url.QueryEscape(vdc.Vdc.ID)), "filterEncoded": "true", }) @@ -215,8 +215,8 @@ func (vdc *Vdc) GetNetworkList() ([]*types.QueryResultOrgVdcNetworkRecordType, e func (vdc *Vdc) FindEdgeGatewayNameByNetwork(networkName string) (string, error) { // Find the list of networks with the wanted name - result, err := vdc.client.QueryWithNotEncodedParams(nil, map[string]string{ - "type": "orgVdcNetwork", + result, err := vdc.client.cumulativeQuery(types.QtOrgVdcNetwork, nil, map[string]string{ + "type": types.QtOrgVdcNetwork, "filter": fmt.Sprintf("name==%s;vdc==%s", url.QueryEscape(networkName), url.QueryEscape(vdc.Vdc.ID)), "filterEncoded": "true", }) diff --git a/govcd/orgvdcnetwork_test.go b/govcd/orgvdcnetwork_test.go index 4d16125ee..440745a1c 100644 --- a/govcd/orgvdcnetwork_test.go +++ b/govcd/orgvdcnetwork_test.go @@ -1,4 +1,4 @@ -// +build network functional ALL +//go:build network || functional || ALL /* * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -210,6 +210,85 @@ func (vcd *TestVCD) Test_GetNetworkList(check *C) { check.Assert(found, Equals, true) } +// Test_GetNetworkListLarge makes sure we can query a number of networks larger than the default query page length +func (vcd *TestVCD) Test_GetNetworkListLarge(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + + defaultPagelength := 25 + externalNetwork, err := vcd.client.GetExternalNetworkByName(vcd.config.VCD.ExternalNetwork) + if err != nil { + check.Skip("[Test_GetNetworkListLarge] parent network not found") + return + } + numOfNetworks := defaultPagelength + 1 + baseName := vcd.config.VCD.Org + for i := 1; i <= numOfNetworks; i++ { + networkName := fmt.Sprintf("net-%s-d-%d", baseName, i) + if testVerbose { + fmt.Printf("creating network %s\n", networkName) + } + description := fmt.Sprintf("Created by govcd test - network n. %d", i) + var networkConfig = types.OrgVDCNetwork{ + Xmlns: types.XMLNamespaceVCloud, + Name: networkName, + Description: description, + Configuration: &types.NetworkConfiguration{ + FenceMode: types.FenceModeBridged, + ParentNetwork: &types.Reference{ + HREF: externalNetwork.ExternalNetwork.HREF, + Name: externalNetwork.ExternalNetwork.Name, + Type: externalNetwork.ExternalNetwork.Type, + }, + BackwardCompatibilityMode: true, + }, + IsShared: false, + } + task, err := vcd.vdc.CreateOrgVDCNetwork(&networkConfig) + check.Assert(err, IsNil) + + AddToCleanupList(networkName, "network", vcd.org.Org.Name+"|"+vcd.vdc.Vdc.Name, "Test_GetNetworkListLarge") + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) + } + + err = vcd.vdc.Refresh() + check.Assert(err, IsNil) + + knownNetworkName1 := fmt.Sprintf("net-%s-d", baseName) + knownNetworkName2 := fmt.Sprintf("net-%s-d-%d", baseName, numOfNetworks) + networks, err := vcd.vdc.GetNetworkList() + check.Assert(err, IsNil) + if testVerbose { + fmt.Printf("Number of networks: %d\n", len(networks)) + } + check.Assert(len(networks) > defaultPagelength, Equals, true) + found1 := false + found2 := false + for _, net := range networks { + if net.Name == knownNetworkName1 { + found1 = true + } + if net.Name == knownNetworkName2 { + found2 = true + } + } + check.Assert(found1, Equals, true) + check.Assert(found2, Equals, true) + + for i := 1; i <= numOfNetworks; i++ { + networkName := fmt.Sprintf("net-%s-d-%d", baseName, i) + if testVerbose { + fmt.Printf("Removing network %s\n", networkName) + } + network, err := vcd.vdc.GetOrgVdcNetworkByName(networkName, false) + check.Assert(err, IsNil) + _, err = network.Delete() + check.Assert(err, IsNil) + } + err = vcd.vdc.Refresh() + check.Assert(err, IsNil) +} + // Tests the creation and update of an isolated Org VDC network func (vcd *TestVCD) Test_CreateUpdateOrgVdcNetworkIso(check *C) { fmt.Printf("Running: %s\n", check.TestName()) @@ -316,6 +395,10 @@ func (vcd *TestVCD) Test_CreateUpdateOrgVdcNetworkIso(check *C) { check.Assert(len(network.OrgVDCNetwork.Configuration.IPScopes.IPScope[0].IPRanges.IPRange), Not(Equals), 0) check.Assert(network.OrgVDCNetwork.Configuration.IPScopes.IPScope[0].IPRanges.IPRange[0].StartAddress, Equals, updatedStartAddress) check.Assert(network.OrgVDCNetwork.Configuration.IPScopes.IPScope[0].IPRanges.IPRange[0].EndAddress, Equals, updatedEndAddress) + task, err := network.Delete() + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) } @@ -369,7 +452,7 @@ func (vcd *TestVCD) Test_CreateUpdateOrgVdcNetworkDirect(check *C) { } check.Assert(task.Task.HREF, Not(Equals), "") - AddToCleanupList(networkName, "network", vcd.org.Org.Name+"|"+vcd.vdc.Vdc.Name, "Test_CreateOrgVdcNetworkDirect") + AddToCleanupList(networkName, "network", vcd.org.Org.Name+"|"+vcd.vdc.Vdc.Name, check.TestName()) // err = task.WaitTaskCompletion() err = task.WaitInspectTaskCompletion(LogTask, 10) diff --git a/govcd/provider_vdc.go b/govcd/provider_vdc.go new file mode 100644 index 000000000..10f580232 --- /dev/null +++ b/govcd/provider_vdc.go @@ -0,0 +1,594 @@ +package govcd + +import ( + "fmt" + "io" + "net/http" + "net/url" + "time" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + "github.com/vmware/go-vcloud-director/v2/util" +) + +// ProviderVdc is the basic Provider VDC structure, contains the minimum set of attributes. +type ProviderVdc struct { + ProviderVdc *types.ProviderVdc + client *Client +} + +// ProviderVdcExtended is the extended Provider VDC structure, contains same attributes as ProviderVdc plus some more. +type ProviderVdcExtended struct { + VMWProviderVdc *types.VMWProviderVdc + client *Client +} + +func newProviderVdc(cli *Client) *ProviderVdc { + return &ProviderVdc{ + ProviderVdc: new(types.ProviderVdc), + client: cli, + } +} + +func newProviderVdcExtended(cli *Client) *ProviderVdcExtended { + return &ProviderVdcExtended{ + VMWProviderVdc: new(types.VMWProviderVdc), + client: cli, + } +} + +// GetProviderVdcByHref finds a Provider VDC by its HREF. +// On success, returns a pointer to the ProviderVdc structure and a nil error +// On failure, returns a nil pointer and an error +func (vcdClient *VCDClient) GetProviderVdcByHref(providerVdcHref string) (*ProviderVdc, error) { + providerVdc := newProviderVdc(&vcdClient.Client) + + _, err := vcdClient.Client.ExecuteRequest(providerVdcHref, http.MethodGet, + "", "error retrieving Provider VDC: %s", nil, providerVdc.ProviderVdc) + if err != nil { + return nil, err + } + + return providerVdc, nil +} + +// GetProviderVdcExtendedByHref finds a Provider VDC with extended attributes by its HREF. +// On success, returns a pointer to the ProviderVdcExtended structure and a nil error +// On failure, returns a nil pointer and an error +func (vcdClient *VCDClient) GetProviderVdcExtendedByHref(providerVdcHref string) (*ProviderVdcExtended, error) { + providerVdc := newProviderVdcExtended(&vcdClient.Client) + + _, err := vcdClient.Client.ExecuteRequest(getAdminExtensionURL(providerVdcHref), http.MethodGet, + "", "error retrieving extended Provider VDC: %s", nil, providerVdc.VMWProviderVdc) + if err != nil { + return nil, err + } + + return providerVdc, nil +} + +// GetProviderVdcById finds a Provider VDC by URN. +// On success, returns a pointer to the ProviderVdc structure and a nil error +// On failure, returns a nil pointer and an error +func (vcdClient *VCDClient) GetProviderVdcById(providerVdcId string) (*ProviderVdc, error) { + providerVdcHref := vcdClient.Client.VCDHREF + providerVdcHref.Path += "/admin/providervdc/" + extractUuid(providerVdcId) + + return vcdClient.GetProviderVdcByHref(providerVdcHref.String()) +} + +// GetProviderVdcExtendedById finds a Provider VDC with extended attributes by URN. +// On success, returns a pointer to the ProviderVdcExtended structure and a nil error +// On failure, returns a nil pointer and an error +func (vcdClient *VCDClient) GetProviderVdcExtendedById(providerVdcId string) (*ProviderVdcExtended, error) { + providerVdcHref := vcdClient.Client.VCDHREF + providerVdcHref.Path += "/admin/extension/providervdc/" + extractUuid(providerVdcId) + + return vcdClient.GetProviderVdcExtendedByHref(providerVdcHref.String()) +} + +// GetProviderVdcByName finds a Provider VDC by name. +// On success, returns a pointer to the ProviderVdc structure and a nil error +// On failure, returns a nil pointer and an error +func (vcdClient *VCDClient) GetProviderVdcByName(providerVdcName string) (*ProviderVdc, error) { + providerVdc, err := getProviderVdcByName(vcdClient, providerVdcName, false) + if err != nil { + return nil, err + } + return providerVdc.(*ProviderVdc), err +} + +// GetProviderVdcExtendedByName finds a Provider VDC with extended attributes by name. +// On success, returns a pointer to the ProviderVdcExtended structure and a nil error +// On failure, returns a nil pointer and an error +func (vcdClient *VCDClient) GetProviderVdcExtendedByName(providerVdcName string) (*ProviderVdcExtended, error) { + providerVdcExtended, err := getProviderVdcByName(vcdClient, providerVdcName, true) + if err != nil { + return nil, err + } + return providerVdcExtended.(*ProviderVdcExtended), err +} + +// Refresh updates the contents of the Provider VDC associated to the receiver object. +func (providerVdc *ProviderVdc) Refresh() error { + if providerVdc.ProviderVdc.HREF == "" { + return fmt.Errorf("cannot refresh, receiver Provider VDC is empty") + } + + unmarshalledVdc := &types.ProviderVdc{} + + _, err := providerVdc.client.ExecuteRequest(providerVdc.ProviderVdc.HREF, http.MethodGet, + "", "error refreshing Provider VDC: %s", nil, unmarshalledVdc) + if err != nil { + return err + } + + providerVdc.ProviderVdc = unmarshalledVdc + + return nil +} + +// Refresh updates the contents of the extended Provider VDC associated to the receiver object. +func (providerVdcExtended *ProviderVdcExtended) Refresh() error { + if providerVdcExtended.VMWProviderVdc.HREF == "" { + return fmt.Errorf("cannot refresh, receiver extended Provider VDC is empty") + } + + unmarshalledVdc := &types.VMWProviderVdc{} + + _, err := providerVdcExtended.client.ExecuteRequest(providerVdcExtended.VMWProviderVdc.HREF, http.MethodGet, + "", "error refreshing extended Provider VDC: %s", nil, unmarshalledVdc) + if err != nil { + return err + } + + providerVdcExtended.VMWProviderVdc = unmarshalledVdc + + return nil +} + +// ToProviderVdc converts the receiver ProviderVdcExtended into the subset ProviderVdc +func (providerVdcExtended *ProviderVdcExtended) ToProviderVdc() (*ProviderVdc, error) { + providerVdcHref := providerVdcExtended.client.VCDHREF + providerVdcHref.Path += "/admin/providervdc/" + extractUuid(providerVdcExtended.VMWProviderVdc.ID) + + providerVdc := newProviderVdc(providerVdcExtended.client) + + _, err := providerVdcExtended.client.ExecuteRequest(providerVdcHref.String(), http.MethodGet, + "", "error retrieving Provider VDC: %s", nil, providerVdc.ProviderVdc) + if err != nil { + return nil, err + } + + return providerVdc, nil +} + +// getProviderVdcByName finds a Provider VDC with extension (extended=true) or without extension (extended=false) by name +// On success, returns a pointer to the ProviderVdc (extended=false) or ProviderVdcExtended (extended=true) structure and a nil error +// On failure, returns a nil pointer and an error +func getProviderVdcByName(vcdClient *VCDClient, providerVdcName string, extended bool) (interface{}, error) { + foundProviderVdcs, err := vcdClient.QueryWithNotEncodedParams(nil, map[string]string{ + "type": "providerVdc", + "filter": fmt.Sprintf("name==%s", url.QueryEscape(providerVdcName)), + "filterEncoded": "true", + }) + if err != nil { + return nil, err + } + if len(foundProviderVdcs.Results.VMWProviderVdcRecord) == 0 { + return nil, ErrorEntityNotFound + } + if len(foundProviderVdcs.Results.VMWProviderVdcRecord) > 1 { + return nil, fmt.Errorf("more than one Provider VDC found with name '%s'", providerVdcName) + } + if extended { + return vcdClient.GetProviderVdcExtendedByHref(foundProviderVdcs.Results.VMWProviderVdcRecord[0].HREF) + } + return vcdClient.GetProviderVdcByHref(foundProviderVdcs.Results.VMWProviderVdcRecord[0].HREF) +} + +// CreateProviderVdc creates a new provider VDC using the passed parameters +func (vcdClient *VCDClient) CreateProviderVdc(params *types.ProviderVdcCreation) (*ProviderVdcExtended, error) { + if !vcdClient.Client.IsSysAdmin { + return nil, fmt.Errorf("functionality requires System Administrator privileges") + } + if params.Name == "" { + return nil, fmt.Errorf("a non-empty name is needed to create a provider VDC") + } + if params.ResourcePoolRefs == nil || len(params.ResourcePoolRefs.VimObjectRef) == 0 { + return nil, fmt.Errorf("resource pool is needed to create a provider VDC") + } + if len(params.StorageProfile) == 0 { + return nil, fmt.Errorf("storage profile is needed to create a provider VDC") + } + if params.VimServer == nil { + return nil, fmt.Errorf("vim server is needed to create a provider VDC") + } + pvdcCreateHREF := vcdClient.Client.VCDHREF + pvdcCreateHREF.Path += "/admin/extension/providervdcsparams" + + resp, err := vcdClient.Client.executeJsonRequest(pvdcCreateHREF.String(), http.MethodPost, params, "error creating provider VDC: %s") + if err != nil { + return nil, err + } + + body, _ := io.ReadAll(resp.Body) + util.ProcessResponseOutput(util.CallFuncName(), resp, string(body)) + + defer closeBody(resp) + + pvdc, err := vcdClient.GetProviderVdcExtendedByName(params.Name) + if err != nil { + return nil, err + } + + // At this stage, the provider VDC is created, but the task may be still working. + // Thus, we retrieve the associated tasks, and wait for their completion. + if pvdc.VMWProviderVdc.Tasks == nil { + err = pvdc.Refresh() + if err != nil { + return pvdc, fmt.Errorf("error refreshing provider VDC %s: %s", params.Name, err) + } + if pvdc.VMWProviderVdc.Tasks == nil { + return pvdc, fmt.Errorf("provider VDC %s was created, but no completion task was found: %s", params.Name, err) + } + } + for _, taskInProgress := range pvdc.VMWProviderVdc.Tasks.Task { + task := Task{ + Task: taskInProgress, + client: pvdc.client, + } + err = task.WaitTaskCompletion() + if err != nil { + return pvdc, fmt.Errorf("provider VDC %s was created, but it is not ready: %s", params.Name, err) + } + } + + err = pvdc.Refresh() + return pvdc, err +} + +// Disable changes the Provider VDC state from enabled to disabled +func (pvdc *ProviderVdcExtended) Disable() error { + util.Logger.Printf("[TRACE] ProviderVdc.Disable") + + href, err := url.JoinPath(pvdc.VMWProviderVdc.HREF, "action", "disable") + + if err != nil { + return err + } + + err = pvdc.client.ExecuteRequestWithoutResponse(href, http.MethodPost, "", "error disabling provider VDC: %s", nil) + if err != nil { + return err + } + err = pvdc.Refresh() + if err != nil { + return err + } + if pvdc.IsEnabled() { + return fmt.Errorf("provider VDC was disabled, but its status is still shown as 'enabled'") + } + return nil +} + +// IsEnabled shows whether the Provider VDC is enabled +func (pvdc *ProviderVdcExtended) IsEnabled() bool { + if pvdc.VMWProviderVdc.IsEnabled == nil { + return false + } + return *pvdc.VMWProviderVdc.IsEnabled +} + +// Enable changes the Provider VDC state from disabled to enabled +func (pvdc *ProviderVdcExtended) Enable() error { + util.Logger.Printf("[TRACE] ProviderVdc.Enable") + + href, err := url.JoinPath(pvdc.VMWProviderVdc.HREF, "action", "enable") + + if err != nil { + return err + } + + err = pvdc.client.ExecuteRequestWithoutResponse(href, http.MethodPost, "", + "error enabling provider VDC: %s", nil) + if err != nil { + return err + } + err = pvdc.Refresh() + if err != nil { + return err + } + if !pvdc.IsEnabled() { + return fmt.Errorf("provider VDC was enabled, but its status is still shown as 'disabled'") + } + return nil +} + +// Delete removes a Provider VDC +// The provider VDC must be disabled for deletion to succeed +// Deletion will also fail if the Provider VDC is backing other resources, such as organization VDCs +func (pvdc *ProviderVdcExtended) Delete() (Task, error) { + util.Logger.Printf("[TRACE] ProviderVdc.Delete") + + if pvdc.IsEnabled() { + return Task{}, fmt.Errorf("provider VDC %s is enabled - can't delete", pvdc.VMWProviderVdc.Name) + } + // Return the task + return pvdc.client.ExecuteTaskRequest(pvdc.VMWProviderVdc.HREF, http.MethodDelete, + "", "error deleting provider VDC: %s", nil) +} + +// Update can change some of the provider VDC internals +// In practical terms, only name and description are guaranteed to be changed through this method. +// The other admitted changes need to go through separate API calls +func (pvdc *ProviderVdcExtended) Update() error { + + resp, err := pvdc.client.executeJsonRequest(pvdc.VMWProviderVdc.HREF, http.MethodPut, pvdc.VMWProviderVdc, + "error updating provider VDC: %s") + + if err != nil { + return err + } + defer closeBody(resp) + + return pvdc.checkProgress("updating") +} + +// Rename changes name and/or description from a provider VDC +func (pvdc *ProviderVdcExtended) Rename(name, description string) error { + if name == "" { + return fmt.Errorf("provider VDC name cannot be empty") + } + pvdc.VMWProviderVdc.Name = name + pvdc.VMWProviderVdc.Description = description + return pvdc.Update() +} + +// AddResourcePools adds resource pools to the Provider VDC +func (pvdc *ProviderVdcExtended) AddResourcePools(resourcePools []*ResourcePool) error { + util.Logger.Printf("[TRACE] ProviderVdc.AddResourcePools") + + href, err := url.JoinPath(pvdc.VMWProviderVdc.HREF, "action", "updateResourcePools") + if err != nil { + return err + } + + var items []*types.VimObjectRef + + for _, rp := range resourcePools { + vcenterUrl, err := rp.vcenter.GetVimServerUrl() + if err != nil { + return err + } + item := types.VimObjectRef{ + MoRef: rp.ResourcePool.Moref, + VimObjectType: "RESOURCE_POOL", + VimServerRef: &types.Reference{ + HREF: vcenterUrl, + ID: extractUuid(rp.vcenter.VSphereVCenter.VcId), + Name: rp.vcenter.VSphereVCenter.Name, + Type: "application/vnd.vmware.admin.vmwvirtualcenter+xml", + }, + } + items = append(items, &item) + } + + input := types.AddResourcePool{VimObjectRef: items} + + resp, err := pvdc.client.executeJsonRequest(href, http.MethodPost, input, "error updating provider VDC resource pools: %s") + if err != nil { + return err + } + task := NewTask(pvdc.client) + err = decodeBody(types.BodyTypeJSON, resp, task.Task) + if err != nil { + return err + } + + defer closeBody(resp) + err = task.WaitTaskCompletion() + if err != nil { + return err + } + return pvdc.Refresh() +} + +// DeleteResourcePools removes resource pools from the Provider VDC +func (pvdc *ProviderVdcExtended) DeleteResourcePools(resourcePools []*ResourcePool) error { + util.Logger.Printf("[TRACE] ProviderVdc.DeleteResourcePools") + + href, err := url.JoinPath(pvdc.VMWProviderVdc.HREF, "action", "updateResourcePools") + if err != nil { + return err + } + + usedResourcePools, err := pvdc.GetResourcePools() + if err != nil { + return fmt.Errorf("error retrieving used resource pools: %s", err) + } + + var items []*types.Reference + + for _, rp := range resourcePools { + + var foundUsed *types.QueryResultResourcePoolRecordType + for _, urp := range usedResourcePools { + if rp.ResourcePool.Moref == urp.Moref { + foundUsed = urp + break + } + } + if foundUsed == nil { + return fmt.Errorf("resource pool %s not found in provider VDC %s", rp.ResourcePool.Name, pvdc.VMWProviderVdc.Name) + } + if foundUsed.IsPrimary { + return fmt.Errorf("resource pool %s (%s) can not be removed, because it is the primary one for provider VDC %s", + rp.ResourcePool.Name, rp.ResourcePool.Moref, pvdc.VMWProviderVdc.Name) + } + if foundUsed.IsEnabled { + err = disableResourcePool(pvdc.client, foundUsed.HREF) + if err != nil { + return fmt.Errorf("error disabling resource pool %s: %s", foundUsed.Name, err) + } + } + + item := types.Reference{ + HREF: foundUsed.HREF, + ID: extractUuid(foundUsed.HREF), + Name: foundUsed.Name, + Type: "application/vnd.vmware.admin.vmwProviderVdcResourcePool+xml", + } + items = append(items, &item) + } + + input := types.DeleteResourcePool{ResourcePoolRefs: items} + + resp, err := pvdc.client.executeJsonRequest(href, http.MethodPost, input, "error removing resource pools from provider VDC: %s") + if err != nil { + return err + } + defer closeBody(resp) + task := NewTask(pvdc.client) + err = decodeBody(types.BodyTypeJSON, resp, task.Task) + if err != nil { + return err + } + err = task.WaitTaskCompletion() + if err != nil { + return err + } + return pvdc.Refresh() +} + +// GetResourcePools returns the Resource Pools belonging to this provider VDC +func (pvdc *ProviderVdcExtended) GetResourcePools() ([]*types.QueryResultResourcePoolRecordType, error) { + resourcePools, err := pvdc.client.cumulativeQuery(types.QtResourcePool, nil, map[string]string{ + "type": types.QtResourcePool, + "filter": fmt.Sprintf("providerVdc==%s", url.QueryEscape(extractUuid(pvdc.VMWProviderVdc.HREF))), + "filterEncoded": "true", + }) + if err != nil { + return nil, fmt.Errorf("could not get the Resource pool: %s", err) + } + return resourcePools.Results.ResourcePoolRecord, nil +} + +// disableResourcePool disables a resource pool while it is assigned to a provider VDC +// Calling this function is a prerequisite to removing a resource pool from a provider VDC +func disableResourcePool(client *Client, resourcePoolHref string) error { + href, err := url.JoinPath(resourcePoolHref, "action", "disable") + if err != nil { + return err + } + return client.ExecuteRequestWithoutResponse(href, http.MethodPost, "", "error disabling resource pool: %s", nil) +} + +// AddStorageProfiles adds the given storage profiles in this provider VDC +func (pvdc *ProviderVdcExtended) AddStorageProfiles(storageProfileNames []string) error { + href, err := url.JoinPath(pvdc.VMWProviderVdc.HREF, "storageProfiles") + if err != nil { + return err + } + + addStorageProfiles := &types.AddStorageProfiles{AddStorageProfile: storageProfileNames} + + resp, err := pvdc.client.executeJsonRequest(href, http.MethodPost, addStorageProfiles, + "error adding storage profiles to provider VDC: %s") + if err != nil { + return err + } + + defer closeBody(resp) + + return pvdc.checkProgress("adding storage profiles") +} + +func (pvdc *ProviderVdcExtended) checkProgress(label string) error { + // Let's keep this timeout as a precaution against an infinite wait + timeout := 2 * time.Minute + start := time.Now() + err := pvdc.Refresh() + if err != nil { + return err + } + + var elapsed time.Duration + for ResourceInProgress(pvdc.VMWProviderVdc.Tasks) { + err = pvdc.Refresh() + if err != nil { + return fmt.Errorf("error %s: %s", label, err) + } + time.Sleep(200 * time.Millisecond) + elapsed = time.Since(start) + if elapsed > timeout { + return fmt.Errorf("error %s within %s", label, timeout) + } + } + util.Logger.Printf("[ProviderVdcExtended.checkProgress] called by %s - running %s - elapsed: %s\n", + util.CallFuncName(), label, elapsed) + return nil +} + +// disableStorageProfile disables a storage profile while it is assigned to a provider VDC +// Calling this function is a prerequisite to removing a storage profile from a provider VDC +func disableStorageProfile(client *Client, storageProfileHref string) error { + disablePayload := &types.EnableStorageProfile{Enabled: false} + resp, err := client.executeJsonRequest(storageProfileHref, http.MethodPut, disablePayload, + "error disabling storage profile in provider VDC: %s") + + defer closeBody(resp) + return err +} + +// DeleteStorageProfiles removes storage profiles from the Provider VDC +func (pvdc *ProviderVdcExtended) DeleteStorageProfiles(storageProfiles []string) error { + util.Logger.Printf("[TRACE] ProviderVdc.DeleteStorageProfiles") + + href, err := url.JoinPath(pvdc.VMWProviderVdc.HREF, "storageProfiles") + if err != nil { + return err + } + + usedStorageProfileRefs := pvdc.VMWProviderVdc.StorageProfiles.ProviderVdcStorageProfile + + var toBeDeleted []*types.Reference + + for _, sp := range storageProfiles { + var foundUsed bool + for _, usp := range usedStorageProfileRefs { + if sp == usp.Name { + foundUsed = true + toBeDeleted = append(toBeDeleted, &types.Reference{HREF: usp.HREF}) + break + } + } + if !foundUsed { + return fmt.Errorf("storage profile %s not found in provider VDC %s", sp, pvdc.VMWProviderVdc.Name) + } + } + + for _, sp := range toBeDeleted { + err = disableStorageProfile(pvdc.client, sp.HREF) + if err != nil { + return fmt.Errorf("error disabling storage profile %s from provider VDC %s: %s", sp.Name, pvdc.VMWProviderVdc.Name, err) + } + } + input := &types.RemoveStorageProfile{RemoveStorageProfile: toBeDeleted} + + resp, err := pvdc.client.executeJsonRequest(href, http.MethodPost, input, "error removing storage profiles from provider VDC: %s") + if err != nil { + return err + } + defer closeBody(resp) + task := NewTask(pvdc.client) + err = decodeBody(types.BodyTypeJSON, resp, task.Task) + if err != nil { + return err + } + err = task.WaitTaskCompletion() + if err != nil { + return err + } + return pvdc.Refresh() +} diff --git a/govcd/provider_vdc_test.go b/govcd/provider_vdc_test.go new file mode 100644 index 000000000..fbd167383 --- /dev/null +++ b/govcd/provider_vdc_test.go @@ -0,0 +1,424 @@ +//go:build providervdc || functional || ALL + +package govcd + +import ( + "fmt" + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" + "strings" +) + +func init() { + testingTags["providervdc"] = "provider_vdc_test.go" +} + +func (vcd *TestVCD) Test_GetProviderVdc(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + + var providerVdcs []*ProviderVdc + providerVdc, err := vcd.client.GetProviderVdcByName(vcd.config.VCD.NsxtProviderVdc.Name) + check.Assert(err, IsNil) + providerVdcs = append(providerVdcs, providerVdc) + providerVdc, err = vcd.client.GetProviderVdcById(providerVdc.ProviderVdc.ID) + check.Assert(err, IsNil) + providerVdcs = append(providerVdcs, providerVdc) + providerVdc, err = vcd.client.GetProviderVdcByHref(providerVdc.ProviderVdc.HREF) + check.Assert(err, IsNil) + providerVdcs = append(providerVdcs, providerVdc) + + // Common asserts + for _, providerVdc := range providerVdcs { + check.Assert(providerVdc.ProviderVdc.Name, Equals, vcd.config.VCD.NsxtProviderVdc.Name) + foundStorageProfile := false + for _, storageProfile := range providerVdc.ProviderVdc.StorageProfiles.ProviderVdcStorageProfile { + if storageProfile.Name == vcd.config.VCD.NsxtProviderVdc.StorageProfile { + foundStorageProfile = true + break + } + } + check.Assert(foundStorageProfile, Equals, true) + check.Assert(*providerVdc.ProviderVdc.IsEnabled, Equals, true) + check.Assert(providerVdc.ProviderVdc.ComputeCapacity, NotNil) + check.Assert(providerVdc.ProviderVdc.Status, Equals, 1) + // This test may fail when the VCD has more than one network pool depending on the same NSX-T manager + //check.Assert(len(providerVdc.ProviderVdc.NetworkPoolReferences.NetworkPoolReference), Equals, 1) + check.Assert(len(providerVdc.ProviderVdc.NetworkPoolReferences.NetworkPoolReference) > 0, Equals, true) + + foundNetworkPool := false + for _, networkPool := range providerVdc.ProviderVdc.NetworkPoolReferences.NetworkPoolReference { + if networkPool.Name == vcd.config.VCD.NsxtProviderVdc.NetworkPool { + foundNetworkPool = true + break + } + } + check.Assert(foundNetworkPool, Equals, true) + check.Assert(providerVdc.ProviderVdc.Link, NotNil) + } +} + +func (vcd *TestVCD) Test_GetProviderVdcExtended(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + + var providerVdcsExtended []*ProviderVdcExtended + providerVdcExtended, err := vcd.client.GetProviderVdcExtendedByName(vcd.config.VCD.NsxtProviderVdc.Name) + check.Assert(err, IsNil) + providerVdcsExtended = append(providerVdcsExtended, providerVdcExtended) + providerVdcExtended, err = vcd.client.GetProviderVdcExtendedById(providerVdcExtended.VMWProviderVdc.ID) + check.Assert(err, IsNil) + providerVdcsExtended = append(providerVdcsExtended, providerVdcExtended) + providerVdcExtended, err = vcd.client.GetProviderVdcExtendedByHref(providerVdcExtended.VMWProviderVdc.HREF) + check.Assert(err, IsNil) + providerVdcsExtended = append(providerVdcsExtended, providerVdcExtended) + + // Common asserts + for _, providerVdcExtended := range providerVdcsExtended { + // Basic PVDC asserts + check.Assert(providerVdcExtended.VMWProviderVdc.Name, Equals, vcd.config.VCD.NsxtProviderVdc.Name) + foundStorageProfile := false + for _, storageProfile := range providerVdcExtended.VMWProviderVdc.StorageProfiles.ProviderVdcStorageProfile { + if storageProfile.Name == vcd.config.VCD.NsxtProviderVdc.StorageProfile { + foundStorageProfile = true + break + } + } + check.Assert(foundStorageProfile, Equals, true) + check.Assert(*providerVdcExtended.VMWProviderVdc.IsEnabled, Equals, true) + check.Assert(providerVdcExtended.VMWProviderVdc.ComputeCapacity, NotNil) + check.Assert(providerVdcExtended.VMWProviderVdc.Status, Equals, 1) + // This test may fail when the NSX-T manager has more than one network pool + //check.Assert(len(providerVdcExtended.VMWProviderVdc.NetworkPoolReferences.NetworkPoolReference), Equals, 1) + check.Assert(len(providerVdcExtended.VMWProviderVdc.NetworkPoolReferences.NetworkPoolReference) > 0, Equals, true) + foundNetworkPool := false + for _, networkPool := range providerVdcExtended.VMWProviderVdc.NetworkPoolReferences.NetworkPoolReference { + if networkPool.Name == vcd.config.VCD.NsxtProviderVdc.NetworkPool { + foundNetworkPool = true + break + } + } + check.Assert(foundNetworkPool, Equals, true) + check.Assert(providerVdcExtended.VMWProviderVdc.Link, NotNil) + // Extended PVDC asserts + check.Assert(providerVdcExtended.VMWProviderVdc.ComputeProviderScope, Equals, "vc1") + check.Assert(len(providerVdcExtended.VMWProviderVdc.DataStoreRefs.VimObjectRef), Equals, 4) + check.Assert(strings.HasPrefix(providerVdcExtended.VMWProviderVdc.HighestSupportedHardwareVersion, "vmx-"), Equals, true) + check.Assert(providerVdcExtended.VMWProviderVdc.HostReferences, NotNil) + check.Assert(providerVdcExtended.VMWProviderVdc.NsxTManagerReference, NotNil) + check.Assert(providerVdcExtended.VMWProviderVdc.NsxTManagerReference.Name, Equals, vcd.config.VCD.Nsxt.Manager) + check.Assert(providerVdcExtended.VMWProviderVdc.ResourcePoolRefs, NotNil) + check.Assert(providerVdcExtended.VMWProviderVdc.VimServer, NotNil) + } +} + +func (vcd *TestVCD) Test_GetNonExistentProviderVdc(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + + providerVdcExtended, err := vcd.client.GetProviderVdcExtendedByName("non-existent-pvdc") + check.Assert(providerVdcExtended, IsNil) + check.Assert(err, NotNil) + providerVdcExtended, err = vcd.client.GetProviderVdcExtendedById("non-existent-pvdc") + check.Assert(providerVdcExtended, IsNil) + check.Assert(err, NotNil) + providerVdcExtended, err = vcd.client.GetProviderVdcExtendedByHref("non-existent-pvdc") + check.Assert(providerVdcExtended, IsNil) + check.Assert(err, NotNil) + providerVdc, err := vcd.client.GetProviderVdcByName("non-existent-pvdc") + check.Assert(providerVdc, IsNil) + check.Assert(err, NotNil) + providerVdc, err = vcd.client.GetProviderVdcById("non-existent-pvdc") + check.Assert(providerVdc, IsNil) + check.Assert(err, NotNil) + providerVdc, err = vcd.client.GetProviderVdcByHref("non-existent-pvdc") + check.Assert(providerVdc, IsNil) + check.Assert(err, NotNil) +} + +func (vcd *TestVCD) Test_GetProviderVdcConvertFromExtendedToNormal(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + + providerVdcExtended, err := vcd.client.GetProviderVdcExtendedByName(vcd.config.VCD.NsxtProviderVdc.Name) + check.Assert(err, IsNil) + providerVdc, err := providerVdcExtended.ToProviderVdc() + check.Assert(err, IsNil) + check.Assert(providerVdc.ProviderVdc.Name, Equals, vcd.config.VCD.NsxtProviderVdc.Name) + foundStorageProfile := false + for _, storageProfile := range providerVdc.ProviderVdc.StorageProfiles.ProviderVdcStorageProfile { + if storageProfile.Name == vcd.config.VCD.NsxtProviderVdc.StorageProfile { + foundStorageProfile = true + break + } + } + check.Assert(foundStorageProfile, Equals, true) + check.Assert(*providerVdc.ProviderVdc.IsEnabled, Equals, true) + check.Assert(providerVdc.ProviderVdc.Status, Equals, 1) + // This test may fail when the NSX-T manager has more than one network pool + //check.Assert(len(providerVdc.ProviderVdc.NetworkPoolReferences.NetworkPoolReference), Equals, 1) + check.Assert(len(providerVdc.ProviderVdc.NetworkPoolReferences.NetworkPoolReference) > 0, Equals, true) + foundNetworkPool := false + + for _, np := range providerVdc.ProviderVdc.NetworkPoolReferences.NetworkPoolReference { + if np.Name == vcd.config.VCD.NsxtProviderVdc.NetworkPool { + foundNetworkPool = true + } + } + check.Assert(foundNetworkPool, Equals, true) + check.Assert(providerVdc.ProviderVdc.Link, NotNil) +} + +type providerVdcCreationElements struct { + label string + name string + description string + resourcePoolName string + params *types.ProviderVdcCreation + vcenter *VCenter + config TestConfig +} + +func (vcd *TestVCD) Test_ProviderVdcCRUD(check *C) { + // Note: you need to have at least one free resource pool to test provider VDC creation, + // and at least two of them to test update. They should be indicated in + // vcd.config.Vsphere.ResourcePoolForVcd1 and vcd.config.Vsphere.ResourcePoolForVcd2 + + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + if vcd.config.Vsphere.ResourcePoolForVcd1 == "" { + check.Skip("no resource pool defined for this VCD") + } + providerVdcName := check.TestName() + providerVdcDescription := check.TestName() + storageProfileList, err := vcd.client.Client.QueryAllProviderVdcStorageProfiles() + check.Assert(err, IsNil) + check.Assert(len(storageProfileList) > 0, Equals, true) + var storageProfile types.QueryResultProviderVdcStorageProfileRecordType + for _, sp := range storageProfileList { + if sp.Name == vcd.config.VCD.NsxtProviderVdc.StorageProfile { + storageProfile = *sp + } + } + check.Assert(storageProfile.HREF, Not(Equals), "") + + vcenter, err := vcd.client.GetVCenterByName(vcd.config.VCD.VimServer) + check.Assert(err, IsNil) + check.Assert(vcenter, NotNil) + + resourcePool, err := vcenter.GetResourcePoolByName(vcd.config.Vsphere.ResourcePoolForVcd1) + check.Assert(err, IsNil) + check.Assert(resourcePool, NotNil) + + nsxtManagers, err := vcd.client.QueryNsxtManagerByName(vcd.config.VCD.Nsxt.Manager) + check.Assert(err, IsNil) + check.Assert(len(nsxtManagers), Equals, 1) + + hwVersion, err := resourcePool.GetDefaultHardwareVersion() + check.Assert(err, IsNil) + + vcenterUrl, err := vcenter.GetVimServerUrl() + check.Assert(err, IsNil) + + networkPool, err := vcd.client.GetNetworkPoolByName(vcd.config.VCD.NsxtProviderVdc.NetworkPool) + check.Assert(err, IsNil) + networkPoolHref, err := networkPool.GetOpenApiUrl() + check.Assert(err, IsNil) + + providerVdcCreation := types.ProviderVdcCreation{ + Name: providerVdcName, + Description: providerVdcDescription, + HighestSupportedHardwareVersion: hwVersion, + IsEnabled: true, + VimServer: []*types.Reference{ + { + HREF: vcenterUrl, + ID: extractUuid(vcenter.VSphereVCenter.VcId), + Name: vcenter.VSphereVCenter.Name, + }, + }, + ResourcePoolRefs: &types.VimObjectRefs{ + VimObjectRef: []*types.VimObjectRef{ + { + VimServerRef: &types.Reference{ + HREF: vcenterUrl, + ID: extractUuid(vcenter.VSphereVCenter.VcId), + Name: vcenter.VSphereVCenter.Name, + }, + MoRef: resourcePool.ResourcePool.Moref, + VimObjectType: "RESOURCE_POOL", + }, + }, + }, + StorageProfile: []string{storageProfile.Name}, + NsxTManagerReference: &types.Reference{ + HREF: nsxtManagers[0].HREF, + ID: extractUuid(nsxtManagers[0].HREF), + Name: nsxtManagers[0].Name, + }, + NetworkPool: &types.Reference{ + HREF: networkPoolHref, + Name: networkPool.NetworkPool.Name, + ID: extractUuid(networkPool.NetworkPool.Id), + Type: networkPool.NetworkPool.PoolType, + }, + AutoCreateNetworkPool: false, + } + providerVdcNoNetworkPoolCreation := types.ProviderVdcCreation{ + Name: providerVdcName, + Description: providerVdcDescription, + HighestSupportedHardwareVersion: hwVersion, + IsEnabled: true, + VimServer: []*types.Reference{ + { + HREF: vcenterUrl, + ID: extractUuid(vcenter.VSphereVCenter.VcId), + Name: vcenter.VSphereVCenter.Name, + }, + }, + ResourcePoolRefs: &types.VimObjectRefs{ + VimObjectRef: []*types.VimObjectRef{ + { + VimServerRef: &types.Reference{ + HREF: vcenterUrl, + ID: extractUuid(vcenter.VSphereVCenter.VcId), + Name: vcenter.VSphereVCenter.Name, + }, + MoRef: resourcePool.ResourcePool.Moref, + VimObjectType: "RESOURCE_POOL", + }, + }, + }, + StorageProfile: []string{storageProfile.Name}, + AutoCreateNetworkPool: false, + } + testProviderVdcCreation(vcd.client, check, providerVdcCreationElements{ + label: "ProviderVDC with network pool", + name: providerVdcName, + description: providerVdcDescription, + resourcePoolName: resourcePool.ResourcePool.Name, + params: &providerVdcCreation, + vcenter: vcenter, + config: vcd.config, + }) + testProviderVdcCreation(vcd.client, check, providerVdcCreationElements{ + label: "ProviderVDC without network pool", + name: providerVdcName, + description: providerVdcDescription, + resourcePoolName: resourcePool.ResourcePool.Name, + params: &providerVdcNoNetworkPoolCreation, + vcenter: vcenter, + config: vcd.config, + }) + providerVdcNoNetworkPoolCreation.AutoCreateNetworkPool = true + testProviderVdcCreation(vcd.client, check, providerVdcCreationElements{ + label: "ProviderVDC with automatic network pool", + name: providerVdcName, + description: providerVdcDescription, + resourcePoolName: resourcePool.ResourcePool.Name, + params: &providerVdcNoNetworkPoolCreation, + vcenter: vcenter, + config: vcd.config, + }) +} + +func testProviderVdcCreation(client *VCDClient, check *C, creationElements providerVdcCreationElements) { + + fmt.Printf("*** %s\n", creationElements.label) + providerVdcName := creationElements.name + providerVdcDescription := creationElements.description + storageProfileName := creationElements.params.StorageProfile[0] + resourcePoolName := creationElements.resourcePoolName + + printVerbose(" creating provider VDC '%s' using resource pool '%s' and storage profile '%s'\n", + providerVdcName, resourcePoolName, storageProfileName) + providerVdcJson, err := client.CreateProviderVdc(creationElements.params) + check.Assert(err, IsNil) + check.Assert(providerVdcJson, NotNil) + check.Assert(providerVdcJson.VMWProviderVdc.Name, Equals, providerVdcName) + + AddToCleanupList(providerVdcName, "provider_vdc", "", check.TestName()) + retrievedPvdc, err := client.GetProviderVdcExtendedByName(providerVdcName) + check.Assert(err, IsNil) + + err = retrievedPvdc.Disable() + check.Assert(err, IsNil) + check.Assert(retrievedPvdc.VMWProviderVdc.IsEnabled, NotNil) + check.Assert(*retrievedPvdc.VMWProviderVdc.IsEnabled, Equals, false) + + err = retrievedPvdc.Enable() + check.Assert(err, IsNil) + check.Assert(retrievedPvdc.VMWProviderVdc.IsEnabled, NotNil) + check.Assert(*retrievedPvdc.VMWProviderVdc.IsEnabled, Equals, true) + + newProviderVdcName := "TestNewName" + newProviderVdcDescription := "Test New provider VDC description" + printVerbose(" renaming provider VDC to '%s'\n", newProviderVdcName) + err = retrievedPvdc.Rename(newProviderVdcName, newProviderVdcDescription) + check.Assert(err, IsNil) + check.Assert(retrievedPvdc.VMWProviderVdc.Name, Equals, newProviderVdcName) + check.Assert(retrievedPvdc.VMWProviderVdc.Description, Equals, newProviderVdcDescription) + + printVerbose(" renaming back provider VDC to '%s'\n", providerVdcName) + err = retrievedPvdc.Rename(providerVdcName, providerVdcDescription) + check.Assert(err, IsNil) + check.Assert(retrievedPvdc.VMWProviderVdc.Name, Equals, providerVdcName) + check.Assert(retrievedPvdc.VMWProviderVdc.Description, Equals, providerVdcDescription) + + secondResourcePoolName := creationElements.config.Vsphere.ResourcePoolForVcd2 + if secondResourcePoolName != "" { + printVerbose(" adding resource pool '%s' to provider VDC\n", secondResourcePoolName) + secondResourcePool, err := creationElements.vcenter.GetResourcePoolByName(secondResourcePoolName) + check.Assert(err, IsNil) + check.Assert(secondResourcePool, NotNil) + err = retrievedPvdc.AddResourcePools([]*ResourcePool{secondResourcePool}) + check.Assert(err, IsNil) + err = retrievedPvdc.Refresh() + check.Assert(err, IsNil) + check.Assert(len(retrievedPvdc.VMWProviderVdc.ResourcePoolRefs.VimObjectRef), Equals, 2) + + printVerbose(" removing resource pool '%s' from provider VDC\n", secondResourcePoolName) + err = retrievedPvdc.DeleteResourcePools([]*ResourcePool{secondResourcePool}) + check.Assert(err, IsNil) + err = retrievedPvdc.Refresh() + check.Assert(err, IsNil) + check.Assert(len(retrievedPvdc.VMWProviderVdc.ResourcePoolRefs.VimObjectRef), Equals, 1) + } + + secondStorageProfile := creationElements.config.VCD.NsxtProviderVdc.StorageProfile2 + if secondStorageProfile != "" { + printVerbose(" adding storage profile '%s' to provider VDC\n", secondStorageProfile) + // Adds a storage profile + err = retrievedPvdc.AddStorageProfiles([]string{secondStorageProfile}) + check.Assert(err, IsNil) + check.Assert(len(retrievedPvdc.VMWProviderVdc.StorageProfiles.ProviderVdcStorageProfile), Equals, 2) + + printVerbose(" removing storage profile '%s' from provider VDC\n", secondStorageProfile) + // Remove a storage profile + err = retrievedPvdc.DeleteStorageProfiles([]string{secondStorageProfile}) + check.Assert(err, IsNil) + check.Assert(len(retrievedPvdc.VMWProviderVdc.StorageProfiles.ProviderVdcStorageProfile), Equals, 1) + } + + // Deleting while the Provider VDC is still enabled will fail + task, err := retrievedPvdc.Delete() + check.Assert(err, NotNil) + + // Properly deleting provider VDC: first disabling, then removing + printVerbose(" disabling provider VDC '%s'\n", providerVdcName) + err = retrievedPvdc.Disable() + check.Assert(err, IsNil) + check.Assert(retrievedPvdc.VMWProviderVdc.IsEnabled, NotNil) + check.Assert(*retrievedPvdc.VMWProviderVdc.IsEnabled, Equals, false) + + printVerbose(" removing provider VDC '%s'\n", providerVdcName) + task, err = retrievedPvdc.Delete() + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) +} diff --git a/govcd/query.go b/govcd/query.go index d3584dfbd..caef88e07 100644 --- a/govcd/query.go +++ b/govcd/query.go @@ -23,12 +23,12 @@ func NewResults(cli *Client) *Results { } } -func (vcdCli *VCDClient) Query(params map[string]string) (Results, error) { +func (vcdClient *VCDClient) Query(params map[string]string) (Results, error) { - req := vcdCli.Client.NewRequest(params, http.MethodGet, vcdCli.QueryHREF, nil) - req.Header.Add("Accept", "vnd.vmware.vcloud.org+xml;version="+vcdCli.Client.APIVersion) + req := vcdClient.Client.NewRequest(params, http.MethodGet, vcdClient.QueryHREF, nil) + req.Header.Add("Accept", "vnd.vmware.vcloud.org+xml;version="+vcdClient.Client.APIVersion) - return getResult(&vcdCli.Client, req) + return getResult(&vcdClient.Client, req) } func (vdc *Vdc) Query(params map[string]string) (Results, error) { @@ -45,27 +45,39 @@ func (client *Client) QueryWithNotEncodedParams(params map[string]string, notEnc return client.QueryWithNotEncodedParamsWithApiVersion(params, notEncodedParams, client.APIVersion) } +func (client *Client) QueryWithNotEncodedParamsWithHeaders(params map[string]string, notEncodedParams map[string]string, headers map[string]string) (Results, error) { + return client.QueryWithNotEncodedParamsWithApiVersionWithHeaders(params, notEncodedParams, client.APIVersion, headers) +} + // QueryWithNotEncodedParams uses Query API to search for requested data func (client *Client) QueryWithNotEncodedParamsWithApiVersion(params map[string]string, notEncodedParams map[string]string, apiVersion string) (Results, error) { - queryUlr := client.VCDHREF - queryUlr.Path += "/query" + return client.QueryWithNotEncodedParamsWithApiVersionWithHeaders(params, notEncodedParams, apiVersion, nil) +} - req := client.NewRequestWitNotEncodedParamsWithApiVersion(params, notEncodedParams, http.MethodGet, queryUlr, nil, apiVersion) +func (client *Client) QueryWithNotEncodedParamsWithApiVersionWithHeaders(params map[string]string, notEncodedParams map[string]string, apiVersion string, headers map[string]string) (Results, error) { + queryUrl := client.VCDHREF + queryUrl.Path += "/query" + + req := client.NewRequestWitNotEncodedParamsWithApiVersion(params, notEncodedParams, http.MethodGet, queryUrl, nil, apiVersion) req.Header.Add("Accept", "vnd.vmware.vcloud.org+xml;version="+apiVersion) + for k, v := range headers { + req.Header.Add(k, v) + } + return getResult(client, req) } -func (vcdCli *VCDClient) QueryWithNotEncodedParams(params map[string]string, notEncodedParams map[string]string) (Results, error) { - return vcdCli.Client.QueryWithNotEncodedParams(params, notEncodedParams) +func (vcdClient *VCDClient) QueryWithNotEncodedParams(params map[string]string, notEncodedParams map[string]string) (Results, error) { + return vcdClient.Client.QueryWithNotEncodedParams(params, notEncodedParams) } func (vdc *Vdc) QueryWithNotEncodedParams(params map[string]string, notEncodedParams map[string]string) (Results, error) { return vdc.client.QueryWithNotEncodedParams(params, notEncodedParams) } -func (vcdCli *VCDClient) QueryWithNotEncodedParamsWithApiVersion(params map[string]string, notEncodedParams map[string]string, apiVersion string) (Results, error) { - return vcdCli.Client.QueryWithNotEncodedParamsWithApiVersion(params, notEncodedParams, apiVersion) +func (vcdClient *VCDClient) QueryWithNotEncodedParamsWithApiVersion(params map[string]string, notEncodedParams map[string]string, apiVersion string) (Results, error) { + return vcdClient.Client.QueryWithNotEncodedParamsWithApiVersion(params, notEncodedParams, apiVersion) } func (vdc *Vdc) QueryWithNotEncodedParamsWithApiVersion(params map[string]string, notEncodedParams map[string]string, apiVersion string) (Results, error) { diff --git a/govcd/query_metadata.go b/govcd/query_metadata.go index d38064636..2b8d708b8 100644 --- a/govcd/query_metadata.go +++ b/govcd/query_metadata.go @@ -62,13 +62,24 @@ func queryFieldsOnDemand(queryType string) ([]string, error) { vmFields = []string{"catalogName", "container", "containerName", "datastoreName", "description", "gcStatus", "guestOs", "hardwareVersion", "hostName", "isAutoNature", "isDeleted", "isDeployed", "isPublished", "isVAppTemplate", "isVdcEnabled", "memoryMB", "moref", "name", "numberOfCpus", "org", "status", - "storageProfileName", "vc", "vdc", "vmToolsVersion", "containerStatus", "pvdcHighestSupportedHardwareVersion", + "storageProfileName", "vc", "vdc", "vdcName", "vmToolsVersion", "containerStatus", "pvdcHighestSupportedHardwareVersion", "isComputePolicyCompliant", "vmSizingPolicyId", "vmPlacementPolicyId", "encrypted", "dateCreated", "totalStorageAllocatedMb", "isExpired"} vappFields = []string{"creationDate", "isBusy", "isDeployed", "isEnabled", "isExpired", "isInMaintenanceMode", "isPublic", "ownerName", "status", "vdc", "vdcName", "numberOfVMs", "numberOfCpus", "cpuAllocationMhz", "cpuAllocationInMhz", "storageKB", "memoryAllocationMB", "isAutoDeleteNotified", "isAutoUndeployNotified", "isVdcEnabled", "honorBookOrder", "pvdcHighestSupportedHardwareVersion", "lowestHardwareVersionInVApp"} + orgVdcFields = []string{"name", "description", "isEnabled", "cpuAllocationMhz", "cpuLimitMhz", "cpuUsedMhz", + "memoryAllocationMB", "memoryLimitMB", "memoryUsedMB", "storageLimitMB", "storageUsedMB", "providerVdcName", + "providerVdc", "orgName", "org", "allocationModel", "numberOfVApps", "numberOfUnmanagedVApps", "numberOfMedia", + "numberOfDisks", "numberOfVAppTemplates", "vcName", "isBusy", "status", "networkPool", "numberOfResourcePools", + "numberOfStorageProfiles", "usedNetworksInVdc", "numberOfVMs", "numberOfRunningVMs", "numberOfDeployedVApps", + "numberOfDeployedUnmanagedVApps", "isThinProvisioned", "isFastProvisioned", "networkProviderType", + "cpuOverheadMhz", "isVCEnabled", "memoryReservedMB", "cpuReservedMhz", "storageOverheadMB", "memoryOverheadMB", "vc"} + taskFields = []string{"href", "id", "type", "org", "orgName", "name", "operationFull", "message", "startDate", + "endDate", "status", "progress", "ownerName", "object", "objectType", "objectName", "serviceNamespace"} + orgFields = []string{"href", "id", "type", "name", "displayName", "isEnabled", "isReadOnly", "canPublishCatalogs", + "deployedVMQuota", "storedVMQuota", "numberOfCatalogs", "numberOfVdcs", "numberOfVApps", "numberOfGroups", "numberOfDisks"} fieldsOnDemand = map[string][]string{ types.QtVappTemplate: vappTemplatefields, types.QtAdminVappTemplate: vappTemplatefields, @@ -84,6 +95,11 @@ func queryFieldsOnDemand(queryType string) ([]string, error) { types.QtAdminVm: vmFields, types.QtVapp: vappFields, types.QtAdminVapp: vappFields, + types.QtOrgVdc: orgVdcFields, + types.QtAdminOrgVdc: orgVdcFields, + types.QtTask: taskFields, + types.QtAdminTask: taskFields, + types.QtOrg: orgFields, } ) @@ -141,7 +157,48 @@ func addResults(queryType string, cumulativeResults, newResults Results) (Result case types.QtAdminVapp: cumulativeResults.Results.AdminVAppRecord = append(cumulativeResults.Results.AdminVAppRecord, newResults.Results.AdminVAppRecord...) size = len(newResults.Results.AdminVAppRecord) - + case types.QtOrgVdc: + cumulativeResults.Results.OrgVdcRecord = append(cumulativeResults.Results.OrgVdcRecord, newResults.Results.OrgVdcRecord...) + size = len(newResults.Results.OrgVdcRecord) + case types.QtAdminOrgVdc: + cumulativeResults.Results.OrgVdcAdminRecord = append(cumulativeResults.Results.OrgVdcAdminRecord, newResults.Results.OrgVdcAdminRecord...) + size = len(newResults.Results.OrgVdcAdminRecord) + case types.QtTask: + cumulativeResults.Results.TaskRecord = append(cumulativeResults.Results.TaskRecord, newResults.Results.TaskRecord...) + size = len(newResults.Results.TaskRecord) + case types.QtAdminTask: + cumulativeResults.Results.AdminTaskRecord = append(cumulativeResults.Results.AdminTaskRecord, newResults.Results.AdminTaskRecord...) + size = len(newResults.Results.AdminTaskRecord) + case types.QtNetworkPool: + cumulativeResults.Results.NetworkPoolRecord = append(cumulativeResults.Results.NetworkPoolRecord, newResults.Results.NetworkPoolRecord...) + size = len(newResults.Results.NetworkPoolRecord) + case types.QtProviderVdcStorageProfile: + cumulativeResults.Results.ProviderVdcStorageProfileRecord = append(cumulativeResults.Results.ProviderVdcStorageProfileRecord, newResults.Results.ProviderVdcStorageProfileRecord...) + size = len(newResults.Results.ProviderVdcStorageProfileRecord) + case types.QtResourcePool: + cumulativeResults.Results.ResourcePoolRecord = append(cumulativeResults.Results.ResourcePoolRecord, newResults.Results.ResourcePoolRecord...) + size = len(newResults.Results.ResourcePoolRecord) + case types.QtVappNetwork: + cumulativeResults.Results.VappNetworkRecord = append(cumulativeResults.Results.VappNetworkRecord, newResults.Results.VappNetworkRecord...) + size = len(newResults.Results.VappNetworkRecord) + case types.QtAdminVappNetwork: + cumulativeResults.Results.AdminVappNetworkRecord = append(cumulativeResults.Results.AdminVappNetworkRecord, newResults.Results.AdminVappNetworkRecord...) + size = len(newResults.Results.AdminVappNetworkRecord) + case types.QtSiteAssociation: + cumulativeResults.Results.SiteAssociationRecord = append(cumulativeResults.Results.SiteAssociationRecord, newResults.Results.SiteAssociationRecord...) + size = len(newResults.Results.SiteAssociationRecord) + case types.QtOrgAssociation: + cumulativeResults.Results.OrgAssociationRecord = append(cumulativeResults.Results.OrgAssociationRecord, newResults.Results.OrgAssociationRecord...) + size = len(newResults.Results.OrgAssociationRecord) + case types.QtOrg: + cumulativeResults.Results.OrgRecord = append(cumulativeResults.Results.OrgRecord, newResults.Results.OrgRecord...) + size = len(newResults.Results.OrgRecord) + case types.QtAdminOrgVdcTemplate: + cumulativeResults.Results.AdminOrgVdcTemplateRecord = append(cumulativeResults.Results.AdminOrgVdcTemplateRecord, newResults.Results.AdminOrgVdcTemplateRecord...) + size = len(newResults.Results.AdminOrgVdcTemplateRecord) + case types.QtOrgVdcTemplate: + cumulativeResults.Results.OrgVdcTemplateRecord = append(cumulativeResults.Results.OrgVdcTemplateRecord, newResults.Results.OrgVdcTemplateRecord...) + size = len(newResults.Results.OrgVdcTemplateRecord) default: return Results{}, 0, fmt.Errorf("query type %s not supported", queryType) } @@ -151,6 +208,11 @@ func addResults(queryType string, cumulativeResults, newResults Results) (Result // cumulativeQuery runs a paginated query and collects all elements until the total number of records is retrieved func (client *Client) cumulativeQuery(queryType string, params, notEncodedParams map[string]string) (Results, error) { + return client.cumulativeQueryWithHeaders(queryType, params, notEncodedParams, nil) +} + +// cumulativeQueryWithHeaders is the same as cumulativeQuery() but let you add headers to the query +func (client *Client) cumulativeQueryWithHeaders(queryType string, params, notEncodedParams map[string]string, headers map[string]string) (Results, error) { var supportedQueryTypes = []string{ types.QtVappTemplate, types.QtAdminVappTemplate, @@ -166,6 +228,20 @@ func (client *Client) cumulativeQuery(queryType string, params, notEncodedParams types.QtAdminVm, types.QtVapp, types.QtAdminVapp, + types.QtOrgVdc, + types.QtAdminOrgVdc, + types.QtTask, + types.QtAdminTask, + types.QtResourcePool, + types.QtNetworkPool, + types.QtProviderVdcStorageProfile, + types.QtVappNetwork, + types.QtAdminVappNetwork, + types.QtSiteAssociation, + types.QtOrgAssociation, + types.QtOrg, + types.QtOrgVdcTemplate, + types.QtAdminOrgVdcTemplate, } // Make sure the query type is supported // We need to check early, as queries that would return less than 25 items (default page size) would succeed, @@ -181,7 +257,14 @@ func (client *Client) cumulativeQuery(queryType string, params, notEncodedParams return Results{}, fmt.Errorf("[cumulativeQuery] query type %s not supported", queryType) } - result, err := client.QueryWithNotEncodedParams(params, notEncodedParams) + if params == nil { + params = make(map[string]string) + } + if len(notEncodedParams) == 0 { + notEncodedParams = map[string]string{"type": queryType} + } + + result, err := client.QueryWithNotEncodedParamsWithHeaders(params, notEncodedParams, headers) if err != nil { return Results{}, err } @@ -204,7 +287,7 @@ func (client *Client) cumulativeQuery(queryType string, params, notEncodedParams page++ notEncodedParams["page"] = fmt.Sprintf("%d", page) var size int - newResult, err := client.QueryWithNotEncodedParams(params, notEncodedParams) + newResult, err := client.QueryWithNotEncodedParamsWithHeaders(params, notEncodedParams, headers) if err != nil { return Results{}, err } diff --git a/govcd/query_metadata_test.go b/govcd/query_metadata_test.go new file mode 100644 index 000000000..4241dc9f5 --- /dev/null +++ b/govcd/query_metadata_test.go @@ -0,0 +1,52 @@ +//go:build query || functional || ALL + +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_CheckCumulativeQuery(check *C) { + vcd.skipIfNotSysAdmin(check) + pvdcs, err := vcd.client.QueryProviderVdcs() + check.Assert(err, IsNil) + var storageProfileMap = make(map[string]bool) + + for _, pvdcRec := range pvdcs { + pvdc, err := vcd.client.GetProviderVdcByHref(pvdcRec.HREF) + check.Assert(err, IsNil) + for _, sp := range pvdc.ProviderVdc.StorageProfiles.ProviderVdcStorageProfile { + storageProfileMap[sp.Name] = true + } + } + if len(storageProfileMap) < 2 { + check.Skip("not enough storage profiles found for this test") + } + + checkQuery := func(pageSize string) { + var foundStorageProfileMap = make(map[string]bool) + results, err := vcd.client.Client.cumulativeQuery(types.QtProviderVdcStorageProfile, nil, map[string]string{ + "type": types.QtProviderVdcStorageProfile, + "pageSize": pageSize, + }) + + check.Assert(err, IsNil) + check.Assert(results, NotNil) + check.Assert(results.Results, NotNil) + check.Assert(results.Results.ProviderVdcStorageProfileRecord, NotNil) + + // Removing duplicates from results + for _, sp := range results.Results.ProviderVdcStorageProfileRecord { + foundStorageProfileMap[sp.Name] = true + } + check.Assert(len(foundStorageProfileMap), Equals, len(storageProfileMap)) + } + checkQuery("1") + checkQuery("2") + checkQuery("25") +} diff --git a/govcd/query_test.go b/govcd/query_test.go index 32e8fb158..6836b5444 100644 --- a/govcd/query_test.go +++ b/govcd/query_test.go @@ -1,4 +1,4 @@ -// +build query functional ALL +//go:build query || functional || ALL /* * Copyright 2018 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. diff --git a/govcd/rights.go b/govcd/rights.go new file mode 100644 index 000000000..fba155e71 --- /dev/null +++ b/govcd/rights.go @@ -0,0 +1,250 @@ +/* + * Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ +package govcd + +import ( + "fmt" + "net/url" + "strings" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +// getAllRights retrieves all rights. Query parameters can be supplied to perform additional +// filtering +func getAllRights(client *Client, queryParameters url.Values, additionalHeader map[string]string) ([]*types.Right, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRights + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + typeResponses := []*types.Right{{}} + err = client.OpenApiGetAllItems(minimumApiVersion, urlRef, queryParameters, &typeResponses, additionalHeader) + if err != nil { + return nil, err + } + + return typeResponses, nil +} + +// GetAllRights retrieves all available rights. +// Query parameters can be supplied to perform additional filtering +func (client *Client) GetAllRights(queryParameters url.Values) ([]*types.Right, error) { + return getAllRights(client, queryParameters, nil) +} + +// GetAllRights retrieves all available rights. Query parameters can be supplied to perform additional +// filtering +func (adminOrg *AdminOrg) GetAllRights(queryParameters url.Values) ([]*types.Right, error) { + tenantContext, err := adminOrg.getTenantContext() + if err != nil { + return nil, err + } + return getAllRights(adminOrg.client, queryParameters, getTenantContextHeader(tenantContext)) +} + +// getRights retrieves rights belonging to a given Role or similar container (global role, rights bundle). +// Query parameters can be supplied to perform additional filtering +func getRights(client *Client, roleId, endpoint string, queryParameters url.Values, additionalHeader map[string]string) ([]*types.Right, error) { + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint + roleId + "/rights") + if err != nil { + return nil, err + } + + typeResponses := []*types.Right{{}} + err = client.OpenApiGetAllItems(minimumApiVersion, urlRef, queryParameters, &typeResponses, additionalHeader) + if err != nil { + return nil, err + } + + return typeResponses, nil +} + +// GetRights retrieves all rights belonging to a given Role. Query parameters can be supplied to perform additional +// filtering +func (role *Role) GetRights(queryParameters url.Values) ([]*types.Right, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRoles + return getRights(role.client, role.Role.ID, endpoint, queryParameters, getTenantContextHeader(role.TenantContext)) +} + +// getRightByName retrieves a right by given name +func getRightByName(client *Client, name string, additionalHeader map[string]string) (*types.Right, error) { + var params = url.Values{} + + slowSearch := false + + // When the right name contains commas or semicolons, the encoding is rejected by the API. + // For this reason, when one or more commas or semicolons are present (6 occurrences in more than 300 right names) + // we run the search brute force, by fetching all the rights, and comparing the names. + // This problem should be fixed in 10.3 + // TODO: revisit this function after 10.3 is released + if strings.Contains(name, ",") || strings.Contains(name, ";") { + slowSearch = true + } else { + params.Set("filter", "name=="+name) + } + rights, err := getAllRights(client, params, additionalHeader) + if err != nil { + return nil, err + } + if len(rights) == 0 { + return nil, ErrorEntityNotFound + } + + if slowSearch { + for _, right := range rights { + if right.Name == name { + return right, nil + } + } + return nil, ErrorEntityNotFound + } + + if len(rights) > 1 { + return nil, fmt.Errorf("more than one right found with name '%s'", name) + } + return rights[0], nil +} + +// GetRightByName retrieves right by given name +func (client *Client) GetRightByName(name string) (*types.Right, error) { + return getRightByName(client, name, nil) +} + +// GetRightByName retrieves right by given name +func (adminOrg *AdminOrg) GetRightByName(name string) (*types.Right, error) { + tenantContext, err := adminOrg.getTenantContext() + if err != nil { + return nil, err + } + return getRightByName(adminOrg.client, name, getTenantContextHeader(tenantContext)) +} + +func getRightById(client *Client, id string, additionalHeader map[string]string) (*types.Right, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRights + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + if id == "" { + return nil, fmt.Errorf("empty role id") + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, id) + if err != nil { + return nil, err + } + + right := &types.Right{} + + err = client.OpenApiGetItem(minimumApiVersion, urlRef, nil, right, additionalHeader) + if err != nil { + return nil, err + } + + return right, nil +} + +func (client *Client) GetRightById(id string) (*types.Right, error) { + return getRightById(client, id, nil) +} + +func (adminOrg *AdminOrg) GetRightById(id string) (*types.Right, error) { + tenantContext, err := adminOrg.getTenantContext() + if err != nil { + return nil, err + } + return getRightById(adminOrg.client, id, getTenantContextHeader(tenantContext)) +} + +// getAllRightsCategories retrieves all rights categories. Query parameters can be supplied to perform additional +// filtering +func getAllRightsCategories(client *Client, queryParameters url.Values, additionalHeader map[string]string) ([]*types.RightsCategory, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRightsCategories + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + typeResponses := []*types.RightsCategory{{}} + err = client.OpenApiGetAllItems(minimumApiVersion, urlRef, queryParameters, &typeResponses, additionalHeader) + if err != nil { + return nil, err + } + + return typeResponses, nil +} + +// GetAllRightsCategories retrieves all rights categories. Query parameters can be supplied to perform additional +// filtering +func (client *Client) GetAllRightsCategories(queryParameters url.Values) ([]*types.RightsCategory, error) { + return getAllRightsCategories(client, queryParameters, nil) +} + +// GetAllRightsCategories retrieves all rights categories. Query parameters can be supplied to perform additional +// filtering +func (adminOrg *AdminOrg) GetAllRightsCategories(queryParameters url.Values) ([]*types.RightsCategory, error) { + tenantContext, err := adminOrg.getTenantContext() + if err != nil { + return nil, err + } + return getAllRightsCategories(adminOrg.client, queryParameters, getTenantContextHeader(tenantContext)) +} + +func getRightCategoryById(client *Client, id string, additionalHeader map[string]string) (*types.RightsCategory, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRightsCategories + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + if id == "" { + return nil, fmt.Errorf("empty category id") + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, id) + if err != nil { + return nil, err + } + + rightsCategory := &types.RightsCategory{} + + err = client.OpenApiGetItem(minimumApiVersion, urlRef, nil, rightsCategory, additionalHeader) + if err != nil { + return nil, err + } + + return rightsCategory, nil +} + +// GetRightsCategoryById retrieves a rights category from its ID +func (adminOrg *AdminOrg) GetRightsCategoryById(id string) (*types.RightsCategory, error) { + tenantContext, err := adminOrg.getTenantContext() + if err != nil { + return nil, err + } + return getRightCategoryById(adminOrg.client, id, getTenantContextHeader(tenantContext)) +} + +// GetRightsCategoryById retrieves a rights category from its ID +func (client *Client) GetRightsCategoryById(id string) (*types.RightsCategory, error) { + return getRightCategoryById(client, id, nil) +} diff --git a/govcd/rights_bundle.go b/govcd/rights_bundle.go new file mode 100644 index 000000000..0a05ce414 --- /dev/null +++ b/govcd/rights_bundle.go @@ -0,0 +1,262 @@ +/* + * Copyright 2021 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" +) + +type RightsBundle struct { + RightsBundle *types.RightsBundle + client *Client +} + +// CreateRightsBundle creates a new rights bundle as a system administrator +func (client *Client) CreateRightsBundle(newRightsBundle *types.RightsBundle) (*RightsBundle, error) { + if !client.IsSysAdmin { + return nil, fmt.Errorf("only system administrator can handle rights bundles") + } + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRightsBundles + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + if newRightsBundle.BundleKey == "" { + newRightsBundle.BundleKey = types.VcloudUndefinedKey + } + if newRightsBundle.PublishAll == nil { + newRightsBundle.PublishAll = addrOf(false) + } + returnBundle := &RightsBundle{ + RightsBundle: &types.RightsBundle{}, + client: client, + } + + err = client.OpenApiPostItem(minimumApiVersion, urlRef, nil, newRightsBundle, returnBundle.RightsBundle, nil) + if err != nil { + return nil, fmt.Errorf("error creating rights bundle: %s", err) + } + + return returnBundle, nil +} + +// Update updates existing rights bundle +func (rb *RightsBundle) Update() (*RightsBundle, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRightsBundles + minimumApiVersion, err := rb.client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + if rb.RightsBundle.Id == "" { + return nil, fmt.Errorf("cannot update role without id") + } + + urlRef, err := rb.client.OpenApiBuildEndpoint(endpoint, rb.RightsBundle.Id) + if err != nil { + return nil, err + } + + returnRightsBundle := &RightsBundle{ + RightsBundle: &types.RightsBundle{}, + client: rb.client, + } + + err = rb.client.OpenApiPutItem(minimumApiVersion, urlRef, nil, rb.RightsBundle, returnRightsBundle.RightsBundle, nil) + if err != nil { + return nil, fmt.Errorf("error updating rights bundle: %s", err) + } + + return returnRightsBundle, nil +} + +// getAllRightsBundles retrieves all rights bundles. Query parameters can be supplied to perform additional +// filtering +func getAllRightsBundles(client *Client, queryParameters url.Values, additionalHeader map[string]string) ([]*RightsBundle, error) { + if !client.IsSysAdmin { + return nil, fmt.Errorf("only system administrator can handle rights bundles") + } + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRightsBundles + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + typeResponses := []*types.RightsBundle{{}} + err = client.OpenApiGetAllItems(minimumApiVersion, urlRef, queryParameters, &typeResponses, additionalHeader) + if err != nil { + return nil, err + } + if len(typeResponses) == 0 { + return []*RightsBundle{}, nil + } + var results = make([]*RightsBundle, len(typeResponses)) + for i, r := range typeResponses { + results[i] = &RightsBundle{ + RightsBundle: r, + client: client, + } + } + + return results, nil +} + +// GetAllRightsBundles retrieves all rights bundles. Query parameters can be supplied to perform additional +// filtering +func (client *Client) GetAllRightsBundles(queryParameters url.Values) ([]*RightsBundle, error) { + return getAllRightsBundles(client, queryParameters, nil) +} + +// GetTenants retrieves all tenants associated to a given Rights Bundle. +// Query parameters can be supplied to perform additional filtering +func (rb *RightsBundle) GetTenants(queryParameters url.Values) ([]types.OpenApiReference, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRightsBundles + return getContainerTenants(rb.client, rb.RightsBundle.Id, endpoint, queryParameters) +} + +func (rb *RightsBundle) GetRights(queryParameters url.Values) ([]*types.Right, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRightsBundles + return getRights(rb.client, rb.RightsBundle.Id, endpoint, queryParameters, nil) +} + +// AddRights adds a collection of rights to a rights bundle +func (rb *RightsBundle) AddRights(newRights []types.OpenApiReference) error { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRightsBundles + return addRightsToRole(rb.client, "RightsBundle", rb.RightsBundle.Name, rb.RightsBundle.Id, endpoint, newRights, nil) +} + +// UpdateRights replaces existing rights with the given collection of rights +func (rb *RightsBundle) UpdateRights(newRights []types.OpenApiReference) error { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRightsBundles + return updateRightsInRole(rb.client, "RightsBundle", rb.RightsBundle.Name, rb.RightsBundle.Id, endpoint, newRights, nil) +} + +// RemoveRights removes specific rights from a rights bundle +func (rb *RightsBundle) RemoveRights(removeRights []types.OpenApiReference) error { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRightsBundles + return removeRightsFromRole(rb.client, "RightsBundle", rb.RightsBundle.Name, rb.RightsBundle.Id, endpoint, removeRights, nil) +} + +// RemoveAllRights removes all rights from a rights bundle +func (rb *RightsBundle) RemoveAllRights() error { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRightsBundles + return removeAllRightsFromRole(rb.client, "RightsBundle", rb.RightsBundle.Name, rb.RightsBundle.Id, endpoint, nil) +} + +// PublishTenants publishes a rights bundle to one or more tenants +func (rb *RightsBundle) PublishTenants(tenants []types.OpenApiReference) error { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRightsBundles + return publishContainerToTenants(rb.client, "RightsBundle", rb.RightsBundle.Name, rb.RightsBundle.Id, endpoint, tenants, "add") +} + +// UnpublishTenants removes publication status in rights bundle from one or more tenants +func (rb *RightsBundle) UnpublishTenants(tenants []types.OpenApiReference) error { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRightsBundles + return publishContainerToTenants(rb.client, "RightsBundle", rb.RightsBundle.Name, rb.RightsBundle.Id, endpoint, tenants, "remove") +} + +// ReplacePublishedTenants publishes a rights bundle to one or more tenants, removing the tenants already present +func (rb *RightsBundle) ReplacePublishedTenants(tenants []types.OpenApiReference) error { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRightsBundles + return publishContainerToTenants(rb.client, "RightsBundle", rb.RightsBundle.Name, rb.RightsBundle.Id, endpoint, tenants, "replace") +} + +// PublishAllTenants removes publication status in rights bundle from one or more tenants +func (rb *RightsBundle) PublishAllTenants() error { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRightsBundles + return publishContainerToAllTenants(rb.client, "RightsBundle", rb.RightsBundle.Name, rb.RightsBundle.Id, endpoint, true) +} + +// UnpublishAllTenants removes publication status in rights bundle from one or more tenants +func (rb *RightsBundle) UnpublishAllTenants() error { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRightsBundles + return publishContainerToAllTenants(rb.client, "RightsBundle", rb.RightsBundle.Name, rb.RightsBundle.Id, endpoint, false) +} + +// GetRightsBundleByName retrieves rights bundle by given name +func (client *Client) GetRightsBundleByName(name string) (*RightsBundle, error) { + queryParams := url.Values{} + queryParams.Add("filter", "name=="+name) + rightsBundles, err := client.GetAllRightsBundles(queryParams) + if err != nil { + return nil, err + } + if len(rightsBundles) == 0 { + return nil, ErrorEntityNotFound + } + if len(rightsBundles) > 1 { + return nil, fmt.Errorf("more than one rights bundle found with name '%s'", name) + } + return rightsBundles[0], nil +} + +// GetRightsBundleById retrieves rights bundle by given ID +func (client *Client) GetRightsBundleById(id string) (*RightsBundle, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRightsBundles + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + if id == "" { + return nil, fmt.Errorf("empty rights bundle id") + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, id) + if err != nil { + return nil, err + } + + rightsBundle := &RightsBundle{ + RightsBundle: &types.RightsBundle{}, + client: client, + } + + err = client.OpenApiGetItem(minimumApiVersion, urlRef, nil, rightsBundle.RightsBundle, nil) + if err != nil { + return nil, err + } + + return rightsBundle, nil +} + +// Delete deletes rights bundle +func (rb *RightsBundle) Delete() error { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRightsBundles + minimumApiVersion, err := rb.client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return err + } + + if rb.RightsBundle.Id == "" { + return fmt.Errorf("cannot delete rights bundle without id") + } + + urlRef, err := rb.client.OpenApiBuildEndpoint(endpoint, rb.RightsBundle.Id) + if err != nil { + return err + } + + err = rb.client.OpenApiDeleteItem(minimumApiVersion, urlRef, nil, nil) + + if err != nil { + return fmt.Errorf("error deleting rights bundle: %s", err) + } + + return nil +} diff --git a/govcd/rights_bundle_test.go b/govcd/rights_bundle_test.go new file mode 100644 index 000000000..a1fe51002 --- /dev/null +++ b/govcd/rights_bundle_test.go @@ -0,0 +1,119 @@ +//go:build functional || openapi || role || ALL + +/* + * Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "net/url" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_RightsBundle(check *C) { + client := vcd.client.Client + if !client.IsSysAdmin { + check.Skip("test Test_RightsBundle requires system administrator privileges") + } + vcd.checkSkipWhenApiToken(check) + + // Step 1 - Get all rights bundles + allExistingRightsBundle, err := client.GetAllRightsBundles(nil) + check.Assert(err, IsNil) + check.Assert(allExistingRightsBundle, NotNil) + + // Step 2 - Get all roles using query filters + for _, oneRightsBundle := range allExistingRightsBundle { + + // Step 2.1 - retrieve specific rights bundle by using FIQL filter + queryParams := url.Values{} + queryParams.Add("filter", "id=="+oneRightsBundle.RightsBundle.Id) + + expectOneRightsBundleResultById, err := client.GetAllRightsBundles(queryParams) + check.Assert(err, IsNil) + check.Assert(len(expectOneRightsBundleResultById) == 1, Equals, true) + + // Step 2.2 - retrieve specific rights bundle by using endpoint + exactItem, err := client.GetRightsBundleById(oneRightsBundle.RightsBundle.Id) + check.Assert(err, IsNil) + + check.Assert(err, IsNil) + check.Assert(exactItem, NotNil) + + // Step 2.3 - compare struct retrieved by using filter and the one retrieved by exact endpoint ID + check.Assert(oneRightsBundle, DeepEquals, expectOneRightsBundleResultById[0]) + + } + + // Step 3 - Create a new rights bundle and ensure it is created as specified by doing deep comparison + + newGR := &types.RightsBundle{ + Name: check.TestName(), + Description: "Global Role created by test", + // This BundleKey is being set by VCD even if it is not sent + BundleKey: types.VcloudUndefinedKey, + ReadOnly: false, + } + + createdRightsBundle, err := client.CreateRightsBundle(newGR) + check.Assert(err, IsNil) + AddToCleanupListOpenApi(createdRightsBundle.RightsBundle.Name, check.TestName(), + types.OpenApiPathVersion1_0_0+types.OpenApiEndpointRightsBundles+createdRightsBundle.RightsBundle.Id) + + // Ensure supplied and created structs differ only by ID + newGR.Id = createdRightsBundle.RightsBundle.Id + check.Assert(createdRightsBundle.RightsBundle, DeepEquals, newGR) + + // Step 4 - updated created rights bundle + createdRightsBundle.RightsBundle.Description = "Updated description" + updatedRightsBundle, err := createdRightsBundle.Update() + check.Assert(err, IsNil) + check.Assert(updatedRightsBundle.RightsBundle, DeepEquals, createdRightsBundle.RightsBundle) + + // Step 5 - add rights to rights bundle + + // These rights include 5 implied rights, which will be added by globalRole.AddRights + rightNames := []string{"Catalog: Add vApp from My Cloud", "Catalog: Edit Properties"} + + rightSet, err := getRightsSet(&client, rightNames) + check.Assert(err, IsNil) + + err = updatedRightsBundle.AddRights(rightSet) + check.Assert(err, IsNil) + + rights, err := updatedRightsBundle.GetRights(nil) + check.Assert(err, IsNil) + check.Assert(len(rights), Equals, len(rightSet)) + + // Step 6 - remove 1 right from rights bundle + + err = updatedRightsBundle.RemoveRights([]types.OpenApiReference{rightSet[0]}) + check.Assert(err, IsNil) + rights, err = updatedRightsBundle.GetRights(nil) + check.Assert(err, IsNil) + check.Assert(len(rights), Equals, len(rightSet)-1) + + testRightsContainerTenants(vcd, check, updatedRightsBundle) + + // Step 7 - remove all rights from rights bundle + err = updatedRightsBundle.RemoveAllRights() + check.Assert(err, IsNil) + + rights, err = updatedRightsBundle.GetRights(nil) + check.Assert(err, IsNil) + check.Assert(len(rights), Equals, 0) + + // Step 8 - delete created rights bundle + err = updatedRightsBundle.Delete() + check.Assert(err, IsNil) + + // Step 9 - try to read deleted rights bundle and expect error to contain 'ErrorEntityNotFound' + // Read is tricky - it throws an error ACCESS_TO_RESOURCE_IS_FORBIDDEN when the resource with ID does not + // exist therefore one cannot know what kind of error occurred. + deletedRightsBundle, err := client.GetRightsBundleById(createdRightsBundle.RightsBundle.Id) + check.Assert(ContainsNotFound(err), Equals, true) + check.Assert(deletedRightsBundle, IsNil) +} diff --git a/govcd/rights_test.go b/govcd/rights_test.go new file mode 100644 index 000000000..d01e7d2bb --- /dev/null +++ b/govcd/rights_test.go @@ -0,0 +1,122 @@ +//go:build functional || openapi || role || ALL + +/* + * Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_RoleTenantContext(check *C) { + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.org.Org.Name) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + + clientRoles, err := adminOrg.client.GetAllRoles(nil) + check.Assert(err, IsNil) + check.Assert(clientRoles, NotNil) + check.Assert(len(clientRoles), Not(Equals), 0) + + if testVerbose { + fmt.Println("Client roles") + for _, role := range clientRoles { + fmt.Printf("%-40s %s\n", role.Role.Name, role.Role.Description) + rights, err := role.GetRights(nil) + check.Assert(err, IsNil) + for i, right := range rights { + fmt.Printf("\t%3d %s\n", i+1, right.Name) + } + fmt.Println() + } + } + + orgRoles, err := adminOrg.GetAllRoles(nil) + check.Assert(err, IsNil) + check.Assert(orgRoles, NotNil) + check.Assert(len(orgRoles), Not(Equals), 0) + + if testVerbose { + fmt.Println("ORG roles") + for _, role := range orgRoles { + fmt.Printf("%-40s %s\n", role.Role.Name, role.Role.Description) + rights, err := role.GetRights(nil) + check.Assert(err, IsNil) + for i, right := range rights { + fmt.Printf("\t%3d %s\n", i+1, right.Name) + } + fmt.Println() + } + } + +} + +func (vcd *TestVCD) Test_Rights(check *C) { + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.org.Org.Name) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + + allOrgRights, err := adminOrg.GetAllRights(nil) + check.Assert(err, IsNil) + check.Assert(allOrgRights, NotNil) + + if testVerbose { + fmt.Printf("(org) how many rights: %d\n", len(allOrgRights)) + for i, oneRight := range allOrgRights { + fmt.Printf("%3d %-20s %-53s %s\n", i, oneRight.Name, oneRight.ID, oneRight.Category) + } + } + allExistingRights, err := adminOrg.client.GetAllRights(nil) + check.Assert(err, IsNil) + check.Assert(allExistingRights, NotNil) + + if testVerbose { + fmt.Printf("(global) how many rights: %d\n", len(allExistingRights)) + for i, oneRight := range allExistingRights { + fmt.Printf("%3d %-20s %-53s %s\n", i, oneRight.Name, oneRight.ID, oneRight.Category) + } + } + // Test a sample of rights + maxItems := 10 + + for i, right := range allExistingRights { + searchRight(adminOrg.client, right.Name, right.ID, check) + if i > maxItems { + break + } + } + var rigthNamesWithCommas = []string{ + "vApp: VM Migrate, Force Undeploy, Relocate, Consolidate", + "Role: Create, Edit, Delete, or Copy", + "Task: Resume, Abort, or Fail", + "VCD Extension: Register, Unregister, Refresh, Associate or Disassociate", + "UI Plugins: Define, Upload, Modify, Delete, Associate or Disassociate", + } + if vcd.client.Client.IsSysAdmin { + for _, name := range rigthNamesWithCommas { + searchRight(adminOrg.client, name, "", check) + } + } + + rightsCategories, err := adminOrg.client.GetAllRightsCategories(nil) + check.Assert(err, IsNil) + check.Assert(len(rightsCategories) > 0, Equals, true) +} + +func searchRight(client *Client, name, id string, check *C) { + fullRightByName, err := client.GetRightByName(name) + check.Assert(err, IsNil) + check.Assert(fullRightByName, NotNil) + if id != "" { + fullRightById, err := client.GetRightById(id) + check.Assert(err, IsNil) + check.Assert(fullRightById, NotNil) + category, err := client.GetRightsCategoryById(fullRightById.Category) + check.Assert(err, IsNil) + check.Assert(fullRightById.Category, Equals, category.Id) + } +} diff --git a/govcd/roles.go b/govcd/roles.go index cebcef061..83eaf90ba 100644 --- a/govcd/roles.go +++ b/govcd/roles.go @@ -1,9 +1,9 @@ -package govcd - /* - * Copyright 2020 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + * Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. */ +package govcd + import ( "fmt" "net/url" @@ -13,12 +13,13 @@ import ( // Role uses OpenAPI endpoint to operate user roles type Role struct { - Role *types.Role - client *Client + Role *types.Role + client *Client + TenantContext *TenantContext } -// GetOpenApiRoleById retrieves role by given ID -func (adminOrg *AdminOrg) GetOpenApiRoleById(id string) (*Role, error) { +// GetRoleById retrieves role by given ID +func (adminOrg *AdminOrg) GetRoleById(id string) (*Role, error) { endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRoles minimumApiVersion, err := adminOrg.client.checkOpenApiEndpointCompatibility(endpoint) if err != nil { @@ -34,12 +35,17 @@ func (adminOrg *AdminOrg) GetOpenApiRoleById(id string) (*Role, error) { return nil, err } + tenantContext, err := adminOrg.getTenantContext() + if err != nil { + return nil, err + } role := &Role{ - Role: &types.Role{}, - client: adminOrg.client, + Role: &types.Role{}, + client: adminOrg.client, + TenantContext: tenantContext, } - err = adminOrg.client.OpenApiGetItem(minimumApiVersion, urlRef, nil, role.Role) + err = adminOrg.client.OpenApiGetItem(minimumApiVersion, urlRef, nil, role.Role, getTenantContextHeader(tenantContext)) if err != nil { return nil, err } @@ -47,22 +53,39 @@ func (adminOrg *AdminOrg) GetOpenApiRoleById(id string) (*Role, error) { return role, nil } -// GetAllOpenApiRoles retrieves all roles using OpenAPI endpoint. Query parameters can be supplied to perform additional +// GetRoleByName retrieves role by given name +func (adminOrg *AdminOrg) GetRoleByName(name string) (*Role, error) { + queryParams := url.Values{} + queryParams.Add("filter", "name=="+name) + roles, err := adminOrg.GetAllRoles(queryParams) + if err != nil { + return nil, err + } + if len(roles) == 0 { + return nil, ErrorEntityNotFound + } + if len(roles) > 1 { + return nil, fmt.Errorf("more than one role found with name '%s'", name) + } + return roles[0], nil +} + +// getAllRoles retrieves all roles using OpenAPI endpoint. Query parameters can be supplied to perform additional // filtering -func (adminOrg *AdminOrg) GetAllOpenApiRoles(queryParameters url.Values) ([]*Role, error) { +func getAllRoles(client *Client, queryParameters url.Values, additionalHeader map[string]string) ([]*Role, error) { endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRoles - minimumApiVersion, err := adminOrg.client.checkOpenApiEndpointCompatibility(endpoint) + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) if err != nil { return nil, err } - urlRef, err := adminOrg.client.OpenApiBuildEndpoint(endpoint) + urlRef, err := client.OpenApiBuildEndpoint(endpoint) if err != nil { return nil, err } typeResponses := []*types.Role{{}} - err = adminOrg.client.OpenApiGetAllItems(minimumApiVersion, urlRef, queryParameters, &typeResponses) + err = client.OpenApiGetAllItems(minimumApiVersion, urlRef, queryParameters, &typeResponses, additionalHeader) if err != nil { return nil, err } @@ -71,15 +94,32 @@ func (adminOrg *AdminOrg) GetAllOpenApiRoles(queryParameters url.Values) ([]*Rol returnRoles := make([]*Role, len(typeResponses)) for sliceIndex := range typeResponses { returnRoles[sliceIndex] = &Role{ - Role: typeResponses[sliceIndex], - client: adminOrg.client, + Role: typeResponses[sliceIndex], + client: client, + TenantContext: getTenantContextFromHeader(additionalHeader), } } return returnRoles, nil } -// CreateRole creates a new role using OpenAPI endpoint +// GetAllRoles retrieves all roles as tenant user. Query parameters can be supplied to perform additional +// filtering +func (adminOrg *AdminOrg) GetAllRoles(queryParameters url.Values) ([]*Role, error) { + tenantContext, err := adminOrg.getTenantContext() + if err != nil { + return nil, err + } + return getAllRoles(adminOrg.client, queryParameters, getTenantContextHeader(tenantContext)) +} + +// GetAllRoles retrieves all roles as System administrator. Query parameters can be supplied to perform additional +// filtering +func (client *Client) GetAllRoles(queryParameters url.Values) ([]*Role, error) { + return getAllRoles(client, queryParameters, nil) +} + +// CreateRole creates a new role as a tenant administrator func (adminOrg *AdminOrg) CreateRole(newRole *types.Role) (*Role, error) { endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRoles minimumApiVersion, err := adminOrg.client.checkOpenApiEndpointCompatibility(endpoint) @@ -87,17 +127,26 @@ func (adminOrg *AdminOrg) CreateRole(newRole *types.Role) (*Role, error) { return nil, err } + if newRole.BundleKey == "" { + newRole.BundleKey = types.VcloudUndefinedKey + } + urlRef, err := adminOrg.client.OpenApiBuildEndpoint(endpoint) if err != nil { return nil, err } + tenantContext, err := adminOrg.getTenantContext() + if err != nil { + return nil, err + } returnRole := &Role{ - Role: &types.Role{}, - client: adminOrg.client, + Role: &types.Role{}, + client: adminOrg.client, + TenantContext: tenantContext, } - err = adminOrg.client.OpenApiPostItem(minimumApiVersion, urlRef, nil, newRole, returnRole.Role) + err = adminOrg.client.OpenApiPostItem(minimumApiVersion, urlRef, nil, newRole, returnRole.Role, getTenantContextHeader(tenantContext)) if err != nil { return nil, fmt.Errorf("error creating role: %s", err) } @@ -123,11 +172,12 @@ func (role *Role) Update() (*Role, error) { } returnRole := &Role{ - Role: &types.Role{}, - client: role.client, + Role: &types.Role{}, + client: role.client, + TenantContext: role.TenantContext, } - err = role.client.OpenApiPutItem(minimumApiVersion, urlRef, nil, role.Role, returnRole.Role) + err = role.client.OpenApiPutItem(minimumApiVersion, urlRef, nil, role.Role, returnRole.Role, getTenantContextHeader(role.TenantContext)) if err != nil { return nil, fmt.Errorf("error updating role: %s", err) } @@ -152,7 +202,7 @@ func (role *Role) Delete() error { return err } - err = role.client.OpenApiDeleteItem(minimumApiVersion, urlRef, nil) + err = role.client.OpenApiDeleteItem(minimumApiVersion, urlRef, nil, getTenantContextHeader(role.TenantContext)) if err != nil { return fmt.Errorf("error deleting role: %s", err) @@ -160,3 +210,245 @@ func (role *Role) Delete() error { return nil } + +// AddRights adds a collection of rights to a role +func (role *Role) AddRights(newRights []types.OpenApiReference) error { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRoles + return addRightsToRole(role.client, "Role", role.Role.Name, role.Role.ID, endpoint, newRights, getTenantContextHeader(role.TenantContext)) +} + +// UpdateRights replaces existing rights with the given collection of rights +func (role *Role) UpdateRights(newRights []types.OpenApiReference) error { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRoles + return updateRightsInRole(role.client, "Role", role.Role.Name, role.Role.ID, endpoint, newRights, getTenantContextHeader(role.TenantContext)) +} + +// RemoveRights removes specific rights from a role +func (role *Role) RemoveRights(removeRights []types.OpenApiReference) error { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRoles + return removeRightsFromRole(role.client, "Role", role.Role.Name, role.Role.ID, endpoint, removeRights, getTenantContextHeader(role.TenantContext)) +} + +// RemoveAllRights removes all rights from a role +func (role *Role) RemoveAllRights() error { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointRoles + return removeAllRightsFromRole(role.client, "Role", role.Role.Name, role.Role.ID, endpoint, getTenantContextHeader(role.TenantContext)) +} + +// addRightsToRole is a generic function that can add rights to a rights collection (Role, Global Role, or Rights bundle) +// roleType is an informative string (one of "Role", "GlobalRole", or "RightsBundle") +// name and id are the name and ID of the collection +// endpoint is the API endpoint used as a basis for the POST operation +// newRights is a collection of rights (ID+name) to be added +// Note: the API call ignores duplicate rights. If the rights to be added already exist, the call succeeds +// but no changes are recorded +func addRightsToRole(client *Client, roleType, name, id, endpoint string, newRights []types.OpenApiReference, additionalHeader map[string]string) error { + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return err + } + + if id == "" { + return fmt.Errorf("cannot update %s without id", roleType) + } + if name == "" { + return fmt.Errorf("empty name given for %s %s", roleType, id) + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, id, "/rights") + if err != nil { + return err + } + + var input types.OpenApiItems + + for _, right := range newRights { + input.Values = append(input.Values, types.OpenApiReference{ + Name: right.Name, + ID: right.ID, + }) + } + var pages types.OpenApiPages + + err = client.OpenApiPostItem(minimumApiVersion, urlRef, nil, &input, &pages, additionalHeader) + + if err != nil { + return fmt.Errorf("error adding rights to %s %s: %s", roleType, name, err) + } + + return nil +} + +// updateRightsInRole is a generic function that can change rights in a Role or Global Role +// roleType is an informative string (either "Role" or "GlobalRole") +// name and id are the name and ID of the role +// endpoint is the API endpoint used as a basis for the PUT operation +// newRights is a collection of rights (ID+name) to be added +func updateRightsInRole(client *Client, roleType, name, id, endpoint string, newRights []types.OpenApiReference, additionalHeader map[string]string) error { + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return err + } + + if id == "" { + return fmt.Errorf("cannot update %s without id", roleType) + } + if name == "" { + return fmt.Errorf("empty name given for %s %s", roleType, id) + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, id, "/rights") + if err != nil { + return err + } + + var input = types.OpenApiItems{ + Values: []types.OpenApiReference{}, + } + + for _, right := range newRights { + input.Values = append(input.Values, types.OpenApiReference{ + Name: right.Name, + ID: right.ID, + }) + } + var pages types.OpenApiPages + + err = client.OpenApiPutItem(minimumApiVersion, urlRef, nil, &input, &pages, additionalHeader) + + if err != nil { + return fmt.Errorf("error updating rights in %s %s: %s", roleType, name, err) + } + + return nil +} + +// removeRightsFromRole is a generic function that can remove rights from a Role or Global Role +// roleType is an informative string (either "Role" or "GlobalRole") +// name and id are the name and ID of the role +// endpoint is the API endpoint used as a basis for the PUT operation +// removeRights is a collection of rights (ID+name) to be removed +func removeRightsFromRole(client *Client, roleType, name, id, endpoint string, removeRights []types.OpenApiReference, additionalHeader map[string]string) error { + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return err + } + + if id == "" { + return fmt.Errorf("cannot update %s without id", roleType) + } + if name == "" { + return fmt.Errorf("empty name given for %s %s", roleType, id) + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, id, "/rights") + if err != nil { + return err + } + + var input = types.OpenApiItems{ + Values: []types.OpenApiReference{}, + } + var pages types.OpenApiPages + + currentRights, err := getRights(client, id, endpoint, nil, additionalHeader) + if err != nil { + return err + } + + var foundToRemove = make(map[string]bool) + + // Set the items to be removed as not found by default + for _, rr := range removeRights { + foundToRemove[rr.Name] = false + } + + // Search the current rights for items to delete + for _, cr := range currentRights { + for _, rr := range removeRights { + if cr.ID == rr.ID { + foundToRemove[cr.Name] = true + } + } + } + + for _, cr := range currentRights { + _, found := foundToRemove[cr.Name] + if !found { + input.Values = append(input.Values, types.OpenApiReference{Name: cr.Name, ID: cr.ID}) + } + } + + // Check that all the items to be removed were found in the current rights list + notFoundNames := "" + for name, found := range foundToRemove { + if !found { + if notFoundNames != "" { + notFoundNames += ", " + } + notFoundNames += `"` + name + `"` + } + } + + if notFoundNames != "" { + return fmt.Errorf("rights in %s %s not found for deletion: [%s]", roleType, name, notFoundNames) + } + + err = client.OpenApiPutItem(minimumApiVersion, urlRef, nil, &input, &pages, additionalHeader) + + if err != nil { + return fmt.Errorf("error updating rights in %s %s: %s", roleType, name, err) + } + + return nil +} + +// removeAllRightsFromRole removes all rights from the given role +func removeAllRightsFromRole(client *Client, roleType, name, id, endpoint string, additionalHeader map[string]string) error { + return updateRightsInRole(client, roleType, name, id, endpoint, []types.OpenApiReference{}, additionalHeader) +} + +// FindMissingImpliedRights returns a list of the rights that are implied in the rights provided as input +func FindMissingImpliedRights(client *Client, rights []types.OpenApiReference) ([]types.OpenApiReference, error) { + var ( + impliedRights []types.OpenApiReference + uniqueInputRights = make(map[string]types.OpenApiReference) + uniqueImpliedRights = make(map[string]types.OpenApiReference) + ) + + // Make a searchable collection of unique rights from the input + // This operation removes duplicates from the list + for _, right := range rights { + uniqueInputRights[right.Name] = right + } + + // Find the implied rights + for _, right := range rights { + fullRight, err := client.GetRightByName(right.Name) + if err != nil { + return nil, err + } + for _, ir := range fullRight.ImpliedRights { + _, seenAsInput := uniqueInputRights[ir.Name] + _, seenAsImplied := uniqueImpliedRights[ir.Name] + // If the right has already been added either as explicit ro as implied right, we skip it + if seenAsInput || seenAsImplied { + continue + } + // Add to the unique collection of implied rights + uniqueImpliedRights[ir.Name] = types.OpenApiReference{ + Name: ir.Name, + ID: ir.ID, + } + } + } + + // Create the output list from the implied rights collection + if len(uniqueImpliedRights) > 0 { + for _, right := range uniqueImpliedRights { + impliedRights = append(impliedRights, right) + } + } + + return impliedRights, nil +} diff --git a/govcd/roles_test.go b/govcd/roles_test.go index f363596e2..92a4ca570 100644 --- a/govcd/roles_test.go +++ b/govcd/roles_test.go @@ -1,4 +1,4 @@ -// +build functional openapi ALL +//go:build functional || openapi || role || ALL /* * Copyright 2020 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -9,17 +9,20 @@ package govcd import ( "net/url" - "github.com/vmware/go-vcloud-director/v2/types/v56" . "gopkg.in/check.v1" + + "github.com/vmware/go-vcloud-director/v2/types/v56" ) func (vcd *TestVCD) Test_Roles(check *C) { - adminOrg, err := vcd.client.GetAdminOrgByName(vcd.org.Org.Name) + + vcd.checkSkipWhenApiToken(check) + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) check.Assert(err, IsNil) check.Assert(adminOrg, NotNil) // Step 1 - Get all roles - allExistingRoles, err := adminOrg.GetAllOpenApiRoles(nil) + allExistingRoles, err := adminOrg.GetAllRoles(nil) check.Assert(err, IsNil) check.Assert(allExistingRoles, NotNil) @@ -30,12 +33,12 @@ func (vcd *TestVCD) Test_Roles(check *C) { queryParams := url.Values{} queryParams.Add("filter", "id=="+oneRole.Role.ID) - expectOneRoleResultById, err := adminOrg.GetAllOpenApiRoles(queryParams) + expectOneRoleResultById, err := adminOrg.GetAllRoles(queryParams) check.Assert(err, IsNil) check.Assert(len(expectOneRoleResultById) == 1, Equals, true) // Step 2.2 - retrieve specific role by using endpoint - exactItem, err := adminOrg.GetOpenApiRoleById(oneRole.Role.ID) + exactItem, err := adminOrg.GetRoleById(oneRole.Role.ID) check.Assert(err, IsNil) check.Assert(err, IsNil) @@ -46,36 +49,74 @@ func (vcd *TestVCD) Test_Roles(check *C) { } - // Step 3 - CreateRole a new role and ensure it is created as specified by doing deep comparison + // Step 3 - Create a new role and ensure it is created as specified by doing deep comparison newR := &types.Role{ Name: check.TestName(), Description: "Role created by test", // This BundleKey is being set by VCD even if it is not sent - BundleKey: "com.vmware.vcloud.undefined.key", + BundleKey: types.VcloudUndefinedKey, ReadOnly: false, } createdRole, err := adminOrg.CreateRole(newR) check.Assert(err, IsNil) + AddToCleanupListOpenApi(createdRole.Role.Name, check.TestName(), types.OpenApiPathVersion1_0_0+types.OpenApiEndpointRoles+createdRole.Role.ID) // Ensure supplied and created structs differ only by ID newR.ID = createdRole.Role.ID check.Assert(createdRole.Role, DeepEquals, newR) + // Check that the new role is found in the Organization structure + roleRef, err := adminOrg.GetRoleReference(createdRole.Role.Name) + check.Assert(err, IsNil) + check.Assert(roleRef, NotNil) + // Step 4 - updated created role createdRole.Role.Description = "Updated description" updatedRole, err := createdRole.Update() check.Assert(err, IsNil) check.Assert(updatedRole.Role, DeepEquals, createdRole.Role) - // Step 5 - delete created role + // Step 5 - add rights to role + + // These rights include 5 implied rights, which will be added by role.AddRights + rightNames := []string{"Catalog: Add vApp from My Cloud", "Catalog: Edit Properties"} + + rightSet, err := getRightsSet(adminOrg.client, rightNames) + check.Assert(err, IsNil) + + err = updatedRole.AddRights(rightSet) + check.Assert(err, IsNil) + + rights, err := updatedRole.GetRights(nil) + check.Assert(err, IsNil) + check.Assert(len(rights), Equals, len(rightSet)) + + // Step 6 - remove 1 right from role + + err = updatedRole.RemoveRights([]types.OpenApiReference{rightSet[0]}) + check.Assert(err, IsNil) + rights, err = updatedRole.GetRights(nil) + check.Assert(err, IsNil) + check.Assert(len(rights), Equals, len(rightSet)-1) + + // Step 7 - remove all rights from role + err = updatedRole.RemoveAllRights() + check.Assert(err, IsNil) + + rights, err = updatedRole.GetRights(nil) + check.Assert(err, IsNil) + check.Assert(len(rights), Equals, 0) + + // Step 8 - delete created role err = updatedRole.Delete() check.Assert(err, IsNil) - // Step 5 - try to read deleted role and expect error to contain 'ErrorEntityNotFound' + + // Step 9 - try to read deleted role and expect error to contain 'ErrorEntityNotFound' // Read is tricky - it throws an error ACCESS_TO_RESOURCE_IS_FORBIDDEN when the resource with ID does not // exist therefore one cannot know what kind of error occurred. - deletedRole, err := adminOrg.GetOpenApiRoleById(createdRole.Role.ID) + deletedRole, err := adminOrg.GetRoleById(createdRole.Role.ID) check.Assert(ContainsNotFound(err), Equals, true) check.Assert(deletedRole, IsNil) } diff --git a/govcd/saml_auth.go b/govcd/saml_auth.go index 2e5140c5b..09e513944 100644 --- a/govcd/saml_auth.go +++ b/govcd/saml_auth.go @@ -63,13 +63,13 @@ auth. // 5 - Authenticate to vCD using SIGN token in order to receive back regular // X-Vcloud-Authorization token // 6 - Set the received X-Vcloud-Authorization for further usage -func (vcdCli *VCDClient) authorizeSamlAdfs(user, pass, org, overrideRptId string) error { +func (vcdClient *VCDClient) authorizeSamlAdfs(user, pass, org, overrideRptId string) error { // Step 1 - find SAML entity ID configured in vCD metadata URL unless overrideRptId is provided // Example URL: url.Scheme + "://" + url.Host + "/cloud/org/" + org + "/saml/metadata/alias/vcd" samlEntityId := overrideRptId var err error if overrideRptId == "" { - samlEntityId, err = getSamlEntityId(vcdCli, org) + samlEntityId, err = getSamlEntityId(vcdClient, org) if err != nil { return fmt.Errorf("SAML - error getting vCD SAML Entity ID: %s", err) } @@ -78,13 +78,13 @@ func (vcdCli *VCDClient) authorizeSamlAdfs(user, pass, org, overrideRptId string // Step 2 - find ADFS server used for SAML by calling vCD SAML endpoint and hoping for a // redirect to ADFS server. Example URL: // url.Scheme + "://" + url.Host + "/login/my-org/saml/login/alias/vcd?service=tenant:" + org - adfsAuthEndPoint, err := getSamlAdfsServer(vcdCli, org) + adfsAuthEndPoint, err := getSamlAdfsServer(vcdClient, org) if err != nil { return fmt.Errorf("SAML - error getting IdP (ADFS): %s", err) } // Step 3 - authenticate to ADFS to receive SIGN token which can be used for vCD authentication - signToken, err := getSamlAuthToken(vcdCli, user, pass, samlEntityId, adfsAuthEndPoint, org) + signToken, err := getSamlAuthToken(vcdClient, user, pass, samlEntityId, adfsAuthEndPoint, org) if err != nil { return fmt.Errorf("SAML - could not get auth token from IdP (ADFS). Did you specify "+ "username in ADFS format ('user@contoso.com' or 'contoso.com\\user')? : %s", err) @@ -99,13 +99,13 @@ func (vcdCli *VCDClient) authorizeSamlAdfs(user, pass, org, overrideRptId string adfsAuthEndPoint, samlEntityId) // Step 5 - authenticate to vCD with SIGN token and receive vCD regular token in exchange - accessToken, err := authorizeSignToken(vcdCli, base64GzippedSignToken, org) + accessToken, err := authorizeSignToken(vcdClient, base64GzippedSignToken, org) if err != nil { return fmt.Errorf("SAML - error submitting SIGN token to vCD: %s", err) } // Step 6 - set regular vCD auth token X-Vcloud-Authorization - err = vcdCli.SetToken(org, AuthorizationHeader, accessToken) + err = vcdClient.SetToken(org, AuthorizationHeader, accessToken) if err != nil { return fmt.Errorf("error during token-based authentication: %s", err) } diff --git a/govcd/saml_auth_test.go b/govcd/saml_auth_test.go index 8e80a75f2..5f1a948e8 100644 --- a/govcd/saml_auth_test.go +++ b/govcd/saml_auth_test.go @@ -1,4 +1,4 @@ -// +build auth functional ALL +//go:build auth || functional || ALL /* * Copyright 2020 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -11,7 +11,9 @@ import ( ) // Test_SamlAdfsAuth checks if SAML ADFS login works using WS-TRUST endpoint -// "/adfs/services/trust/13/usernamemixed". +// +// "/adfs/services/trust/13/usernamemixed". +// // Credential variables must be specified in test configuration for it to work // The steps of this test are: // * Query object using test framework vCD connection @@ -26,6 +28,7 @@ func (vcd *TestVCD) Test_SamlAdfsAuth(check *C) { if cfg.Provider.SamlUser == "" || cfg.Provider.SamlPassword == "" || cfg.VCD.Org == "" { check.Skip("Skipping test because no Org, SamlUser, SamlPassword and was specified") } + vcd.checkSkipWhenApiToken(check) // Get vDC details using existing vCD client org, err := vcd.client.GetOrgByName(cfg.VCD.Org) diff --git a/govcd/saml_auth_unit_test.go b/govcd/saml_auth_unit_test.go index 891bfbc6c..077a0b0fe 100644 --- a/govcd/saml_auth_unit_test.go +++ b/govcd/saml_auth_unit_test.go @@ -1,4 +1,4 @@ -// +build unit ALL +//go:build unit || ALL /* * Copyright 2020 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -7,7 +7,7 @@ package govcd import ( - "io/ioutil" + "io" "log" "net/http" "net/http/httptest" @@ -19,6 +19,7 @@ import ( // testVcdMockAuthToken is the expected vcdCli.Client.VCDToken value after `Authentication()` // function passes mock SAML authentication process +// #nosec G101 -- These credentials are fake for testing purposes const testVcdMockAuthToken = "e3b02b30b8ff4e87ac38db785b0172b5" // samlMockServer struct allows to attach HTTP handlers to use additional variables (like @@ -173,7 +174,7 @@ func (mockServer *samlMockServer) adfsSamlAuthHandler(w http.ResponseWriter, r * } // Replace known dynamic strings to 'REPLACED' string - gotBody, _ := ioutil.ReadAll(r.Body) + gotBody, _ := io.ReadAll(r.Body) gotBodyString := string(gotBody) re := regexp.MustCompile(`().*()`) gotBodyString = re.ReplaceAllString(gotBodyString, `${1}REPLACED${2}`) diff --git a/govcd/sample_govcd_test_config.yaml b/govcd/sample_govcd_test_config.yaml index ca31347d0..f4a99e976 100644 --- a/govcd/sample_govcd_test_config.yaml +++ b/govcd/sample_govcd_test_config.yaml @@ -33,13 +33,29 @@ provider: # The organization you are authenticating with sysOrg: System # (Optional) MaxRetryTimeout specifies a time limit (in seconds) for retrying requests made by the SDK - # where vCloud director may take time to respond and retry mechanism is needed. + # where VMware Cloud Director may take time to respond and retry mechanism is needed. # This must be >0 to avoid instant timeout errors. If omitted - default value is set. # maxRetryTimeout: 60 # # (Optional) httpTimeout specifies a time limit (in seconds) for waiting http response. # If omitted - default value is set. # httpTimeout: 600 +# 'tenants' is an array of org users with their relative organization +# This structure makes it easier to run go-vcloud-director tests as org-user +# with the options '-vcd-test-org-user' (bool) and '-vcd-connect-tenant' (int) +tenants: + # the first user is the one that will be picked by default when -vcd-test-org-user is set + - user: user-first-org + password: password-first-user + sysOrg: first-org + token: optional-token + api_token: optional-api-token + # with -vcd-connect-tenant=1 the second user will be picked for connection + - user: user-second-org + password: password-second-org + sysOrg: second-org + token: optional-token + api_token: optional-api-token vcd: # Name of the organization (mandatory) org: myorg @@ -57,7 +73,11 @@ vcd: nsxt_provider_vdc: name: nsxTPvdc1 storage_profile: "*" + storage_profile_2: "Development2" network_pool: "NSX-T Overlay 1" + # A VM Group that needs to exist in the backing vSphere. This VM Group can be used + # to create VM Placement Policies. + placementPolicyVmGroup: testVmGroup nsxt: # NSX-T manager name to be used as defined in VCD manager: nsxManager1 @@ -65,20 +85,54 @@ vcd: tier0router: tier-0-router # NSX-T tier-0 VRF router used for external network tests tier0routerVrf: tier-0-router-vrf + # Gateway QoS Profile used for NSX-T Edge Gateway Rate Limiting (defined in NSX-T Manager) + gatewayQosProfile: Gateway QoS Profile 1 # Existing External Network with correct configuration 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 nsxtImportSegment: vcd-org-vdc-imported-network-backing + # Existing NSX-T segment to test Edge Gateway Uplinks + nsxtImportSegment2: vcd-org-vdc-imported-network-backing2 + # Existing NSX-T Edge Cluster name + nsxtEdgeCluster: existing-nsxt-edge-cluster + # AVI Controller URL + nsxtAlbControllerUrl: https://unknown-hostname.com + # AVI Controller username + nsxtAlbControllerUser: admin + # AVI Controller password + nsxtAlbControllerPassword: CHANGE-ME + # AVI Controller importable Cloud name + nsxtAlbImportableCloud: NSXT AVI Cloud + # Service Engine Group name within (Should be configured in Active Standby mode) + nsxtAlbServiceEngineGroup: active-standby-service-engine-group + # IP Discovery profile defined in NSX-T Manager + ipDiscoveryProfile: "ip-discovery-profile" + # MAC Discovery profile defined in NSX-T Manager + macDiscoveryProfile: "mac-discovery-profile" + # Spoof Guard profile defined in NSX-T Manager + spoofGuardProfile: "spoof-guard-profile" + # QoS profile defined in NSX-T Manager + qosProfile: "qos-profile" + # Segment Security profile defined in NSX-T Manager + segmentSecurityProfile: "segment-security-profile" # An Org catalog, possibly containing at least one item catalog: name: mycat + nsxtBackedCatalogName: my-nsxt-catalog # One item in the catalog. It will be used to compose test vApps. Some tests rely on it # being Photon OS. If it is not Photon OS - some tests will be skipped catalogItem: myitem + # One item in the NSX-T catalog. It will be used to compose test vApps. Some tests rely on it + # being Photon OS. If it is not Photon OS - some tests will be skipped + nsxtCatalogItem: my-nsxt-item + # Item in the NSX-T catalog that has a newer hardware version and supports EFI boot. + catalogItemWithEfiSupport: my-cat-item-with-efi-support # # An optional description for the catalog. Its test will be skipped if omitted. # If provided, it must be the current description of the catalog @@ -91,6 +145,8 @@ vcd: catalogItemWithMultiVms: my item with multi VMs # Name of VM in `catalogItemWithMultiVms` template or in `ovaMultiVmPath` if `catalogItemWithMultiVms` isn't provided. Default vmName `thirdVM` in default OVA. vmNameInMultiVmItem: thirdVM + # DSE Solution Add-On catalog media name within 'nsxtBackedCatalogName' + nsxtCatalogAddonDse: vmware-vcd-ds-1.4.0-23376809.iso # Existing VDC networks. At least one is needed. network: # First vdc network (mandatory) @@ -132,16 +188,20 @@ vcd: # A vSphere server name for creating an external network vimServer: vc9 # - # Independent disk parameters for testing - disk: - # - # Disk size (bytes) for create disk, skip disk tests if it is less than or equal to 0 - size: 1048576 - # - # Disk size (bytes) for update disk, skip some disk tests if it is less than or equal to 0 - # The minimum size is 1048576 (1 MB). While theoretically smaller amounts are allowed, - # there is an issue when using < 1MB during updates - sizeForUpdate: 1048576 + # IP of a pre-configured LDAP server + # using Docker image https://github.com/rroemhild/docker-test-openldap + ldap_server: 10.10.10.99 + # + # Details of pre-configured OIDC server + oidcServer: + # Server URL + url: "10.10.10.100/oidc-server" + # Well-known endpoint + wellKnownEndpoint: "/.well-known/openid-configuration" +vsphere: + # resource pools needed to create new provider VDCs + resourcePoolForVcd1: resource-pool-for-vcd-01 + resourcePoolForVcd2: resource-pool-for-vcd-02 logging: # All items in this section are optional # Logging is disabled by default. @@ -187,11 +247,53 @@ ova: # # The ovf for uploading catalog item for tests. ovfPath: ../test-resources/test_vapp_template_ovf/descriptor.ovf + # + # The OVF URL for uploading catalog item for tests. + ovfUrl: https://raw.githubusercontent.com/vmware/go-vcloud-director/main/test-resources/test_vapp_template_ovf/descriptor.ovf media: # The iso for uploading media item for tests. # Default paths are simple iso provided by project # Empty values skips the tests # Absolute or relative path mediaPath: ../test-resources/test.iso - # Existing media in test system + # Existing media in NSX-V backed VDC mediaName: uploadedMediaName + # Existing media in NSX-T backed VDC + nsxtBackedMediaName: nsxtMediaName + # A valid UI Plugin to use in tests + uiPluginPath: ../test-resources/ui_plugin.zip +cse: + # The CSE version installed in VCD + version: "4.2.0" + # The organization where Container Service Extension (CSE) Server is running + solutionsOrg: "solutions_org" + # The organization where the Kubernetes clusters are created + tenantOrg: "tenant_org" + # The VDC where the Kubernetes clusters are created + tenantVdc: "tenant_vdc" + # The network which the Kubernetes clusters use + routedNetwork: "tenant_net_routed" + # The edge gateway which the Kubernetes clusters use + edgeGateway: "tenant_edgegateway" + # The storage profile which the Kubernetes clusters use + storageProfile: "*" + # The catalog which the Kubernetes clusters use + ovaCatalog: "tkgm_catalog" + # The TKGm OVA which the Kubernetes clusters use + ovaName: "ubuntu-2004-kube-v1.25.7+vmware.2-tkg.1-8a74b9f12e488c54605b3537acb683bc" +# Config branch to test Solution Add-Ons (requires CSE to succesfully publish Add-On Instances) +solutionAddOn: + # Org for Landing Zone configuration and Add-On deployment + org: "solutions_org" + # VDC for Landing Zone configuration and Add-On deployment + vdc: "solutions_vdc" + # Routed Network for Landing Zone configuration + routedNetwork: "solutions_routed_network" + # Compute Policy for Landing Zone configuration + computePolicy: "System Default" + # Storage Policy for Landing Zone configuration + storagePolicy: "*" + # Catalog Landing Zone configuration (that hosts Add-On images) + catalog: "cse_catalog" + # An existing Add-On image within catalog + addonImageDse: "vmware-vcd-ds-1.4.0-23376809.iso" diff --git a/govcd/security_tags.go b/govcd/security_tags.go new file mode 100644 index 000000000..c87b5f31e --- /dev/null +++ b/govcd/security_tags.go @@ -0,0 +1,175 @@ +package govcd + +import ( + "fmt" + "github.com/vmware/go-vcloud-director/v2/types/v56" + "net/url" +) + +// GetAllSecurityTaggedEntities Retrieves the list of entities that have at least one tag assigned to it. +// queryParameters allows users to pass filters: I.e: filter=(tag==Web;entityType==vm) +// This function works from API v36.0 (VCD 10.3.0+) +func (org *Org) GetAllSecurityTaggedEntities(queryParameters url.Values) ([]types.SecurityTaggedEntity, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointSecurityTags + apiVersion, err := org.client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := org.client.OpenApiBuildEndpoint(endpoint, "/entities") + if err != nil { + return nil, err + } + + tenantContext, err := org.getTenantContext() + if err != nil { + return nil, err + } + + var securityTaggedEntities []types.SecurityTaggedEntity + err = org.client.OpenApiGetAllItems(apiVersion, urlRef, queryParameters, &securityTaggedEntities, getTenantContextHeader(tenantContext)) + if err != nil { + return nil, err + } + + return securityTaggedEntities, nil +} + +// GetAllSecurityTaggedEntitiesByName wraps GetAllSecurityTaggedEntities and returns ErrorEntityNotFound if nothing was found +// This function works from API v36.0 (VCD 10.3.0+) +func (org *Org) GetAllSecurityTaggedEntitiesByName(securityTagName string) ([]types.SecurityTaggedEntity, error) { + queryParameters := copyOrNewUrlValues(nil) + queryParameters.Add("filter", "tag=="+securityTagName) + + securityTagEntities, err := org.GetAllSecurityTaggedEntities(queryParameters) + if err != nil { + return nil, err + } + + if len(securityTagEntities) == 0 { + return nil, ErrorEntityNotFound + } + + return securityTagEntities, nil +} + +// GetAllSecurityTagValues Retrieves the list of security tags that are in the organization and can be reused to tag an entity. +// The list of tags include tags assigned to entities within the organization. +// This function works from API v36.0 (VCD 10.3.0+) +func (org *Org) GetAllSecurityTagValues(queryParameters url.Values) ([]types.SecurityTagValue, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointSecurityTags + apiVersion, err := org.client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := org.client.OpenApiBuildEndpoint(endpoint, "/values") + if err != nil { + return nil, err + } + + tenantContext, err := org.getTenantContext() + if err != nil { + return nil, err + } + + var securityTaggedValues []types.SecurityTagValue + err = org.client.OpenApiGetAllItems(apiVersion, urlRef, queryParameters, &securityTaggedValues, getTenantContextHeader(tenantContext)) + if err != nil { + return nil, err + } + + return securityTaggedValues, nil +} + +// GetVMSecurityTags Retrieves the list of tags for a specific VM. If user has view right to the VM, user can view its tags. +// This function works from API v36.0 (VCD 10.3.0+) +func (vm *VM) GetVMSecurityTags() (*types.EntitySecurityTags, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointSecurityTags + apiVersion, err := vm.client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := vm.client.OpenApiBuildEndpoint(endpoint, fmt.Sprintf("/vm/%s", vm.VM.ID)) + if err != nil { + return nil, err + } + + var entitySecurityTags types.EntitySecurityTags + err = vm.client.OpenApiGetItem(apiVersion, urlRef, nil, &entitySecurityTags, nil) + if err != nil { + return nil, err + } + + return &entitySecurityTags, nil +} + +// UpdateSecurityTag updates the entities associated with a Security Tag. +// Only the list of tagged entities can be updated. The name cannot be updated. +// Any other existing entities not in the list will be untagged. +// This function works from API v36.0 (VCD 10.3.0+) +func (org *Org) UpdateSecurityTag(securityTag *types.SecurityTag) (*types.SecurityTag, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointSecurityTags + apiVersion, err := org.client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := org.client.OpenApiBuildEndpoint(endpoint, "/tag") + if err != nil { + return nil, err + } + + tenantContext, err := org.getTenantContext() + if err != nil { + return nil, err + } + + err = org.client.OpenApiPutItem(apiVersion, urlRef, nil, securityTag, nil, getTenantContextHeader(tenantContext)) + if err != nil { + return nil, err + } + + queryParameters := copyOrNewUrlValues(nil) + queryParameters.Add("filter", "tag=="+securityTag.Tag) + readEntities, err := org.GetAllSecurityTaggedEntities(queryParameters) + if err != nil { + return nil, err + } + + returnSecurityTags := &types.SecurityTag{ + Tag: securityTag.Tag, + Entities: make([]string, len(readEntities)), + } + + for i, entity := range readEntities { + returnSecurityTags.Entities[i] = entity.ID + } + + return returnSecurityTags, nil +} + +// UpdateVMSecurityTags updates the list of tags for a specific VM. An empty list of tags means to delete all tags +// for the VM. If user has edit permission on the VM, user can edit its tags. +// This function works from API v36.0 (VCD 10.3.0+) +func (vm *VM) UpdateVMSecurityTags(entitySecurityTags *types.EntitySecurityTags) (*types.EntitySecurityTags, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointSecurityTags + apiVersion, err := vm.client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := vm.client.OpenApiBuildEndpoint(endpoint, fmt.Sprintf("/vm/%s", vm.VM.ID)) + if err != nil { + return nil, err + } + + var serverEntitySecurityTags types.EntitySecurityTags + err = vm.client.OpenApiPutItem(apiVersion, urlRef, nil, entitySecurityTags, &serverEntitySecurityTags, nil) + if err != nil { + return nil, err + } + + return &serverEntitySecurityTags, nil +} diff --git a/govcd/security_tags_test.go b/govcd/security_tags_test.go new file mode 100644 index 000000000..253a1c734 --- /dev/null +++ b/govcd/security_tags_test.go @@ -0,0 +1,151 @@ +//go:build network || nsxt || functional || openapi || ALL + +package govcd + +import ( + "fmt" + "strings" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +func init() { + testingTags["vm"] = "security_tags_test.go" +} + +func (vcd *TestVCD) Test_SecurityTags(check *C) { + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointSecurityTags) + + securityTagName1 := strings.ToLower(fmt.Sprintf("%s_%d", check.TestName(), 1)) // Security tags are always lowercase in server-side + securityTagName2 := strings.ToLower(fmt.Sprintf("%s_%d", check.TestName(), 2)) + nonExistingSecurityTag := "icompletelymadeupthistag1234" + + // Get testing Org + testingOrg, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(testingOrg, NotNil) + + // Get testing VM + testingVM, _ := vcd.findFirstVm(*vcd.vapp) + vm := &VM{ + VM: &testingVM, + client: vcd.org.client, + } + + // Create a security tag using UpdateSecurityTag + sentSecurityTag := &types.SecurityTag{ + Tag: securityTagName1, + Entities: []string{testingVM.ID}, + } + + receivedSecurityTag, err := testingOrg.UpdateSecurityTag(sentSecurityTag) + check.Assert(err, IsNil) + check.Assert(sentSecurityTag, DeepEquals, receivedSecurityTag) + + // Check that the security tag exist using Org.GetAllSecurityTaggedEntitiesByName + securityTagEntities, err := testingOrg.GetAllSecurityTaggedEntitiesByName(securityTagName1) + check.Assert(err, IsNil) + check.Assert(len(securityTagEntities) > 0, Equals, true) + + // Check that ErrorEntityNotFound is returned if no entities where found with Org.GetAllSecurityTaggedEntitiesByName + securityTagEntities, err = testingOrg.GetAllSecurityTaggedEntitiesByName(nonExistingSecurityTag) + check.Assert(err, NotNil) + check.Assert(err, Equals, ErrorEntityNotFound) + check.Assert(securityTagEntities, IsNil) + + // Create a security tag using UpdateVMSecurityTags + inputEntitySecurityTags := &types.EntitySecurityTags{ + Tags: []string{ + securityTagName1, + securityTagName2, + }, + } + outputEntitySecurityTags, err := vm.UpdateVMSecurityTags(inputEntitySecurityTags) + check.Assert(err, IsNil) + check.Assert(outputEntitySecurityTags, NotNil) + check.Assert(outputEntitySecurityTags, DeepEquals, inputEntitySecurityTags) + + // Check that the VM with security tags is retrieved using GetSecurityTaggedEntities + securityTaggedEntities, err := testingOrg.GetAllSecurityTaggedEntities(nil) + check.Assert(err, IsNil) + check.Assert(len(securityTaggedEntities) > 0, Equals, true) + + var securityTaggedEntity types.SecurityTaggedEntity + for _, v := range securityTaggedEntities { + if v.ID == testingVM.ID { + securityTaggedEntity = v + break + } + } + + check.Assert(securityTaggedEntity, NotNil) + + // Check that security tags added before exist (As sysadm) + securityTagValues, err := testingOrg.GetAllSecurityTagValues(nil) + check.Assert(err, IsNil) + check.Assert(len(securityTagValues) > 0, Equals, true) + check.Assert(checkIfSecurityTagsExist(securityTagValues, securityTagName1, securityTagName2), Equals, true) + + // Check that security tags added before exist (As org adm) + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + + userName := strings.ToLower(check.TestName()) + fmt.Printf("# Running Get Security Tag Values test as Org Admin user '%s'\n", userName) + orgUserVcdClient, _, err := newOrgUserConnection(adminOrg, userName, "CHANGE-ME", vcd.config.Provider.Url, true) + check.Assert(err, IsNil) + check.Assert(orgUserVcdClient, NotNil) + + orgUserOrg, err := orgUserVcdClient.GetOrgById(adminOrg.AdminOrg.ID) + check.Assert(err, IsNil) + + securityTagValues, err = orgUserOrg.GetAllSecurityTagValues(nil) + check.Assert(err, IsNil) + check.Assert(len(securityTagValues) > 0, Equals, true) + check.Assert(checkIfSecurityTagsExist(securityTagValues, securityTagName1, securityTagName2), Equals, true) + + // Get security tags by VM + entitySecurityTags, err := vm.GetVMSecurityTags() + check.Assert(err, IsNil) + check.Assert(securityTagValues, NotNil) + check.Assert(contains(securityTagName1, entitySecurityTags.Tags), Equals, true) + check.Assert(contains(securityTagName2, entitySecurityTags.Tags), Equals, true) + + // Remove tags + sentSecurityTag = &types.SecurityTag{ + Tag: securityTagName1, + Entities: []string{}, + } + + receivedSecurityTag, err = testingOrg.UpdateSecurityTag(sentSecurityTag) + check.Assert(err, IsNil) + check.Assert(receivedSecurityTag.Tag, Equals, sentSecurityTag.Tag) + check.Assert(len(receivedSecurityTag.Entities), Equals, 0) + + sentSecurityTag2 := &types.SecurityTag{ + Tag: securityTagName2, + Entities: []string{}, + } + + receivedSecurityTag2, err := testingOrg.UpdateSecurityTag(sentSecurityTag2) + check.Assert(err, IsNil) + check.Assert(receivedSecurityTag2.Tag, Equals, sentSecurityTag2.Tag) + check.Assert(len(receivedSecurityTag2.Entities), Equals, 0) +} + +func checkIfSecurityTagsExist(securityTagValues []types.SecurityTagValue, securityTagName ...string) bool { + var numberFound int + for _, v := range securityTagName { + for _, tag := range securityTagValues { + if tag.Tag == v { + numberFound++ + break + } + } + } + + return numberFound == len(securityTagName) +} diff --git a/govcd/service_account.go b/govcd/service_account.go new file mode 100644 index 000000000..05173507b --- /dev/null +++ b/govcd/service_account.go @@ -0,0 +1,343 @@ +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ +package govcd + +import ( + "fmt" + "net/url" + "strings" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +type ServiceAccount struct { + ServiceAccount *types.ServiceAccount + authParams *types.ServiceAccountAuthParams + org *Org +} + +// GetServiceAccountById gets a Service Account by its ID +func (org *Org) GetServiceAccountById(serviceAccountId string) (*ServiceAccount, error) { + client := org.client + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointServiceAccounts + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, serviceAccountId) + if err != nil { + return nil, err + } + + newServiceAccount := &ServiceAccount{ + ServiceAccount: &types.ServiceAccount{}, + org: org, + } + + err = client.OpenApiGetItem(apiVersion, urlRef, nil, newServiceAccount.ServiceAccount, nil) + if err != nil { + return nil, err + } + + return newServiceAccount, nil +} + +// GetAllServiceAccounts gets all service accounts with the specified query parameters +func (org *Org) GetAllServiceAccounts(queryParams url.Values) ([]*ServiceAccount, error) { + client := org.client + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointServiceAccounts + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + tenantContext, err := org.getTenantContext() + if err != nil { + return nil, err + } + + // VCD has a pageSize limit on this specific endpoint + queryParams.Add("pageSize", "32") + typeResponses := []*types.ServiceAccount{{}} + err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParams, &typeResponses, getTenantContextHeader(tenantContext)) + + if err != nil { + return nil, fmt.Errorf("failed to get service accounts: %s", err) + } + + results := make([]*ServiceAccount, len(typeResponses)) + for sliceIndex := range typeResponses { + results[sliceIndex] = &ServiceAccount{ + ServiceAccount: typeResponses[sliceIndex], + org: org, + } + } + + return results, nil +} + +// GetServiceAccountByName gets a service account by its name +func (org *Org) GetServiceAccountByName(name string) (*ServiceAccount, error) { + queryParams := url.Values{} + queryParams.Add("filter", fmt.Sprintf("name==%s", name)) + + serviceAccounts, err := org.GetAllServiceAccounts(queryParams) + if err != nil { + return nil, fmt.Errorf("error getting service account by name: %s", err) + } + + serviceAccount, err := oneOrError("name", name, serviceAccounts) + if err != nil { + return nil, err + } + + return serviceAccount, nil +} + +// CreateServiceAccount creates a Service Account and sets it in `Created` status +func (vcdClient *VCDClient) CreateServiceAccount(orgName, name, scope, softwareId, softwareVersion, clientUri string) (*ServiceAccount, error) { + saParams := &types.ApiTokenParams{ + ClientName: name, + Scope: scope, + SoftwareID: softwareId, + SoftwareVersion: softwareVersion, + ClientURI: clientUri, + } + + newSaParams, err := vcdClient.RegisterToken(orgName, saParams) + if err != nil { + return nil, fmt.Errorf("failed to register Service account: %s", err) + } + + org, err := vcdClient.GetOrgByName(orgName) + if err != nil { + return nil, fmt.Errorf("failed to get Org by name: %s", err) + } + + serviceAccountID := "urn:vcloud:serviceAccount:" + newSaParams.ClientID + serviceAccount, err := org.GetServiceAccountById(serviceAccountID) + if err != nil { + return nil, fmt.Errorf("failed to get Service account by ID: %s", err) + } + + return serviceAccount, nil +} + +// Update updates the modifiable fields of a Service Account +func (sa *ServiceAccount) Update(saConfig *types.ServiceAccount) (*ServiceAccount, error) { + client := sa.org.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointServiceAccounts + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + saConfig.ID = sa.ServiceAccount.ID + saConfig.Name = sa.ServiceAccount.Name + saConfig.Status = sa.ServiceAccount.Status + urlRef, err := client.OpenApiBuildEndpoint(endpoint, saConfig.ID) + if err != nil { + return nil, err + } + + returnServiceAccount := &ServiceAccount{ + ServiceAccount: &types.ServiceAccount{}, + org: sa.org, + } + + err = client.OpenApiPutItem(apiVersion, urlRef, nil, saConfig, returnServiceAccount.ServiceAccount, nil) + if err != nil { + return nil, fmt.Errorf("error updating Service Account: %s", err) + } + + return returnServiceAccount, nil +} + +// Authorize authorizes a service account and returns a DeviceID and UserCode which will be used while granting +// the request, and sets the Service Account in `Requested` status +func (sa *ServiceAccount) Authorize() error { + client := sa.org.client + + uuid := extractUuid(sa.ServiceAccount.ID) + data := map[string]string{ + "client_id": uuid, + } + + userDef := "tenant/" + sa.org.orgName() + if strings.EqualFold(sa.org.orgName(), "system") { + userDef = "provider" + } + + endpoint := fmt.Sprintf("%s://%s/oauth/%s/device_authorization", client.VCDHREF.Scheme, client.VCDHREF.Host, userDef) + urlRef, err := url.ParseRequestURI(endpoint) + if err != nil { + return fmt.Errorf("error getting request url from %s: %s", urlRef.String(), err) + } + + // Not an OpenAPI endpoint so hardcoding the Service Account minimal version + err = client.OpenApiPostUrlEncoded("37.0", urlRef, nil, data, &sa.authParams, nil) + if err != nil { + return fmt.Errorf("error authorizing service account: %s", err) + } + + return nil +} + +// Grant Grants access to the Service Account and sets it in `Granted` status +func (sa *ServiceAccount) Grant() error { + if sa.authParams == nil { + return fmt.Errorf("error: userCode is unset, service account needs to be authorized") + } + + client := sa.org.client + // This is the only place where this field is used, so a local struct is created + type serviceAccountGrant struct { + UserCode string `json:"userCode"` + } + + userCode := &serviceAccountGrant{ + UserCode: sa.authParams.UserCode, + } + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointServiceAccountGrant + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return fmt.Errorf("error granting service account: %s", err) + } + + tenantContext, err := sa.org.getTenantContext() + if err != nil { + return fmt.Errorf("error granting service account: %s", err) + } + + err = client.OpenApiPostItem(apiVersion, urlRef, nil, userCode, nil, getTenantContextHeader(tenantContext)) + if err != nil { + return fmt.Errorf("error granting service account: %s", err) + } + + return nil +} + +// GetInitialApiToken gets the initial API token for the Service Account and sets it in `Active` status +func (sa *ServiceAccount) GetInitialApiToken() (*types.ApiTokenRefresh, error) { + if sa.authParams == nil { + return nil, fmt.Errorf("error: service account must be authorized and granted") + } + client := sa.org.client + uuid := extractUuid(sa.ServiceAccount.ID) + data := map[string]string{ + "grant_type": "urn:ietf:params:oauth:grant-type:device_code", + "client_id": uuid, + "device_code": sa.authParams.DeviceCode, + } + token, err := client.getAccessToken(sa.ServiceAccount.Org.Name, "CreateServiceAccount", data) + if err != nil { + return nil, fmt.Errorf("error getting initial api token: %s", err) + } + return token, nil +} + +// Refresh updates the Service Account object +func (sa *ServiceAccount) Refresh() error { + if sa.ServiceAccount == nil || sa.org.client == nil || sa.ServiceAccount.ID == "" { + return fmt.Errorf("cannot refresh Service Account without ID") + } + + updatedServiceAccount, err := sa.org.GetServiceAccountById(sa.ServiceAccount.ID) + if err != nil { + return err + } + sa.ServiceAccount = updatedServiceAccount.ServiceAccount + + return nil +} + +// Revoke revokes the service account and its' API token and puts it back in 'Created' stage +func (sa *ServiceAccount) Revoke() error { + client := sa.org.client + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointServiceAccounts + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, sa.ServiceAccount.ID, "/revoke") + if err != nil { + return err + } + + err = client.OpenApiPostItem(apiVersion, urlRef, nil, nil, nil, nil) + if err != nil { + return err + } + + return nil +} + +// Delete deletes a Service Account +func (sa *ServiceAccount) Delete() error { + client := sa.org.client + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointServiceAccounts + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, sa.ServiceAccount.ID) + if err != nil { + return err + } + + err = client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) + if err != nil { + return err + } + + return nil +} + +// SetServiceAccountApiToken gets the refresh token from a provided file, fetches +// the bearer token and updates the provided file with the new refresh token for +// next usage as service account API tokens are one-time use +func (vcdClient *VCDClient) SetServiceAccountApiToken(org, apiTokenFile string) error { + if vcdClient.Client.APIVCDMaxVersionIs("< 37.0") { + version, err := vcdClient.Client.GetVcdFullVersion() + if err == nil { + return fmt.Errorf("minimum version for Service Account authentication is 10.4 - Version detected: %s", version.Version) + } + // If we can't get the VCD version, we return API version info + return fmt.Errorf("minimum API version for Service Account authentication is 37.0 - Version detected: %s", vcdClient.Client.APIVersion) + } + + apiToken, err := vcdClient.SetApiTokenFromFile(org, apiTokenFile) + if err != nil { + return err + } + + err = SaveServiceAccountToFile(apiTokenFile, vcdClient.Client.UserAgent, apiToken) + if err != nil { + return fmt.Errorf("failed to save service account token to %s: %s", apiTokenFile, err) + } + return nil +} + +// SaveServiceAccountToFile saves the API token of the Service Account to a file +func SaveServiceAccountToFile(filename, useragent string, saToken *types.ApiTokenRefresh) error { + return saveTokenToFile(filename, "Service Account", useragent, saToken) +} diff --git a/govcd/service_account_test.go b/govcd/service_account_test.go new file mode 100644 index 000000000..8c7568490 --- /dev/null +++ b/govcd/service_account_test.go @@ -0,0 +1,144 @@ +//go:build api || functional || ALL + +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_ServiceAccount(check *C) { + isApiTokenEnabled, err := vcd.client.Client.VersionEqualOrGreater("10.4.0", 3) + check.Assert(err, IsNil) + if !isApiTokenEnabled { + check.Skip("This test requires VCD 10.4.0 or greater") + } + + serviceAccount, err := vcd.client.CreateServiceAccount( + vcd.config.VCD.Org, + check.TestName(), + "urn:vcloud:role:vApp%20Author", + "12345678-1234-1234-1234-1234567890ab", + "", + "", + ) + check.Assert(err, IsNil) + check.Assert(serviceAccount, NotNil) + check.Assert(serviceAccount.ServiceAccount.Status, Equals, "CREATED") + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointServiceAccounts + check.Assert(err, IsNil) + + AddToCleanupListOpenApi(check.TestName(), check.TestName(), endpoint+serviceAccount.ServiceAccount.ID) + + err = serviceAccount.Authorize() + check.Assert(err, IsNil) + + err = serviceAccount.Refresh() + check.Assert(err, IsNil) + check.Assert(serviceAccount.ServiceAccount.Status, Equals, "REQUESTED") + + err = serviceAccount.Grant() + check.Assert(err, IsNil) + + err = serviceAccount.Refresh() + check.Assert(err, IsNil) + check.Assert(serviceAccount.ServiceAccount.Status, Equals, "GRANTED") + + _, err = serviceAccount.GetInitialApiToken() + check.Assert(err, IsNil) + + err = serviceAccount.Refresh() + check.Assert(err, IsNil) + check.Assert(serviceAccount.ServiceAccount.Status, Equals, "ACTIVE") + + err = serviceAccount.Revoke() + check.Assert(err, IsNil) + + err = serviceAccount.Refresh() + check.Assert(err, IsNil) + check.Assert(serviceAccount.ServiceAccount.Status, Equals, "CREATED") + + err = serviceAccount.Delete() + check.Assert(err, IsNil) + + org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(org, NotNil) + + notFound, err := org.GetServiceAccountById(serviceAccount.ServiceAccount.ID) + check.Assert(err, NotNil) + check.Assert(notFound, IsNil) +} + +func (vcd *TestVCD) Test_ServiceAccount_SysOrg(check *C) { + isApiTokenEnabled, err := vcd.client.Client.VersionEqualOrGreater("10.4.0", 3) + check.Assert(err, IsNil) + if !isApiTokenEnabled { + check.Skip("This test requires VCD 10.4.0 or greater") + } + + if !vcd.org.client.IsSysAdmin { + check.Skip("This test requires System Administrator role") + } + + serviceAccountSysOrg, err := vcd.client.CreateServiceAccount( + vcd.config.Provider.SysOrg, + check.TestName(), + "urn:vcloud:role:System%20Administrator", + "12345678-1234-1234-1234-1234567890ab", + "", + "", + ) + check.Assert(err, IsNil) + check.Assert(serviceAccountSysOrg, NotNil) + check.Assert(serviceAccountSysOrg.ServiceAccount.Status, Equals, "CREATED") + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointServiceAccounts + check.Assert(err, IsNil) + + AddToCleanupListOpenApi(check.TestName(), check.TestName(), endpoint+serviceAccountSysOrg.ServiceAccount.ID) + + err = serviceAccountSysOrg.Authorize() + check.Assert(err, IsNil) + + err = serviceAccountSysOrg.Refresh() + check.Assert(err, IsNil) + check.Assert(serviceAccountSysOrg.ServiceAccount.Status, Equals, "REQUESTED") + + err = serviceAccountSysOrg.Grant() + check.Assert(err, IsNil) + + err = serviceAccountSysOrg.Refresh() + check.Assert(err, IsNil) + check.Assert(serviceAccountSysOrg.ServiceAccount.Status, Equals, "GRANTED") + + _, err = serviceAccountSysOrg.GetInitialApiToken() + check.Assert(err, IsNil) + + err = serviceAccountSysOrg.Refresh() + check.Assert(err, IsNil) + check.Assert(serviceAccountSysOrg.ServiceAccount.Status, Equals, "ACTIVE") + + err = serviceAccountSysOrg.Revoke() + check.Assert(err, IsNil) + + err = serviceAccountSysOrg.Refresh() + check.Assert(err, IsNil) + check.Assert(serviceAccountSysOrg.ServiceAccount.Status, Equals, "CREATED") + + err = serviceAccountSysOrg.Delete() + check.Assert(err, IsNil) + + sysorg, err := vcd.client.GetOrgByName(vcd.config.Provider.SysOrg) + check.Assert(err, IsNil) + check.Assert(sysorg, NotNil) + + notFound, err := sysorg.GetServiceAccountById(serviceAccountSysOrg.ServiceAccount.ID) + check.Assert(err, NotNil) + check.Assert(notFound, IsNil) +} diff --git a/govcd/session_info.go b/govcd/session_info.go new file mode 100644 index 000000000..9eebb2c97 --- /dev/null +++ b/govcd/session_info.go @@ -0,0 +1,129 @@ +package govcd + +/* + * Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + "github.com/vmware/go-vcloud-director/v2/util" +) + +// ExtendedSessionInfo collects data regarding a VCD connection +type ExtendedSessionInfo struct { + User string + Org string + Roles []string + Rights []string + Version string + ConnectionType string +} + +// GetSessionInfo collects the basic session information for a VCD connection +func (client *Client) GetSessionInfo() (*types.CurrentSessionInfo, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointSessionCurrent + + // We get the maximum supported version, as early versions of the API return less data + apiVersion, err := client.MaxSupportedVersion() + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + var info types.CurrentSessionInfo + + err = client.OpenApiGetItem(apiVersion, urlRef, nil, &info, nil) + if err != nil { + return nil, err + } + + return &info, nil +} + +// GetExtendedSessionInfo collects extended session information for support and debugging +// It will try to collect as much data as possible, failing only if the minimum data can't +// be collected. +func (vcdClient *VCDClient) GetExtendedSessionInfo() (*ExtendedSessionInfo, error) { + var extendedSessionInfo ExtendedSessionInfo + sessionInfo, err := vcdClient.Client.GetSessionInfo() + if err != nil { + return nil, err + } + switch { + case vcdClient.Client.UsingBearerToken: + extendedSessionInfo.ConnectionType = "Bearer token" + case vcdClient.Client.UsingAccessToken: + extendedSessionInfo.ConnectionType = "API Access token" + default: + extendedSessionInfo.ConnectionType = "Username + password" + } + version, err := vcdClient.Client.GetVcdFullVersion() + if err == nil { + extendedSessionInfo.Version = version.Version.String() + } + if sessionInfo.User.Name == "" { + return nil, fmt.Errorf("no user reference found") + } + extendedSessionInfo.User = sessionInfo.User.Name + + if sessionInfo.Org.Name == "" { + return nil, fmt.Errorf("no Org reference found") + } + extendedSessionInfo.Org = sessionInfo.Org.Name + + if len(sessionInfo.Roles) == 0 { + return &extendedSessionInfo, nil + } + extendedSessionInfo.Roles = append(extendedSessionInfo.Roles, sessionInfo.Roles...) + org, err := vcdClient.GetAdminOrgById(sessionInfo.Org.ID) + if err != nil { + return &extendedSessionInfo, err + } + for _, roleRef := range sessionInfo.RoleRefs { + role, err := org.GetRoleById(roleRef.ID) + if err != nil { + continue + } + rights, err := role.GetRights(nil) + if err != nil { + continue + } + for _, right := range rights { + extendedSessionInfo.Rights = append(extendedSessionInfo.Rights, right.Name) + } + } + return &extendedSessionInfo, nil +} + +// LogSessionInfo prints session information into the default logs +func (client *VCDClient) LogSessionInfo() { + + // If logging is disabled, there is no point in collecting session info + if util.EnableLogging { + info, err := client.GetExtendedSessionInfo() + if err != nil { + util.Logger.Printf("no session info collected: %s\n", err) + return + } + text, err := json.MarshalIndent(info, " ", " ") + if err != nil { + util.Logger.Printf("error formatting session info %s\n", err) + return + } + util.Logger.Println(strings.Repeat("*", 80)) + util.Logger.Println("START SESSION INFO") + util.Logger.Println(strings.Repeat("*", 80)) + util.Logger.Printf("%s\n", text) + util.Logger.Println(strings.Repeat("*", 80)) + util.Logger.Println("END SESSION INFO") + util.Logger.Println(strings.Repeat("*", 80)) + } +} diff --git a/govcd/session_info_test.go b/govcd/session_info_test.go new file mode 100644 index 000000000..6d1c2d976 --- /dev/null +++ b/govcd/session_info_test.go @@ -0,0 +1,57 @@ +//go:build api || functional || ALL + +/* + * Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "encoding/json" + "fmt" + + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_GetSessionInfo(check *C) { + info, err := vcd.client.Client.GetSessionInfo() + check.Assert(err, IsNil) + check.Assert(info, NotNil) + + if testVerbose { + var data []byte + data, err = json.MarshalIndent(info, " ", " ") + check.Assert(err, IsNil) + fmt.Printf("%s\n", data) + org, err := vcd.client.GetAdminOrgById(info.Org.ID) + check.Assert(err, IsNil) + for _, roleRef := range info.RoleRefs { + role, err := org.GetRoleById(roleRef.ID) + check.Assert(err, IsNil) + fmt.Printf("%s\n", role.Role.Name) + rights, err := role.GetRights(nil) + check.Assert(err, IsNil) + for i, right := range rights { + fmt.Printf("\t%3d %s\n", i, right.Name) + } + } + } + check.Assert(info.Org, NotNil) + check.Assert(info.Org.Name, Not(Equals), "") + if vcd.client.Client.IsSysAdmin { + check.Assert(info.Org.Name, Equals, "System") + } + check.Assert(info.User, Not(Equals), "") + check.Assert(len(info.Roles), Not(Equals), 0) +} + +func (vcd *TestVCD) Test_GetExtendedSessionInfo(check *C) { + info, err := vcd.client.GetExtendedSessionInfo() + check.Assert(err, IsNil) + check.Assert(info, NotNil) + if testVerbose { + text, err := json.MarshalIndent(info, " ", " ") + check.Assert(err, IsNil) + fmt.Printf("%s\n", text) + } +} diff --git a/govcd/solution_add_on.go b/govcd/solution_add_on.go new file mode 100644 index 000000000..891418682 --- /dev/null +++ b/govcd/solution_add_on.go @@ -0,0 +1,491 @@ +/* +* Copyright 2024 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "net/url" + "os" + "path" + "path/filepath" + "regexp" + "time" + + "github.com/vmware/go-vcloud-director/v2/govcd/internal/udf" + "github.com/vmware/go-vcloud-director/v2/types/v56" + "sigs.k8s.io/yaml" +) + +var slzAddOnRdeType = [3]string{"vmware", "solutions_add_on", "1.0.0"} + +// SolutionAddOn is the main structure to handle Solution Add-Ons within Solution Landing Zone. It +// packs parent RDE and Solution Add-On entity itself +type SolutionAddOn struct { + SolutionAddOnEntity *types.SolutionAddOn + DefinedEntity *DefinedEntity + vcdClient *VCDClient +} + +// SolutionAddOnConfig defines configuration for Solution Add-On creation which is used for +// 'VCDClient.CreateSolutionAddOn'. +type SolutionAddOnConfig struct { + IsoFilePath string + User string + CatalogItemId string + AutoTrustCertificate bool +} + +const ( + manifestKey = "manifest" + iconKey = "icon" + eulaKey = "eula" + certificateKey = "certificate" + manifestVersion = "version" + manifestName = "name" + manifestVendor = "vendor" +) + +// wantedFiles are the files expected in solution Add-Ons +var wantedFiles = map[string]isoFileDef{ + eulaKey: {namePattern: `(?i)^eula\.txt$`}, + manifestKey: {namePattern: `^manifest\.yaml$`}, + iconKey: {namePattern: `^icon\.(png|jpg|jpeg|jfif|svg|bmp|ico)$`}, + certificateKey: {namePattern: `^certificate\.pem$`}, +} + +// isoFileDef describes a file wanted from the .ISO container +type isoFileDef struct { + namePattern string // name expression seeked + nameRegexp *regexp.Regexp // regular expression later used to check if the file name matches + foundFileName string // Name of the file matched by the regular expression + contents []byte // contents of the found file +} + +// iconTypes defines the type of the icon as retrieved from the .ISO file +var iconTypes = map[string]string{ + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".jfif": "image/jpeg", + ".svg": "image/svg+xml", + ".bmp": "image/bmp", + ".ico": "image/x-icon", +} + +func createSolutionAddOnValidator(cfg SolutionAddOnConfig) error { + if cfg.IsoFilePath == "" { + return fmt.Errorf("'isoFilePath' must be specified") + } + + if cfg.User == "" { + return fmt.Errorf("'user' must be specified") + } + + if cfg.CatalogItemId == "" { + return fmt.Errorf("'catalogItemId' must be specified") + } + + return nil +} + +// CreateSolutionAddOn creates Solution Add-On instance in VCD based on given +// Requirements - the ISO image defined in `isoFilePath` must already be uploaded to a catalog and +// `catalogItemId` must reflect exactly the same image +// +// Order of operations that this method provides: +// * Get contents of Solution Add-On ISO file defined in 'isoFilePath' +// * Create the 'Entity' payload for creating RDE based on the given image +// * Get Solution Add-On RDE Name from the manifest within 'isoFilePath' +// * If 'autoTrustCertificate' is set to true - the code will check if VCD trusts the certificate +// and trust it if it wasn't already trusted +// * Lookup RDE type 'vmware:solutions_add_on:1.0.0' +// * Create an RDE entity with payload from the 'isoFilePath' contents +func (vcdClient *VCDClient) CreateSolutionAddOn(cfg SolutionAddOnConfig) (*SolutionAddOn, error) { + err := createSolutionAddOnValidator(cfg) + if err != nil { + return nil, err + } + + foundFiles, err := getContentsFromIsoFiles(cfg.IsoFilePath, wantedFiles) + if err != nil { + return nil, fmt.Errorf("error reading contents of '%s': %s", cfg.IsoFilePath, err) + } + + solutionAddOnEntityRde, err := buildSolutionAddonRdeEntity(foundFiles, "administrator", cfg.CatalogItemId) + if err != nil { + return nil, fmt.Errorf("error building Solution Add-On RDE: %s", err) + } + + rdeName, err := extractSolutionAddonRdeName(foundFiles) + if err != nil { + return nil, fmt.Errorf("error finding RDE Entity Name: %s", err) + } + + if cfg.AutoTrustCertificate { + certificateText, err := extractSolutionAddOnCertificate(foundFiles) + if err != nil { + return nil, fmt.Errorf("error extracting Certificate from Add-On image: %s", err) + } + isoFileName := filepath.Base(cfg.IsoFilePath) + err = vcdClient.TrustAddOnImageCertificate(certificateText, isoFileName) + if err != nil { + return nil, fmt.Errorf("certificate trust was request, but it failed: %s", err) + } + } + + rdeType, err := vcdClient.GetRdeType(slzAddOnRdeType[0], slzAddOnRdeType[1], slzAddOnRdeType[2]) + if err != nil { + return nil, fmt.Errorf("error retrieving RDE Type for Solution Add-On: %s", err) + } + + unmarshalledRdeEntityJson, err := convertAnyToRdeEntity(solutionAddOnEntityRde) + if err != nil { + return nil, err + } + + entityCfg := &types.DefinedEntity{ + EntityType: fmt.Sprintf("urn:vcloud:type:%s:%s:%s", slzAddOnRdeType[0], slzAddOnRdeType[1], slzAddOnRdeType[2]), + Name: rdeName, + State: addrOf("PRE_CREATED"), + Entity: unmarshalledRdeEntityJson, + } + + createdRdeEntity, err := rdeType.CreateRde(*entityCfg, nil) + if err != nil { + return nil, fmt.Errorf("error creating RDE entity: %s", err) + } + + err = createdRdeEntity.Resolve() + if err != nil { + return nil, fmt.Errorf("error resolving Solutions Add-On after creating: %s", err) + } + + result, err := convertRdeEntityToAny[types.SolutionAddOn](createdRdeEntity.DefinedEntity.Entity) + if err != nil { + return nil, err + } + + returnType := SolutionAddOn{ + SolutionAddOnEntity: result, + vcdClient: vcdClient, + DefinedEntity: createdRdeEntity, + } + + return &returnType, nil +} + +// GetAllSolutionAddons retrieves all Solution Add-Ons with a given filter +func (vcdClient *VCDClient) GetAllSolutionAddons(queryParameters url.Values) ([]*SolutionAddOn, error) { + allAddons, err := vcdClient.GetAllRdes(slzAddOnRdeType[0], slzAddOnRdeType[1], slzAddOnRdeType[2], queryParameters) + if err != nil { + return nil, fmt.Errorf("error retrieving all Solution Add-ons: %s", err) + } + + results := make([]*SolutionAddOn, len(allAddons)) + for index, rde := range allAddons { + addon, err := convertRdeEntityToAny[types.SolutionAddOn](rde.DefinedEntity.Entity) + if err != nil { + return nil, fmt.Errorf("error converting RDE to Solution Add-on: %s", err) + } + + results[index] = &SolutionAddOn{ + vcdClient: vcdClient, + DefinedEntity: rde, + SolutionAddOnEntity: addon, + } + } + + return results, nil +} + +// GetSolutionAddonById retrieves Solution Add-On by ID +func (vcdClient *VCDClient) GetSolutionAddonById(id string) (*SolutionAddOn, error) { + if id == "" { + return nil, fmt.Errorf("id must be specified") + } + rde, err := getRdeById(&vcdClient.Client, id) + if err != nil { + return nil, fmt.Errorf("error retrieving Solution Add-On by ID: %s", err) + } + + result, err := convertRdeEntityToAny[types.SolutionAddOn](rde.DefinedEntity.Entity) + if err != nil { + return nil, err + } + + packages := &SolutionAddOn{ + SolutionAddOnEntity: result, + vcdClient: vcdClient, + DefinedEntity: rde, + } + + return packages, nil +} + +// GetSolutionAddonByName retrieves Solution Add-Ons by name +// Example name: "vmware.ds-1.4.0-23376809" +func (vcdClient *VCDClient) GetSolutionAddonByName(name string) (*SolutionAddOn, error) { + if name == "" { + return nil, fmt.Errorf("name must be specified") + } + + queryParams := url.Values{} + queryParams.Add("filter", fmt.Sprintf("name==%s", name)) + results, err := vcdClient.GetAllSolutionAddons(queryParams) + if err != nil { + return nil, fmt.Errorf("error retrieving Solution Add-Ons: %s", err) + } + + return oneOrError("name", name, results) +} + +func (s *SolutionAddOn) Update(saoCfg *types.SolutionAddOn) (*SolutionAddOn, error) { + unmarshalledRdeEntityJson, err := convertAnyToRdeEntity(saoCfg) + if err != nil { + return nil, err + } + + newStructure, err := s.vcdClient.GetSolutionAddonById(s.RdeId()) + if err != nil { + return nil, fmt.Errorf("error creating a copy of Solution Add-On: %s", err) + } + + newStructure.DefinedEntity.DefinedEntity.Entity = unmarshalledRdeEntityJson + err = newStructure.DefinedEntity.Update(*newStructure.DefinedEntity.DefinedEntity) + if err != nil { + return nil, err + } + + newStructure.SolutionAddOnEntity, err = convertRdeEntityToAny[types.SolutionAddOn](s.DefinedEntity.DefinedEntity.Entity) + if err != nil { + return nil, err + } + + return newStructure, nil +} + +func (s *SolutionAddOn) Delete() error { + if s.DefinedEntity == nil { + return fmt.Errorf("error - parent Defined Entity is nil") + } + return s.DefinedEntity.Delete() +} + +// RdeId is a shortcut of SolutionEntity.DefinedEntity.DefinedEntity.ID +func (s *SolutionAddOn) RdeId() string { + if s == nil || s.DefinedEntity == nil || s.DefinedEntity.DefinedEntity == nil { + return "" + } + + return s.DefinedEntity.DefinedEntity.ID +} + +// TrustAddOnImageCertificate will check if a given certificate is trusted by VCD and trust it if it +// is not there yet +func (vcdClient *VCDClient) TrustAddOnImageCertificate(certificateText, source string) error { + if certificateText == "" { + return fmt.Errorf("certificate field is empty") + } + + if source == "" { + return fmt.Errorf("source field is empty") + } + + foundCertificateInLibrary, err := vcdClient.Client.CountMatchingCertificates(certificateText) + if err != nil { + return err + } + if foundCertificateInLibrary == 0 { + addonCertificateName := fmt.Sprintf("addon.%s_%s", source, time.Now().Format(time.RFC3339)) + + certificateConfig := types.CertificateLibraryItem{ + Alias: addonCertificateName, + Certificate: certificateText, + Description: "certificate retrieved from " + source, + } + _, err = vcdClient.Client.AddCertificateToLibrary(&certificateConfig) + if err != nil { + return fmt.Errorf("error adding certificate '%s' to library: %s", addonCertificateName, err) + } + } + + return nil +} + +func getContentsFromIsoFiles(isoFileName string, wanted map[string]isoFileDef) (map[string]isoFileDef, error) { + if stat, err := os.Stat(isoFileName); err != nil || stat.IsDir() { + return nil, fmt.Errorf("file %s does not exist", isoFileName) + } + var err error + var result = make(map[string]isoFileDef) + for key, elem := range wanted { + if elem.nameRegexp == nil { + elem.nameRegexp, err = regexp.Compile(elem.namePattern) + if err != nil { + return nil, fmt.Errorf("error compiling regular expression '%s': %s", elem.namePattern, err) + } + } + result[key] = elem + } + + file, err := os.Open(filepath.Clean(isoFileName)) + if err != nil { + return nil, err + } + defer func() { + err = file.Close() + if err != nil { + panic(err) + } + }() + + reader, err := udf.Open(file) + if err != nil { + return nil, err + } + + rootDir, err := reader.RootDir() + if err != nil { + return nil, err + } + + children, err := reader.ReadDir(rootDir) + if err != nil { + return nil, err + } + + matchItem := func(name string) string { + for k, elem := range result { + if elem.nameRegexp.MatchString(name) { + return k + } + } + return "" + } + for idx := range children { + child := &children[idx] + if itemId := matchItem(child.Name()); itemId != "" { + fileReader, err := reader.NewFileReader(child) + + if err != nil { + return nil, err + } + var fileContent []byte = make([]byte, child.Size()) + if _, err = fileReader.Read(fileContent); err != nil { + return nil, err + } + + elem := result[itemId] + elem.contents = fileContent + elem.foundFileName = child.Name() + result[itemId] = elem + } + } + var missing []string + var zeroContents []string + + // Making sure that all the wanted elements were retrieved + for key, elem := range result { + if elem.foundFileName == "" { + missing = append(missing, key) + } + if len(elem.contents) == 0 { + zeroContents = append(zeroContents, key) + } + } + if len(missing) > 0 { + return nil, fmt.Errorf("elements %v not found in .ISO '%s'", missing, isoFileName) + } + if len(zeroContents) > 0 { + return nil, fmt.Errorf("elements %v have zero contents in .ISO '%s'", zeroContents, isoFileName) + } + + return result, nil +} + +func buildSolutionAddonRdeEntity(foundFiles map[string]isoFileDef, user, catalogItemId string) (*types.SolutionAddOn, error) { + iconContents := foundFiles[iconKey].contents + iconText := base64.StdEncoding.EncodeToString(iconContents) + licenseText := foundFiles[eulaKey].contents + manifestText := foundFiles[manifestKey].contents + + jsonManifestText, err := yaml.YAMLToJSON(manifestText) + if err != nil { + return nil, fmt.Errorf("error converting manifest file '%s' to JSON: %s", foundFiles[manifestKey].foundFileName, err) + } + var manifestStruct map[string]any + err = json.Unmarshal(jsonManifestText, &manifestStruct) + if err != nil { + return nil, fmt.Errorf("error encoding manifest file '%s' to JSON: %s", foundFiles[manifestKey].foundFileName, err) + } + + iconEntry := "" + fileSuffix := path.Ext(foundFiles[iconKey].foundFileName) + + iconDef, ok := iconTypes[fileSuffix] + if !ok { + return nil, fmt.Errorf("no icon definition found for file suffix '%s'", fileSuffix) + } + iconEntry = fmt.Sprintf("data:%s;base64,%s", iconDef, iconText) + + solutionEntity := &types.SolutionAddOn{ + Eula: string(licenseText), + Status: "READY", + Manifest: manifestStruct, + Icon: iconEntry, + Origin: types.SolutionAddOnOrigin{ + Type: "CATALOG", + AcceptedBy: user, + AcceptedOn: time.Now().UTC().Format(time.RFC3339), + CatalogItemId: catalogItemId, + }, + } + + return solutionEntity, nil +} + +func extractSolutionAddOnCertificate(foundFiles map[string]isoFileDef) (string, error) { + certificateText := foundFiles[certificateKey].contents + certificateString := string(certificateText) + + if certificateString == "" { + certFilter := wantedFiles[certificateKey].namePattern + return "", fmt.Errorf("%s: no certificate file found based on name filter '%s'", ErrorEntityNotFound, certFilter) + } + + return certificateString, nil +} + +func extractSolutionAddonRdeName(foundFiles map[string]isoFileDef) (string, error) { + manifestText := foundFiles[manifestKey].contents + + jsonManifestText, err := yaml.YAMLToJSON(manifestText) + if err != nil { + return "", fmt.Errorf("error converting manifest file '%s' to JSON: %s", foundFiles[manifestKey].foundFileName, err) + } + var manifestStruct map[string]any + err = json.Unmarshal(jsonManifestText, &manifestStruct) + if err != nil { + return "", fmt.Errorf("error encoding manifest file '%s' to JSON: %s", foundFiles[manifestKey].foundFileName, err) + } + + vendor, ok := manifestStruct[manifestVendor] + if !ok { + return "", fmt.Errorf("missing 'vendor' information from manifest file") + } + name, ok := manifestStruct[manifestName] + if !ok { + return "", fmt.Errorf("missing 'name' information from manifest file") + } + version, ok := manifestStruct[manifestVersion] + if !ok { + return "", fmt.Errorf("missing 'version' information from manifest file ") + } + solutionRdeName := fmt.Sprintf("%s.%s-%s", vendor, name, version) + + return solutionRdeName, nil +} diff --git a/govcd/solution_add_on_instance.go b/govcd/solution_add_on_instance.go new file mode 100644 index 000000000..d071ea562 --- /dev/null +++ b/govcd/solution_add_on_instance.go @@ -0,0 +1,444 @@ +/* +* Copyright 2024 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "bytes" + "encoding/json" + "fmt" + "maps" + "net/url" + "reflect" + "strconv" + "strings" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + "github.com/vmware/go-vcloud-director/v2/util" +) + +var slzAddOnInstanceRdeType = [3]string{"vmware", "solutions_add_on_instance", "1.0.0"} + +var addOnCreateInstanceBehaviorId = "urn:vcloud:behavior-interface:createInstance:vmware:solutions_add_on:1.0.0" +var addOnInstanceRemovalBehaviorId = "urn:vcloud:behavior-interface:invoke:vmware:solutions_add_on_instance:1.0.0" + +type SolutionAddOnInstance struct { + SolutionAddOnInstance *types.SolutionAddOnInstance + DefinedEntity *DefinedEntity + vcdClient *VCDClient +} + +// CreateSolutionAddOnInstance instantiates a new Solution Add-On. Some inputs may be mandatory for +// creation depending on the Solution Add-On itself. Methods 'ValidateInputs' can help to +// dynamically validate inputs based on the requirements in Solution Add-On. +func (addon *SolutionAddOn) CreateSolutionAddOnInstance(inputs map[string]interface{}) (*SolutionAddOnInstance, string, error) { + // copy inputs to prevent mutation of function argument + inputsCopy := make(map[string]interface{}) + maps.Copy(inputsCopy, inputs) + + inputsCopy["operation"] = "create instance" + + // Name is always mandatory + name := inputsCopy["name"].(string) + if name == "" { + return nil, "", fmt.Errorf("'name' field must be present in the inputs") + } + + behaviorInvocation := types.BehaviorInvocation{ + Arguments: inputsCopy, + } + + parentRde := addon.DefinedEntity + result, err := parentRde.InvokeBehavior(addOnCreateInstanceBehaviorId, behaviorInvocation) + if err != nil { + return nil, "", fmt.Errorf("error invoking RDE behavior: %s", err) + } + + // Once the task is done and no error are here, one must find that instance from scratch + createdAddOnInstance, err := addon.GetInstanceByName(name) + if err != nil { + return nil, "", fmt.Errorf("error retrieving Solution Add-On instance '%s' after creation: %s", name, err) + } + + return createdAddOnInstance, result, nil +} + +// GetAllInstances retrieves all Solution Add-On Instances +func (addon *SolutionAddOn) GetAllInstances() ([]*SolutionAddOnInstance, error) { + vcdClient := addon.vcdClient + + // This filter ensures that only Add-On instances, that are based of this particular Add-On are + // returned + queryParams := copyOrNewUrlValues(nil) + queryParams = queryParameterFilterAnd(fmt.Sprintf("entity.prototype==%s", addon.RdeId()), queryParams) + + return vcdClient.GetAllSolutionAddonInstances(queryParams) +} + +// GetInstanceByName retrieves Solution Add-On Instance by name for a particular Solution Add-On. +// It will return an error if there is more than one Solution Add-On Instance with such name. +func (addon *SolutionAddOn) GetInstanceByName(name string) (*SolutionAddOnInstance, error) { + vcdClient := addon.vcdClient + + queryParams := copyOrNewUrlValues(nil) + queryParams = queryParameterFilterAnd(fmt.Sprintf("entity.prototype==%s;entity.name==%s", addon.RdeId(), name), queryParams) + + addOnInstances, err := vcdClient.GetAllSolutionAddonInstances(queryParams) + if err != nil { + return nil, fmt.Errorf("error retrieving Solution Add-On Instance with name '%s': %s", name, err) + } + + return oneOrError("name", name, addOnInstances) +} + +// GetAllSolutionAddonInstancesByName will retrieve all Solution Add-On Instances available +func (vcdClient *VCDClient) GetAllSolutionAddonInstancesByName(name string) ([]*SolutionAddOnInstance, error) { + queryParams := copyOrNewUrlValues(nil) + queryParams = queryParameterFilterAnd(fmt.Sprintf("entity.name==%s", name), queryParams) + + return vcdClient.GetAllSolutionAddonInstances(queryParams) +} + +// GetSolutionAddonInstanceByName will retrieve a single Solution Add-On Instance by name or fail +func (vcdClient *VCDClient) GetSolutionAddonInstanceByName(name string) (*SolutionAddOnInstance, error) { + queryParams := copyOrNewUrlValues(nil) + queryParams = queryParameterFilterAnd(fmt.Sprintf("entity.name==%s", name), queryParams) + + addOnInstances, err := vcdClient.GetAllSolutionAddonInstances(queryParams) + if err != nil { + return nil, fmt.Errorf("error retrieving Solution Add-On Instance with name '%s': %s", name, err) + } + + return oneOrError("name", name, addOnInstances) +} + +// GetAllSolutionAddonInstances will retrieve Solution Add-On Instances based on given query parameters +func (vcdClient *VCDClient) GetAllSolutionAddonInstances(queryParameters url.Values) ([]*SolutionAddOnInstance, error) { + allAddonInstances, err := vcdClient.GetAllRdes(slzAddOnInstanceRdeType[0], slzAddOnInstanceRdeType[1], slzAddOnInstanceRdeType[2], queryParameters) + if err != nil { + return nil, fmt.Errorf("error retrieving all Solution Add-on Instances: %s", err) + } + + results := make([]*SolutionAddOnInstance, len(allAddonInstances)) + for index, rde := range allAddonInstances { + addon, err := convertRdeEntityToAny[types.SolutionAddOnInstance](rde.DefinedEntity.Entity) + if err != nil { + return nil, fmt.Errorf("error converting RDE to Solution Add-on Instance: %s", err) + } + + results[index] = &SolutionAddOnInstance{ + vcdClient: vcdClient, + DefinedEntity: rde, + SolutionAddOnInstance: addon, + } + } + + return results, nil +} + +// GetSolutionAddOnInstanceById retrieves a Solution Add-On Instance with a given ID +func (vcdClient *VCDClient) GetSolutionAddOnInstanceById(id string) (*SolutionAddOnInstance, error) { + addOnInstanceRde, err := getRdeById(&vcdClient.Client, id) + if err != nil { + return nil, fmt.Errorf("error retrieving Solution Add-On Instance RDE: %s", err) + } + + addOnInstanceEntity, err := convertRdeEntityToAny[types.SolutionAddOnInstance](addOnInstanceRde.DefinedEntity.Entity) + if err != nil { + return nil, err + } + result := &SolutionAddOnInstance{ + vcdClient: vcdClient, + DefinedEntity: addOnInstanceRde, + SolutionAddOnInstance: addOnInstanceEntity, + } + + return result, nil +} + +// Delete will delete a Solution Add-On instance with given 'deleteInputs'. Some fields in +// 'deleteInputs' might be mandatory for deletion of an instance. One can use 'ValidateInputs' +// method to check what inputs are defined for a particular Solution Add-On +func (addonInstance *SolutionAddOnInstance) Delete(deleteInputs map[string]interface{}) (string, error) { + // copy deleteInputs to prevent mutation of function argument + deleteInputsCopy := make(map[string]interface{}) + maps.Copy(deleteInputsCopy, deleteInputs) + + deleteInputsCopy["operation"] = "delete instance" + + behaviorInvocation := types.BehaviorInvocation{ + Arguments: deleteInputsCopy, + } + + parentRde := addonInstance.DefinedEntity + result, err := parentRde.InvokeBehavior(addOnInstanceRemovalBehaviorId, behaviorInvocation) + if err != nil { + return "", fmt.Errorf("error invoking removal of Solution Add-On instance '%s': %s", addonInstance.SolutionAddOnInstance.Name, err) + } + + return result, nil +} + +// GetParentSolutionAddOn retrieves parent Solution Add-On that is specified in the Prototype field +func (addOnInstance *SolutionAddOnInstance) GetParentSolutionAddOn() (*SolutionAddOn, error) { + if addOnInstance == nil || addOnInstance.DefinedEntity == nil || addOnInstance.DefinedEntity.DefinedEntity == nil { + return nil, fmt.Errorf("cannot retrieve parent Solution Add-On from empty instance") + } + + return addOnInstance.vcdClient.GetSolutionAddonById(addOnInstance.SolutionAddOnInstance.Prototype) +} + +// RdeId is a shortcut to retrieve parent RDE ID +func (addOnInstance *SolutionAddOnInstance) RdeId() string { + if addOnInstance == nil || addOnInstance.DefinedEntity == nil || addOnInstance.DefinedEntity.DefinedEntity == nil { + return "" + } + + return addOnInstance.DefinedEntity.DefinedEntity.ID +} + +// ReadCreationInputValues will read all input values that were specified upon instance creation and return them +// either in their natural types, or all values converted to strings +func (addOnInstance *SolutionAddOnInstance) ReadCreationInputValues(convertAllValuesToStrings bool) (map[string]interface{}, error) { + if addOnInstance == nil || addOnInstance.SolutionAddOnInstance == nil || addOnInstance.SolutionAddOnInstance.Properties == nil { + return nil, fmt.Errorf("cannot extract properties - they are nil") + } + + parentAddOn, err := addOnInstance.GetParentSolutionAddOn() + if err != nil { + return nil, fmt.Errorf("error retrieving parent Solution Add-On: %s", err) + } + + schemaInputFields, err := parentAddOn.extractInputs() + if err != nil { + return nil, fmt.Errorf("error extracting inputs from Solution Add-On manifests: %s", err) + } + + // Fields are specified within addOnInstance.SolutionAddOnInstance.Properties but they contain more values + // that just the inputs themselves. Searching for values of all defined inputs. + resultMap := make(map[string]interface{}) + for _, schemaInputField := range schemaInputFields { + // Deletion fields are not stored in schema because they are not supplied for creating the + // instance + if schemaInputField.Delete { + continue + } + + util.Logger.Printf("[TRACE] Solution Add-On Instance Input field - looking for field '%s'", schemaInputField.Name) + if foundValue, ok := addOnInstance.SolutionAddOnInstance.Properties[schemaInputField.Name]; ok { + util.Logger.Printf("[TRACE] Solution Add-On Instance Input field - found field '%s' of type %s", + schemaInputField.Name, schemaInputField.Type) + resultMap[schemaInputField.Name] = foundValue + } + } + + if convertAllValuesToStrings { + convertedResultMap := make(map[string]interface{}) + util.Logger.Printf("[TRACE] Solution Add-On Instance Inputs - converting all values to strings") + + for fieldName, fieldValue := range resultMap { + convertedResultMap[fieldName] = fmt.Sprintf("%v", fieldValue) + } + return convertedResultMap, nil + } + + return resultMap, nil +} + +// ValidateInputs will check if 'userInputs' match required fields as defined in the Solution Add-On +// itself. Error will contained detailed information about missing fields. +func (addon *SolutionAddOn) ValidateInputs(userInputs map[string]interface{}, validateOnlyRequired, isDeleteOperation bool) error { + schemaInputs, err := addon.extractInputs() + if err != nil { + return err + } + + requiredFields := make(map[string]bool) + for _, si := range schemaInputs { + // Skip field if the operation does not match + // Required fields can be defined either for create or for update operations + if si.Delete != isDeleteOperation { + continue + } + + // Validating only required fields is set + // Skipping a non required field. + if !si.Required && validateOnlyRequired { + continue + } + + // Setting the key, but not marking as found yet + requiredFields[si.Name] = false + } + + // Check if all required fields are set in inputs + for requiredFieldKey := range requiredFields { + for userInputKey := range userInputs { + if requiredFieldKey == userInputKey || fmt.Sprintf("input-%s", requiredFieldKey) == userInputKey { // field found + requiredFields[requiredFieldKey] = true + } + } + } + + // Check if all field constraints are satisfied + missingFields := make([]string, 0) + msFields := make([]*types.SolutionAddOnInputField, 0) + for k := range requiredFields { + if !requiredFields[k] { + missingFields = append(missingFields, k) + field, err := localFilterOneOrError("Solution Add-On filter value", schemaInputs, "Name", k) + if err != nil { + return fmt.Errorf("error finding field with key '%s'", k) + } + msFields = append(msFields, field) + } + } + + if len(missingFields) > 0 { + fieldInfo, err := printAddonFieldData(msFields) + if err != nil { + return fmt.Errorf("error processing missing fields '%s' for: %s", addon.DefinedEntity.DefinedEntity.Name, err) + } + + return fmt.Errorf("%s\n\nERROR: Missing fields '%s' for Solution Add-On '%s'", + fieldInfo, strings.Join(missingFields, ", "), addon.DefinedEntity.DefinedEntity.Name) + } + + return nil +} + +// ConvertInputTypes will make sure that values will match types as defined in Add-On schema +// The needs for this operation comes from the fact that at least some of the Solution Add-Ons will +// fail if a boolean "false" is sent as a string +func (addon *SolutionAddOn) ConvertInputTypes(userInputs map[string]interface{}) (map[string]interface{}, error) { + schemaInputs, err := addon.extractInputs() + if err != nil { + return nil, err + } + + userInputsCopy := make(map[string]interface{}) + maps.Copy(userInputsCopy, userInputs) + + for userInputKey, userInputValue := range userInputsCopy { + // search for key in the schema inputs and find correct type + util.Logger.Printf("[TRACE] Solution Add-On Schema conversion - user input key %s, value of type %T", userInputKey, userInputValue) + + typeOfUserInputValue := reflect.TypeOf(userInputValue) + + var foundField *types.SolutionAddOnInputField + for _, field := range schemaInputs { + util.Logger.Printf("[TRACE] Solution Add-On Schema conversion - checking field %#v against user specified field %s", field, userInputKey) + if field.Name == userInputKey || fmt.Sprintf("input-%s", field.Name) == userInputKey { + foundField = field + util.Logger.Printf("[TRACE] Solution Add-On Schema conversion - found field in schema %#v", field) + break + } + } + + if foundField == nil { + util.Logger.Printf("[TRACE] Solution Add-On Schema conversion - field '%s' not found in schema", userInputKey) + } + + // User supplied string value, but actual type is different + // Only attempting to convert fields that are in the schema + if foundField != nil && typeOfUserInputValue.String() == "string" && foundField.Type != "String" { + userInputStringValue := userInputValue.(string) + util.Logger.Printf("[TRACE] Solution Add-On Schema conversion - found field in schema %#v", foundField) + switch foundField.Type { + case "Boolean": + util.Logger.Printf("[TRACE] Solution Add-On Schema conversion - converting field '%s' to bool", userInputKey) + // override string key to match boolean type + boolValue, err := strconv.ParseBool(userInputStringValue) + if err != nil { + return nil, fmt.Errorf("error converting field '%s' to boolean: %s", userInputKey, err) + } + userInputsCopy[userInputKey] = boolValue + case "Integer": + util.Logger.Printf("[TRACE] Solution Add-On Schema conversion - converting field '%s' to int", userInputKey) + // override string key to match integer type + intValue, err := strconv.Atoi(userInputStringValue) + if err != nil { + return nil, fmt.Errorf("error converting field '%s' to integer: %s", userInputKey, err) + } + userInputsCopy[userInputKey] = intValue + default: + return nil, fmt.Errorf("unknown field type '%s' for field '%s'", foundField.Type, userInputKey) + } + } + } + + util.Logger.Printf("[TRACE] Solution Add-On Schema conversion - final result %#v", userInputsCopy) + + return userInputsCopy, nil +} + +// extractInputs retrieves input field definitions for instantiating and removing Solution Add-On from manifest +func (addon *SolutionAddOn) extractInputs() ([]*types.SolutionAddOnInputField, error) { + // Extract inputs definition / Manifest["inputs"] + inputValidation := addon.SolutionAddOnEntity.Manifest["inputs"] + inputValidationSlice, ok := inputValidation.([]any) + if !ok { + return nil, fmt.Errorf("error processing Solution Add-On input validation metadata") + } + + inputFieldMetadata, err := convertAllAddonInputFields(inputValidationSlice) + if err != nil { + if err != nil { + return nil, fmt.Errorf("error converting Solution Add-On input validation metadata: %s", err) + } + } + + return inputFieldMetadata, nil +} + +func printAddonFieldData(allFields []*types.SolutionAddOnInputField) (string, error) { + buf := bytes.NewBufferString("\n") + + _, _ = fmt.Fprintf(buf, "-----------------\n") + for _, f := range allFields { + _, _ = fmt.Fprintf(buf, "Field: %s\n", f.Name) + _, _ = fmt.Fprintf(buf, "Title: %s\n", f.Title) + _, _ = fmt.Fprintf(buf, "Type: %s\n", f.Type) + _, _ = fmt.Fprintf(buf, "Required: %t\n", f.Required) + _, _ = fmt.Fprintf(buf, "IsDelete: %t\n", f.Delete) + _, _ = fmt.Fprintf(buf, "Description: %s\n", f.Description) + if f.Default != nil { + _, _ = fmt.Fprintf(buf, "Default: %v\n", f.Default) + } + + _, _ = fmt.Fprintf(buf, "-----------------\n") + } + + return buf.String(), nil +} + +func convertAllAddonInputFields(allInputFields []any) ([]*types.SolutionAddOnInputField, error) { + allFields := make([]*types.SolutionAddOnInputField, len(allInputFields)) + + for index, inputField := range allInputFields { + inpField, err := convertAddonInputField(inputField) + if err != nil { + return nil, err + } + + allFields[index] = inpField + } + + return allFields, nil +} + +func convertAddonInputField(field any) (*types.SolutionAddOnInputField, error) { + txt, err := json.Marshal(field) + if err != nil { + return nil, fmt.Errorf("failed marshalling Input Field: %s", err) + } + + fieldType := types.SolutionAddOnInputField{} + err = json.Unmarshal(txt, &fieldType) + if err != nil { + return nil, fmt.Errorf("failed unmarshalling Input Field to exact type: %s", err) + } + + return &fieldType, nil +} diff --git a/govcd/solution_add_on_instance_publishing.go b/govcd/solution_add_on_instance_publishing.go new file mode 100644 index 000000000..df2db1152 --- /dev/null +++ b/govcd/solution_add_on_instance_publishing.go @@ -0,0 +1,40 @@ +/* +* Copyright 2024 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "strings" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +var addOnInstancePublishBehaviorId = "urn:vcloud:behavior-interface:invoke:vmware:solutions_add_on_instance:1.0.0" + +// Publishing manages publish and Unpublish operations, which are managed in the same API call +// To unpublish, the `scopeAll` has to be `false and `scope` must be empty +func (addonInstance *SolutionAddOnInstance) Publishing(scope []string, scopeAll bool) (string, error) { + arguments := make(map[string]interface{}) + arguments["operation"] = "publish instance" + arguments["name"] = addonInstance.SolutionAddOnInstance.Name + if scope != nil { + arguments["scope"] = strings.Join(scope, ",") + } else { + arguments["scope"] = "" + } + arguments["scope-all"] = scopeAll + + behaviorInvocation := types.BehaviorInvocation{ + Arguments: arguments, + } + + parentRde := addonInstance.DefinedEntity + result, err := parentRde.InvokeBehavior(addOnInstancePublishBehaviorId, behaviorInvocation) + if err != nil { + return "", fmt.Errorf("error invoking publish behavior of Solution Add-On instance '%s': %s", addonInstance.SolutionAddOnInstance.Name, err) + } + + return result, nil +} diff --git a/govcd/solution_add_on_instance_test.go b/govcd/solution_add_on_instance_test.go new file mode 100644 index 000000000..81e78921a --- /dev/null +++ b/govcd/solution_add_on_instance_test.go @@ -0,0 +1,175 @@ +//go:build slz || functional || ALL + +/* +* Copyright 2024 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_SolutionAddOnInstanceAndPublishing(check *C) { + if vcd.client.Client.APIVCDMaxVersionIs("< 37.1") { + check.Skip("Solution Landing Zones are supported in VCD 10.4.1+") + } + + if vcd.config.SolutionAddOn.Org == "" || vcd.config.SolutionAddOn.Catalog == "" { + check.Skip("Solution Add-On configuration is not present") + } + + // Prerequisites + slz, addOn := createSlzAddOn(vcd, check) + + // Create Solution Add-On Instance + inputs := make(map[string]interface{}) + inputs["name"] = check.TestName() + inputs["input-delete-previous-uiplugin-versions"] = false + + addOnInstance, res, err := addOn.CreateSolutionAddOnInstance(inputs) + check.Assert(err, IsNil) + check.Assert(addOnInstance, NotNil) + check.Assert(res, Not(Equals), "") + + // Get by Id + addOnInstanceByName, err := vcd.client.GetSolutionAddOnInstanceById(addOnInstance.RdeId()) + check.Assert(err, IsNil) + check.Assert(addOnInstanceByName.SolutionAddOnInstance.Name, Equals, addOnInstance.SolutionAddOnInstance.Name) + + // Get all by Name + allAddOnInstancesByName, err := vcd.client.GetAllSolutionAddonInstancesByName(addOnInstance.SolutionAddOnInstance.Name) + check.Assert(err, IsNil) + check.Assert(len(allAddOnInstancesByName), Equals, 1) + check.Assert(allAddOnInstancesByName[0].SolutionAddOnInstance.Name, Equals, addOnInstance.SolutionAddOnInstance.Name) + + // Get all instances of a specific Add-On + allAddOnChildren, err := addOn.GetAllInstances() + check.Assert(err, IsNil) + check.Assert(len(allAddOnChildren), Equals, 1) + check.Assert(allAddOnChildren[0].SolutionAddOnInstance.Name, Equals, addOnInstance.SolutionAddOnInstance.Name) + + // Get child instance by name + addOnChildByName, err := addOn.GetInstanceByName(addOnInstance.SolutionAddOnInstance.Name) + check.Assert(err, IsNil) + check.Assert(addOnChildByName.RdeId(), Equals, addOnInstance.RdeId()) + + // Get parent Solution Add-On + parentSolutionAddOn, err := addOnInstance.GetParentSolutionAddOn() + check.Assert(err, IsNil) + check.Assert(parentSolutionAddOn.RdeId(), Equals, addOn.RdeId()) + + // Publish Solution Add-On Instance + scope := []string{vcd.config.Cse.TenantOrg} + _, err = addOnInstance.Publishing(scope, false) + check.Assert(err, IsNil) + + // Unpublish Solution Add-On Instance + _, err = addOnInstance.Publishing(nil, false) + check.Assert(err, IsNil) + + // Delete Solution Add-On Instance + deleteInputs := make(map[string]interface{}) + deleteInputs["name"] = addOnInstance.SolutionAddOnInstance.AddonInstanceSolutionName + deleteInputs["input-force-delete"] = true + res2, err := addOnInstance.Delete(deleteInputs) + check.Assert(err, IsNil) + check.Assert(res2, Not(Equals), "") + + // Cleanup + err = addOn.Delete() + check.Assert(err, IsNil) + + err = slz.Delete() + check.Assert(err, IsNil) +} + +// createSlzAddOn depends on CSE build (having vcd.config.SolutionAddOn configuration present) +func createSlzAddOn(vcd *TestVCD, check *C) (*SolutionLandingZone, *SolutionAddOn) { + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.SolutionAddOn.Org) + check.Assert(err, IsNil) + + adminVdc, err := adminOrg.GetAdminVDCByName(vcd.config.SolutionAddOn.Vdc, false) + check.Assert(err, IsNil) + vdc, err := adminOrg.GetVDCByName(vcd.config.SolutionAddOn.Vdc, false) + check.Assert(err, IsNil) + + orgNetwork, err := vdc.GetOpenApiOrgVdcNetworkByName(vcd.config.SolutionAddOn.RoutedNetwork) + check.Assert(err, IsNil) + check.Assert(orgNetwork, NotNil) + computePolicy, err := adminVdc.GetAllAssignedVdcComputePoliciesV2(nil) + check.Assert(err, IsNil) + check.Assert(computePolicy, NotNil) + storageProfileRef, err := adminVdc.GetDefaultStorageProfileReference() + check.Assert(err, IsNil) + check.Assert(storageProfileRef, NotNil) + catalog, err := adminOrg.GetCatalogByName(vcd.config.SolutionAddOn.Catalog, false) + check.Assert(err, IsNil) + check.Assert(catalog, NotNil) + slzCfg := &types.SolutionLandingZoneType{ + Name: adminOrg.AdminOrg.Name, + ID: adminOrg.AdminOrg.ID, + Vdcs: []types.SolutionLandingZoneVdc{ + { + ID: adminVdc.AdminVdc.ID, + Name: adminVdc.AdminVdc.Name, + Capabilities: []string{}, + Networks: []types.SolutionLandingZoneVdcChild{ + { + ID: orgNetwork.OpenApiOrgVdcNetwork.ID, + Name: orgNetwork.OpenApiOrgVdcNetwork.Name, + IsDefault: true, + Capabilities: []string{}, + }, + }, + ComputePolicies: []types.SolutionLandingZoneVdcChild{ + { + ID: computePolicy[0].VdcComputePolicyV2.ID, + Name: computePolicy[0].VdcComputePolicyV2.Name, + IsDefault: true, + Capabilities: []string{}, + }, + }, + StoragePolicies: []types.SolutionLandingZoneVdcChild{ + { + ID: storageProfileRef.ID, + Name: storageProfileRef.Name, + IsDefault: true, + Capabilities: []string{}, + }, + }, + }, + }, + Catalogs: []types.SolutionLandingZoneCatalog{ + { + ID: catalog.Catalog.ID, + Name: catalog.Catalog.Name, + Capabilities: []string{}, + }, + }, + } + slz, err := vcd.client.CreateSolutionLandingZone(slzCfg) + check.Assert(err, IsNil) + check.Assert(slz, NotNil) + + AddToCleanupListOpenApi(slz.DefinedEntity.DefinedEntity.ID, check.TestName(), types.OpenApiPathVersion1_0_0+types.OpenApiEndpointRdeEntities+slz.DefinedEntity.DefinedEntity.ID) + + cacheFilePath, err := fetchCacheFile(catalog, vcd.config.SolutionAddOn.AddonImageDse, check) + check.Assert(err, IsNil) + + catItem, err := catalog.GetCatalogItemByName(vcd.config.SolutionAddOn.AddonImageDse, false) + check.Assert(err, IsNil) + + createCfg := SolutionAddOnConfig{ + IsoFilePath: cacheFilePath, + User: "administrator", + CatalogItemId: catItem.CatalogItem.ID, + AutoTrustCertificate: true, + } + solutionAddOn, err := vcd.client.CreateSolutionAddOn(createCfg) + check.Assert(err, IsNil) + PrependToCleanupListOpenApi(solutionAddOn.DefinedEntity.DefinedEntity.ID, check.TestName(), types.OpenApiPathVersion1_0_0+types.OpenApiEndpointRdeEntities+solutionAddOn.DefinedEntity.DefinedEntity.ID) + + return slz, solutionAddOn +} diff --git a/govcd/solution_add_on_test.go b/govcd/solution_add_on_test.go new file mode 100644 index 000000000..f9262142e --- /dev/null +++ b/govcd/solution_add_on_test.go @@ -0,0 +1,157 @@ +//go:build slz || functional || ALL + +/* +* Copyright 2024 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "errors" + "fmt" + "os" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_SolutionAddOn(check *C) { + if vcd.client.Client.APIVCDMaxVersionIs("< 37.1") { + check.Skip("Solution Landing Zones are supported in VCD 10.4.1+") + } + + if vcd.config.VCD.Catalog.NsxtCatalogAddonDse == "" { + check.Skip("missing 'VCD.Catalog.NsxtCatalogAddonDse' value") + } + catalogMediaName := vcd.config.VCD.Catalog.NsxtCatalogAddonDse + + slz := createSlz(vcd, check) + err := slz.Refresh() + check.Assert(err, IsNil) + + addOnCatalog, err := vcd.org.GetCatalogByName(vcd.config.VCD.Catalog.NsxtBackedCatalogName, false) + check.Assert(err, IsNil) + + cacheFilePath, err := fetchCacheFile(addOnCatalog, catalogMediaName, check) + check.Assert(err, IsNil) + + org, err := vcd.client.GetOrgById(slz.SolutionLandingZoneType.ID) + check.Assert(err, IsNil) + catalog, err := org.GetCatalogById(slz.SolutionLandingZoneType.Catalogs[0].ID, false) + check.Assert(err, IsNil) + catItem, err := catalog.GetCatalogItemByName(catalogMediaName, false) + check.Assert(err, IsNil) + + createCfg := SolutionAddOnConfig{ + IsoFilePath: cacheFilePath, + User: "administrator", + CatalogItemId: catItem.CatalogItem.ID, + AutoTrustCertificate: true, + } + solutionAddOn, err := vcd.client.CreateSolutionAddOn(createCfg) + check.Assert(err, IsNil) + PrependToCleanupListOpenApi(solutionAddOn.DefinedEntity.DefinedEntity.ID, check.TestName(), types.OpenApiPathVersion1_0_0+types.OpenApiEndpointRdeEntities+solutionAddOn.DefinedEntity.DefinedEntity.ID) + + // Get all + allSolutionAddOns, err := vcd.client.GetAllSolutionAddons(nil) + check.Assert(err, IsNil) + check.Assert(len(allSolutionAddOns) >= 1, Equals, true) // VCD has a few baked in Solution Add-Ons (e.g. 'service-account-solutions-system-user', 'vmware.solution-addon-landing-zone-1.0.0') + + foundId := false + for addOnIndex := range allSolutionAddOns { + if allSolutionAddOns[addOnIndex].RdeId() == solutionAddOn.RdeId() { + foundId = true + break + } + } + check.Assert(foundId, Equals, true) + + // Get all with filter + queryParams := queryParameterFilterAnd("id=="+solutionAddOn.RdeId(), nil) + filteredSolutionAddon, err := vcd.client.GetAllSolutionAddons(queryParams) + check.Assert(err, IsNil) + check.Assert(len(filteredSolutionAddon), Equals, 1) + + // By ID + sao, err := vcd.client.GetSolutionAddonById(solutionAddOn.RdeId()) + check.Assert(err, IsNil) + check.Assert(sao.RdeId(), Equals, solutionAddOn.RdeId()) + + // By Name + saoByName, err := vcd.client.GetSolutionAddonByName(solutionAddOn.DefinedEntity.DefinedEntity.Name) + check.Assert(err, IsNil) + check.Assert(saoByName.RdeId(), Equals, solutionAddOn.RdeId()) + + // Update - pointing at wrong catalog image + catItemPhoton, err := catalog.GetCatalogItemByName(vcd.config.VCD.Catalog.NsxtCatalogItem, false) + check.Assert(err, IsNil) + + solutionAddOnUpdate := saoByName.SolutionAddOnEntity + solutionAddOnUpdate.Origin.CatalogItemId = catItemPhoton.CatalogItem.ID + + updatedSao, err := sao.Update(solutionAddOnUpdate) + check.Assert(err, IsNil) + check.Assert(updatedSao.RdeId(), Equals, sao.RdeId()) + + // Delete + err = sao.Delete() + check.Assert(err, IsNil) + + // Verify no more Add-Ons remaining + allSolutionAddOnsAfterCleanup, err := vcd.client.GetAllSolutionAddons(nil) + check.Assert(err, IsNil) + check.Assert(len(allSolutionAddOnsAfterCleanup), Equals, len(allSolutionAddOns)-1) + + err = slz.Delete() + check.Assert(err, IsNil) +} + +func fetchCacheFile(catalog *Catalog, fileName string, check *C) (string, error) { + pwd, err := os.Getwd() + check.Assert(err, IsNil) + cacheDirPath := pwd + "/test-resources/cache" + cacheFilePath := cacheDirPath + "/" + fileName + printVerbose("# Using '%s' file to cache Solution Add-On\n", cacheFilePath) + + if _, err := os.Stat(cacheFilePath); errors.Is(err, os.ErrNotExist) || !dirExists(cacheDirPath) { + // Create cache directory if it doesn't exist + if fileInfo, err := os.Stat(cacheDirPath); os.IsNotExist(err) || !fileInfo.IsDir() { + // test-resources/cache is a file, not a directory, it should be removed + if !os.IsNotExist(err) && !fileInfo.IsDir() { + fmt.Printf("# %s is a file, not a directory - removing\n", cacheDirPath) + err := os.Remove(cacheDirPath) + check.Assert(err, IsNil) + } + + printVerbose("# Creating directory '%s'\n", cacheDirPath) + err := os.Mkdir(cacheDirPath, 0750) + check.Assert(err, IsNil) + } + + fmt.Printf("# Downloading Solution Add-On '%s' from VCD...", fileName) + addOnMediaItem, err := catalog.GetMediaByName(fileName, false) + check.Assert(err, IsNil) + + addOn, err := addOnMediaItem.Download() + check.Assert(err, IsNil) + + err = os.WriteFile(cacheFilePath, addOn, 0600) + check.Assert(err, IsNil) + addOn = nil // free memory + fmt.Println("Done") + } else { + printVerbose("# File '%s' is present, not downloading\n", cacheFilePath) + } + + return cacheFilePath, nil +} + +// Checks if a directory exists +func dirExists(filename string) bool { + f, err := os.Stat(filename) + if os.IsNotExist(err) { + return false + } + fileMode := f.Mode() + return fileMode.IsDir() +} diff --git a/govcd/solution_add_on_unit_test.go b/govcd/solution_add_on_unit_test.go new file mode 100644 index 000000000..931500ca5 --- /dev/null +++ b/govcd/solution_add_on_unit_test.go @@ -0,0 +1,365 @@ +//go:build unit || ALL + +/* + * Copyright 2024 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "encoding/json" + "reflect" + "testing" + + "github.com/kr/pretty" + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +func TestSolutionAddOn_ValidateInputs(t *testing.T) { + emptyInputs := make(map[string]interface{}) + + requiredInputsCreate := make(map[string]interface{}) + requiredInputsCreate["delete-previous-uiplugin-versions"] = true + + requiredInputsDelete := make(map[string]interface{}) + requiredInputsDelete["force-delete"] = true + + type args struct { + userInputs map[string]interface{} + validateOnlyRequired bool + isDeleteOperation bool + } + + tests := []struct { + name string + manifest []byte + args args + wantErr bool + }{ + { + name: "MissingRequiredCreate", + manifest: sampleSolutionAddonManifest1, + args: args{userInputs: emptyInputs, validateOnlyRequired: false, isDeleteOperation: false}, + wantErr: true, + }, + { + name: "SpecifiedRequiredCreate", + manifest: sampleSolutionAddonManifest1, + args: args{userInputs: requiredInputsCreate, validateOnlyRequired: true, isDeleteOperation: false}, + wantErr: false, + }, + { + name: "SpecifiedRequiredDelete", + manifest: sampleSolutionAddonManifest1, + args: args{userInputs: requiredInputsDelete, validateOnlyRequired: true, isDeleteOperation: true}, + wantErr: false, + }, + { + name: "MissingRequiredCreate2", + manifest: sampleSolutionAddonManifest2, + args: args{userInputs: emptyInputs, validateOnlyRequired: false, isDeleteOperation: false}, + wantErr: true, + }, + { + name: "MissingRequiredDelete", + manifest: sampleSolutionAddonManifest1, + args: args{userInputs: emptyInputs, validateOnlyRequired: false, isDeleteOperation: true}, + wantErr: true, + }, + { + name: "MissingRequiredDelete2", + manifest: sampleSolutionAddonManifest2, + args: args{userInputs: emptyInputs, validateOnlyRequired: true, isDeleteOperation: true}, + wantErr: false, + }, + { + name: "NoRequiredFieldsEmptyInputsCreate", + manifest: sampleSolutionAddonManifestNoRequired, + args: args{userInputs: emptyInputs, validateOnlyRequired: true, isDeleteOperation: false}, + wantErr: false, + }, + { + name: "NoRequiredFieldsEmptyInputsDelete", + manifest: sampleSolutionAddonManifestNoRequired, + args: args{userInputs: emptyInputs, validateOnlyRequired: true, isDeleteOperation: true}, + wantErr: false, + }, + { + name: "EmptyAddonInputsRequiredOnlyCreate", + manifest: sampleSolutionAddonManifestEmptyInputs, + args: args{userInputs: emptyInputs, validateOnlyRequired: true, isDeleteOperation: false}, + wantErr: false, + }, + { + name: "EmptyAddonInputsRequiredOnlyDelete", + manifest: sampleSolutionAddonManifestEmptyInputs, + args: args{userInputs: emptyInputs, validateOnlyRequired: true, isDeleteOperation: true}, + wantErr: false, + }, + { + name: "EmptyAddonInputsAllFieldsCreate", + manifest: sampleSolutionAddonManifestEmptyInputs, + args: args{userInputs: emptyInputs, validateOnlyRequired: false, isDeleteOperation: false}, + wantErr: false, + }, + { + name: "EmptyAddonInputsAllFieldsDelete", + manifest: sampleSolutionAddonManifestEmptyInputs, + args: args{userInputs: emptyInputs, validateOnlyRequired: false, isDeleteOperation: true}, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + addOnManifest := make(map[string]any) + err := json.Unmarshal(tt.manifest, &addOnManifest) + if err != nil { + t.Fatalf("error unmarshalling sample Solution Add-On manifest: %s", err) + } + + addon := SolutionAddOn{ + DefinedEntity: &DefinedEntity{DefinedEntity: &types.DefinedEntity{Name: "vmware.ds-1.4.0-23376809"}}, + SolutionAddOnEntity: &types.SolutionAddOn{ + Manifest: addOnManifest, + }, + } + + if err := addon.ValidateInputs(tt.args.userInputs, tt.args.validateOnlyRequired, tt.args.isDeleteOperation); (err != nil) != tt.wantErr { + t.Errorf("SolutionAddOn.ValidateInputs() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestSolutionAddOn_ConvertInputTypes(t *testing.T) { + // emptyInputs := make(map[string]interface{}) + + requiredInputsCreateBool := make(map[string]interface{}) + requiredInputsCreateBool["name"] = "name" + requiredInputsCreateBool["delete-previous-uiplugin-versions"] = true + + requiredInputsCreateString := make(map[string]interface{}) + requiredInputsCreateString["name"] = "name" + requiredInputsCreateString["delete-previous-uiplugin-versions"] = "true" + + requiredInputsCreateBoolWithInput := make(map[string]interface{}) + requiredInputsCreateBoolWithInput["name"] = "name" + requiredInputsCreateBoolWithInput["input-delete-previous-uiplugin-versions"] = true + + requiredInputsCreateStringWithInput := make(map[string]interface{}) + requiredInputsCreateStringWithInput["name"] = "name" + requiredInputsCreateStringWithInput["input-delete-previous-uiplugin-versions"] = "true" + + requiredInputsDelete := make(map[string]interface{}) + requiredInputsDelete["force-delete"] = true + + type args struct { + userInputs map[string]interface{} + // validateOnlyRequired bool + // isDeleteOperation bool + } + + tests := []struct { + name string + manifest []byte + args args + expectedOutput map[string]interface{} + wantErr bool + }{ + { + name: "StringToBool", + manifest: sampleSolutionAddonManifest1, + args: args{userInputs: requiredInputsCreateString}, + expectedOutput: requiredInputsCreateBool, + wantErr: false, + }, + { + name: "StringToString", + manifest: sampleSolutionAddonManifest1, + args: args{userInputs: requiredInputsCreateString}, + expectedOutput: requiredInputsCreateString, + wantErr: true, + }, + { + name: "StringToBoolWithInput", + manifest: sampleSolutionAddonManifest1, + args: args{userInputs: requiredInputsCreateStringWithInput}, + expectedOutput: requiredInputsCreateBoolWithInput, + wantErr: false, + }, + { + name: "StringToStringWithInput", + manifest: sampleSolutionAddonManifest1, + args: args{userInputs: requiredInputsCreateStringWithInput}, + expectedOutput: requiredInputsCreateStringWithInput, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + addOnManifest := make(map[string]any) + err := json.Unmarshal(tt.manifest, &addOnManifest) + if err != nil { + t.Fatalf("error unmarshalling sample Solution Add-On manifest: %s", err) + } + + addon := SolutionAddOn{ + DefinedEntity: &DefinedEntity{DefinedEntity: &types.DefinedEntity{Name: "vmware.ds-1.4.0-23376809"}}, + SolutionAddOnEntity: &types.SolutionAddOn{ + Manifest: addOnManifest, + }, + } + + convertedInputs, err := addon.ConvertInputTypes(tt.args.userInputs) + if (err != nil) && !tt.wantErr { + t.Errorf("SolutionAddOn.ConvertInputTypes() error = %v, wantErr %v", err, tt.wantErr) + } + + if reflect.DeepEqual(convertedInputs, tt.expectedOutput) == tt.wantErr { + diff := pretty.Diff(tt.expectedOutput, convertedInputs) + t.Errorf("SolutionAddOn.ConvertInputTypes() values are not identical\n\n%s", diff) + } + }) + } +} + +var sampleSolutionAddonManifest1 = []byte(` +{ + "name": "ds", + "inputs": [ + { + "name": "delete-previous-uiplugin-versions", + "type": "Boolean", + "title": "Delete Previous UI Plugin Versions", + "default": false, + "required": true, + "description": "If setting true, the installation will delete all previous versions of this ui plugin. If setting false, the installation will just disable previous versions" + }, + { + "name": "force-delete", + "type": "Boolean", + "title": "Force Delete", + "delete": true, + "required": true, + "default": false, + "description": "If setting true, the uninstallation will remove all Data Solution records from Cloud Director but actual Data Solution instances will stay in Kubernetes clusters. If setting false, the uninstallation proceeds only when Data Solution records are not found in Cloud Director." + } + ] +} +`) + +var sampleSolutionAddonManifestNoRequired = []byte(` +{ + "name": "ds", + "inputs": [ + { + "name": "delete-previous-uiplugin-versions", + "type": "Boolean", + "title": "Delete Previous UI Plugin Versions", + "default": false, + "required": false, + "description": "If setting true, the installation will delete all previous versions of this ui plugin. If setting false, the installation will just disable previous versions" + }, + { + "name": "force-delete", + "type": "Boolean", + "title": "Force Delete", + "delete": true, + "default": false, + "description": "If setting true, the uninstallation will remove all Data Solution records from Cloud Director but actual Data Solution instances will stay in Kubernetes clusters. If setting false, the uninstallation proceeds only when Data Solution records are not found in Cloud Director." + } + ] +} +`) + +var sampleSolutionAddonManifestEmptyInputs = []byte(` +{ + "name": "ds", + "inputs": [], + "vendor": "vmware", + "runtime": { + "sdkVersion": "1.1.1.8577774" + } +} +`) + +var sampleSolutionAddonManifest2 = []byte(` +{ + "name": "ose", + "inputs": [ + { + "name": "kube-cluster-location", + "type": "String", + "title": "Kubernetes Cluster Location", + "values": { + "SLZ": "SLZ", + "EXTERNAL": "EXTERNAL" + }, + "default": "SLZ", + "required": true, + "description": "The Kubernetes cluster location is used to specify where Object Storage Extension will be installed. By SLZ, you specify an existing TKG cluster in the Solutions organization. Object Storage Extension Kubernetes Operator will be automatically installed by SLZ. By EXTERNAL, you manually install Object Storage Extension Kubernetes Operator onto a CNCF compliant Kubernetes cluster afterwards. Defaults to SLZ." + }, + { + "name": "vcd-api-token", + "type": "String", + "title": "VCD API Token", + "secure": true, + "required": true, + "description": "This Cloud Director API token of provider administrator user is used for Object Storage Extension Operator installation." + }, + { + "name": "kube-cluster-name", + "type": "String", + "title": "Kubernetes Cluster Name", + "description": "This is the name of an existing TKG cluster in the Solutions organization where you install Object Storage Extension. This parameter is required by SLZ." + }, + { + "name": "registry-url", + "type": "String", + "title": "Container Registry URL", + "default": "projects.registry.vmware.com/vcd-ose", + "validation": "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])(/[A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])*$", + "description": "The container registry URL is used to pull Object Storage Extension container packages during installation, i.e., registry.mydomain.com/myproject. If you host Object Storage Extension container packages in a private registry, you must specify it here." + }, + { + "name": "registry-username", + "type": "String", + "title": "Container Registry User Name", + "description": "The username of the container registry for Basic authentication." + }, + { + "name": "registry-password", + "type": "String", + "title": "Container Registry Password", + "secure": true, + "description": "The password of the container registry for Basic authentication." + }, + { + "name": "registry-ca-bundle", + "type": "String", + "view": "multiline", + "title": "Container Registry CA Bundle", + "description": "This is CA bundle in PEM format of the container registry's TLS certificate." + }, + { + "name": "force-delete", + "type": "Boolean", + "title": "Force Delete", + "delete": true, + "default": true, + "description": "The force-delete is used to control the error handling in case the deletion of Object Storage Extension in the Kubernetes cluster is not completed. When it is true, the add-on will continue to delete itself if the deletion of Object Storage Extension Kubernetes Operator and server runs into an error; otherwise, the add-on stops at the stage where the error occurs. Defaults to true." + }, + { + "name": "deploy-timeout", + "type": "Integer", + "title": "Deploy Timeout", + "default": 3600, + "description": "The deploy timeout is used to set the timeout in seconds for Object Storage Extension Operator installation. Defaults to 3600." + } + ] +} +`) diff --git a/govcd/solution_landing_zone.go b/govcd/solution_landing_zone.go new file mode 100644 index 000000000..8ebf07526 --- /dev/null +++ b/govcd/solution_landing_zone.go @@ -0,0 +1,214 @@ +/* +* Copyright 2024 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "net/url" + "strings" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +// slzRdeType sets Runtime Defined Entity Type to be used across multiple calls +var slzRdeType = [3]string{"vmware", "solutions_organization", "1.0.0"} + +// SolutionLandingZone controls VCD Solution Add-On Landing Zone. It does so by wrapping RDE for +// entity types vmware:solutions_organization:1.0.0. +// +// Up to VCD 10.5.1.1 ,there can only be one single RDE instance for landing zone. +type SolutionLandingZone struct { + // SolutionLandingZoneType defines internal content of RDE (`types.DefinedEntity.State`) + SolutionLandingZoneType *types.SolutionLandingZoneType + // DefinedEntity contains parent defined entity that contains SolutionLandingZoneType in + // "Entity" field + DefinedEntity *DefinedEntity + vcdClient *VCDClient +} + +// CreateSolutionLandingZone configures VCD Solution Add-On Landing Zone. It does so by performing +// the following steps: +// +// 1. Creates Solution Landing Zone RDE based on type urn:vcloud:type:vmware:solutions_organization:1.0.0 +// 2. Resolves the RDE +func (vcdClient *VCDClient) CreateSolutionLandingZone(slzCfg *types.SolutionLandingZoneType) (*SolutionLandingZone, error) { + // 1. Check that RDE type exists + rdeType, err := vcdClient.GetRdeType(slzRdeType[0], slzRdeType[1], slzRdeType[2]) + if err != nil { + return nil, fmt.Errorf("error retrieving RDE Type for Solution Landing zone: %s", err) + } + + // 2. Convert more precise structure to fit DefinedEntity.DefinedEntity.Entity + unmarshalledRdeEntityJson, err := convertAnyToRdeEntity(slzCfg) + if err != nil { + return nil, err + } + + // 3. Construct payload + entityCfg := &types.DefinedEntity{ + EntityType: "urn:vcloud:type:" + strings.Join(slzRdeType[:], ":"), + Name: "Solutions Organization", + State: addrOf("PRE_CREATED"), + // Processed solution landing zone + Entity: unmarshalledRdeEntityJson, + } + + // 4. Create RDE + createdRdeEntity, err := rdeType.CreateRde(*entityCfg, nil) + if err != nil { + return nil, fmt.Errorf("error creating RDE entity: %s", err) + } + + // 5. Resolve RDE + err = createdRdeEntity.Resolve() + if err != nil { + return nil, fmt.Errorf("error resolving Solutions add-on after creating: %s", err) + } + + // 6. Reload RDE + err = createdRdeEntity.Refresh() + if err != nil { + return nil, fmt.Errorf("error refreshing RDE after resolving: %s", err) + } + + result, err := convertRdeEntityToAny[types.SolutionLandingZoneType](createdRdeEntity.DefinedEntity.Entity) + if err != nil { + return nil, err + } + + returnType := SolutionLandingZone{ + SolutionLandingZoneType: result, + vcdClient: vcdClient, + DefinedEntity: createdRdeEntity, + } + + return &returnType, nil +} + +// GetAllSolutionLandingZones retrieves all Solution Landing Zones +// +// Note: Up to VCD 10.5.1.1 there can be only a single RDE entry (one SLZ per VCD) +func (vcdClient *VCDClient) GetAllSolutionLandingZones(queryParameters url.Values) ([]*SolutionLandingZone, error) { + allSlzs, err := vcdClient.GetAllRdes(slzRdeType[0], slzRdeType[1], slzRdeType[2], queryParameters) + if err != nil { + return nil, fmt.Errorf("error retrieving all SLZs: %s", err) + } + + results := make([]*SolutionLandingZone, len(allSlzs)) + for slzRdeIndex, slzRde := range allSlzs { + + slz, err := convertRdeEntityToAny[types.SolutionLandingZoneType](slzRde.DefinedEntity.Entity) + if err != nil { + return nil, fmt.Errorf("error converting RDE to SLZ: %s", err) + } + + results[slzRdeIndex] = &SolutionLandingZone{ + vcdClient: vcdClient, + DefinedEntity: slzRde, + SolutionLandingZoneType: slz, + } + } + + return results, nil +} + +// GetExactlyOneSolutionLandingZone will get single Solution Landing Zone RDE or fail. +// There can be only one Solution Landing Zone in VCD, but because it is backed by RDE - it can +// occur that due to some error there is more than one RDE Entity +func (vcdClient *VCDClient) GetExactlyOneSolutionLandingZone() (*SolutionLandingZone, error) { + allSlzs, err := vcdClient.GetAllSolutionLandingZones(nil) + if err != nil { + return nil, fmt.Errorf("error retrieving all Solution Landing Zones: %s", err) + } + + return oneOrError("rde", strings.Join(slzRdeType[:], ":"), allSlzs) +} + +// GetSolutionLandingZoneById retrieves Solution Landing Zone by ID +// +// Note: defined entity ID must be used that can be accessed either by `SolutionLandingZone.Id()` +// method or directly in `SolutionLandingZone.DefinedEntity.DefinedEntity.ID` field +func (vcdClient *VCDClient) GetSolutionLandingZoneById(id string) (*SolutionLandingZone, error) { + if id == "" { + return nil, fmt.Errorf("id must be specified") + } + rde, err := getRdeById(&vcdClient.Client, id) + if err != nil { + return nil, fmt.Errorf("error retrieving RDE by ID: %s", err) + } + + result, err := convertRdeEntityToAny[types.SolutionLandingZoneType](rde.DefinedEntity.Entity) + if err != nil { + return nil, err + } + + packages := &SolutionLandingZone{ + SolutionLandingZoneType: result, + vcdClient: vcdClient, + DefinedEntity: rde, + } + + return packages, nil +} + +// Refresh reloads parent RDE data +func (slz *SolutionLandingZone) Refresh() error { + err := slz.DefinedEntity.Refresh() + if err != nil { + return err + } + + // Repackage created RDE "Entity" to more exact type + result, err := convertRdeEntityToAny[types.SolutionLandingZoneType](slz.DefinedEntity.DefinedEntity.Entity) + if err != nil { + return err + } + + slz.SolutionLandingZoneType = result + + return nil +} + +// RdeId is a shorthand to retrieve ID of parent runtime defined entity +func (slz *SolutionLandingZone) RdeId() string { + if slz == nil || slz.DefinedEntity == nil || slz.DefinedEntity.DefinedEntity == nil { + return "" + } + + return slz.DefinedEntity.DefinedEntity.ID +} + +// Update Solution Landing Zone +func (slz *SolutionLandingZone) Update(slzCfg *types.SolutionLandingZoneType) (*SolutionLandingZone, error) { + unmarshalledRdeEntityJson, err := convertAnyToRdeEntity(slzCfg) + if err != nil { + return nil, err + } + + slz.DefinedEntity.DefinedEntity.Entity = unmarshalledRdeEntityJson + + err = slz.DefinedEntity.Update(*slz.DefinedEntity.DefinedEntity) + if err != nil { + return nil, err + } + + result, err := convertRdeEntityToAny[types.SolutionLandingZoneType](slz.DefinedEntity.DefinedEntity.Entity) + if err != nil { + return nil, err + } + + packages := SolutionLandingZone{ + SolutionLandingZoneType: result, + vcdClient: slz.vcdClient, + DefinedEntity: slz.DefinedEntity, + } + + return &packages, nil +} + +// Delete removes the RDE that defines Solution Landing Zone +func (slz *SolutionLandingZone) Delete() error { + return slz.DefinedEntity.Delete() +} diff --git a/govcd/solution_landing_zone_test.go b/govcd/solution_landing_zone_test.go new file mode 100644 index 000000000..0d2fe2733 --- /dev/null +++ b/govcd/solution_landing_zone_test.go @@ -0,0 +1,207 @@ +//go:build slz || functional || ALL + +/* +* Copyright 2024 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_CreateLandingZone(check *C) { + if vcd.client.Client.APIVCDMaxVersionIs("< 37.1") { + check.Skip("Solution Landing Zones are supported in VCD 10.4.1+") + } + + if vcd.config.VCD.Nsxt.RoutedNetwork == "" { + check.Skip("Solution Landing Zones require 'vcd.config.VCD.Nsxt.RoutedNetwork' to be present") + } + + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.org.Org.Name) + check.Assert(err, IsNil) + + adminVdc, err := adminOrg.GetAdminVDCById(vcd.nsxtVdc.Vdc.ID, false) + check.Assert(err, IsNil) + + orgNetwork, err := vcd.nsxtVdc.GetOpenApiOrgVdcNetworkByName(vcd.config.VCD.Nsxt.RoutedNetwork) + check.Assert(err, IsNil) + check.Assert(orgNetwork, NotNil) + + computePolicy, err := adminVdc.GetAllAssignedVdcComputePoliciesV2(nil) + check.Assert(err, IsNil) + check.Assert(computePolicy, NotNil) + + storageProfileRef, err := adminVdc.GetDefaultStorageProfileReference() + check.Assert(err, IsNil) + check.Assert(storageProfileRef, NotNil) + + catalog, err := adminOrg.GetCatalogByName(vcd.config.VCD.Catalog.NsxtBackedCatalogName, false) + check.Assert(err, IsNil) + check.Assert(catalog, NotNil) + + slzCfg := &types.SolutionLandingZoneType{ + Name: adminOrg.AdminOrg.Name, + ID: adminOrg.AdminOrg.ID, + Vdcs: []types.SolutionLandingZoneVdc{ + { + ID: adminVdc.AdminVdc.ID, + Name: adminVdc.AdminVdc.Name, + Capabilities: []string{}, + Networks: []types.SolutionLandingZoneVdcChild{ + { + ID: orgNetwork.OpenApiOrgVdcNetwork.ID, + Name: orgNetwork.OpenApiOrgVdcNetwork.Name, + IsDefault: true, + Capabilities: []string{}, + }, + }, + ComputePolicies: []types.SolutionLandingZoneVdcChild{ + { + ID: computePolicy[0].VdcComputePolicyV2.ID, + Name: computePolicy[0].VdcComputePolicyV2.Name, + IsDefault: true, + Capabilities: []string{}, + }, + }, + StoragePolicies: []types.SolutionLandingZoneVdcChild{ + { + ID: storageProfileRef.ID, + Name: storageProfileRef.Name, + IsDefault: true, + Capabilities: []string{}, + }, + }, + }, + }, + Catalogs: []types.SolutionLandingZoneCatalog{ + { + ID: catalog.Catalog.ID, + Name: catalog.Catalog.Name, + Capabilities: []string{}, + }, + }, + } + + slz, err := vcd.client.CreateSolutionLandingZone(slzCfg) + check.Assert(err, IsNil) + check.Assert(slz, NotNil) + + AddToCleanupListOpenApi(slz.DefinedEntity.DefinedEntity.ID, check.TestName(), types.OpenApiPathVersion1_0_0+types.OpenApiEndpointRdeEntities+slz.DefinedEntity.DefinedEntity.ID) + + err = slz.Refresh() + check.Assert(err, IsNil) + + // Get all + allEntries, err := vcd.client.GetAllSolutionLandingZones(nil) + check.Assert(err, IsNil) + + check.Assert(len(allEntries), Equals, 1) + check.Assert(allEntries[0].RdeId(), Equals, slz.RdeId()) + + // Get by ID + slzById, err := vcd.client.GetSolutionLandingZoneById(slz.RdeId()) + check.Assert(err, IsNil) + check.Assert(slzById.RdeId(), Equals, slz.RdeId()) + + // Get exactly one + slzSingle, err := vcd.client.GetExactlyOneSolutionLandingZone() + check.Assert(err, IsNil) + check.Assert(slzSingle.RdeId(), Equals, slz.RdeId()) + + // Update + // Lookup one more Org network and add it + orgNetwork2, err := vcd.nsxtVdc.GetOpenApiOrgVdcNetworkByName(vcd.config.VCD.Nsxt.IsolatedNetwork) + check.Assert(err, IsNil) + check.Assert(orgNetwork2, NotNil) + + slzCfg.Vdcs[0].Networks = append(slzCfg.Vdcs[0].Networks, types.SolutionLandingZoneVdcChild{ + ID: orgNetwork2.OpenApiOrgVdcNetwork.ID, + Name: orgNetwork2.OpenApiOrgVdcNetwork.Name, + IsDefault: false, + Capabilities: []string{}, + }) + + updatedSlz, err := slz.Update(slzCfg) + check.Assert(err, IsNil) + check.Assert(len(updatedSlz.SolutionLandingZoneType.Vdcs[0].Networks), Equals, 2) + + err = slz.Delete() + check.Assert(err, IsNil) + + // Check that no entry exists + slzByIdErr, err := vcd.client.GetSolutionLandingZoneById(slz.RdeId()) + check.Assert(err, NotNil) + check.Assert(slzByIdErr, IsNil) +} + +func createSlz(vcd *TestVCD, check *C) *SolutionLandingZone { + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.org.Org.Name) + check.Assert(err, IsNil) + + adminVdc, err := adminOrg.GetAdminVDCById(vcd.nsxtVdc.Vdc.ID, false) + check.Assert(err, IsNil) + orgNetwork, err := vcd.nsxtVdc.GetOpenApiOrgVdcNetworkByName(vcd.config.VCD.Nsxt.RoutedNetwork) + check.Assert(err, IsNil) + check.Assert(orgNetwork, NotNil) + computePolicy, err := adminVdc.GetAllAssignedVdcComputePoliciesV2(nil) + check.Assert(err, IsNil) + check.Assert(computePolicy, NotNil) + storageProfileRef, err := adminVdc.GetDefaultStorageProfileReference() + check.Assert(err, IsNil) + check.Assert(storageProfileRef, NotNil) + catalog, err := adminOrg.GetCatalogByName(vcd.config.VCD.Catalog.NsxtBackedCatalogName, false) + check.Assert(err, IsNil) + check.Assert(catalog, NotNil) + slzCfg := &types.SolutionLandingZoneType{ + Name: adminOrg.AdminOrg.Name, + ID: adminOrg.AdminOrg.ID, + Vdcs: []types.SolutionLandingZoneVdc{ + { + ID: adminVdc.AdminVdc.ID, + Name: adminVdc.AdminVdc.Name, + Capabilities: []string{}, + Networks: []types.SolutionLandingZoneVdcChild{ + { + ID: orgNetwork.OpenApiOrgVdcNetwork.ID, + Name: orgNetwork.OpenApiOrgVdcNetwork.Name, + IsDefault: true, + Capabilities: []string{}, + }, + }, + ComputePolicies: []types.SolutionLandingZoneVdcChild{ + { + ID: computePolicy[0].VdcComputePolicyV2.ID, + Name: computePolicy[0].VdcComputePolicyV2.Name, + IsDefault: true, + Capabilities: []string{}, + }, + }, + StoragePolicies: []types.SolutionLandingZoneVdcChild{ + { + ID: storageProfileRef.ID, + Name: storageProfileRef.Name, + IsDefault: true, + Capabilities: []string{}, + }, + }, + }, + }, + Catalogs: []types.SolutionLandingZoneCatalog{ + { + ID: catalog.Catalog.ID, + Name: catalog.Catalog.Name, + Capabilities: []string{}, + }, + }, + } + slz, err := vcd.client.CreateSolutionLandingZone(slzCfg) + check.Assert(err, IsNil) + check.Assert(slz, NotNil) + + AddToCleanupListOpenApi(slz.DefinedEntity.DefinedEntity.ID, check.TestName(), types.OpenApiPathVersion1_0_0+types.OpenApiEndpointRdeEntities+slz.DefinedEntity.DefinedEntity.ID) + + return slz +} diff --git a/govcd/system.go b/govcd/system.go index a6277b2ef..532d64d44 100644 --- a/govcd/system.go +++ b/govcd/system.go @@ -6,6 +6,7 @@ package govcd import ( "encoding/xml" + "errors" "fmt" "net/http" "net/url" @@ -16,6 +17,8 @@ import ( "github.com/vmware/go-vcloud-director/v2/util" ) +const labelGlobalDefaultSegmentProfileTemplate = "Global Default Segment Profile Template" + // Simple structure to pass Edge Gateway creation parameters. type EdgeGatewayCreation struct { ExternalNetworks []string // List of external networks to be linked to this gateway @@ -285,6 +288,42 @@ func CreateEdgeGateway(vcdClient *VCDClient, egwc EdgeGatewayCreation) (EdgeGate return createEdgeGateway(vcdClient, egwc, nil) } +func getOrgByHref(vcdClient *Client, href string) (*Org, error) { + org := NewOrg(vcdClient) + + _, err := vcdClient.ExecuteRequest(href, http.MethodGet, + "", "error retrieving org list: %s", nil, org.Org) + if err != nil { + return nil, err + } + + tenantContext, err := org.getTenantContext() + if err != nil { + return nil, err + } + org.TenantContext = tenantContext + + return org, nil +} + +func getAdminOrgByHref(vcdClient *Client, href string) (*AdminOrg, error) { + adminOrg := NewAdminOrg(vcdClient) + + _, err := vcdClient.ExecuteRequest(href, http.MethodGet, + "", "error retrieving org list: %s", nil, adminOrg.AdminOrg) + if err != nil { + return nil, err + } + + tenantContext, err := adminOrg.getTenantContext() + if err != nil { + return nil, err + } + adminOrg.TenantContext = tenantContext + + return adminOrg, nil +} + // If user specifies a valid organization name, then this returns a // organization object. If no valid org is found, it returns an empty // org and no error. Otherwise it returns an error and an empty @@ -391,8 +430,9 @@ func getOrgHREFById(vcdClient *VCDClient, orgId string) (string, error) { // E.g. filter could look like: name==vC1 func QueryVirtualCenters(vcdClient *VCDClient, filter string) ([]*types.QueryResultVirtualCenterRecordType, error) { results, err := vcdClient.QueryWithNotEncodedParams(nil, map[string]string{ - "type": "virtualCenter", - "filter": filter, + "type": "virtualCenter", + "filter": filter, + "filterEncode": "true", }) if err != nil { return nil, err @@ -650,19 +690,23 @@ func CreateExternalNetwork(vcdClient *VCDClient, externalNetworkData *types.Exte externalNetHREF := vcdClient.Client.VCDHREF externalNetHREF.Path += "/admin/extension/externalnets" + if externalNetwork.Configuration == nil { + externalNetwork.Configuration = &types.NetworkConfiguration{} + } externalNetwork.Configuration.FenceMode = "isolated" // Return the task task, err := vcdClient.Client.ExecuteTaskRequest(externalNetHREF.String(), http.MethodPost, types.MimeExternalNetwork, "error instantiating a new ExternalNetwork: %s", externalNetwork) - // Real task in task array - if err == nil { - if task.Task != nil && task.Task.Tasks != nil && len(task.Task.Tasks.Task) == 0 { - return Task{}, fmt.Errorf("create external network task wasn't found") - } - task.Task = task.Task.Tasks.Task[0] + if err != nil { + return Task{}, err } + if task.Task == nil || task.Task.Tasks == nil || len(task.Task.Tasks.Task) == 0 { + return Task{}, fmt.Errorf("create external network task wasn't found") + } + // Real task in task array + task.Task = task.Task.Tasks.Task[0] return task, err } @@ -679,13 +723,43 @@ func getExtension(client *Client) (*types.Extension, error) { return extensions, err } +// GetStorageProfileById fetches a storage profile using its ID. +func (vcdClient *VCDClient) GetStorageProfileById(id string) (*types.VdcStorageProfile, error) { + return getStorageProfileById(&vcdClient.Client, id) +} + +// getStorageProfileById fetches a storage profile using its ID. +func getStorageProfileById(client *Client, id string) (*types.VdcStorageProfile, error) { + storageProfileHref := client.VCDHREF + storageProfileHref.Path += "/admin/vdcStorageProfile/" + extractUuid(id) + + vdcStorageProfile := &types.VdcStorageProfile{} + + _, err := client.ExecuteRequest(storageProfileHref.String(), http.MethodGet, "", "error retrieving storage profile: %s", nil, vdcStorageProfile) + if err != nil { + return nil, err + } + + return vdcStorageProfile, nil +} + // GetStorageProfileByHref fetches storage profile using provided HREF. +// Deprecated: use client.GetStorageProfileByHref or vcdClient.GetStorageProfileByHref func GetStorageProfileByHref(vcdClient *VCDClient, url string) (*types.VdcStorageProfile, error) { + return vcdClient.Client.GetStorageProfileByHref(url) +} + +// GetStorageProfileByHref fetches a storage profile using its HREF. +func (vcdClient *VCDClient) GetStorageProfileByHref(url string) (*types.VdcStorageProfile, error) { + return vcdClient.Client.GetStorageProfileByHref(url) +} + +// GetStorageProfileByHref fetches a storage profile using its HREF. +func (client *Client) GetStorageProfileByHref(url string) (*types.VdcStorageProfile, error) { vdcStorageProfile := &types.VdcStorageProfile{} - _, err := vcdClient.Client.ExecuteRequest(url, http.MethodGet, - "", "error retrieving storage profile: %s", nil, vdcStorageProfile) + _, err := client.ExecuteRequest(url, http.MethodGet, "", "error retrieving storage profile: %s", nil, vdcStorageProfile) if err != nil { return nil, err } @@ -694,6 +768,48 @@ func GetStorageProfileByHref(vcdClient *VCDClient, url string) (*types.VdcStorag } // QueryProviderVdcStorageProfileByName finds a provider VDC storage profile by name +// There are four cases: +// 1. [FOUND] The name matches and is unique among all the storage profiles +// 2. [FOUND] The name matches, it is not unique, and it is disambiguated by the provider VDC HREF +// 3. [NOT FOUND] The name matches, is not unique, but no Provider HREF was given: the search will fail +// 4. [NOT FOUND] The name does not match any of the storage profiles +func (vcdClient *VCDClient) QueryProviderVdcStorageProfileByName(name, providerVDCHref string) (*types.QueryResultProviderVdcStorageProfileRecordType, error) { + + results, err := vcdClient.Client.cumulativeQuery(types.QtProviderVdcStorageProfile, nil, map[string]string{ + "type": types.QtProviderVdcStorageProfile}) + if err != nil { + return nil, err + } + + var recs []*types.QueryResultProviderVdcStorageProfileRecordType + for _, rec := range results.Results.ProviderVdcStorageProfileRecord { + if rec.Name == name { + // Double match: both the name and the provider VDC match: we can return the result + if providerVDCHref != "" && providerVDCHref == rec.ProviderVdcHREF { + return rec, nil + } + // if there is a name match, but no provider VDC was given, we add to the result, and we will check later. + if providerVDCHref == "" { + recs = append(recs, rec) + } + } + } + + providerVDCMessage := "" + if providerVDCHref != "" { + providerVDCMessage = fmt.Sprintf("in provider VDC '%s'", providerVDCHref) + } + if len(recs) == 0 { + return nil, fmt.Errorf("no records found for storage profile '%s' %s", name, providerVDCMessage) + } + if len(recs) > 1 { + return nil, fmt.Errorf("more than 1 record found for storage profile '%s'. Add Provider VDC HREF in the search to disambiguate", name) + } + return recs[0], nil +} + +// QueryProviderVdcStorageProfileByName finds a provider VDC storage profile by name +// Deprecated: wrong implementation. Use VCDClient.QueryProviderVdcStorageProfileByName func QueryProviderVdcStorageProfileByName(vcdCli *VCDClient, name string) ([]*types.QueryResultProviderVdcStorageProfileRecordType, error) { results, err := vcdCli.QueryWithNotEncodedParams(nil, map[string]string{ "type": "providerVdcStorageProfile", @@ -709,8 +825,8 @@ func QueryProviderVdcStorageProfileByName(vcdCli *VCDClient, name string) ([]*ty // QueryNetworkPoolByName finds a network pool by name func QueryNetworkPoolByName(vcdCli *VCDClient, name string) ([]*types.QueryResultNetworkPoolRecordType, error) { - results, err := vcdCli.QueryWithNotEncodedParams(nil, map[string]string{ - "type": "networkPool", + results, err := vcdCli.Client.cumulativeQuery(types.QtNetworkPool, nil, map[string]string{ + "type": types.QtNetworkPool, "filter": fmt.Sprintf("name==%s", url.QueryEscape(name)), "filterEncoded": "true", }) @@ -749,9 +865,7 @@ func (vcdClient *VCDClient) QueryProviderVdcs() ([]*types.QueryResultVMWProvider // QueryNetworkPools gets the list of network pools func (vcdClient *VCDClient) QueryNetworkPools() ([]*types.QueryResultNetworkPoolRecordType, error) { - results, err := vcdClient.QueryWithNotEncodedParams(nil, map[string]string{ - "type": "networkPool", - }) + results, err := vcdClient.Client.cumulativeQuery(types.QtNetworkPool, nil, map[string]string{"type": types.QtNetworkPool}) if err != nil { return nil, err } @@ -759,10 +873,27 @@ func (vcdClient *VCDClient) QueryNetworkPools() ([]*types.QueryResultNetworkPool return results.Results.NetworkPoolRecord, nil } -// QueryProviderVdcStorageProfiles gets the list of provider VDC storage profiles +// QueryProviderVdcStorageProfiles gets the list of provider VDC storage profiles from ALL provider VDCs +// Deprecated: use either client.QueryProviderVdcStorageProfiles or client.QueryAllProviderVdcStorageProfiles func (vcdClient *VCDClient) QueryProviderVdcStorageProfiles() ([]*types.QueryResultProviderVdcStorageProfileRecordType, error) { - results, err := vcdClient.QueryWithNotEncodedParams(nil, map[string]string{ - "type": "providerVdcStorageProfile", + return vcdClient.Client.QueryAllProviderVdcStorageProfiles() +} + +// QueryAllProviderVdcStorageProfiles gets the list of provider VDC storage profiles from ALL provider VDCs +func (client *Client) QueryAllProviderVdcStorageProfiles() ([]*types.QueryResultProviderVdcStorageProfileRecordType, error) { + results, err := client.cumulativeQuery(types.QtProviderVdcStorageProfile, nil, map[string]string{"type": types.QtProviderVdcStorageProfile}) + if err != nil { + return nil, err + } + + return results.Results.ProviderVdcStorageProfileRecord, nil +} + +// QueryProviderVdcStorageProfiles gets the list of provider VDC storage profiles for a given Provider VDC +func (client *Client) QueryProviderVdcStorageProfiles(providerVdcHref string) ([]*types.QueryResultProviderVdcStorageProfileRecordType, error) { + results, err := client.cumulativeQuery(types.QtProviderVdcStorageProfile, nil, map[string]string{ + "type": types.QtProviderVdcStorageProfile, + "filter": fmt.Sprintf("providerVdc==%s", providerVdcHref), }) if err != nil { return nil, err @@ -771,6 +902,12 @@ func (vcdClient *VCDClient) QueryProviderVdcStorageProfiles() ([]*types.QueryRes return results.Results.ProviderVdcStorageProfileRecord, nil } +// QueryCompatibleStorageProfiles retrieves all storage profiles belonging to the same provider VDC to which +// the Org VDC belongs +func (adminVdc *AdminVdc) QueryCompatibleStorageProfiles() ([]*types.QueryResultProviderVdcStorageProfileRecordType, error) { + return adminVdc.client.QueryProviderVdcStorageProfiles(adminVdc.AdminVdc.ProviderVdcReference.HREF) +} + // GetNetworkPoolByHREF functions fetches an network pool using VDC client and network pool href func GetNetworkPoolByHREF(client *VCDClient, href string) (*types.VMWNetworkPool, error) { util.Logger.Printf("[TRACE] Get network pool by HREF: %s\n", href) @@ -799,9 +936,19 @@ func QueryOrgVdcNetworkByName(vcdCli *VCDClient, name string) ([]*types.QueryRes return results.Results.OrgVdcNetworkRecord, nil } +// QueryAllVdcs returns all Org VDCs in a VCD instance +// +// This function requires "System" user or returns an error +func (client *Client) QueryAllVdcs() ([]*types.QueryResultOrgVdcRecordType, error) { + if !client.IsSysAdmin { + return nil, errors.New("this function only works with 'System' user") + } + return queryOrgVdcList(client, nil) +} + // QueryNsxtManagerByName searches for NSX-T managers available in VCD -func (vcdCli *VCDClient) QueryNsxtManagerByName(name string) ([]*types.QueryResultNsxtManagerRecordType, error) { - results, err := vcdCli.QueryWithNotEncodedParams(nil, map[string]string{ +func (vcdClient *VCDClient) QueryNsxtManagerByName(name string) ([]*types.QueryResultNsxtManagerRecordType, error) { + results, err := vcdClient.QueryWithNotEncodedParams(nil, map[string]string{ "type": "nsxTManager", "filter": fmt.Sprintf("name==%s", url.QueryEscape(name)), "filterEncoded": "true", @@ -813,6 +960,32 @@ func (vcdCli *VCDClient) QueryNsxtManagerByName(name string) ([]*types.QueryResu return results.Results.NsxtManagerRecord, nil } +// QueryNsxtManagers retrieves all NSX-T managers available in VCD +func (vcdClient *VCDClient) QueryNsxtManagers() ([]*types.QueryResultNsxtManagerRecordType, error) { + results, err := vcdClient.QueryWithNotEncodedParams(nil, map[string]string{ + "type": "nsxTManager", + }) + if err != nil { + return nil, err + } + + return results.Results.NsxtManagerRecord, nil +} + +// QueryNsxtManagerByHref searches for NSX-T managers available in VCD +func (vcdClient *VCDClient) QueryNsxtManagerByHref(href string) ([]*types.QueryResultNsxtManagerRecordType, error) { + results, err := vcdClient.QueryWithNotEncodedParams(nil, map[string]string{ + "type": "nsxTManager", + "filter": fmt.Sprintf("href==%s", extractUuid(href)), + "filterEncoded": "true", + }) + if err != nil { + return nil, err + } + + return results.Results.NsxtManagerRecord, nil +} + // GetOrgByName finds an Organization by name // On success, returns a pointer to the Org structure and a nil error // On failure, returns a nil pointer and an error @@ -829,7 +1002,10 @@ func (vcdClient *VCDClient) GetOrgByName(orgName string) (*Org, error) { if err != nil { return nil, err } - + org.TenantContext = &TenantContext{ + OrgId: extractUuid(org.Org.ID), + OrgName: org.Org.Name, + } return org, nil } @@ -849,7 +1025,10 @@ func (vcdClient *VCDClient) GetOrgById(orgId string) (*Org, error) { if err != nil { return nil, err } - + org.TenantContext = &TenantContext{ + OrgId: extractUuid(org.Org.ID), + OrgName: org.Org.Name, + } return org, nil } @@ -884,6 +1063,10 @@ func (vcdClient *VCDClient) GetAdminOrgByName(orgName string) (*AdminOrg, error) if err != nil { return nil, err } + adminOrg.TenantContext = &TenantContext{ + OrgId: extractUuid(adminOrg.AdminOrg.ID), + OrgName: adminOrg.AdminOrg.Name, + } return adminOrg, nil } @@ -906,7 +1089,10 @@ func (vcdClient *VCDClient) GetAdminOrgById(orgId string) (*AdminOrg, error) { if err != nil { return nil, err } - + adminOrg.TenantContext = &TenantContext{ + OrgId: extractUuid(adminOrg.AdminOrg.ID), + OrgName: adminOrg.AdminOrg.Name, + } return adminOrg, nil } @@ -951,16 +1137,111 @@ func GetUuidFromHref(href string, idAtEnd bool) (string, error) { } // GetOrgList returns the list ov available orgs -func (vcdCli *VCDClient) GetOrgList() (*types.OrgList, error) { - orgListHREF := vcdCli.Client.VCDHREF +func (vcdClient *VCDClient) GetOrgList() (*types.OrgList, error) { + orgListHREF := vcdClient.Client.VCDHREF orgListHREF.Path += "/org" orgList := new(types.OrgList) - _, err := vcdCli.Client.ExecuteRequest(orgListHREF.String(), http.MethodGet, + _, err := vcdClient.Client.ExecuteRequest(orgListHREF.String(), http.MethodGet, "", "error getting list of organizations: %s", nil, orgList) if err != nil { return nil, err } return orgList, nil } + +// QueryAdminOrgVdcStorageProfileByID finds a StorageProfile of VDC by ID as admin +func QueryAdminOrgVdcStorageProfileByID(vcdCli *VCDClient, id string) (*types.QueryResultAdminOrgVdcStorageProfileRecordType, error) { + if !vcdCli.Client.IsSysAdmin { + return nil, errors.New("can't query type QueryAdminOrgVdcStorageProfileByID as tenant user") + } + results, err := vcdCli.QueryWithNotEncodedParams(nil, map[string]string{ + "type": types.QtAdminOrgVdcStorageProfile, + "filter": fmt.Sprintf("id==%s", url.QueryEscape(id)), + "filterEncoded": "true", + }) + if err != nil { + return nil, err + } + if len(results.Results.AdminOrgVdcStorageProfileRecord) == 0 { + return nil, ErrorEntityNotFound + } + if len(results.Results.AdminOrgVdcStorageProfileRecord) > 1 { + return nil, fmt.Errorf("more than one Storage Profile found with ID %s", id) + } + return results.Results.AdminOrgVdcStorageProfileRecord[0], nil +} + +// queryAdminOrgVdcStorageProfilesByVdcId finds all Storage Profiles of a VDC +func queryAdminOrgVdcStorageProfilesByVdcId(client *Client, vdcId string) ([]*types.QueryResultAdminOrgVdcStorageProfileRecordType, error) { + if !client.IsSysAdmin { + return nil, errors.New("can't query type QueryResultAdminOrgVdcStorageProfileRecordType as Tenant user") + } + results, err := client.QueryWithNotEncodedParams(nil, map[string]string{ + "type": types.QtAdminOrgVdcStorageProfile, + "filter": fmt.Sprintf("vdc==%s", url.QueryEscape(vdcId)), + "filterEncoded": "true", + }) + if err != nil { + return nil, err + } + return results.Results.AdminOrgVdcStorageProfileRecord, nil +} + +// queryOrgVdcStorageProfilesByVdcId finds all Storage Profiles of a VDC +func queryOrgVdcStorageProfilesByVdcId(client *Client, vdcId string) ([]*types.QueryResultOrgVdcStorageProfileRecordType, error) { + if client.IsSysAdmin { + return nil, errors.New("can't query type QueryResultAdminOrgVdcStorageProfileRecordType as System administrator") + } + results, err := client.QueryWithNotEncodedParams(nil, map[string]string{ + "type": types.QtOrgVdcStorageProfile, + "filter": fmt.Sprintf("vdc==%s", url.QueryEscape(vdcId)), + "filterEncoded": "true", + }) + if err != nil { + return nil, err + } + return results.Results.OrgVdcStorageProfileRecord, nil +} + +// QueryOrgVdcStorageProfileByID finds a StorageProfile of VDC by ID +func QueryOrgVdcStorageProfileByID(vcdCli *VCDClient, id string) (*types.QueryResultOrgVdcStorageProfileRecordType, error) { + if vcdCli.Client.IsSysAdmin { + return nil, errors.New("can't query type QueryOrgVdcStorageProfileByID as System administrator") + } + results, err := vcdCli.QueryWithNotEncodedParams(nil, map[string]string{ + "type": types.QtOrgVdcStorageProfile, + "filter": fmt.Sprintf("id==%s", url.QueryEscape(id)), + "filterEncoded": "true", + }) + if err != nil { + return nil, err + } + if len(results.Results.OrgVdcStorageProfileRecord) == 0 { + return nil, ErrorEntityNotFound + } + if len(results.Results.OrgVdcStorageProfileRecord) > 1 { + return nil, fmt.Errorf("more than one Storage Profile found with ID %s", id) + } + return results.Results.OrgVdcStorageProfileRecord[0], nil +} + +// GetGlobalDefaultSegmentProfileTemplates retrieves VCD global configuration for Segment Profile Templates +func (vcdClient *VCDClient) GetGlobalDefaultSegmentProfileTemplates() (*types.NsxtGlobalDefaultSegmentProfileTemplate, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtGlobalDefaultSegmentProfileTemplates, + entityLabel: labelGlobalDefaultSegmentProfileTemplate, + } + + return getInnerEntity[types.NsxtGlobalDefaultSegmentProfileTemplate](&vcdClient.Client, c) +} + +// UpdateGlobalDefaultSegmentProfileTemplates updates VCD global configuration for Segment Profile Templates +func (vcdClient *VCDClient) UpdateGlobalDefaultSegmentProfileTemplates(entityConfig *types.NsxtGlobalDefaultSegmentProfileTemplate) (*types.NsxtGlobalDefaultSegmentProfileTemplate, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtGlobalDefaultSegmentProfileTemplates, + entityLabel: labelGlobalDefaultSegmentProfileTemplate, + } + return updateInnerEntity(&vcdClient.Client, c, entityConfig) +} diff --git a/govcd/system_test.go b/govcd/system_test.go index 312d8070b..74fb4a8de 100644 --- a/govcd/system_test.go +++ b/govcd/system_test.go @@ -1,4 +1,4 @@ -// +build system functional ALL +//go:build system || functional || ALL /* * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -8,6 +8,7 @@ package govcd import ( "fmt" + "strings" . "gopkg.in/check.v1" @@ -118,6 +119,11 @@ func (vcd *TestVCD) Test_CreateOrg(check *C) { } orgName := TestCreateOrg + "_" + od.name + if vcd.client.Client.APIVCDMaxVersionIs("= 37.2") && !od.enabled { + // TODO revisit once bug is fixed in VCD + fmt.Println("[INFO] VCD 10.4.2 has a bug that prevents creating a disabled Org - Changing 'enabled' parameter to 'true'") + od.enabled = true + } fmt.Printf("# org %s (enabled: %v - catalogs: %v [%d %d])\n", orgName, od.enabled, od.canPublishCatalogs, od.storedVmQuota, od.deployedVmQuota) settings.OrgGeneralSettings.CanPublishCatalogs = od.canPublishCatalogs settings.OrgGeneralSettings.DeployedVMQuota = od.deployedVmQuota @@ -150,7 +156,7 @@ func (vcd *TestVCD) Test_CreateOrg(check *C) { } func (vcd *TestVCD) Test_CreateDeleteEdgeGateway(check *C) { - + vcd.skipIfNotSysAdmin(check) if vcd.config.VCD.ExternalNetwork == "" { check.Skip("No external network provided") } @@ -258,18 +264,18 @@ func (vcd *TestVCD) Test_CreateDeleteEdgeGatewayAdvanced(check *C) { Name: edgeName, Description: edgeName, Configuration: &types.GatewayConfiguration{ - HaEnabled: takeBoolPointer(false), + HaEnabled: addrOf(false), GatewayBackingConfig: "compact", GatewayInterfaces: &types.GatewayInterfaces{ GatewayInterface: []*types.GatewayInterface{}, }, - AdvancedNetworkingEnabled: takeBoolPointer(true), - DistributedRoutingEnabled: takeBoolPointer(false), - UseDefaultRouteForDNSRelay: takeBoolPointer(true), + AdvancedNetworkingEnabled: addrOf(true), + DistributedRoutingEnabled: addrOf(false), + UseDefaultRouteForDNSRelay: addrOf(true), }, } - edgeGatewayConfig.Configuration.FipsModeEnabled = takeBoolPointer(false) + edgeGatewayConfig.Configuration.FipsModeEnabled = addrOf(false) // Create subnet participation structure subnetParticipation := make([]*types.SubnetParticipation, len(externalNetwork.Configuration.IPScopes.IPScope)) @@ -374,6 +380,7 @@ func (vcd *TestVCD) Test_FindBadlyNamedStorageProfile(check *C) { // Test getting network pool by href and vdc client func (vcd *TestVCD) Test_GetNetworkPoolByHREF(check *C) { + vcd.skipIfNotSysAdmin(check) if vcd.config.VCD.ProviderVdc.NetworkPool == "" { check.Skip("Skipping test because network pool is not configured") } @@ -453,7 +460,7 @@ func (vcd *TestVCD) Test_QueryOrgVdcNetworkByNameWithSpace(check *C) { } check.Assert(task.Task.HREF, Not(Equals), "") - AddToCleanupList(networkName, "network", vcd.org.Org.Name+"|"+vcd.vdc.Vdc.Name, "Test_CreateOrgVdcNetworkDirect") + AddToCleanupList(networkName, "network", vcd.org.Org.Name+"|"+vcd.vdc.Vdc.Name, check.TestName()) // err = task.WaitTaskCompletion() err = task.WaitInspectTaskCompletion(LogTask, 10) @@ -467,9 +474,16 @@ func (vcd *TestVCD) Test_QueryOrgVdcNetworkByNameWithSpace(check *C) { check.Assert(len(orgVdcNetwork), Not(Equals), 0) check.Assert(orgVdcNetwork[0].Name, Equals, networkName) check.Assert(orgVdcNetwork[0].ConnectedTo, Equals, externalNetwork.ExternalNetwork.Name) + network, err := vcd.vdc.GetOrgVdcNetworkByName(networkName, true) + check.Assert(err, IsNil) + task, err = network.Delete() + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) } func (vcd *TestVCD) Test_QueryProviderVdcEntities(check *C) { + vcd.skipIfNotSysAdmin(check) providerVdcName := vcd.config.VCD.ProviderVdc.Name networkPoolName := vcd.config.VCD.ProviderVdc.NetworkPool storageProfileName := vcd.config.VCD.ProviderVdc.StorageProfile @@ -519,7 +533,7 @@ func (vcd *TestVCD) Test_QueryProviderVdcEntities(check *C) { if storageProfileName == "" { check.Skip("Skipping storage profile query: no storage profile was given") } - storageProfiles, err := vcd.client.QueryProviderVdcStorageProfiles() + storageProfiles, err := vcd.client.Client.QueryAllProviderVdcStorageProfiles() check.Assert(err, IsNil) check.Assert(len(storageProfiles) > 0, Equals, true) storageProfileFound := false @@ -542,6 +556,7 @@ func (vcd *TestVCD) Test_QueryProviderVdcEntities(check *C) { } func (vcd *TestVCD) Test_QueryProviderVdcByName(check *C) { + vcd.skipIfNotSysAdmin(check) if vcd.config.VCD.ProviderVdc.Name == "" { check.Skip("Skipping Provider VDC query: no provider VDC was given") } @@ -567,7 +582,77 @@ func (vcd *TestVCD) Test_QueryProviderVdcByName(check *C) { } +func (vcd *TestVCD) Test_QueryAdminOrgVdcStorageProfileByID(check *C) { + if !vcd.client.Client.IsSysAdmin { + check.Skip("Skipping Admin VDC StorageProfile query: can't query as tenant user") + } + if vcd.config.VCD.StorageProfile.SP1 == "" { + check.Skip("Skipping VDC StorageProfile query: no StorageProfile ID was given") + } + ref, err := vcd.vdc.FindStorageProfileReference(vcd.config.VCD.StorageProfile.SP1) + check.Assert(err, IsNil) + expectedStorageProfileID, err := GetUuidFromHref(ref.HREF, true) + check.Assert(err, IsNil) + vdcStorageProfile, err := QueryAdminOrgVdcStorageProfileByID(vcd.client, ref.ID) + check.Assert(err, IsNil) + + storageProfileFound := false + + storageProfileID, err := GetUuidFromHref(vdcStorageProfile.HREF, true) + check.Assert(err, IsNil) + if storageProfileID == expectedStorageProfileID { + storageProfileFound = true + } + + if testVerbose { + fmt.Printf("StorageProfile %s\n", vdcStorageProfile.Name) + fmt.Printf("\t href %s\n", vdcStorageProfile.HREF) + fmt.Printf("\t enabled %v\n", vdcStorageProfile.IsEnabled) + fmt.Println("") + } + + check.Assert(storageProfileFound, Equals, true) +} + +func (vcd *TestVCD) Test_QueryOrgVdcStorageProfileByID(check *C) { + if vcd.config.VCD.StorageProfile.SP1 == "" { + check.Skip("Skipping VDC StorageProfile query: no StorageProfile ID was given") + } + + // Setup Org user and connection + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + + orgUserVcdClient, _, err := newOrgUserConnection(adminOrg, "query-org-vdc-storage-profile-by-id", "CHANGE-ME", vcd.config.Provider.Url, true) + check.Assert(err, IsNil) + + ref, err := vcd.vdc.FindStorageProfileReference(vcd.config.VCD.StorageProfile.SP1) + check.Assert(err, IsNil) + expectedStorageProfileID, err := GetUuidFromHref(ref.HREF, true) + check.Assert(err, IsNil) + vdcStorageProfile, err := QueryOrgVdcStorageProfileByID(orgUserVcdClient, ref.ID) + check.Assert(err, IsNil) + + storageProfileFound := false + + storageProfileID, err := GetUuidFromHref(vdcStorageProfile.HREF, true) + check.Assert(err, IsNil) + if storageProfileID == expectedStorageProfileID { + storageProfileFound = true + } + + if testVerbose { + fmt.Printf("StorageProfile %s\n", vdcStorageProfile.Name) + fmt.Printf("\t href %s\n", vdcStorageProfile.HREF) + fmt.Printf("\t enabled %v\n", vdcStorageProfile.IsEnabled) + fmt.Println("") + } + + check.Assert(storageProfileFound, Equals, true) +} + func (vcd *TestVCD) Test_QueryNetworkPoolByName(check *C) { + vcd.skipIfNotSysAdmin(check) if vcd.config.VCD.ProviderVdc.NetworkPool == "" { check.Skip("Skipping Provider VDC network pool query: no provider VDC network pool was given") } @@ -591,8 +676,8 @@ func (vcd *TestVCD) Test_QueryNetworkPoolByName(check *C) { } -// Test getting storage profile by href and vdc client -func (vcd *TestVCD) Test_GetStorageProfileByHref(check *C) { +// Test_GetStorageProfile tests all the getters of Storage Profile +func (vcd *TestVCD) Test_GetStorageProfile(check *C) { if vcd.config.VCD.ProviderVdc.StorageProfile == "" { check.Skip("Skipping test because storage profile is not configured") } @@ -608,10 +693,17 @@ func (vcd *TestVCD) Test_GetStorageProfileByHref(check *C) { check.Assert(adminVdc, NotNil) // Get storage profile by href - foundStorageProfile, err := GetStorageProfileByHref(vcd.client, adminVdc.AdminVdc.VdcStorageProfiles.VdcStorageProfile[0].HREF) + foundStorageProfile, err := vcd.client.Client.GetStorageProfileByHref(adminVdc.AdminVdc.VdcStorageProfiles.VdcStorageProfile[0].HREF) check.Assert(err, IsNil) - check.Assert(foundStorageProfile, Not(Equals), types.VdcStorageProfile{}) check.Assert(foundStorageProfile, NotNil) + check.Assert(foundStorageProfile.IopsSettings, NotNil) + check.Assert(foundStorageProfile, Not(Equals), types.VdcStorageProfile{}) + check.Assert(foundStorageProfile.IopsSettings, Not(Equals), types.VdcStorageProfileIopsSettings{}) + + // Get storage profile by ID + foundStorageProfile2, err := vcd.client.GetStorageProfileById(foundStorageProfile.ID) + check.Assert(err, IsNil) + check.Assert(foundStorageProfile, DeepEquals, foundStorageProfile2) } func (vcd *TestVCD) Test_GetOrgList(check *C) { @@ -630,3 +722,192 @@ func (vcd *TestVCD) Test_GetOrgList(check *C) { check.Assert(foundOrg, Equals, true) } } + +func (vcd *TestVCD) TestQueryAllVdcs(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + + allVdcs, err := vcd.client.Client.QueryAllVdcs() + check.Assert(err, IsNil) + + // Check for at least that many VDCs in VCD + // expectedVdcCountInSystem = 1 NSX-V VDC + expectedVdcCountInSystem := 1 + // If an NSX-T VDC exists - then expected count of VDCs is at least 2 + if vcd.config.VCD.Nsxt.Vdc != "" { + expectedVdcCountInSystem++ + } + + if testVerbose { + fmt.Printf("# List contains at least %d VDCs.", expectedVdcCountInSystem) + } + check.Assert(len(allVdcs) >= expectedVdcCountInSystem, Equals, true) + // Check that known VDCs are inside the list + + knownVdcs := []string{vcd.config.VCD.Vdc} + if vcd.config.VCD.Nsxt.Vdc != "" { + knownVdcs = append(knownVdcs, vcd.config.VCD.Nsxt.Vdc) + } + + foundVdcNames := make([]string, len(allVdcs)) + for vdcIndex, vdc := range allVdcs { + foundVdcNames[vdcIndex] = vdc.Name + } + + if testVerbose { + fmt.Printf("# Checking result contains all known VDCs (%s).", strings.Join((knownVdcs), ", ")) + } + for _, knownVdcName := range knownVdcs { + check.Assert(contains(knownVdcName, foundVdcNames), Equals, true) + } +} + +func (vcd *TestVCD) Test_NsxtGlobalDefaultSegmentProfileTemplate(check *C) { + skipNoNsxtConfiguration(vcd, check) + vcd.skipIfNotSysAdmin(check) + + nsxtManager, err := vcd.client.GetNsxtManagerByName(vcd.config.VCD.Nsxt.Manager) + check.Assert(err, IsNil) + check.Assert(nsxtManager, NotNil) + nsxtManagerUrn, err := nsxtManager.Urn() + check.Assert(err, IsNil) + + // Filter by NSX-T Manager + queryParams := copyOrNewUrlValues(nil) + queryParams = queryParameterFilterAnd(fmt.Sprintf("nsxTManagerRef.id==%s", nsxtManagerUrn), queryParams) + + // Lookup prerequisite profiles for Segment Profile template creation + ipDiscoveryProfile, err := vcd.client.GetIpDiscoveryProfileByName(vcd.config.VCD.Nsxt.IpDiscoveryProfile, queryParams) + check.Assert(err, IsNil) + macDiscoveryProfile, err := vcd.client.GetMacDiscoveryProfileByName(vcd.config.VCD.Nsxt.MacDiscoveryProfile, queryParams) + check.Assert(err, IsNil) + spoofGuardProfile, err := vcd.client.GetSpoofGuardProfileByName(vcd.config.VCD.Nsxt.SpoofGuardProfile, queryParams) + check.Assert(err, IsNil) + qosProfile, err := vcd.client.GetQoSProfileByName(vcd.config.VCD.Nsxt.QosProfile, queryParams) + check.Assert(err, IsNil) + segmentSecurityProfile, err := vcd.client.GetSegmentSecurityProfileByName(vcd.config.VCD.Nsxt.SegmentSecurityProfile, queryParams) + check.Assert(err, IsNil) + + config := &types.NsxtSegmentProfileTemplate{ + Name: check.TestName(), + Description: check.TestName() + "-description", + IPDiscoveryProfile: &types.Reference{ID: ipDiscoveryProfile.ID}, + MacDiscoveryProfile: &types.Reference{ID: macDiscoveryProfile.ID}, + QosProfile: &types.Reference{ID: qosProfile.ID}, + SegmentSecurityProfile: &types.Reference{ID: segmentSecurityProfile.ID}, + SpoofGuardProfile: &types.Reference{ID: spoofGuardProfile.ID}, + SourceNsxTManagerRef: &types.OpenApiReference{ID: nsxtManager.NsxtManager.ID}, + } + + createdSegmentProfileTemplate, err := vcd.client.CreateSegmentProfileTemplate(config) + check.Assert(err, IsNil) + check.Assert(createdSegmentProfileTemplate, NotNil) + + // Add to cleanup list + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtSegmentProfileTemplates + createdSegmentProfileTemplate.NsxtSegmentProfileTemplate.ID + AddToCleanupListOpenApi(config.Name, check.TestName(), openApiEndpoint) + + // Set global profile template + globalDefaultSegmentProfileConfig := &types.NsxtGlobalDefaultSegmentProfileTemplate{ + VappNetworkSegmentProfileTemplateRef: &types.OpenApiReference{ID: createdSegmentProfileTemplate.NsxtSegmentProfileTemplate.ID}, + VdcNetworkSegmentProfileTemplateRef: &types.OpenApiReference{ID: createdSegmentProfileTemplate.NsxtSegmentProfileTemplate.ID}, + } + + updatedDefaults, err := vcd.client.UpdateGlobalDefaultSegmentProfileTemplates(globalDefaultSegmentProfileConfig) + check.Assert(err, IsNil) + check.Assert(updatedDefaults, NotNil) + check.Assert(updatedDefaults.VappNetworkSegmentProfileTemplateRef.ID, Equals, createdSegmentProfileTemplate.NsxtSegmentProfileTemplate.ID) + check.Assert(updatedDefaults.VdcNetworkSegmentProfileTemplateRef.ID, Equals, createdSegmentProfileTemplate.NsxtSegmentProfileTemplate.ID) + + openApiEndpoint = types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointNsxtGlobalDefaultSegmentProfileTemplates + PrependToCleanupList(openApiEndpoint, "OpenApiEntityGlobalDefaultSegmentProfileTemplate", "", check.TestName()) + + // Cleanup + resetDefaults, err := vcd.client.UpdateGlobalDefaultSegmentProfileTemplates(&types.NsxtGlobalDefaultSegmentProfileTemplate{}) + check.Assert(err, IsNil) + check.Assert(resetDefaults, NotNil) + check.Assert(resetDefaults.VappNetworkSegmentProfileTemplateRef, IsNil) + check.Assert(resetDefaults.VdcNetworkSegmentProfileTemplateRef, IsNil) + + err = createdSegmentProfileTemplate.Delete() + check.Assert(err, IsNil) +} + +// Test retrieval of all Orgs +func (vcd *TestVCD) Test_QueryAllOrgs(check *C) { + vcd.skipIfNotSysAdmin(check) + if vcd.config.VCD.Org == "" { + check.Skip("Test_QueryOrgByName: Org Name not given") + return + } + + orgs, err := vcd.client.QueryAllOrgs() + check.Assert(err, IsNil) + check.Assert(orgs, NotNil) + + foundOrg := false + for _, org := range orgs { + if org.Name == vcd.config.VCD.Org { + foundOrg = true + } + } + check.Assert(foundOrg, Equals, true) +} + +// Tests Org retrieval by name, by ID, and by a combination of name and ID +func (vcd *TestVCD) Test_QueryOrgByName(check *C) { + vcd.skipIfNotSysAdmin(check) + if vcd.config.VCD.Org == "" { + check.Skip("Test_QueryOrgByName: Org Name not given") + return + } + + org, err := vcd.client.QueryOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + + orgFound := false + if vcd.config.VCD.Org == org.Name { + orgFound = true + } + + if testVerbose { + fmt.Printf("Org %s\n", org.Name) + fmt.Printf("\t href %s\n", org.HREF) + fmt.Printf("\t enabled %v\n", org.IsEnabled) + fmt.Println("") + } + + check.Assert(orgFound, Equals, true) +} + +// Tests Org retrieval by name, by ID, and by a combination of name and ID +func (vcd *TestVCD) Test_QueryOrgById(check *C) { + vcd.skipIfNotSysAdmin(check) + if vcd.config.VCD.Org == "" { + check.Skip("Test_QueryOrgByName: Org Name not given") + return + } + + namedOrg, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + + orgFound := false + if vcd.config.VCD.Org == namedOrg.Org.Name { + + idOrg, err := vcd.client.QueryOrgByID(namedOrg.Org.ID) + check.Assert(err, IsNil) + + if idOrg.HREF == namedOrg.Org.HREF { + orgFound = true + } + + if testVerbose { + fmt.Printf("Org %s\n", namedOrg.Org.Name) + fmt.Printf("\t Org HREF (by Name): %s\n", namedOrg.Org.HREF) + fmt.Printf("\t Org HREF (by ID): %s\n", idOrg.HREF) + fmt.Println("") + } + } + check.Assert(orgFound, Equals, true) +} diff --git a/govcd/system_unit_test.go b/govcd/system_unit_test.go index 7eb89950b..160bc86c3 100644 --- a/govcd/system_unit_test.go +++ b/govcd/system_unit_test.go @@ -1,4 +1,4 @@ -// +build unit ALL +//go:build unit || ALL /* * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. diff --git a/govcd/task.go b/govcd/task.go index 618db133b..9a57bc1a1 100644 --- a/govcd/task.go +++ b/govcd/task.go @@ -1,5 +1,5 @@ /* - * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + * Copyright 2022 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. */ package govcd @@ -10,6 +10,7 @@ import ( "net/url" "os" "strconv" + "strings" "time" "github.com/vmware/go-vcloud-director/v2/types/v56" @@ -28,11 +29,14 @@ func NewTask(cli *Client) *Task { } } -// If the error is not nil, composes an error message -// made of the error itself + the information from the task's Error component. +const errorRetrievingTask = "error retrieving task" + +// getErrorMessage composes a new error message, if the error is not nil. +// The message is made of the error itself + the information from the task's Error component. // See: -// https://code.vmware.com/apis/220/vcloud#/doc/doc/types/TaskType.html -// https://code.vmware.com/apis/220/vcloud#/doc/doc/types/ErrorType.html +// +// https://code.vmware.com/apis/220/vcloud#/doc/doc/types/TaskType.html +// https://code.vmware.com/apis/220/vcloud#/doc/doc/types/ErrorType.html func (task *Task) getErrorMessage(err error) string { errorMessage := "" if err != nil { @@ -48,19 +52,20 @@ func (task *Task) getErrorMessage(err error) string { return errorMessage } +// Refresh retrieves a fresh copy of the task func (task *Task) Refresh() error { if task.Task == nil { return fmt.Errorf("cannot refresh, Object is empty") } - refreshUrl, _ := url.ParseRequestURI(task.Task.HREF) + refreshUrl := urlParseRequestURI(task.Task.HREF) req := task.client.NewRequest(map[string]string{}, http.MethodGet, *refreshUrl, nil) resp, err := checkResp(task.client.Http.Do(req)) if err != nil { - return fmt.Errorf("error retrieving task: %s", err) + return fmt.Errorf("%s: %s", errorRetrievingTask, err) } // Empty struct before a new unmarshal, otherwise we end up with duplicate @@ -75,7 +80,7 @@ func (task *Task) Refresh() error { return nil } -// This callback function can be passed to task.WaitInspectTaskCompletion +// InspectionFunc is a callback function that can be passed to task.WaitInspectTaskCompletion // to perform user defined operations // * task is the task object being processed // * howManyTimes is the number of times the task has been refreshed @@ -84,7 +89,10 @@ func (task *Task) Refresh() error { // * last is true if the function is being called for the last time. type InspectionFunc func(task *types.Task, howManyTimes int, elapsed time.Duration, first, last bool) -// Customizable version of WaitTaskCompletion. +// TaskMonitoringFunc can run monitoring operations on a task +type TaskMonitoringFunc func(*types.Task) + +// WaitInspectTaskCompletion is a customizable version of WaitTaskCompletion. // Users can define the sleeping duration and an optional callback function for // extra monitoring. func (task *Task) WaitInspectTaskCompletion(inspectionFunc InspectionFunc, delay time.Duration) error { @@ -101,7 +109,7 @@ func (task *Task) WaitInspectTaskCompletion(inspectionFunc InspectionFunc, delay elapsed := time.Since(startTime) err := task.Refresh() if err != nil { - return fmt.Errorf("error retrieving task: %s", err) + return fmt.Errorf("%s : %s", errorRetrievingTask, err) } // If an inspection function is provided, we pass information about the task processing: @@ -113,13 +121,13 @@ func (task *Task) WaitInspectTaskCompletion(inspectionFunc InspectionFunc, delay // It's up to the inspection function to render this information fittingly. // If task is not in a waiting status we're done, check if there's an error and return it. - if task.Task.Status != "queued" && task.Task.Status != "preRunning" && task.Task.Status != "running" { + if !isTaskRunning(task.Task.Status) { if inspectionFunc != nil { inspectionFunc(task.Task, howManyTimesRefreshed, elapsed, - howManyTimesRefreshed == 1, // first - task.Task.Status == "error" || task.Task.Status == "success", // last + howManyTimesRefreshed == 1, // first + isTaskCompleteOrError(task.Task.Status), // last ) } if task.Task.Status == "error" { @@ -141,6 +149,8 @@ func (task *Task) WaitInspectTaskCompletion(inspectionFunc InspectionFunc, delay inspectionFunc = SimpleLogTask // writes a summary line for the task to the log case "simple_show": inspectionFunc = SimpleShowTask // writes a summary line for the task to the screen + case "minimal_show": + inspectionFunc = MinimalShowTask // writes a dot for each iteration, or "+" for success, "-" for failure } } } @@ -158,12 +168,13 @@ func (task *Task) WaitInspectTaskCompletion(inspectionFunc InspectionFunc, delay } } -// Checks the status of the task every 3 seconds and returns when the +// WaitTaskCompletion checks the status of the task every 3 seconds and returns when the // task is either completed or failed func (task *Task) WaitTaskCompletion() error { return task.WaitInspectTaskCompletion(nil, 3*time.Second) } +// GetTaskProgress retrieves the task progress as a string func (task *Task) GetTaskProgress() (string, error) { if task.Task == nil { return "", fmt.Errorf("cannot refresh, Object is empty") @@ -171,7 +182,7 @@ func (task *Task) GetTaskProgress() (string, error) { err := task.Refresh() if err != nil { - return "", fmt.Errorf("error retreiving task: %s", err) + return "", fmt.Errorf("error retrieving task: %s", err) } if task.Task.Status == "error" { @@ -181,18 +192,307 @@ func (task *Task) GetTaskProgress() (string, error) { return strconv.Itoa(task.Task.Progress), nil } +// CancelTask attempts a task cancellation, returning an error if cancellation fails func (task *Task) CancelTask() error { cancelTaskURL, err := url.ParseRequestURI(task.Task.HREF + "/action/cancel") if err != nil { - util.Logger.Printf("[Error] Error cancelling task %v: %s", cancelTaskURL.String(), err) + util.Logger.Printf("[CancelTask] Error parsing task request URI %v: %s", cancelTaskURL.String(), err) return err } request := task.client.NewRequest(map[string]string{}, http.MethodPost, *cancelTaskURL, nil) _, err = checkResp(task.client.Http.Do(request)) if err != nil { - util.Logger.Printf("[Error] Error cancelling task %v: %s", cancelTaskURL.String(), err) + util.Logger.Printf("[CancelTask] Error cancelling task %v: %s", cancelTaskURL.String(), err) return err } + util.Logger.Printf("[CancelTask] task %s CANCELED\n", task.Task.ID) + return nil +} + +// ResourceInProgress returns true if any of the provided tasks is still running +func ResourceInProgress(tasksInProgress *types.TasksInProgress) bool { + util.Logger.Printf("[TRACE] ResourceInProgress - has tasks %v\n", tasksInProgress != nil) + if tasksInProgress == nil { + return false + } + tasks := tasksInProgress.Task + for _, task := range tasks { + if isTaskCompleteOrError(task.Status) { + continue + } + if isTaskRunning(task.Status) { + return true + } + } + return false +} + +// ResourceComplete return true is none of its tasks are running +func ResourceComplete(tasksInProgress *types.TasksInProgress) bool { + util.Logger.Printf("[TRACE] ResourceComplete - has tasks %v\n", tasksInProgress != nil) + return !ResourceInProgress(tasksInProgress) +} + +// WaitResource waits for the tasks associated to a given resource to complete +func WaitResource(refresh func() (*types.TasksInProgress, error)) error { + util.Logger.Printf("[TRACE] WaitResource \n") + tasks, err := refresh() + if tasks == nil { + return nil + } + for err == nil { + time.Sleep(time.Second) + tasks, err = refresh() + if err != nil { + return err + } + if tasks == nil || ResourceComplete(tasks) { + return nil + } + } + return nil +} + +// SkimTasksList checks a list of tasks and returns a list of tasks still in progress and a list of failed ones +func SkimTasksList(taskList []*Task) ([]*Task, []*Task, error) { + return SkimTasksListMonitor(taskList, nil) +} + +// SkimTasksListMonitor checks a list of tasks and returns a list of tasks in progress and a list of failed ones +// It can optionally do something with each task by means of a monitoring function +func SkimTasksListMonitor(taskList []*Task, monitoringFunc TaskMonitoringFunc) ([]*Task, []*Task, error) { + var newTaskList []*Task + var errorList []*Task + for _, task := range taskList { + if task == nil { + continue + } + err := task.Refresh() + if err != nil { + if strings.Contains(err.Error(), errorRetrievingTask) { + // Task was not found. Probably expired. We don't need it anymore + continue + } + return newTaskList, errorList, err + } + if monitoringFunc != nil { + monitoringFunc(task.Task) + } + // if a cancellation was requested, we can ignore the task + if task.Task.CancelRequested { + continue + } + // If the task was completed successfully, or it was abandoned, we don't need further processing + if isTaskComplete(task.Task.Status) { + continue + } + // if the task failed, we add it to the special list + if task.Task.Status == "error" && !task.Task.CancelRequested { + errorList = append(errorList, task) + continue + } + // If the task is running, we add it to the list that will continue to be monitored + if isTaskRunning(task.Task.Status) { + newTaskList = append(newTaskList, task) + } + } + return newTaskList, errorList, nil +} + +// isTaskRunning returns true if the task has started or is about to start +func isTaskRunning(status string) bool { + return status == "running" || status == "preRunning" || status == "queued" +} + +// isTaskComplete returns true if the task has finished successfully or was interrupted, but not if it finished with error +func isTaskComplete(status string) bool { + return status == "success" || status == "aborted" +} + +// isTaskCompleteOrError returns true if the status has finished, regardless of the outcome +func isTaskCompleteOrError(status string) bool { + return isTaskComplete(status) || status == "error" +} + +// WaitTaskListCompletion continuously skims the task list until no tasks in progress are left +func WaitTaskListCompletion(taskList []*Task) ([]*Task, error) { + return WaitTaskListCompletionMonitor(taskList, nil) +} + +// WaitTaskListCompletionMonitor continuously skims the task list until no tasks in progress are left +// Using a TaskMonitoringFunc, it can display or log information as the list reduction happens +func WaitTaskListCompletionMonitor(taskList []*Task, f TaskMonitoringFunc) ([]*Task, error) { + var failedTaskList []*Task + var err error + for len(taskList) > 0 { + taskList, failedTaskList, err = SkimTasksListMonitor(taskList, f) + if err != nil { + return failedTaskList, err + } + time.Sleep(3 * time.Second) + } + if len(failedTaskList) == 0 { + return nil, nil + } + return failedTaskList, fmt.Errorf("%d tasks have failed", len(failedTaskList)) +} + +// GetTaskByHREF retrieves a task by its HREF +func (client *Client) GetTaskByHREF(taskHref string) (*Task, error) { + task := NewTask(client) + + _, err := client.ExecuteRequest(taskHref, http.MethodGet, + "", "error retrieving task: %s", nil, task.Task) + if err != nil { + return nil, fmt.Errorf("%s : %s", ErrorEntityNotFound, err) + } + + return task, nil +} + +// GetTaskById retrieves a task by ID +func (client *Client) GetTaskById(taskId string) (*Task, error) { + // Builds the task HREF using the VCD HREF + /task/{ID} suffix + taskHref, err := url.JoinPath(client.VCDHREF.String(), "task", extractUuid(taskId)) + if err != nil { + return nil, err + } + return client.GetTaskByHREF(taskHref) +} + +// SkimTasksList checks a list of task IDs and returns a list of IDs for tasks in progress and a list of IDs for failed ones +func (client Client) SkimTasksList(taskIdList []string) ([]string, []string, error) { + var seenTasks = make(map[string]bool) + var newTaskList []string + var errorList []string + for i, taskId := range taskIdList { + _, seen := seenTasks[taskId] + if seen { + continue + } + seenTasks[taskId] = true + task, err := client.GetTaskById(taskId) + if err != nil { + if strings.Contains(err.Error(), errorRetrievingTask) { + // Task was not found. Probably expired. We don't need it anymore + continue + } + return newTaskList, errorList, err + } + util.Logger.Printf("[SkimTasksList] {%d} task %s %s (status %s - cancel requested: %v)\n", i, task.Task.Name, task.Task.ID, task.Task.Status, task.Task.CancelRequested) + if isTaskComplete(task.Task.Status) { + continue + } + if isTaskRunning(task.Task.Status) { + newTaskList = append(newTaskList, taskId) + } + if task.Task.Status == "error" && !task.Task.CancelRequested { + errorList = append(errorList, taskId) + } + } + return newTaskList, errorList, nil +} + +// WaitTaskListCompletion waits until all tasks in the list are completed, removed, or failed +// Returns a list of failed tasks and an error +func (client Client) WaitTaskListCompletion(taskIdList []string, ignoreFailed bool) ([]string, error) { + var failedTaskList []string + var err error + for len(taskIdList) > 0 { + taskIdList, failedTaskList, err = client.SkimTasksList(taskIdList) + if err != nil { + return failedTaskList, err + } + time.Sleep(time.Second) + } + if len(failedTaskList) == 0 || ignoreFailed { + return nil, nil + } + return failedTaskList, fmt.Errorf("%d tasks have failed", len(failedTaskList)) +} + +// QueryTaskList performs a query for tasks according to a specific filter +func (client *Client) QueryTaskList(filter map[string]string) ([]*types.QueryResultTaskRecordType, error) { + taskType := types.QtTask + if client.IsSysAdmin { + taskType = types.QtAdminTask + } + + filterText := buildFilterTextWithLogicalOr(filter) + + notEncodedParams := map[string]string{ + "type": taskType, + } + if filterText != "" { + notEncodedParams["filter"] = filterText + } + results, err := client.cumulativeQuery(taskType, nil, notEncodedParams) + if err != nil { + return nil, fmt.Errorf("error querying task %s", err) + } + + if client.IsSysAdmin { + return results.Results.AdminTaskRecord, nil + } else { + return results.Results.TaskRecord, nil + } +} + +// buildFilterTextWithLogicalOr creates a filter with multiple values for a single column +// Given a map entry "key": "value1,value2" +// it creates a filter with a logical OR: "key==value1,key==value2" +func buildFilterTextWithLogicalOr(filter map[string]string) string { + filterText := "" + for k, v := range filter { + if filterText != "" { + filterText += ";" // logical AND + } + if strings.Contains(v, ",") { + valueText := "" + values := strings.Split(v, ",") + for _, value := range values { + if valueText != "" { + valueText += "," // logical OR + } + valueText += fmt.Sprintf("%s==%s", k, url.QueryEscape(value)) + } + filterText += valueText + } else { + filterText += fmt.Sprintf("%s==%s", k, url.QueryEscape(v)) + } + } + return filterText +} + +// WaitForRouteAdvertisementTasks is a convenience function to query for unfinished Route +// Advertisement tasks. An exact case for it was that updating some IP Space related objects (IP +// Spaces, IP Space Uplinks). Updating such an object sometimes results in a separate task for Route +// Advertisement being spun up (name="ipSpaceUplinkRouteAdvertisementSync"). When such task is +// running - other operations may fail so it is best to wait for completion of such task before +// triggering any other jobs. +func (client *Client) WaitForRouteAdvertisementTasks() error { + name := "ipSpaceUplinkRouteAdvertisementSync" + + util.Logger.Printf("[TRACE] WaitForRouteAdvertisementTasks attempting to search for unfinished tasks with name='%s'", name) + allTasks, err := client.QueryTaskList(map[string]string{ + "status": "running,preRunning,queued", + "name": name, + }) + if err != nil { + return fmt.Errorf("error retrieving all running '%s' tasks: %s", name, err) + } + + util.Logger.Printf("[TRACE] WaitForRouteAdvertisementTasks got %d unifinished tasks with name='%s'", len(allTasks), name) + for _, singleQueryTask := range allTasks { + task := NewTask(client) + task.Task.HREF = singleQueryTask.HREF + + err = task.WaitTaskCompletion() + if err != nil { + return fmt.Errorf("error waiting for task '%s' of type '%s' to finish: %s", singleQueryTask.HREF, name, err) + } + } + return nil } diff --git a/govcd/task_test.go b/govcd/task_test.go index a4e3026c5..1f9f834c1 100644 --- a/govcd/task_test.go +++ b/govcd/task_test.go @@ -1,30 +1,73 @@ -// +build task functional ALL +//go:build task || functional || ALL /* - * Copyright 2018 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + * Copyright 2022 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. */ package govcd -/* -TO BE REMOVED (or reintroduced with different scope) : task completion is tested as part of vdc_test.go import ( "fmt" + "time" + + "github.com/kr/pretty" . "gopkg.in/check.v1" ) -func (vcd *TestVCD) Test_WaitTaskCompletion(check *C) { - +func (vcd *TestVCD) Test_QueryTaskList(check *C) { fmt.Printf("Running: %s\n", check.TestName()) - check.Skip("Disabled: need a reliable way of triggering a task") - fmt.Printf("%#v\n", vcd.vapp.VApp) - task, err := vcd.vapp.Deploy() - err = task.WaitTaskCompletion() + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + + catalog, err := adminOrg.GetCatalogByName(vcd.config.VCD.Catalog.Name, false) + check.Assert(err, IsNil) + adminCatalog, err := adminOrg.GetAdminCatalogByName(vcd.config.VCD.Catalog.Name, false) + check.Assert(err, IsNil) + startQuery := time.Now() + allTasks, err := vcd.client.Client.QueryTaskList(map[string]string{ + "status": "running,preRunning,queued", + }) + check.Assert(err, IsNil) + if testVerbose { + fmt.Printf("%# v\n%s\n", pretty.Formatter(allTasks), time.Since(startQuery)) + } + // search using a client, giving the org and catalog names + resultByClient, err := vcd.client.Client.QueryTaskList(map[string]string{ + "orgName": vcd.config.VCD.Org, + "objectName": adminCatalog.AdminCatalog.Name, + "name": "catalogCreateCatalog"}) + check.Assert(err, IsNil) + + // search using an admin catalog, which will search by its HREF + resultByAdminCatalog, err := adminCatalog.QueryTaskList(map[string]string{ + "name": "catalogCreateCatalog", + }) + check.Assert(err, IsNil) + // search using a catalog, which will search by its HREF + resultByCatalog, err := catalog.QueryTaskList(map[string]string{ + "name": "catalogCreateCatalog", + }) check.Assert(err, IsNil) + check.Assert(len(resultByClient), Equals, len(resultByAdminCatalog)) + check.Assert(len(resultByClient), Equals, len(resultByCatalog)) + if len(resultByAdminCatalog) > 0 { + // there should be only one task for catalog creation + check.Assert(len(resultByClient), Equals, 1) + // check correspondence between task and its related object + // and also that all sets have returned the same result + catalogHref, err := adminCatalog.GetCatalogHref() + check.Assert(err, IsNil) + check.Assert(resultByAdminCatalog[0].HREF, Equals, resultByClient[0].HREF) + check.Assert(resultByCatalog[0].HREF, Equals, resultByClient[0].HREF) + check.Assert(resultByClient[0].Object, Equals, catalogHref) + check.Assert(resultByAdminCatalog[0].ObjectName, Equals, adminCatalog.AdminCatalog.Name) + check.Assert(resultByCatalog[0].ObjectName, Equals, adminCatalog.AdminCatalog.Name) + check.Assert(resultByAdminCatalog[0].ObjectType, Equals, "catalog") + check.Assert(resultByCatalog[0].ObjectType, Equals, "catalog") + } } -*/ func init() { testingTags["task"] = "task_test.go" diff --git a/govcd/tenant_context.go b/govcd/tenant_context.go new file mode 100644 index 000000000..cc7e7f163 --- /dev/null +++ b/govcd/tenant_context.go @@ -0,0 +1,211 @@ +/* + * Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ +package govcd + +import ( + "fmt" + "strings" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +// TenantContext stores the information needed for an object to be used in the context of a given organization +type TenantContext struct { + OrgId string // The bare ID (without prefix) of an organization + OrgName string // The organization name +} + +// organization is an abstraction of types Org and AdminOrg +type organization interface { + orgId() string + orgName() string + tenantContext() (*TenantContext, error) + fullObject() interface{} +} + +//lint:ignore U1000 for future usage +type genericVdc interface { + vdcId() string + vdcName() string + vdcParent() interface{} +} + +//lint:ignore U1000 for future usage +type genericCatalog interface { + catalogId() string + catalogName() string + catalogParent() interface{} +} + +// Implementation of organization interface for Org +func (org *Org) orgId() string { return org.Org.ID } +func (org *Org) orgName() string { return org.Org.Name } +func (org *Org) tenantContext() (*TenantContext, error) { return org.getTenantContext() } +func (org *Org) fullObject() interface{} { return org } + +// Implementation of organization interface for AdminOrg +func (adminOrg *AdminOrg) orgId() string { return adminOrg.AdminOrg.ID } +func (adminOrg *AdminOrg) orgName() string { return adminOrg.AdminOrg.Name } +func (adminOrg *AdminOrg) tenantContext() (*TenantContext, error) { return adminOrg.getTenantContext() } +func (adminOrg *AdminOrg) fullObject() interface{} { return adminOrg } + +// Implementation of genericVdc interface for Vdc +func (vdc *Vdc) vdcId() string { return vdc.Vdc.ID } +func (vdc *Vdc) vdcName() string { return vdc.Vdc.Name } +func (vdc *Vdc) vdcParent() interface{} { return vdc.parent } + +// Implementation of genericVdc interface for AdminVdc +func (adminVdc *AdminVdc) vdcId() string { return adminVdc.AdminVdc.ID } +func (adminVdc *AdminVdc) vdcName() string { return adminVdc.AdminVdc.Name } +func (adminVdc *AdminVdc) vdcParent() interface{} { return adminVdc.parent } + +// Implementation of genericCatalog interface for AdminCatalog +func (adminCatalog *AdminCatalog) catalogId() string { return adminCatalog.AdminCatalog.ID } +func (adminCatalog *AdminCatalog) catalogName() string { return adminCatalog.AdminCatalog.Name } +func (adminCatalog *AdminCatalog) catalogParent() interface{} { return adminCatalog.parent } + +// Implementation of genericCatalog interface for AdminCatalog +func (catalog *Catalog) catalogId() string { return catalog.Catalog.ID } +func (catalog *Catalog) catalogName() string { return catalog.Catalog.Name } +func (catalog *Catalog) catalogParent() interface{} { return catalog.parent } + +// getTenantContext returns the tenant context information for an Org +// If the information was not stored, it gets created and stored for future use +func (org *Org) getTenantContext() (*TenantContext, error) { + if org.TenantContext == nil { + id, err := getBareEntityUuid(org.Org.ID) + if err != nil { + return nil, err + } + org.TenantContext = &TenantContext{ + OrgId: id, + OrgName: org.Org.Name, + } + } + return org.TenantContext, nil +} + +// getTenantContext returns the tenant context information for an AdminOrg +// If the information was not stored, it gets created and stored for future use +func (org *AdminOrg) getTenantContext() (*TenantContext, error) { + if org.TenantContext == nil { + id, err := getBareEntityUuid(org.AdminOrg.ID) + if err != nil { + return nil, err + } + org.TenantContext = &TenantContext{ + OrgId: id, + OrgName: org.AdminOrg.Name, + } + } + return org.TenantContext, nil +} + +// getTenantContext retrieves the tenant context for an AdminVdc +func (vdc *AdminVdc) getTenantContext() (*TenantContext, error) { + org := vdc.parent + + if org == nil { + return nil, fmt.Errorf("VDC %s has no parent", vdc.AdminVdc.Name) + } + return org.tenantContext() +} + +// getTenantContext retrieves the tenant context for a VDC +func (vdc *Vdc) getTenantContext() (*TenantContext, error) { + org := vdc.parent + + if org == nil { + return nil, fmt.Errorf("VDC %s has no parent", vdc.Vdc.Name) + } + return org.tenantContext() +} + +// getTenantContext retrieves the tenant context for an AdminCatalog +func (catalog *AdminCatalog) getTenantContext() (*TenantContext, error) { + org := catalog.parent + + if org == nil { + return nil, fmt.Errorf("catalog %s has no parent", catalog.AdminCatalog.Name) + } + return org.tenantContext() +} + +// getTenantContext retrieves the tenant context for a Catalog +func (catalog *Catalog) getTenantContext() (*TenantContext, error) { + org := catalog.parent + + if org == nil { + return nil, fmt.Errorf("catalog %s has no parent", catalog.Catalog.Name) + } + return org.tenantContext() +} + +// getTenantContextHeader returns a map of strings containing the tenant context items +// needed to be used in http.Request.Header +func getTenantContextHeader(tenantContext *TenantContext) map[string]string { + if tenantContext == nil { + return nil + } + if tenantContext.OrgName == "" || strings.EqualFold(tenantContext.OrgName, "system") { + return nil + } + return map[string]string{ + // All VCD 10.2.X versions do not like when URN is sent for Tenant context ID - they fail + // with 401 Unauthorized when such request is sent with URN formatted ID: + // * Fails with 401: urn:vcloud:org:6127c856-7315-46b8-b774-f2b8f1686c80 + // * Works fine: 6127c856-7315-46b8-b774-f2b8f1686c80 + types.HeaderTenantContext: extractUuid(tenantContext.OrgId), + types.HeaderAuthContext: tenantContext.OrgName, + } +} + +// getTenantContextFromHeader does the opposite of getTenantContextHeader: +// given a header, returns a TenantContext +func getTenantContextFromHeader(header map[string]string) *TenantContext { + if len(header) == 0 { + return nil + } + tenantContext, okTenant := header[types.HeaderTenantContext] + AuthContext, okAuth := header[types.HeaderAuthContext] + if okTenant && okAuth { + return &TenantContext{ + OrgId: tenantContext, + OrgName: AuthContext, + } + } + return nil +} + +// getTenantContext retrieves the tenant context for a VdcGroup +func (vdcGroup *VdcGroup) getTenantContext() (*TenantContext, error) { + org := vdcGroup.parent + + if org == nil { + return nil, fmt.Errorf("VDC group %s has no parent", vdcGroup.VdcGroup.Name) + } + return org.tenantContext() +} + +// getTenantContext retrieves the tenant context for a IpSpaceIpAllocation +func (ipSpaceAllocation *IpSpaceIpAllocation) getTenantContext() (*TenantContext, error) { + org := ipSpaceAllocation.parent + + if org == nil { + return nil, fmt.Errorf("IP Space IP Allocation %s has no parent", ipSpaceAllocation.IpSpaceIpAllocation.Description) + } + return org.tenantContext() +} + +func (egw *NsxtEdgeGateway) getTenantContext() (*TenantContext, error) { + if egw != nil && egw.EdgeGateway.Org != nil { + if egw.EdgeGateway.Org.Name == "" || egw.EdgeGateway.Org.ID == "" { + return nil, fmt.Errorf("either parent NsxtEdgeGateway Org name or ID is empty and both must be set. Org name is [%s] and Org ID is [%s]", egw.EdgeGateway.Org.Name, egw.EdgeGateway.Org.ID) + } + + return &TenantContext{OrgId: egw.EdgeGateway.Org.ID, OrgName: egw.EdgeGateway.Org.Name}, nil + } + + return nil, fmt.Errorf("NsxtEdgeGateway is not fully initialized. Please initialize it before using this method") +} diff --git a/govcd/tenant_context_test.go b/govcd/tenant_context_test.go new file mode 100644 index 000000000..e5c2215fa --- /dev/null +++ b/govcd/tenant_context_test.go @@ -0,0 +1,221 @@ +//go:build functional || openapi || ALL + +package govcd + +import ( + "fmt" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +// Test_TenantContext checks that different members of a hierarchy +// with an Admin Org at its top are all reporting the same tenant context +// using different methods to retrieve each member +// When running with -vcd-verbose, you should see a long list of entities +// with the corresponding tenant context. If no errors occur, the tenant context +// values in all rows should be the same. +func (vcd *TestVCD) Test_TenantContext(check *C) { + // Check the tenant context of the AdminOrg (top of the hierarchy) + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + check.Assert(adminOrg.TenantContext, NotNil) + check.Assert(adminOrg.TenantContext.OrgId, Equals, extractUuid(adminOrg.AdminOrg.ID)) + check.Assert(adminOrg.TenantContext.OrgName, Equals, adminOrg.AdminOrg.Name) + adminOrgTenantContext := adminOrg.TenantContext + checkTenantContext(check, "adminOrg by name", adminOrgTenantContext, adminOrgTenantContext) + + adminOrgById, err := vcd.client.GetAdminOrgById(adminOrg.AdminOrg.ID) + check.Assert(err, IsNil) + check.Assert(adminOrgById, NotNil) + check.Assert(adminOrgById.TenantContext, NotNil) + check.Assert(adminOrgById.TenantContext.OrgId, Equals, extractUuid(adminOrgById.AdminOrg.ID)) + check.Assert(adminOrgById.TenantContext.OrgName, Equals, adminOrgById.AdminOrg.Name) + adminOrgByIdTenantContext := adminOrgById.TenantContext + checkTenantContext(check, "adminOrg by ID", adminOrgByIdTenantContext, adminOrgTenantContext) + + adminOrgByNameOrId, err := vcd.client.GetAdminOrgByNameOrId(adminOrg.AdminOrg.ID) + check.Assert(err, IsNil) + check.Assert(adminOrgByNameOrId, NotNil) + check.Assert(adminOrgByNameOrId.TenantContext, NotNil) + check.Assert(adminOrgByNameOrId.TenantContext.OrgId, Equals, extractUuid(adminOrgByNameOrId.AdminOrg.ID)) + check.Assert(adminOrgByNameOrId.TenantContext.OrgName, Equals, adminOrgByNameOrId.AdminOrg.Name) + adminOrgByNameOrIdTenantContext := adminOrgByNameOrId.TenantContext + checkTenantContext(check, "adminOrg by ID", adminOrgByNameOrIdTenantContext, adminOrgTenantContext) + + // Check the tenant context of the Org (top of the hierarchy) + org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(org, NotNil) + check.Assert(org.TenantContext, NotNil) + check.Assert(org.TenantContext.OrgId, Equals, extractUuid(org.Org.ID)) + check.Assert(org.TenantContext.OrgName, Equals, org.Org.Name) + orgTenantContext := org.TenantContext + checkTenantContext(check, "org by name", orgTenantContext, adminOrgTenantContext) + + orgById, err := vcd.client.GetOrgById(org.Org.ID) + check.Assert(err, IsNil) + check.Assert(orgById, NotNil) + check.Assert(orgById.TenantContext, NotNil) + check.Assert(orgById.TenantContext.OrgId, Equals, extractUuid(orgById.Org.ID)) + check.Assert(orgById.TenantContext.OrgName, Equals, orgById.Org.Name) + orgTenantContext = orgById.TenantContext + checkTenantContext(check, "org by ID", orgTenantContext, adminOrgTenantContext) + + orgByNameOrId, err := vcd.client.GetOrgByNameOrId(org.Org.ID) + check.Assert(err, IsNil) + check.Assert(orgByNameOrId, NotNil) + check.Assert(orgByNameOrId.TenantContext, NotNil) + check.Assert(orgByNameOrId.TenantContext.OrgId, Equals, extractUuid(orgByNameOrId.Org.ID)) + check.Assert(orgByNameOrId.TenantContext.OrgName, Equals, orgByNameOrId.Org.Name) + orgTenantContext = orgByNameOrId.TenantContext + checkTenantContext(check, "org by name or ID", orgTenantContext, adminOrgTenantContext) + + // Check that an admin VDC depending from our org has the same tenant context + adminVdc, err := adminOrg.GetAdminVDCByName(vcd.config.VCD.Vdc, false) + check.Assert(err, IsNil) + check.Assert(adminVdc, NotNil) + adminVdcTenantContext, err := adminVdc.getTenantContext() + check.Assert(err, IsNil) + checkTenantContext(check, "adminVdc by name", adminVdcTenantContext, adminOrgTenantContext) + + adminVdcById, err := adminOrg.GetAdminVDCById(adminVdc.AdminVdc.ID, false) + check.Assert(err, IsNil) + check.Assert(adminVdcById, NotNil) + adminVdcTenantContext, err = adminVdcById.getTenantContext() + check.Assert(err, IsNil) + checkTenantContext(check, "adminVdc by ID", adminVdcTenantContext, adminOrgTenantContext) + + adminVdcByNameOrId, err := adminOrg.GetAdminVDCByNameOrId(adminVdc.AdminVdc.ID, false) + check.Assert(err, IsNil) + check.Assert(adminVdcByNameOrId, NotNil) + adminVdcTenantContext, err = adminVdcByNameOrId.getTenantContext() + check.Assert(err, IsNil) + checkTenantContext(check, "adminVdc by name or ID", adminVdcTenantContext, adminOrgTenantContext) + + // Check that a VDC depending from our org has the same tenant context + vdc, err := adminOrg.GetVDCByName(vcd.config.VCD.Vdc, false) + check.Assert(err, IsNil) + check.Assert(vdc, NotNil) + vdcTenantContext, err := vdc.getTenantContext() + check.Assert(err, IsNil) + checkTenantContext(check, "VDC by name", vdcTenantContext, adminOrgTenantContext) + + vdcById, err := adminOrg.GetVDCById(vdc.Vdc.ID, false) + check.Assert(err, IsNil) + check.Assert(vdcById, NotNil) + vdcTenantContext, err = vdcById.getTenantContext() + check.Assert(err, IsNil) + checkTenantContext(check, "VDC by ID", vdcTenantContext, adminOrgTenantContext) + + vdcByNameOrId, err := adminOrg.GetVDCByNameOrId(vdc.Vdc.ID, false) + check.Assert(err, IsNil) + check.Assert(vdcByNameOrId, NotNil) + vdcTenantContext, err = vdcByNameOrId.getTenantContext() + check.Assert(err, IsNil) + checkTenantContext(check, "VDC by name or ID", vdcTenantContext, adminOrgTenantContext) + + // Check that an admin catalog depending from our org has the same tenant context + adminCatalog, err := adminOrg.GetAdminCatalogByName(vcd.config.VCD.Catalog.Name, false) + check.Assert(err, IsNil) + check.Assert(adminCatalog, NotNil) + adminCatalogTenantContext, err := adminCatalog.getTenantContext() + check.Assert(err, IsNil) + checkTenantContext(check, "adminCatalog by name", adminCatalogTenantContext, adminOrgTenantContext) + + adminCatalogById, err := adminOrg.GetAdminCatalogById(adminCatalog.AdminCatalog.ID, false) + check.Assert(err, IsNil) + check.Assert(adminCatalogById, NotNil) + adminCatalogTenantContext, err = adminCatalogById.getTenantContext() + check.Assert(err, IsNil) + checkTenantContext(check, "adminCatalog by ID", adminCatalogTenantContext, adminOrgTenantContext) + + adminCatalogByNameOrId, err := adminOrg.GetAdminCatalogByNameOrId(adminCatalog.AdminCatalog.ID, false) + check.Assert(err, IsNil) + check.Assert(adminCatalogByNameOrId, NotNil) + adminCatalogTenantContext, err = adminCatalogByNameOrId.getTenantContext() + check.Assert(err, IsNil) + checkTenantContext(check, "adminCatalog by Name or ID", adminCatalogTenantContext, adminOrgTenantContext) + + // Check that a catalog depending from our org has the same tenant context + catalog, err := adminOrg.GetCatalogByName(vcd.config.VCD.Catalog.Name, false) + check.Assert(err, IsNil) + check.Assert(catalog, NotNil) + catalogTenantContext, err := catalog.getTenantContext() + check.Assert(err, IsNil) + checkTenantContext(check, "catalog by Name", catalogTenantContext, adminOrgTenantContext) + + catalogById, err := adminOrg.GetCatalogById(catalog.Catalog.ID, false) + check.Assert(err, IsNil) + check.Assert(catalogById, NotNil) + catalogTenantContext, err = catalogById.getTenantContext() + check.Assert(err, IsNil) + checkTenantContext(check, "catalog by ID", catalogTenantContext, adminOrgTenantContext) + + catalogByNameOrId, err := adminOrg.GetCatalogByNameOrId(catalog.Catalog.ID, false) + check.Assert(err, IsNil) + check.Assert(catalogByNameOrId, NotNil) + catalogTenantContext, err = catalogByNameOrId.getTenantContext() + check.Assert(err, IsNil) + checkTenantContext(check, "catalog by ID", catalogTenantContext, adminOrgTenantContext) + + vappList := vdc.GetVappList() + + if len(vappList) > 0 { + // Check that a vApp depending from our org has the same tenant context + vapp, err := vdc.GetVAppByName(vappList[0].Name, false) + check.Assert(err, IsNil) + check.Assert(vapp, NotNil) + vappTenantContext, err := vapp.getTenantContext() + check.Assert(err, IsNil) + checkTenantContext(check, "vapp by name", vappTenantContext, adminOrgTenantContext) + + vappById, err := vdc.GetVAppById(vapp.VApp.ID, false) + check.Assert(err, IsNil) + check.Assert(vappById, NotNil) + vappTenantContext, err = vappById.getTenantContext() + check.Assert(err, IsNil) + checkTenantContext(check, "vapp by ID", vappTenantContext, adminOrgTenantContext) + + vappByNameOrId, err := vdc.GetVAppByNameOrId(vapp.VApp.ID, false) + check.Assert(err, IsNil) + check.Assert(vappByNameOrId, NotNil) + vappTenantContext, err = vappByNameOrId.getTenantContext() + check.Assert(err, IsNil) + checkTenantContext(check, "vapp by name or ID", vappTenantContext, adminOrgTenantContext) + + vmList, err := vdc.QueryVmList(types.VmQueryFilterOnlyDeployed) + check.Assert(err, IsNil) + if len(vmList) > 0 { + // Check that a VM depending from our org has the same tenant context + vm, err := vcd.client.Client.GetVMByHref(vmList[0].HREF) + check.Assert(err, IsNil) + check.Assert(vm, NotNil) + + vmTenantContext, err := vm.getTenantContext() + check.Assert(err, IsNil) + checkTenantContext(check, "VM by Href", vmTenantContext, adminOrgTenantContext) + } + } + + // Check that a VM depending from our org has the same tenant context + role, err := adminOrg.GetRoleByName("vApp Author") + check.Assert(err, IsNil) + check.Assert(role, NotNil) + checkTenantContext(check, "role by name", role.TenantContext, adminOrgTenantContext) + + roleById, err := adminOrg.GetRoleById(role.Role.ID) + check.Assert(err, IsNil) + check.Assert(role, NotNil) + checkTenantContext(check, "role by Id", roleById.TenantContext, adminOrgTenantContext) +} + +func checkTenantContext(check *C, label string, tenantContext, parentTenantContext *TenantContext) { + check.Assert(tenantContext, DeepEquals, parentTenantContext) + check.Assert(tenantContext.OrgId, Equals, parentTenantContext.OrgId) + check.Assert(tenantContext.OrgName, Equals, parentTenantContext.OrgName) + if testVerbose { + fmt.Printf("%-30s %-20s -> %s\n", label, tenantContext.OrgId, parentTenantContext.OrgName) + } +} diff --git a/govcd/test-resources/capiYaml.yaml b/govcd/test-resources/capiYaml.yaml new file mode 100644 index 000000000..79bc707e7 --- /dev/null +++ b/govcd/test-resources/capiYaml.yaml @@ -0,0 +1,200 @@ +apiVersion: cluster.x-k8s.io/v1beta1 +kind: MachineHealthCheck +metadata: + name: "test1" + namespace: "test1-ns" + labels: + clusterctl.cluster.x-k8s.io: "" + clusterctl.cluster.x-k8s.io/move: "" +spec: + clusterName: "test1" + maxUnhealthy: "100%" + nodeStartupTimeout: "900s" + selector: + matchLabels: + cluster.x-k8s.io/cluster-name: "test1" + unhealthyConditions: + - type: Ready + status: Unknown + timeout: "200s" + - type: Ready + status: "False" + timeout: "300s" +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 +kind: VCDMachineTemplate +metadata: + name: "node-pool-1" + namespace: "test1-ns" +spec: + template: + spec: + catalog: "tkgm_catalog" + template: "ubuntu-2004-kube-v1.25.7+vmware.2-tkg.1-8a74b9f12e488c54605b3537acb683bc" + sizingPolicy: "TKG small" + placementPolicy: "" + storageProfile: "*" + diskSize: "20Gi" + enableNvidiaGPU: false +--- +apiVersion: cluster.x-k8s.io/v1beta1 +kind: MachineDeployment +metadata: + name: "node-pool-1" + namespace: "test1-ns" +spec: + clusterName: "test1" + replicas: 1 + selector: + matchLabels: null + template: + spec: + bootstrap: + configRef: + apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 + kind: KubeadmConfigTemplate + name: "test1-kct" + namespace: "test1-ns" + clusterName: "test1" + infrastructureRef: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 + kind: VCDMachineTemplate + name: "node-pool-1" + namespace: "test1-ns" + version: "v1.25.7+vmware.2" +--- + +apiVersion: cluster.x-k8s.io/v1beta1 +kind: Cluster +metadata: + name: "test1" + namespace: "test1-ns" + labels: + cluster-role.tkg.tanzu.vmware.com/management: "" + tanzuKubernetesRelease: "v1.25.7---vmware.2-tkg.1" + tkg.tanzu.vmware.com/cluster-name: "test1" + annotations: + osInfo: "ubuntu,20.04,amd64" + TKGVERSION: "v2.2.0" +spec: + clusterNetwork: + pods: + cidrBlocks: + - "100.96.0.0/11" + serviceDomain: cluster.local + services: + cidrBlocks: + - "100.64.0.0/13" + controlPlaneRef: + apiVersion: controlplane.cluster.x-k8s.io/v1beta1 + kind: KubeadmControlPlane + name: "test1-control-plane-node-pool" + namespace: "test1-ns" + infrastructureRef: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 + kind: VCDCluster + name: "test1" + namespace: "test1-ns" +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 +kind: VCDCluster +metadata: + name: "test1" + namespace: "test1-ns" +spec: + site: "https://www.my-vcd-instance.com" + org: "tenant_org" + ovdc: "tenant_vdc" + ovdcNetwork: "tenant_net_routed" + useAsManagementCluster: false + userContext: + secretRef: + name: capi-user-credentials + namespace: "test1-ns" +--- +apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 +kind: VCDMachineTemplate +metadata: + name: "test1-control-plane-node-pool" + namespace: "test1-ns" +spec: + template: + spec: + catalog: "tkgm_catalog" + template: "ubuntu-2004-kube-v1.25.7+vmware.2-tkg.1-8a74b9f12e488c54605b3537acb683bc" + sizingPolicy: "TKG small" + placementPolicy: "" + storageProfile: "*" + diskSize: 20Gi +--- +apiVersion: controlplane.cluster.x-k8s.io/v1beta1 +kind: KubeadmControlPlane +metadata: + name: "test1-control-plane-node-pool" + namespace: "test1-ns" +spec: + kubeadmConfigSpec: + preKubeadmCommands: + - mv /etc/ssl/certs/custom_certificate_*.crt /usr/local/share/ca-certificates && update-ca-certificates + clusterConfiguration: + apiServer: + certSANs: + - localhost + - 127.0.0.1 + controllerManager: + extraArgs: + enable-hostpath-provisioner: "true" + dns: + imageRepository: "projects.registry.vmware.com/tkg" + imageTag: "v1.9.3_vmware.8" + etcd: + local: + imageRepository: "projects.registry.vmware.com/tkg" + imageTag: "v3.5.6_vmware.9" + imageRepository: "projects.registry.vmware.com/tkg" + users: + - name: root + sshAuthorizedKeys: + - "" + initConfiguration: + nodeRegistration: + criSocket: /run/containerd/containerd.sock + kubeletExtraArgs: + eviction-hard: nodefs.available<0%%,nodefs.inodesFree<0%%,imagefs.available<0%% + cloud-provider: external + joinConfiguration: + nodeRegistration: + criSocket: /run/containerd/containerd.sock + kubeletExtraArgs: + eviction-hard: nodefs.available<0%%,nodefs.inodesFree<0%%,imagefs.available<0%% + cloud-provider: external + machineTemplate: + infrastructureRef: + apiVersion: infrastructure.cluster.x-k8s.io/v1beta2 + kind: VCDMachineTemplate + name: "test1-control-plane-node-pool" + namespace: "test1-ns" + replicas: 1 + version: "v1.25.7+vmware.2" +--- +apiVersion: bootstrap.cluster.x-k8s.io/v1beta1 +kind: KubeadmConfigTemplate +metadata: + name: "test1-kct" + namespace: "test1-ns" +spec: + template: + spec: + users: + - name: root + sshAuthorizedKeys: + - "" + useExperimentalRetryJoin: true + preKubeadmCommands: + - mv /etc/ssl/certs/custom_certificate_*.crt /usr/local/share/ca-certificates && update-ca-certificates + joinConfiguration: + nodeRegistration: + criSocket: /run/containerd/containerd.sock + kubeletExtraArgs: + eviction-hard: nodefs.available<0%%,nodefs.inodesFree<0%%,imagefs.available<0%% + cloud-provider: external diff --git a/govcd/test-resources/cert.pem b/govcd/test-resources/cert.pem new file mode 100644 index 000000000..f1b42acee --- /dev/null +++ b/govcd/test-resources/cert.pem @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE----- +MIIEgzCCAusCCQDsvB5Pim4CNzANBgkqhkiG9w0BAQsFADBPMR4wHAYDVQQKExVt +a2NlcnQgZGV2ZWxvcG1lbnQgQ0ExEjAQBgNVBAsTCVRlcnJhZm9ybTEZMBcGA1UE +AxMQbWtjZXJ0IFRlcnJhZm9ybTAgFw0yMzAzMDIwNjM0MjZaGA8yMTIzMDIwNjA2 +MzQyNlowNjELMAkGA1UEBhMCVVMxDTALBgNVBAMMBGNlcnQxGDAWBgkqhkiG9w0B +CQEWCWNlcnRAdGVzdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKS2 +q+REopxvA3HY1zHRcNO9AcBZ3pGkVR2bSJ+awL9xZk7yjMrhqZ29XtScvs4ZUguS +3i6hrsY2hUqFWgbucJSObvUc6OBYoelUmNGtzhdwcsppFdZiJSTp2h6+/cZkX+fm +3xJcrTfzZVeBSniGGuzJHoJNXOaps9ilrOibm7/OZNF13NAe/VIjKPlQA1V3gdGd +6MAZ0p1IUF3flA9s29bCdsDcdbD6yjIfWfvztWIEx1PbARu9pd6tmg8jc3fvkqru +d/nfNb3y3rHnSITXxJg3zO8OERXHmw/lGPiJahmzXjTvsk2Qwfb8TKnwtuh7eCEo +VM54hyY4kxlWKQlpPRv1D+dLzp1BlzJjJxcg+U+86OlwNIaA9+bv9Kzfsw111IOG +L4CWgxV4F7iYt2CAHdygEqkLImmPpjSXGZN96Rnf3vkSlNMIHatQDIYRZHKP8bq5 +Ww0zIC+pflTA8s1+KfN5qccgiqAwRXv1AKqA9YYx35chljp2qdNbyBQcsnNPiyA5 +XDSgNGsjinKiKWy/VLv947nvgRrPv+iJTuzNyIhmdZr1dDJDPhtjWa7nMgv668e+ +NuAQOitQcFDN9NxjZ6CCJmw33hz0bjXESKsCvUlvfNra/DpESZ8MRKB8CoTgx0ey +r72C1u/6o+qXZS3mzWdNmyzqR7mUeiRozmxqmiczAgMBAAEwDQYJKoZIhvcNAQEL +BQADggGBAEf+CntVZefcgpzRNdxgpnEFh4SRiJyjUK7n2mVzd+kzk9K+E9lvJ1Ho +PPIOdFRvj9rY3k+Q7G4eZL+2tNlN1KSfeRus5awp8tmFDd75kRGSkdCFWjebaq2k +xvMkxp0E7v/zAN+OghF3ek7JLGQC4e4gCiyYDdB/Rvq3zEex471riqQu8vbs0CCV +rz8d0NBSWc2XRKFRhLjODhDTLkLnJjIKW7863iFxXxYGHw4ngIXuctXN+QzRuX2r +OioQSkmmtcmDwugDCX8YcHxZQgqz5+FthO76MugBTcgyBJK4UrJlyW56RZ6orLf3 +527ZRRzWNd2xXMkcXQaneVvqhWydXfk2+vShN+iaU4GMZANP+d+imZuNyHHJKZcP +CjQJbSQfO3cupfddXUuhEYoCE9WA8GNOWKWBbdyG1gQDyrINCU7XpH0sNH9Sbukz +iobq+k9KqwClkUOpVr6OIFjmh0s1hdIY3qVa8OVp5Y687FcIGzE/euDqsoFMvkEL +BIQP07pT0Q== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/govcd/test-resources/key.pem b/govcd/test-resources/key.pem new file mode 100644 index 000000000..8aa25508f --- /dev/null +++ b/govcd/test-resources/key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQCktqvkRKKcbwNx +2Ncx0XDTvQHAWd6RpFUdm0ifmsC/cWZO8ozK4amdvV7UnL7OGVILkt4uoa7GNoVK +hVoG7nCUjm71HOjgWKHpVJjRrc4XcHLKaRXWYiUk6doevv3GZF/n5t8SXK0382VX +gUp4hhrsyR6CTVzmqbPYpazom5u/zmTRddzQHv1SIyj5UANVd4HRnejAGdKdSFBd +35QPbNvWwnbA3HWw+soyH1n787ViBMdT2wEbvaXerZoPI3N375Kq7nf53zW98t6x +50iE18SYN8zvDhEVx5sP5Rj4iWoZs14077JNkMH2/Eyp8Lboe3ghKFTOeIcmOJMZ +VikJaT0b9Q/nS86dQZcyYycXIPlPvOjpcDSGgPfm7/Ss37MNddSDhi+AloMVeBe4 +mLdggB3coBKpCyJpj6Y0lxmTfekZ3975EpTTCB2rUAyGEWRyj/G6uVsNMyAvqX5U +wPLNfinzeanHIIqgMEV79QCqgPWGMd+XIZY6dqnTW8gUHLJzT4sgOVw0oDRrI4py +oilsv1S7/eO574Eaz7/oiU7szciIZnWa9XQyQz4bY1mu5zIL+uvHvjbgEDorUHBQ +zfTcY2eggiZsN94c9G41xEirAr1Jb3za2vw6REmfDESgfAqE4MdHsq+9gtbv+qPq +l2Ut5s1nTZss6ke5lHokaM5saponMwIDAQABAoICAELFRmMvq5esrQHOvFWWqJ09 +BmO6Sq5Rpqts0oDY1AAHcUjZrFdmKUMnjDS2IeccfpTwgZ73rgjt+xSdgERFDmA6 +aSJ2CLVBWMlkoNqHEX+Q9we0l8SjXplbLy+9jtSIxhQVFCK2bQW8Zj2VzOGUw39v +fC2oPNvIuX4+kxxsUDPt8BK1K8E2fsx4Mlj2pZNU8cxOrhaJoUZfFS0owDWMlIW3 +qTo/ZHpNAABXkzu+rK3CcCc/JXDgbUgaqdQvM9TPym3+Y6ZoZLnOpZYKwuwPJ8Pp +Aut5kVV56BMGdRvzYI5wluTwsiAdaXO9DTrquMr/mlAesFpOo8LLtl3T/qiw/7MZ +4AV8E4uYSxkLL7fAfec9CbOoO3sup1imM8JTMxx0gRuJBn6wi82NtDhd2q2qfFbO +hgZIP5GwIUGDMwZMz1CniGKiUGKz/YkKo7qXF4MewQadBGmfUDuRoCQ/j9QcdvAF +cJ+OsvrC7T3XLiD/GegJuw/cBX281bCoLYItA7F9+CA4RuLpIxphUH4ITqhbtvtJ +XCECXYcYQN0LOPhw9BbtsVV/pYoQ2CWNmFRWZADw03rpSWVChMbnDUfI6QAbMx8g +pyVNll75Eb6+FHDOVaMIfIL6yDlMKrGSVjsvgWKN5pL+zqb4kLTUGiQZXyLrgDUJ +TJ1K8qYPxJx2r+Jga34xAoIBAQDZZzyLgvHR1J842LAR5EpgR9AyEQMbx4zhfP0p +r3GoxVYKxfiA0bpD02dc+D18Q2I9RtCnfTkKsk5IZXxRN7MzfyPqh34ztazUPszg +/9O/RLFHWSPUPq4C/HmrA+dPiZ9V0s5eL/CIsvqSczcRkEkwj4TEY/ERTI4hbunJ +mOSase4Cmx1T1wvtx1USiuBYErCf8laxM0R95aiKX+YNwM8nzWmy2UD2+JOvMsMn +MZFlj8QiE87C9Fbpq90ePwYoKz7azOw3ATRzgi5/ScYzSDlTlsEMasvy4D8+l+XL +bY50nquZ9YdER+t2/bj1BzoVR9Kf5eiecW/mZREW35EtgxAbAoIBAQDB9L1n0UHF +A0l+xKp9Xp+YsO761G+mV67eD9f2BZ3ATi/rMJDzENub2zAOTYD+V3HZiGoQWl3S +Nv+nyIOZwdD9LASnv5QE4fngO3wqd0YOJIUamVNl+XGFZpPUiGdtWzRxidDwqOWx +uoapVFE5nigqehQ+BUKKps7Ihq61cj4Q3czjJrNzHuMTKRSXgSvlI703MUfQhAkL +u05N8um1ORv8sinyVj2VIxjGwqO5Nv8IN3ecSceipcMFC/tKy10Q82oWX4Xf0IVC +DzEosWR1+RUZaFFCZBozlB9C10bgjnM+owlMQSqaGpnkwuDcOCg7cwRCOziQVJZD +VnQe3lIE06bJAoIBAQCZ4YzlYwYvc8RPxHC7+U7731jqV0hP/WsmoAXB38EfqK5C +aeZ/p/Oj1psvHzbGIhwDK4C9TNF3VMY8UDkyc66QIMoXU1hs2Yc/pEP4bpw+oiyp +R9sofEVHL9YeymCL1+nEIbaYzG4BFE5wIsUz1WE40h0ztVoI2Jsx5wPsAiCtrou9 +pHWZxnlXEOSSf2JUdMY4MJxUSOmOA2TMrRx1V6hJkAfk5Aorxb8jH1crAtbbgGtf +g42ySKjMNS4KHqoI/LM8xBfexyeNKvQmfN2hptmM5QQ3+c/qVffuIi4xU5alzTnB +fB0Go7FzRBwKs9bVAUWAkIeavshp19fEzPJBuKdJAoIBAG5Tp/XJC29kykao2g4M +aB4z7wyREJ1/XQIF4yOX2D8OeqV+78TDvxft220XWxvSY/mIZkS9EodEL7KiFXG9 +1QJeKpu9Fxab8EZDsAJ77EaZMXmK4+yqso9eZRLNMH/9FFzNNyPd/yJU5sqlIrry +owhefus0lMBH2HIqYnDl9jYj5KsFVahTVnmMsaDooi5qYPRnPOF4aajZt9YRKi2i +ua/JLKEju039M9fD2du+U925p3koYr27Kq7RPPUzrtG4lIz7cyx38YU9HQp3tZyB +viXAuBBa3qieRhYAXNnZTebAWMaefvw/y3BcBgpei0wdxbti8m7vHrZZFB6G+gKy +3jkCggEBAJZzYmGvjlDumPElX3hzzAjPMhz3CmoyGC6aJN05ezK/z3iOG9CGJLcG +vyU3yD/iEy2TbQtPjrVz+HMBF0Zs7FAK9XKNj20TCu0tOKIUa7KWTaKJjbn7qg0q +k3v09Pj/ZevPrdNL4DKGLCbgsTJLGZK+HSE3jJ/SPSNHuoOaRetpsJtMcjJgDmqr +URs3t0xNZt6zRR9/1SrAmVCBrJpMuiPQNgJJppx+jw6d8FUTHqp4iyfX190UeJV8 +X/g4SS47VcjjJBJMKyMmZl+D8p32wuc4YE99/ycZdLpUxVhla5Q1alKIj5bUc0o+ +8YHtmZ19oGLMOCHqJ8T4bvoSYGTi+8c= +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/govcd/test-resources/rootCA.pem b/govcd/test-resources/rootCA.pem new file mode 100644 index 000000000..231dd8fed --- /dev/null +++ b/govcd/test-resources/rootCA.pem @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEbzCCAtegAwIBAgIQYl7uyKCXFwpi+qNH8QN6pTANBgkqhkiG9w0BAQsFADBP +MR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExEjAQBgNVBAsTCVRlcnJh +Zm9ybTEZMBcGA1UEAxMQbWtjZXJ0IFRlcnJhZm9ybTAgFw0yMzAzMDIwNjE4MDNa +GA8zMTIzMDMwMjA2MTgwM1owTzEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50 +IENBMRIwEAYDVQQLEwlUZXJyYWZvcm0xGTAXBgNVBAMTEG1rY2VydCBUZXJyYWZv +cm0wggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCwlYSqjG7xrleLkO2D +aOB3Z2cZQH1XCLFD//o8ctIDY1mmvlthlZ47H8FOpbI9/O+40oon27XmBxPCuYyy ++/ZbmdBf4QB3BBcGGxhR8gInuCT65rraoAeLSRqNOwYcPduRBvrdNeCj2vyK98kR +4U8M+K64nx0hQlusu6Cyd5KiOyKg6CuoXnBKWZ1W2s8En23SIaxg+rydPM/jtjmW +G6ArF2TtzuBPZ/z0rqroHw2Kwtna5r6mTaW9u4EKIaPyWn1Ay7iFABIs6DS/BEiS +xnZHeXTqRD32HnMKXawrG0Fm++MX1c+qA2/k5JM2CF2BtoPbnekK7gWj38JHSrNg +YLafYkMA5d/eTR8EnrGLGvHUTRQwK8CpG1bHRJSdbDwhvgQxgjLpcbeZzv+g42v8 +vXaXImDZEhiLeMIDM4XphyfH3l2W0WztqBM1GlM3Ycf9LvISqTH5z9QRM3JX0mRS +xYFfCb8XC7vWaWkWoIHJ5RYQ6tHZ8wTtrvO34qtEAVSIte0CAwEAAaNFMEMwDgYD +VR0PAQH/BAQDAgIEMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFNA4nzDg +xEjwbJXhr47p/mub8cKEMA0GCSqGSIb3DQEBCwUAA4IBgQAyg+3HGr4dUnEJXpk3 +FocS0Bup2ds+wREejACEIgP1PzbcdJpHG4Qswt/6FvIRAzH84nGucZVeWdRI7jOk +qT5I8nOQ/UTlZkEnt5QBIQX/ghg77mQrRY6neeI99NRm/28k9SERrpfpJStLeDwH +jNnAGkfSxZP1QZACebrTPFAY8vGGGIDZ1ZeUwxJgfbrD9dF5cTJYftSwOndQVeKS +SeRBsgwe7NLCmLzQOlmlo83KoGoGd6n9P7vTtB8Uj8WPv7O12+XTNjv8CuWR1Zhq +LERLEBwtHd6POAjSvi1/58UKRJIPqa04dCSCGRrF5eeJVAzP3IiFWZz0aGZNj/gW +Ynp8DVyF5ur5fGrA9Ao5r12avoYMnqzbgRPTY/u55Ab1SowU4xLbSzTGmD/Msg3s +A2+dDBPQn3+6z88TZSVFKy/t83qXM156YMpgk2f37yIxHcPp+MJNNxKp1GzkrCXJ +uo41rJCOGpVx/gWXsG+DKr0ZITWf5/oQF6AsWWxIIfaUEjU= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/govcd/test-resources/saml-test-idp.xml b/govcd/test-resources/saml-test-idp.xml new file mode 100644 index 000000000..69111a2e4 --- /dev/null +++ b/govcd/test-resources/saml-test-idp.xml @@ -0,0 +1,133 @@ + + + + + + + samltest.id + + + + SAMLtest IdP + A free and basic IdP for testing SAML deployments + https://samltest.id/saml/logo.png + + + + + + + + MIIDETCCAfmgAwIBAgIUZRpDhkNKl5eWtJqk0Bu1BgTTargwDQYJKoZIhvcNAQEL + BQAwFjEUMBIGA1UEAwwLc2FtbHRlc3QuaWQwHhcNMTgwODI0MjExNDEwWhcNMzgw + ODI0MjExNDEwWjAWMRQwEgYDVQQDDAtzYW1sdGVzdC5pZDCCASIwDQYJKoZIhvcN + AQEBBQADggEPADCCAQoCggEBAJrh9/PcDsiv3UeL8Iv9rf4WfLPxuOm9W6aCntEA + 8l6c1LQ1Zyrz+Xa/40ZgP29ENf3oKKbPCzDcc6zooHMji2fBmgXp6Li3fQUzu7yd + +nIC2teejijVtrNLjn1WUTwmqjLtuzrKC/ePoZyIRjpoUxyEMJopAd4dJmAcCq/K + k2eYX9GYRlqvIjLFoGNgy2R4dWwAKwljyh6pdnPUgyO/WjRDrqUBRFrLQJorR2kD + c4seZUbmpZZfp4MjmWMDgyGM1ZnR0XvNLtYeWAyt0KkSvFoOMjZUeVK/4xR74F8e + 8ToPqLmZEg9ZUx+4z2KjVK00LpdRkH9Uxhh03RQ0FabHW6UCAwEAAaNXMFUwHQYD + VR0OBBYEFJDbe6uSmYQScxpVJhmt7PsCG4IeMDQGA1UdEQQtMCuCC3NhbWx0ZXN0 + LmlkhhxodHRwczovL3NhbWx0ZXN0LmlkL3NhbWwvaWRwMA0GCSqGSIb3DQEBCwUA + A4IBAQBNcF3zkw/g51q26uxgyuy4gQwnSr01Mhvix3Dj/Gak4tc4XwvxUdLQq+jC + cxr2Pie96klWhY/v/JiHDU2FJo9/VWxmc/YOk83whvNd7mWaNMUsX3xGv6AlZtCO + L3JhCpHjiN+kBcMgS5jrtGgV1Lz3/1zpGxykdvS0B4sPnFOcaCwHe2B9SOCWbDAN + JXpTjz1DmJO4ImyWPJpN1xsYKtm67Pefxmn0ax0uE2uuzq25h0xbTkqIQgJzyoE/ + DPkBFK1vDkMfAW11dQ0BXatEnW7Gtkc0lh2/PIbHWj4AzxYMyBf5Gy6HSVOftwjC + voQR2qr2xJBixsg+MIORKtmKHLfU + + + + + + + + + + MIIDEjCCAfqgAwIBAgIVAMECQ1tjghafm5OxWDh9hwZfxthWMA0GCSqGSIb3DQEB + CwUAMBYxFDASBgNVBAMMC3NhbWx0ZXN0LmlkMB4XDTE4MDgyNDIxMTQwOVoXDTM4 + MDgyNDIxMTQwOVowFjEUMBIGA1UEAwwLc2FtbHRlc3QuaWQwggEiMA0GCSqGSIb3 + DQEBAQUAA4IBDwAwggEKAoIBAQC0Z4QX1NFKs71ufbQwoQoW7qkNAJRIANGA4iM0 + ThYghul3pC+FwrGv37aTxWXfA1UG9njKbbDreiDAZKngCgyjxj0uJ4lArgkr4AOE + jj5zXA81uGHARfUBctvQcsZpBIxDOvUUImAl+3NqLgMGF2fktxMG7kX3GEVNc1kl + bN3dfYsaw5dUrw25DheL9np7G/+28GwHPvLb4aptOiONbCaVvh9UMHEA9F7c0zfF + /cL5fOpdVa54wTI0u12CsFKt78h6lEGG5jUs/qX9clZncJM7EFkN3imPPy+0HC8n + spXiH/MZW8o2cqWRkrw3MzBZW3Ojk5nQj40V6NUbjb7kfejzAgMBAAGjVzBVMB0G + A1UdDgQWBBQT6Y9J3Tw/hOGc8PNV7JEE4k2ZNTA0BgNVHREELTArggtzYW1sdGVz + dC5pZIYcaHR0cHM6Ly9zYW1sdGVzdC5pZC9zYW1sL2lkcDANBgkqhkiG9w0BAQsF + AAOCAQEASk3guKfTkVhEaIVvxEPNR2w3vWt3fwmwJCccW98XXLWgNbu3YaMb2RSn + 7Th4p3h+mfyk2don6au7Uyzc1Jd39RNv80TG5iQoxfCgphy1FYmmdaSfO8wvDtHT + TNiLArAxOYtzfYbzb5QrNNH/gQEN8RJaEf/g/1GTw9x/103dSMK0RXtl+fRs2nbl + D1JJKSQ3AdhxK/weP3aUPtLxVVJ9wMOQOfcy02l+hHMb6uAjsPOpOVKqi3M8XmcU + ZOpx4swtgGdeoSpeRyrtMvRwdcciNBp9UZome44qZAYH1iqrpmmjsfI9pJItsgWu + 3kXPjhSfj1AJGR1l9JGvJrHki1iHTA== + + + + + + + + + + MIIDEjCCAfqgAwIBAgIVAPVbodo8Su7/BaHXUHykx0Pi5CFaMA0GCSqGSIb3DQEB + CwUAMBYxFDASBgNVBAMMC3NhbWx0ZXN0LmlkMB4XDTE4MDgyNDIxMTQwOVoXDTM4 + MDgyNDIxMTQwOVowFjEUMBIGA1UEAwwLc2FtbHRlc3QuaWQwggEiMA0GCSqGSIb3 + DQEBAQUAA4IBDwAwggEKAoIBAQCQb+1a7uDdTTBBFfwOUun3IQ9nEuKM98SmJDWa + MwM877elswKUTIBVh5gB2RIXAPZt7J/KGqypmgw9UNXFnoslpeZbA9fcAqqu28Z4 + sSb2YSajV1ZgEYPUKvXwQEmLWN6aDhkn8HnEZNrmeXihTFdyr7wjsLj0JpQ+VUlc + 4/J+hNuU7rGYZ1rKY8AA34qDVd4DiJ+DXW2PESfOu8lJSOteEaNtbmnvH8KlwkDs + 1NvPTsI0W/m4SK0UdXo6LLaV8saIpJfnkVC/FwpBolBrRC/Em64UlBsRZm2T89ca + uzDee2yPUvbBd5kLErw+sC7i4xXa2rGmsQLYcBPhsRwnmBmlAgMBAAGjVzBVMB0G + A1UdDgQWBBRZ3exEu6rCwRe5C7f5QrPcAKRPUjA0BgNVHREELTArggtzYW1sdGVz + dC5pZIYcaHR0cHM6Ly9zYW1sdGVzdC5pZC9zYW1sL2lkcDANBgkqhkiG9w0BAQsF + AAOCAQEABZDFRNtcbvIRmblnZItoWCFhVUlq81ceSQddLYs8DqK340//hWNAbYdj + WcP85HhIZnrw6NGCO4bUipxZXhiqTA/A9d1BUll0vYB8qckYDEdPDduYCOYemKkD + dmnHMQWs9Y6zWiYuNKEJ9mf3+1N8knN/PK0TYVjVjXAf2CnOETDbLtlj6Nqb8La3 + sQkYmU+aUdopbjd5JFFwbZRaj6KiHXHtnIRgu8sUXNPrgipUgZUOVhP0C0N5OfE4 + JW8ZBrKgQC/6vJ2rSa9TlzI6JAa5Ww7gMXMP9M+cJUNQklcq+SBnTK8G+uBHgPKR + zBDsMIEzRtQZm4GIoHJae4zmnCekkQ== + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/govcd/test-resources/saml-test-sp-invalid.xml b/govcd/test-resources/saml-test-sp-invalid.xml new file mode 100644 index 000000000..32ed53dd2 --- /dev/null +++ b/govcd/test-resources/saml-test-sp-invalid.xml @@ -0,0 +1,69 @@ + + + + + + + MIIDJTCCAg2gAwIBAgIIS30ETjKyfIIwDQYJKoZIhvcNAQELBQAwOTE3MDUGA1UEAwwuVk13YXJl + IENsb3VkIERpcmVjdG9yIG9yZ2FuaXphdGlvbiBDZXJ0aWZpY2F0ZTAeFw0yMzA1MTUwOTM1MTZa + Fw0yNDA1MTQwOTM1MTZaMDkxNzA1BgNVBAMMLlZNd2FyZSBDbG91ZCBEaXJlY3RvciBvcmdhbml6 + YXRpb24gQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCL04KFXtIb + gJZyqJB75Z358iQ/XlPheHWMnTTOqJqjpOn+M8kLGufRMrjbNtLkbaVaIb9Rm/mrw9rBYHbSY6sw + LtjKtbvHpl4HegZmhXZvkNIbSXa6h+VO20ZXQvvNgDrdmQ9fWTZQDjzYTZrQxkidcsVaL7LdIMnx + SVFkUaUnU+fV41Q+hDuUr1F/dT1JzAPQR6JWViaASGmbZe/h3T5u2qZqDOl8uxED+GU/La51ESED + ZfyNxhhhgQGWlTlu9Jb+Pyl+ikk0Sc8mi4pBcrqEfEDRLLR4LvFb1BJoZlGv7UHup80OzuZ/LV6t + Ft9UHV3vHxUu3lp1BOrgv/X2bjyZAgMBAAGjMTAvMB0GA1UdDgQWBBRE8cLNFQtgw8sqnqhdKGoI + gGsuczAOBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQELBQADggEBAHf/fL4f+QpJQ69n1uU7nLeJ + 7gUVzLZDRpEzJ073h4dJsWxxjPUXS/0qkExVairsXd5KzNhk/lHovDYT4q5ijZFl78rnID2ha6d8 + /ejTvL0a3Fn46z9bjcSnr0Iyn4Gw3oT4YQ5rckML4JUEPncaBdBny+5bjXgENbgnsEwcQt7Uoebg + i3SWr7Dl/D5yb02VURtkXhef1MJTv+UHpjdCxDiVt3DpZ+JZcwtMqqN3qxJkGr774R8CwstUCOsp + PlDQeVqaE4RbJMWdHhNgSfKnsf/jZabSLq0dxgCX69AvOklZDpi8kRubDUWA4khVhTIbScVu4fXc + 9FjRf/AoIuzReJs= + + + + + + + + MIIDJTCCAg2gAwIBAgIITNq8LW1qHXYwDQYJKoZIhvcNAQELBQAwOTE3MDUGA1UEAwwuVk13YXJl + IENsb3VkIERpcmVjdG9yIG9yZ2FuaXphdGlvbiBDZXJ0aWZpY2F0ZTAeFw0yMzA1MTUwOTM1MTda + Fw0yNDA1MTQwOTM1MTdaMDkxNzA1BgNVBAMMLlZNd2FyZSBDbG91ZCBEaXJlY3RvciBvcmdhbml6 + YXRpb24gQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCgXzxYni+y + d1cyT1v73yRqi88Us9sxinuMTs5AFCHVfhu2Nc+zuy4Qep15bl/ygnw90SmCLs1cKR1Q+kBxYFiP + N/ar6Zz7VpeA3iJpYdLH2zlUAxvDm5sOWa2E31wa12K7ursxygOKuSnXdL6d9vIDv/4S6u5qntRn + fw5tPHCgMBAtgYBhCDcUR7g7nmg+niAeJ5hrdWNZ8ILYFFF7Rsh14NdjwFHTpMWp/F+rI9tpCZDo + KdEAwulMdqMg06pUw56EwYHuxlfHyZYtNi/6qo2CQLc4IAKYc0O9b4K4c1PNFcz41x8EwTp6Qo+S + Jope3t4b6kbksXdgBXsrpTy+z3ptAgMBAAGjMTAvMB0GA1UdDgQWBBQ49IQcbwdh3OFVCSMUfz// + dC3pgDAOBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQELBQADggEBAG/lFYg8C6roBNiUFQEf9GRo + nak06njZC39mfw05NPh6qewOs3KhWP4Y7GRh+rvDbPWHLux23MCuh4ir9TB9Gdo1sUoe90IHfsNw + brCN3+0sfOBDLyGtpd/0jEW0zHdQitWThwzTWefVMgLGSlNZNy0VZZHROqQy2XRE45A9vg+Q63hg + nfsCBPIn1Y4z9LcSJKWLhbtqDo+JMiqHiF2V5Z8L2MoTe1eQaQQSiiF9a3Yh9MA5zJhSgOVazuAW + k+XThUd0aNwuuLGOIa73DjnYiY4BiFAUAZBgfbiQljXoCnZvBWXNBcfy8WdczlqwuJuImnG9MR8L + wshfq04S3w8z9rs= + + + + + + + urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress + urn:oasis:names:tc:SAML:2.0:nameid-format:transient + urn:oasis:names:tc:SAML:2.0:nameid-format:persistent + urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified + urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName + + + + diff --git a/govcd/test-resources/saml-test-sp.xml b/govcd/test-resources/saml-test-sp.xml new file mode 100644 index 000000000..624e63439 --- /dev/null +++ b/govcd/test-resources/saml-test-sp.xml @@ -0,0 +1,69 @@ + + + + + + + MIIDJTCCAg2gAwIBAgIIS30ETjKyfIIwDQYJKoZIhvcNAQELBQAwOTE3MDUGA1UEAwwuVk13YXJl + IENsb3VkIERpcmVjdG9yIG9yZ2FuaXphdGlvbiBDZXJ0aWZpY2F0ZTAeFw0yMzA1MTUwOTM1MTZa + Fw0yNDA1MTQwOTM1MTZaMDkxNzA1BgNVBAMMLlZNd2FyZSBDbG91ZCBEaXJlY3RvciBvcmdhbml6 + YXRpb24gQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCL04KFXtIb + gJZyqJB75Z358iQ/XlPheHWMnTTOqJqjpOn+M8kLGufRMrjbNtLkbaVaIb9Rm/mrw9rBYHbSY6sw + LtjKtbvHpl4HegZmhXZvkNIbSXa6h+VO20ZXQvvNgDrdmQ9fWTZQDjzYTZrQxkidcsVaL7LdIMnx + SVFkUaUnU+fV41Q+hDuUr1F/dT1JzAPQR6JWViaASGmbZe/h3T5u2qZqDOl8uxED+GU/La51ESED + ZfyNxhhhgQGWlTlu9Jb+Pyl+ikk0Sc8mi4pBcrqEfEDRLLR4LvFb1BJoZlGv7UHup80OzuZ/LV6t + Ft9UHV3vHxUu3lp1BOrgv/X2bjyZAgMBAAGjMTAvMB0GA1UdDgQWBBRE8cLNFQtgw8sqnqhdKGoI + gGsuczAOBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQELBQADggEBAHf/fL4f+QpJQ69n1uU7nLeJ + 7gUVzLZDRpEzJ073h4dJsWxxjPUXS/0qkExVairsXd5KzNhk/lHovDYT4q5ijZFl78rnID2ha6d8 + /ejTvL0a3Fn46z9bjcSnr0Iyn4Gw3oT4YQ5rckML4JUEPncaBdBny+5bjXgENbgnsEwcQt7Uoebg + i3SWr7Dl/D5yb02VURtkXhef1MJTv+UHpjdCxDiVt3DpZ+JZcwtMqqN3qxJkGr774R8CwstUCOsp + PlDQeVqaE4RbJMWdHhNgSfKnsf/jZabSLq0dxgCX69AvOklZDpi8kRubDUWA4khVhTIbScVu4fXc + 9FjRf/AoIuzReJs= + + + + + + + + MIIDJTCCAg2gAwIBAgIITNq8LW1qHXYwDQYJKoZIhvcNAQELBQAwOTE3MDUGA1UEAwwuVk13YXJl + IENsb3VkIERpcmVjdG9yIG9yZ2FuaXphdGlvbiBDZXJ0aWZpY2F0ZTAeFw0yMzA1MTUwOTM1MTda + Fw0yNDA1MTQwOTM1MTdaMDkxNzA1BgNVBAMMLlZNd2FyZSBDbG91ZCBEaXJlY3RvciBvcmdhbml6 + YXRpb24gQ2VydGlmaWNhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCgXzxYni+y + d1cyT1v73yRqi88Us9sxinuMTs5AFCHVfhu2Nc+zuy4Qep15bl/ygnw90SmCLs1cKR1Q+kBxYFiP + N/ar6Zz7VpeA3iJpYdLH2zlUAxvDm5sOWa2E31wa12K7ursxygOKuSnXdL6d9vIDv/4S6u5qntRn + fw5tPHCgMBAtgYBhCDcUR7g7nmg+niAeJ5hrdWNZ8ILYFFF7Rsh14NdjwFHTpMWp/F+rI9tpCZDo + KdEAwulMdqMg06pUw56EwYHuxlfHyZYtNi/6qo2CQLc4IAKYc0O9b4K4c1PNFcz41x8EwTp6Qo+S + Jope3t4b6kbksXdgBXsrpTy+z3ptAgMBAAGjMTAvMB0GA1UdDgQWBBQ49IQcbwdh3OFVCSMUfz// + dC3pgDAOBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQELBQADggEBAG/lFYg8C6roBNiUFQEf9GRo + nak06njZC39mfw05NPh6qewOs3KhWP4Y7GRh+rvDbPWHLux23MCuh4ir9TB9Gdo1sUoe90IHfsNw + brCN3+0sfOBDLyGtpd/0jEW0zHdQitWThwzTWefVMgLGSlNZNy0VZZHROqQy2XRE45A9vg+Q63hg + nfsCBPIn1Y4z9LcSJKWLhbtqDo+JMiqHiF2V5Z8L2MoTe1eQaQQSiiF9a3Yh9MA5zJhSgOVazuAW + k+XThUd0aNwuuLGOIa73DjnYiY4BiFAUAZBgfbiQljXoCnZvBWXNBcfy8WdczlqwuJuImnG9MR8L + wshfq04S3w8z9rs= + + + + + + + urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress + urn:oasis:names:tc:SAML:2.0:nameid-format:transient + urn:oasis:names:tc:SAML:2.0:nameid-format:persistent + urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified + urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName + + + + diff --git a/govcd/test-resources/test.json b/govcd/test-resources/test.json new file mode 100644 index 000000000..10f2877cb --- /dev/null +++ b/govcd/test-resources/test.json @@ -0,0 +1 @@ +{ "Name": "test" } diff --git a/govcd/test-resources/test_empty.json b/govcd/test-resources/test_empty.json new file mode 100644 index 000000000..e69de29bb diff --git a/govcd/test-resources/test_emptyJSON.json b/govcd/test-resources/test_emptyJSON.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/govcd/test-resources/test_emptyJSON.json @@ -0,0 +1 @@ +{} diff --git a/govcd/ui_plugin.go b/govcd/ui_plugin.go new file mode 100644 index 000000000..af0fb1b60 --- /dev/null +++ b/govcd/ui_plugin.go @@ -0,0 +1,427 @@ +package govcd + +import ( + "archive/zip" + "crypto/sha256" + "encoding/json" + "fmt" + "github.com/vmware/go-vcloud-director/v2/types/v56" + "github.com/vmware/go-vcloud-director/v2/util" + "io" + "net/http" + "os" + "path/filepath" + "regexp" + "strings" +) + +type UIPlugin struct { + UIPluginMetadata *types.UIPluginMetadata + client *Client +} + +// AddUIPlugin reads the plugin ZIP file located in the input path, obtains the inner metadata, sends it to +// VCD and performs the plugin upload. +func (vcdClient *VCDClient) AddUIPlugin(pluginPath string, enabled bool) (*UIPlugin, error) { + if strings.TrimSpace(pluginPath) == "" { + return nil, fmt.Errorf("plugin path must not be empty") + } + uiPluginMetadataPayload, err := getPluginMetadata(pluginPath) + if err != nil { + return nil, fmt.Errorf("error retrieving the metadata for the given plugin %s: %s", pluginPath, err) + } + uiPluginMetadataPayload.Enabled = enabled + uiPluginMetadata, err := createUIPlugin(&vcdClient.Client, uiPluginMetadataPayload) + if err != nil { + return nil, fmt.Errorf("error creating the UI plugin: %s", err) + } + err = uiPluginMetadata.upload(pluginPath) + if err != nil { + return nil, fmt.Errorf("error uploading the UI plugin: %s", err) + } + + return uiPluginMetadata, nil +} + +// GetAllUIPlugins retrieves a slice with all the available UIPlugin objects present in VCD. +func (vcdClient *VCDClient) GetAllUIPlugins() ([]*UIPlugin, error) { + endpoint := types.OpenApiEndpointExtensionsUi // This one is not versioned, hence not using types.OpenApiPathVersion1_0_0 or alike + apiVersion, err := vcdClient.Client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := vcdClient.Client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + var typeResponses []*types.UIPluginMetadata + err = vcdClient.Client.OpenApiGetItem(apiVersion, urlRef, nil, &typeResponses, nil) + if err != nil { + return nil, err + } + + // Wrap all typeResponses into UIPlugin types with client + uiPlugins := make([]*UIPlugin, len(typeResponses)) + for sliceIndex := range typeResponses { + uiPlugins[sliceIndex] = &UIPlugin{ + UIPluginMetadata: typeResponses[sliceIndex], + client: &vcdClient.Client, + } + } + + return uiPlugins, nil +} + +// GetUIPluginById obtains a unique UIPlugin identified by its URN. +func (vcdClient *VCDClient) GetUIPluginById(id string) (*UIPlugin, error) { + endpoint := types.OpenApiEndpointExtensionsUi // This one is not versioned, hence not using types.OpenApiPathVersion1_0_0 or alike + apiVersion, err := vcdClient.Client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := vcdClient.Client.OpenApiBuildEndpoint(endpoint, id) + if err != nil { + return nil, err + } + + result := &UIPlugin{ + UIPluginMetadata: &types.UIPluginMetadata{}, + client: &vcdClient.Client, + } + err = vcdClient.Client.OpenApiGetItem(apiVersion, urlRef, nil, result.UIPluginMetadata, nil) + if err != nil { + return nil, amendUIPluginGetByIdError(id, err) + } + + return result, nil +} + +// amendUIPluginGetByIdError is a workaround for a bug in VCD that causes the GET endpoint to return an ugly error 500 with a NullPointerException +// when the UI Plugin with given ID is not found +func amendUIPluginGetByIdError(id string, err error) error { + if err != nil && strings.Contains(err.Error(), "NullPointerException") { + return fmt.Errorf("could not find any UI plugin with ID '%s': %s", id, ErrorEntityNotFound) + } + return err +} + +// GetUIPlugin obtains a unique UIPlugin identified by the combination of its vendor, plugin name and version. +func (vcdClient *VCDClient) GetUIPlugin(vendor, pluginName, version string) (*UIPlugin, error) { + allUIPlugins, err := vcdClient.GetAllUIPlugins() + if err != nil { + return nil, err + } + for _, plugin := range allUIPlugins { + if plugin.IsTheSameAs(&UIPlugin{UIPluginMetadata: &types.UIPluginMetadata{ + Vendor: vendor, + PluginName: pluginName, + Version: version, + }}) { + return plugin, nil + } + } + + return nil, fmt.Errorf("could not find any UI plugin with vendor '%s', pluginName '%s' and version '%s': %s", vendor, pluginName, version, ErrorEntityNotFound) +} + +// GetPublishedTenants gets all the Organization references where the receiver UIPlugin is published. +func (uiPlugin *UIPlugin) GetPublishedTenants() (types.OpenApiReferences, error) { + if strings.TrimSpace(uiPlugin.UIPluginMetadata.ID) == "" { + return nil, fmt.Errorf("plugin ID is required but it is empty") + } + + endpoint := types.OpenApiEndpointExtensionsUiTenants // This one is not versioned, hence not using types.OpenApiPathVersion1_0_0 or alike + apiVersion, err := uiPlugin.client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := uiPlugin.client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, uiPlugin.UIPluginMetadata.ID)) + if err != nil { + return nil, err + } + + var orgRefs types.OpenApiReferences + err = uiPlugin.client.OpenApiGetAllItems(apiVersion, urlRef, nil, &orgRefs, nil) + if err != nil { + return nil, err + } + return orgRefs, nil +} + +// Publish publishes the receiver UIPlugin to the given Organizations. +// Does not modify the receiver UIPlugin. +func (uiPlugin *UIPlugin) Publish(orgs types.OpenApiReferences) error { + if len(orgs) == 0 { + return nil + } + return publishOrUnpublishFromOrgs(uiPlugin.client, uiPlugin.UIPluginMetadata.ID, orgs, types.OpenApiEndpointExtensionsUiTenantsPublish) +} + +// Unpublish unpublishes the receiver UIPlugin from the given Organizations. +// Does not modify the receiver UIPlugin. +func (uiPlugin *UIPlugin) Unpublish(orgs types.OpenApiReferences) error { + if len(orgs) == 0 { + return nil + } + return publishOrUnpublishFromOrgs(uiPlugin.client, uiPlugin.UIPluginMetadata.ID, orgs, types.OpenApiEndpointExtensionsUiTenantsUnpublish) +} + +// PublishAll publishes the receiver UIPlugin to all available Organizations. +// Does not modify the receiver UIPlugin. +func (uiPlugin *UIPlugin) PublishAll() error { + return publishOrUnpublishFromOrgs(uiPlugin.client, uiPlugin.UIPluginMetadata.ID, nil, types.OpenApiEndpointExtensionsUiTenantsPublishAll) +} + +// UnpublishAll unpublishes the receiver UIPlugin from all available Organizations. +// Does not modify the receiver UIPlugin. +func (uiPlugin *UIPlugin) UnpublishAll() error { + return publishOrUnpublishFromOrgs(uiPlugin.client, uiPlugin.UIPluginMetadata.ID, nil, types.OpenApiEndpointExtensionsUiTenantsUnpublishAll) +} + +// IsTheSameAs retruns true if the receiver UIPlugin has the same name, vendor and version as the input. +func (uiPlugin *UIPlugin) IsTheSameAs(otherUiPlugin *UIPlugin) bool { + if otherUiPlugin == nil { + return false + } + return uiPlugin.UIPluginMetadata.PluginName == otherUiPlugin.UIPluginMetadata.PluginName && + uiPlugin.UIPluginMetadata.Version == otherUiPlugin.UIPluginMetadata.Version && + uiPlugin.UIPluginMetadata.Vendor == otherUiPlugin.UIPluginMetadata.Vendor +} + +// Update performs an update to several receiver plugin attributes +func (uiPlugin *UIPlugin) Update(enable, providerScoped, tenantScoped bool) error { + if strings.TrimSpace(uiPlugin.UIPluginMetadata.ID) == "" { + return fmt.Errorf("plugin ID is required but it is empty") + } + + endpoint := types.OpenApiEndpointExtensionsUi // This one is not versioned, hence not using types.OpenApiPathVersion1_0_0 or alike + apiVersion, err := uiPlugin.client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return err + } + + urlRef, err := uiPlugin.client.OpenApiBuildEndpoint(endpoint, uiPlugin.UIPluginMetadata.ID) + if err != nil { + return err + } + + payload := &types.UIPluginMetadata{ + Vendor: uiPlugin.UIPluginMetadata.Vendor, + License: uiPlugin.UIPluginMetadata.License, + Link: uiPlugin.UIPluginMetadata.Link, + PluginName: uiPlugin.UIPluginMetadata.PluginName, + Version: uiPlugin.UIPluginMetadata.Version, + Description: uiPlugin.UIPluginMetadata.Description, + ProviderScoped: providerScoped, + TenantScoped: tenantScoped, + Enabled: enable, + } + err = uiPlugin.client.OpenApiPutItem(apiVersion, urlRef, nil, payload, uiPlugin.UIPluginMetadata, nil) + if err != nil { + return err + } + return nil +} + +// Delete deletes the receiver UIPlugin from VCD. +func (uiPlugin *UIPlugin) Delete() error { + if strings.TrimSpace(uiPlugin.UIPluginMetadata.ID) == "" { + return fmt.Errorf("plugin ID must not be empty") + } + + endpoint := types.OpenApiEndpointExtensionsUi // This one is not versioned, hence not using types.OpenApiPathVersion1_0_0 or alike + apiVersion, err := uiPlugin.client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return err + } + + urlRef, err := uiPlugin.client.OpenApiBuildEndpoint(endpoint, uiPlugin.UIPluginMetadata.ID) + if err != nil { + return err + } + + err = uiPlugin.client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) + if err != nil { + return err + } + uiPlugin.UIPluginMetadata = &types.UIPluginMetadata{} + return nil +} + +// getPluginMetadata retrieves the types.UIPluginMetadata information stored inside the given plugin file, that should +// be a ZIP file. +func getPluginMetadata(pluginPath string) (*types.UIPluginMetadata, error) { + archive, err := zip.OpenReader(filepath.Clean(pluginPath)) + if err != nil { + return nil, err + } + defer func() { + if err := archive.Close(); err != nil { + util.Logger.Printf("Error closing ZIP file: %s\n", err) + } + }() + + var manifest *zip.File + for _, f := range archive.File { + if f.Name == "manifest.json" { + manifest = f + break + } + } + if manifest == nil { + return nil, fmt.Errorf("could not find manifest.json inside the file %s", pluginPath) + } + + manifestContents, err := manifest.Open() + if err != nil { + return nil, err + } + defer func() { + if err := manifestContents.Close(); err != nil { + util.Logger.Printf("Error closing manifest file: %s\n", err) + } + }() + + manifestBytes, err := io.ReadAll(manifestContents) + if err != nil { + return nil, err + } + + var unmarshaledJson map[string]interface{} + err = json.Unmarshal(manifestBytes, &unmarshaledJson) + if err != nil { + return nil, err + } + + result := &types.UIPluginMetadata{ + Vendor: unmarshaledJson["vendor"].(string), + License: unmarshaledJson["license"].(string), + Link: unmarshaledJson["link"].(string), + PluginName: unmarshaledJson["name"].(string), + Version: unmarshaledJson["version"].(string), + Description: unmarshaledJson["description"].(string), + } + + for _, scope := range unmarshaledJson["scope"].([]interface{}) { + if strings.Contains(scope.(string), "provider") { + result.ProviderScoped = true + } else if strings.Contains(scope.(string), "tenant") { + result.TenantScoped = true + } + } + + return result, nil +} + +// createUIPlugin creates a new empty UIPlugin in VCD and sets the provided plugin metadata. +// The UI plugin contents should be uploaded afterwards. +func createUIPlugin(client *Client, uiPluginMetadata *types.UIPluginMetadata) (*UIPlugin, error) { + endpoint := types.OpenApiEndpointExtensionsUi // This one is not versioned, hence not using types.OpenApiPathVersion1_0_0 or alike + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + result := &UIPlugin{ + UIPluginMetadata: &types.UIPluginMetadata{}, + client: client, + } + + err = client.OpenApiPostItem(apiVersion, urlRef, nil, uiPluginMetadata, result.UIPluginMetadata, nil) + if err != nil { + return nil, err + } + + return result, nil +} + +// This function uploads the given UI Plugin to VCD. Only the plugin path is required. +func (ui *UIPlugin) upload(pluginPath string) error { + fileContents, err := os.ReadFile(filepath.Clean(pluginPath)) + if err != nil { + return err + } + + endpoint := types.OpenApiEndpointExtensionsUiPlugin // This one is not versioned, hence not using types.OpenApiPathVersion1_0_0 or alike + apiVersion, err := ui.client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return err + } + + urlRef, err := ui.client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, ui.UIPluginMetadata.ID)) + if err != nil { + return err + } + + uploadSpec := types.UploadSpec{ + FileName: filepath.Base(pluginPath), + ChecksumAlgo: "sha256", + Checksum: fmt.Sprintf("%x", sha256.Sum256(fileContents)), + Size: int64(len(fileContents)), + } + + headers, err := ui.client.OpenApiPostItemAndGetHeaders(apiVersion, urlRef, nil, uploadSpec, nil, nil) + if err != nil { + return err + } + + transferId, err := getTransferIdFromHeader(headers) + if err != nil { + return err + } + + transferEndpoint := fmt.Sprintf("%s://%s/transfer/%s", ui.client.VCDHREF.Scheme, ui.client.VCDHREF.Host, transferId) + request, err := newFileUploadRequest(ui.client, transferEndpoint, fileContents, 0, uploadSpec.Size, uploadSpec.Size) + if err != nil { + return err + } + + response, err := ui.client.Http.Do(request) + if err != nil { + return err + } + return response.Body.Close() +} + +// getTransferIdFromHeader retrieves a valid transfer ID from any given HTTP headers, that can be used to upload +// a UI Plugin to VCD. +func getTransferIdFromHeader(headers http.Header) (string, error) { + rawLinkContent := headers.Get("link") + if rawLinkContent == "" { + return "", fmt.Errorf("error during UI plugin upload, the POST call didn't return any transfer link") + } + linkRegex := regexp.MustCompile(`<\S+/transfer/(\S+)>`) + matches := linkRegex.FindStringSubmatch(rawLinkContent) + if len(matches) < 2 { + return "", fmt.Errorf("error during UI plugin upload, the POST call didn't return a valid transfer link: %s", rawLinkContent) + } + return matches[1], nil +} + +// publishOrUnpublishFromOrgs publishes or unpublishes (depending on the input endpoint) the UI Plugin with given ID from all available +// organizations. +func publishOrUnpublishFromOrgs(client *Client, pluginId string, orgs types.OpenApiReferences, endpoint string) error { + if strings.TrimSpace(pluginId) == "" { + return fmt.Errorf("plugin ID is required but it is empty") + } + + apiVersion, err := client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, pluginId)) + if err != nil { + return err + } + + return client.OpenApiPostItem(apiVersion, urlRef, nil, orgs, nil, nil) +} diff --git a/govcd/ui_plugin_test.go b/govcd/ui_plugin_test.go new file mode 100644 index 000000000..6ce2e5366 --- /dev/null +++ b/govcd/ui_plugin_test.go @@ -0,0 +1,152 @@ +//go:build functional || openapi || uiPlugin || ALL + +package govcd + +import ( + "fmt" + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" + "strings" +) + +func init() { + testingTags["uiPlugin"] = "ui_plugin_test.go" +} + +// Test_UIPlugin tests all the possible operations that can be done with a UIPlugin object in VCD. +func (vcd *TestVCD) Test_UIPlugin(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + + if vcd.config.Media.UiPluginPath == "" { + check.Skip("The testing configuration property 'media.uiPluginPath' is empty") + } + + testUIPluginMetadata, err := getPluginMetadata(vcd.config.Media.UiPluginPath) + check.Assert(err, IsNil) + + // Add a plugin present on disk + newUIPlugin, err := vcd.client.AddUIPlugin(vcd.config.Media.UiPluginPath, true) + check.Assert(err, IsNil) + AddToCleanupListOpenApi(newUIPlugin.UIPluginMetadata.ID, check.TestName(), types.OpenApiEndpointExtensionsUi+newUIPlugin.UIPluginMetadata.ID) + + // Assert that the returned metadata from VCD corresponds to the one present inside the ZIP file. + check.Assert(newUIPlugin.UIPluginMetadata.ID, Not(Equals), "") + check.Assert(newUIPlugin.UIPluginMetadata.Vendor, Equals, testUIPluginMetadata.Vendor) + check.Assert(newUIPlugin.UIPluginMetadata.License, Equals, testUIPluginMetadata.License) + check.Assert(newUIPlugin.UIPluginMetadata.Link, Equals, testUIPluginMetadata.Link) + check.Assert(newUIPlugin.UIPluginMetadata.PluginName, Equals, testUIPluginMetadata.PluginName) + check.Assert(newUIPlugin.UIPluginMetadata.Version, Equals, testUIPluginMetadata.Version) + check.Assert(newUIPlugin.UIPluginMetadata.Description, Equals, testUIPluginMetadata.Description) + check.Assert(newUIPlugin.UIPluginMetadata.ProviderScoped, Equals, testUIPluginMetadata.ProviderScoped) + check.Assert(newUIPlugin.UIPluginMetadata.TenantScoped, Equals, testUIPluginMetadata.TenantScoped) + check.Assert(newUIPlugin.UIPluginMetadata.Enabled, Equals, true) + + // Try to add the same plugin twice, it should fail + _, err = vcd.client.AddUIPlugin(vcd.config.Media.UiPluginPath, true) + check.Assert(err, NotNil) + check.Assert(true, Equals, strings.Contains(err.Error(), "same pluginName-version-vendor")) + + // We refresh it to have the latest status + newUIPlugin, err = vcd.client.GetUIPluginById(newUIPlugin.UIPluginMetadata.ID) + check.Assert(err, IsNil) + check.Assert(true, Equals, newUIPlugin.UIPluginMetadata.PluginStatus == "ready" || newUIPlugin.UIPluginMetadata.PluginStatus == "unavailable") + + // We check that the error returned by a non-existent ID is correct: + _, err = vcd.client.GetUIPluginById("urn:vcloud:uiPlugin:00000000-0000-0000-0000-000000000000") + check.Assert(err, NotNil) + check.Assert(true, Equals, strings.Contains(err.Error(), "could not find any UI plugin with ID")) + + // Retrieve the created plugin using different getters + allUIPlugins, err := vcd.client.GetAllUIPlugins() + check.Assert(err, IsNil) + for _, plugin := range allUIPlugins { + if plugin.IsTheSameAs(newUIPlugin) { + plugin.UIPluginMetadata.PluginStatus = newUIPlugin.UIPluginMetadata.PluginStatus // We ignore status as it can be quite arbitrary + check.Assert(plugin.UIPluginMetadata, DeepEquals, newUIPlugin.UIPluginMetadata) + } + } + retrievedUIPlugin, err := vcd.client.GetUIPlugin(newUIPlugin.UIPluginMetadata.Vendor, newUIPlugin.UIPluginMetadata.PluginName, newUIPlugin.UIPluginMetadata.Version) + check.Assert(err, IsNil) + retrievedUIPlugin.UIPluginMetadata.PluginStatus = newUIPlugin.UIPluginMetadata.PluginStatus // We ignore status as it can be quite arbitrary + check.Assert(retrievedUIPlugin.UIPluginMetadata, DeepEquals, newUIPlugin.UIPluginMetadata) + + // Publishing the plugin to all tenants + err = newUIPlugin.PublishAll() + check.Assert(err, IsNil) + + // Retrieving the published tenants, it should be at least the number of tenants provided in the test configuration + 1 (the System one) + orgRefs, err := newUIPlugin.GetPublishedTenants() + check.Assert(err, IsNil) + check.Assert(orgRefs, NotNil) + check.Assert(len(orgRefs) >= len(vcd.config.Tenants)+1, Equals, true) + + // Unpublishing the plugin from all the tenants + err = newUIPlugin.UnpublishAll() + check.Assert(err, IsNil) + + // Retrieving the published tenants, it should equal to 0 + orgRefs, err = newUIPlugin.GetPublishedTenants() + check.Assert(err, IsNil) + check.Assert(orgRefs, NotNil) + check.Assert(len(orgRefs), Equals, 0) + + // Publishing/Unpublishing to/from a specific tenant, if available + if len(vcd.config.Tenants) > 0 { + existingOrg, err := vcd.client.GetOrgByName(vcd.config.Tenants[0].SysOrg) + check.Assert(err, IsNil) + existingOrgRefs := types.OpenApiReferences{{Name: existingOrg.Org.Name, ID: existingOrg.Org.ID}} + + // Publish to the retrieved tenant + err = newUIPlugin.Publish(existingOrgRefs) + check.Assert(err, IsNil) + + // Retrieving the published tenants, it should equal to the tenant above + orgRefs, err = newUIPlugin.GetPublishedTenants() + check.Assert(err, IsNil) + check.Assert(orgRefs, NotNil) + check.Assert(len(orgRefs), Equals, 1) + check.Assert(orgRefs[0].Name, Equals, existingOrg.Org.Name) + + // Unpublishing from the same specific tenant + err = newUIPlugin.Unpublish(existingOrgRefs) + check.Assert(err, IsNil) + + // Retrieving the published tenants, it should equal to 0 as we just unpublished it + orgRefs, err = newUIPlugin.GetPublishedTenants() + check.Assert(err, IsNil) + check.Assert(orgRefs, NotNil) + check.Assert(len(orgRefs), Equals, 0) + } + + // Check that the plugin can be disabled and its scope changed + err = newUIPlugin.Update(false, false, false) + check.Assert(err, IsNil) + check.Assert(newUIPlugin.UIPluginMetadata.Enabled, Equals, false) + check.Assert(newUIPlugin.UIPluginMetadata.ProviderScoped, Equals, false) + check.Assert(newUIPlugin.UIPluginMetadata.TenantScoped, Equals, false) + + // Check that the plugin can be enabled again and its scope changed + err = newUIPlugin.Update(true, true, true) + check.Assert(err, IsNil) + check.Assert(newUIPlugin.UIPluginMetadata.Enabled, Equals, true) + check.Assert(newUIPlugin.UIPluginMetadata.ProviderScoped, Equals, true) + check.Assert(newUIPlugin.UIPluginMetadata.TenantScoped, Equals, true) + + check.Assert(newUIPlugin.IsTheSameAs(retrievedUIPlugin), Equals, true) + check.Assert(newUIPlugin.IsTheSameAs(&UIPlugin{UIPluginMetadata: &types.UIPluginMetadata{Vendor: "foo", Version: "1.2.3", PluginName: "bar"}}), Equals, false) + + // Delete the created plugin + err = newUIPlugin.Delete() + check.Assert(err, IsNil) + check.Assert(*newUIPlugin.UIPluginMetadata, DeepEquals, types.UIPluginMetadata{}) + + // Check that the plugin was correctly deleted + _, err = vcd.client.GetUIPlugin(retrievedUIPlugin.UIPluginMetadata.Vendor, retrievedUIPlugin.UIPluginMetadata.PluginName, retrievedUIPlugin.UIPluginMetadata.Version) + check.Assert(err, NotNil) + check.Assert(true, Equals, strings.Contains(err.Error(), ErrorEntityNotFound.Error())) + _, err = vcd.client.GetUIPluginById(retrievedUIPlugin.UIPluginMetadata.ID) + check.Assert(err, NotNil) + check.Assert(true, Equals, strings.Contains(err.Error(), ErrorEntityNotFound.Error())) +} diff --git a/govcd/ui_plugin_unit_test.go b/govcd/ui_plugin_unit_test.go new file mode 100644 index 000000000..472467966 --- /dev/null +++ b/govcd/ui_plugin_unit_test.go @@ -0,0 +1,139 @@ +//go:build unit || ALL + +package govcd + +import ( + "net/http" + "net/textproto" + "reflect" + "testing" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +// Test_getPluginMetadata tests that getPluginMetadata can retrieve correctly the UI plugin metadata information +// stored inside the ZIP file. +func Test_getPluginMetadata(t *testing.T) { + + // This object is equivalent to the manifest.json that is inside the ../test-resources/ui_plugin.zip file + var testUIPluginMetadata = &types.UIPluginMetadata{ + Vendor: "VMware", + License: "BSD-2-Clause", + Link: "http://www.vmware.com", + PluginName: "Test Plugin", + Version: "1.2.3", + Description: "Test Plugin description", + ProviderScoped: true, + TenantScoped: true, + } + + tests := []struct { + name string + pluginPath string + want *types.UIPluginMetadata + wantErr bool + }{ + { + name: "get ui plugin metadata", + pluginPath: "../test-resources/ui_plugin.zip", + want: testUIPluginMetadata, + }, + { + name: "invalid plugin", + pluginPath: "../test-resources/udf_test.iso", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := getPluginMetadata(tt.pluginPath) + if (err != nil) != tt.wantErr { + t.Errorf("getPluginMetadata() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("getPluginMetadata() got = %v, want %v", got, tt.want) + } + }) + } +} + +// Test_getTransferIdFromHeader tests that getTransferIdFromHeader can retrieve correctly a transfer ID from the headers +// of any HTTP response. +func Test_getTransferIdFromHeader(t *testing.T) { + tests := []struct { + name string + headers http.Header + want string + wantErr bool + }{ + { + name: "valid link in header", + headers: http.Header{ + textproto.CanonicalMIMEHeaderKey("link"): { + ";rel=\"upload:default\";type=\"application/octet-stream\"", + }, + }, + want: "cb63b0f6-ba56-43a8-8fe3-a64f0b25e7e5/my-amazing-plugin1.0.zip", + wantErr: false, + }, + { + name: "valid link in header with special URI", + headers: http.Header{ + textproto.CanonicalMIMEHeaderKey("link"): { + ";rel=\"upload:default\";type=\"application/octet-stream\"", + }, + }, + want: "cb63b0f6-ba56-43a8-8fe3-a64f0b25e7e5/my-amazing-plugin1.1.zip", + wantErr: false, + }, + { + name: "empty header", + headers: http.Header{ + textproto.CanonicalMIMEHeaderKey("link"): { + "", + }, + }, + wantErr: true, + }, + { + name: "empty link in header", + headers: http.Header{ + textproto.CanonicalMIMEHeaderKey("link"): { + "<>;rel=\"upload:default\";type=\"application/octet-stream\"", + }, + }, + wantErr: true, + }, + { + name: "no link part in header", + headers: http.Header{ + textproto.CanonicalMIMEHeaderKey("link"): { + "rel=\"upload:default\";type=\"application/octet-stream\"", + }, + }, + wantErr: true, + }, + { + name: "invalid header", + headers: http.Header{ + textproto.CanonicalMIMEHeaderKey("link"): { + "Error", + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := getTransferIdFromHeader(tt.headers) + if (err != nil) != tt.wantErr { + t.Errorf("getTransferIdFromHeader() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("getTransferIdFromHeader() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/govcd/upload.go b/govcd/upload.go index 82f987e36..42625e91e 100644 --- a/govcd/upload.go +++ b/govcd/upload.go @@ -67,8 +67,7 @@ func uploadFile(client *Client, filePath string, uDetails uploadDetails) (int64, var count int var pieceSize int64 - // #nosec G304 - linter does not like 'filePath' to be a variable. However this is necessary for file uploads. - file, err := os.Open(filePath) + file, err := os.Open(filepath.Clean(filePath)) if err != nil { util.Logger.Printf("[ERROR] during upload process - file open issue : %s, error %s ", filePath, err) *uDetails.uploadError = err @@ -82,7 +81,7 @@ func uploadFile(client *Client, filePath string, uDetails uploadDetails) (int64, return 0, err } - defer file.Close() + defer safeClose(file) fileSize := fileInfo.Size() // when file size in OVF does not exist, use real file size instead @@ -160,8 +159,9 @@ func newFileUploadRequest(client *Client, requestUrl string, filePart []byte, of rangeExpression := "bytes " + strconv.FormatInt(int64(offset), 10) + "-" + strconv.FormatInt(int64(offset+filePartSize-1), 10) + "/" + strconv.FormatInt(int64(fileSizeToUpload), 10) uploadReq.Header.Set("Content-Range", rangeExpression) - for key, value := range uploadReq.Header { - util.Logger.Printf("[TRACE] Header: %s :%s \n", key, value) + sanitizedHeader := util.SanitizedHeader(uploadReq.Header) + for key, value := range sanitizedHeader { + util.Logger.Printf("[TRACE] Header: %s: %s \n", key, value) } return uploadReq, nil @@ -185,7 +185,10 @@ func uploadPartFile(client *Client, part []byte, partDataSize int64, uDetails up if err != nil { return fmt.Errorf("file upload failed. Err: %s", err) } - response.Body.Close() + err = response.Body.Close() + if err != nil { + return fmt.Errorf("file closing failed. Err: %s", err) + } uDetails.callBack(uDetails.uploadedBytesForCallback+partDataSize, uDetails.allFilesSize) @@ -197,8 +200,11 @@ func makeEmptyRequest(client *Client) { apiEndpoint := client.VCDHREF apiEndpoint.Path += "/query?type=task&format=records&page=1&pageSize=5&" - _, _ = client.ExecuteRequest(apiEndpoint.String(), http.MethodGet, + _, err := client.ExecuteRequest(apiEndpoint.String(), http.MethodGet, "", "error making empty request: %s", nil, nil) + if err != nil { + util.Logger.Printf("[DEBUG - makeEmptyRequest] error executing request: %s", err) + } } func getUploadLink(files *types.FilesList) (*url.URL, error) { diff --git a/govcd/user.go b/govcd/user.go index d2cc2a502..e08f77dc3 100644 --- a/govcd/user.go +++ b/govcd/user.go @@ -29,6 +29,7 @@ type OrgUserConfiguration struct { ProviderType string // Optional: defaults to "INTEGRATED" IsEnabled bool // Optional: defaults to false IsLocked bool // Only used for updates + IsExternal bool // Optional: defaults to false DeployedVmQuota int // Optional: 0 means "unlimited" StoredVmQuota int // Optional: 0 means "unlimited" FullName string // Optional @@ -170,16 +171,16 @@ func (adminOrg *AdminOrg) GetUserByNameOrId(identifier string, refresh bool) (*O return entity.(*OrgUser), err } -// GetRole finds a role within the organization -// Deprecated: use GetRoleReference -func (adminOrg *AdminOrg) GetRole(roleName string) (*types.Reference, error) { - return adminOrg.GetRoleReference(roleName) -} - // GetRoleReference finds a role within the organization func (adminOrg *AdminOrg) GetRoleReference(roleName string) (*types.Reference, error) { - // There is no need to refresh the AdminOrg, until we implement CRUD for roles + // We force refresh of the organization, to make sure that roles recently created + // are taken into account. + // This will become unnecessary when we refactor the User management with OpenAPI + err := adminOrg.Refresh() + if err != nil { + return nil, err + } for _, role := range adminOrg.AdminOrg.RoleReferences.RoleReference { if role.Name == roleName { return role, nil @@ -281,9 +282,14 @@ func (adminOrg *AdminOrg) CreateUserSimple(userData OrgUserConfiguration) (*OrgU if userData.Name == "" { return nil, fmt.Errorf("name is mandatory to create a user") } - if userData.Password == "" { + if userData.Password == "" && !userData.IsExternal { return nil, fmt.Errorf("password is mandatory to create a user") } + if userData.Password != "" && userData.IsExternal { + // External users don't need to provide a password + userData.Password = "" + } + if userData.RoleName == "" { return nil, fmt.Errorf("role is mandatory to create a user") } @@ -298,6 +304,7 @@ func (adminOrg *AdminOrg) CreateUserSimple(userData OrgUserConfiguration) (*OrgU ProviderType: userData.ProviderType, Name: userData.Name, IsEnabled: userData.IsEnabled, + IsExternal: userData.IsExternal, Password: userData.Password, DeployedVmQuota: userData.DeployedVmQuota, StoredVmQuota: userData.StoredVmQuota, @@ -330,8 +337,9 @@ func (user *OrgUser) GetRoleName() string { // Expected behaviour: // with takeOwnership = true, all entities owned by the user being deleted will be transferred to the caller. // with takeOwnership = false, if the user own catalogs, networks, or running VMs/vApps, the call will fail. -// If the user owns only powered-off VMs/vApps, the call will succeeds and the -// VMs/vApps will be removed. +// +// If the user owns only powered-off VMs/vApps, the call will succeeds and the +// VMs/vApps will be removed. func (user *OrgUser) Delete(takeOwnership bool) error { util.Logger.Printf("[TRACE] Deleting user: %#v (take ownership: %v)", user.User.Name, takeOwnership) @@ -383,6 +391,7 @@ func (user *OrgUser) UpdateSimple(userData OrgUserConfiguration) error { user.User.DeployedVmQuota = userData.DeployedVmQuota user.User.IsEnabled = userData.IsEnabled user.User.IsLocked = userData.IsLocked + user.User.IsExternal = userData.IsExternal if userData.RoleName != "" && user.User.Role != nil && user.User.Role.Name != userData.RoleName { newRole, err := user.AdminOrg.GetRoleReference(userData.RoleName) @@ -522,9 +531,13 @@ func validateUserForCreation(user *types.User) error { if user.Name == "" { return fmt.Errorf(missingField, "Name") } - if user.Password == "" { + if user.Password == "" && !user.IsExternal { return fmt.Errorf(missingField, "Password") } + if user.Password != "" && user.IsExternal { + // External users don't need to provide a password + user.Password = "" + } if user.ProviderType != "" { validProviderType := false for _, pt := range OrgUserProviderTypes { diff --git a/govcd/user_test.go b/govcd/user_test.go index 7d5ef3238..c1571b524 100644 --- a/govcd/user_test.go +++ b/govcd/user_test.go @@ -1,4 +1,4 @@ -// +build user functional ALL +//go:build user || functional || ALL /* * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -102,6 +102,7 @@ func (vcd *TestVCD) Test_GetUserByNameOrId(check *C) { // Furthermore, disables, and then enables the users again // and finally deletes all of them func (vcd *TestVCD) Test_UserCRUD(check *C) { + vcd.checkSkipWhenApiToken(check) adminOrg, err := vcd.client.GetAdminOrgByName(vcd.org.Org.Name) check.Assert(err, IsNil) check.Assert(adminOrg, NotNil) @@ -158,6 +159,7 @@ func (vcd *TestVCD) Test_UserCRUD(check *C) { FullName: strings.ReplaceAll(ud.name, "_", " "), Description: "user " + strings.ReplaceAll(ud.name, "_", " "), IsEnabled: true, + IsExternal: false, IM: "TextIM", EmailAddress: "somename@somedomain.com", Telephone: "999 888-7777", @@ -179,6 +181,20 @@ func (vcd *TestVCD) Test_UserCRUD(check *C) { check.Assert(user.User.Telephone, Equals, userDefinition.Telephone) check.Assert(user.User.StoredVmQuota, Equals, userDefinition.StoredVmQuota) check.Assert(user.User.DeployedVmQuota, Equals, userDefinition.DeployedVmQuota) + check.Assert(user.User.IsExternal, Equals, userDefinition.IsExternal) + + // change DeployedVmQuota and StoredVmQuota to 0 and assert + // this will make DeployedVmQuota and StoredVmQuota unlimited + user.User.DeployedVmQuota = 0 + user.User.StoredVmQuota = 0 + err = user.Update() + check.Assert(err, IsNil) + + // Get the user from API again + user, err = adminOrg.GetUserByHref(user.User.Href) + check.Assert(err, IsNil) + check.Assert(user.User.DeployedVmQuota, Equals, 0) + check.Assert(user.User.StoredVmQuota, Equals, 0) err = user.Disable() check.Assert(err, IsNil) diff --git a/govcd/vapp.go b/govcd/vapp.go index c1103f17d..9e59d19de 100644 --- a/govcd/vapp.go +++ b/govcd/vapp.go @@ -1,14 +1,14 @@ /* - * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. */ package govcd import ( + "encoding/xml" "errors" "fmt" "net/http" - "net/url" "strconv" "time" @@ -28,7 +28,7 @@ func NewVApp(cli *Client) *VApp { } } -func (vcdCli *VCDClient) NewVApp(client *Client) VApp { +func (vcdClient *VCDClient) NewVApp(client *Client) VApp { newvapp := NewVApp(client) return *newvapp } @@ -40,6 +40,7 @@ type VappNetworkSettings struct { Description string Gateway string NetMask string + SubnetPrefixLength string DNS1 string DNS2 string DNSSuffix string @@ -59,9 +60,9 @@ type DhcpSettings struct { } // Returns the vdc where the vapp resides in. -func (vapp *VApp) getParentVDC() (Vdc, error) { +func (vapp *VApp) GetParentVDC() (Vdc, error) { for _, link := range vapp.VApp.Link { - if link.Type == "application/vnd.vmware.vcloud.vdc+xml" { + if (link.Type == types.MimeVDC || link.Type == types.MimeAdminVDC) && link.Rel == "up" { vdc := NewVdc(vapp.client) @@ -71,6 +72,11 @@ func (vapp *VApp) getParentVDC() (Vdc, error) { return Vdc{}, err } + parent, err := vdc.getParentOrg() + if err != nil { + return Vdc{}, err + } + vdc.parent = parent return *vdc, nil } } @@ -147,6 +153,41 @@ func (vapp *VApp) AddVM(orgVdcNetworks []*types.OrgVDCNetwork, vappNetworkName s return vapp.AddNewVM(name, vappTemplate, &networkConnectionSection, acceptAllEulas) } +// AddRawVM accepts raw types.ReComposeVAppParams which contains all information for VM creation +func (vapp *VApp) AddRawVM(vAppComposition *types.ReComposeVAppParams) (*VM, error) { + apiEndpoint := urlParseRequestURI(vapp.VApp.HREF) + apiEndpoint.Path += "/action/recomposeVApp" + + // Return the task + task, err := vapp.client.ExecuteTaskRequestWithApiVersion(apiEndpoint.String(), http.MethodPost, + types.MimeRecomposeVappParams, "error instantiating a new VM: %s", + vAppComposition, vapp.client.GetSpecificApiVersionOnCondition(">=37.1", "37.1")) + if err != nil { + return nil, err + } + + err = task.WaitTaskCompletion() + if err != nil { + return nil, fmt.Errorf("VM creation task failed: %s", err) + } + + // VM task does not return any reference to VM therefore it must be looked up by name after + // creation + + var vmName string + if vAppComposition.SourcedItem != nil && vAppComposition.SourcedItem.Source != nil { + vmName = vAppComposition.SourcedItem.Source.Name + } + + vm, err := vapp.GetVMByName(vmName, true) + if err != nil { + return nil, fmt.Errorf("error finding VM %s in vApp %s after creation: %s", vAppComposition.Name, vapp.VApp.Name, err) + } + + return vm, nil + +} + // AddNewVM adds VM from vApp template with custom NetworkConnectionSection func (vapp *VApp) AddNewVM(name string, vappTemplate VAppTemplate, network *types.NetworkConnectionSection, acceptAllEulas bool) (Task, error) { return vapp.AddNewVMWithStorageProfile(name, vappTemplate, network, nil, acceptAllEulas) @@ -223,11 +264,8 @@ func addNewVMW(vapp *VApp, name string, vappTemplate VAppTemplate, vAppComposition.SourcedItem.StorageProfile = storageProfileRef } - if computePolicy != nil && vapp.client.APIVCDMaxVersionIs("< 33.0") { - util.Logger.Printf("[Warning] compute policy is ignored because VCD version doesn't support it") - } // Add compute policy - if computePolicy != nil && computePolicy.ID != "" && vapp.client.APIVCDMaxVersionIs("> 32.0") { + if computePolicy != nil && computePolicy.ID != "" { vdcComputePolicyHref, err := vapp.client.OpenApiBuildEndpoint(types.OpenApiPathVersion1_0_0, types.OpenApiEndpointVdcComputePolicies, computePolicy.ID) if err != nil { return Task{}, fmt.Errorf("error constructing HREF for compute policy") @@ -238,13 +276,12 @@ func addNewVMW(vapp *VApp, name string, vappTemplate VAppTemplate, // Inject network config vAppComposition.SourcedItem.InstantiationParams.NetworkConnectionSection = network - apiEndpoint, _ := url.ParseRequestURI(vapp.VApp.HREF) + apiEndpoint := urlParseRequestURI(vapp.VApp.HREF) apiEndpoint.Path += "/action/recomposeVApp" // Return the task - return vapp.client.ExecuteTaskRequestWithApiVersion(apiEndpoint.String(), http.MethodPost, - types.MimeRecomposeVappParams, "error instantiating a new VM: %s", vAppComposition, - vapp.client.GetSpecificApiVersionOnCondition(">= 33.0", "33.0")) + return vapp.client.ExecuteTaskRequest(apiEndpoint.String(), http.MethodPost, + types.MimeRecomposeVappParams, "error instantiating a new VM: %s", vAppComposition) } @@ -281,7 +318,7 @@ func (vapp *VApp) RemoveVM(vm VM) error { }, } - apiEndpoint, _ := url.ParseRequestURI(vapp.VApp.HREF) + apiEndpoint := urlParseRequestURI(vapp.VApp.HREF) apiEndpoint.Path += "/action/recomposeVApp" deleteTask, err := vapp.client.ExecuteTaskRequest(apiEndpoint.String(), http.MethodPost, @@ -305,7 +342,7 @@ func (vapp *VApp) PowerOn() (Task, error) { return Task{}, fmt.Errorf("error powering on vApp: %s", err) } - apiEndpoint, _ := url.ParseRequestURI(vapp.VApp.HREF) + apiEndpoint := urlParseRequestURI(vapp.VApp.HREF) apiEndpoint.Path += "/power/action/powerOn" // Return the task @@ -315,7 +352,7 @@ func (vapp *VApp) PowerOn() (Task, error) { func (vapp *VApp) PowerOff() (Task, error) { - apiEndpoint, _ := url.ParseRequestURI(vapp.VApp.HREF) + apiEndpoint := urlParseRequestURI(vapp.VApp.HREF) apiEndpoint.Path += "/power/action/powerOff" // Return the task @@ -326,7 +363,7 @@ func (vapp *VApp) PowerOff() (Task, error) { func (vapp *VApp) Reboot() (Task, error) { - apiEndpoint, _ := url.ParseRequestURI(vapp.VApp.HREF) + apiEndpoint := urlParseRequestURI(vapp.VApp.HREF) apiEndpoint.Path += "/power/action/reboot" // Return the task @@ -336,7 +373,7 @@ func (vapp *VApp) Reboot() (Task, error) { func (vapp *VApp) Reset() (Task, error) { - apiEndpoint, _ := url.ParseRequestURI(vapp.VApp.HREF) + apiEndpoint := urlParseRequestURI(vapp.VApp.HREF) apiEndpoint.Path += "/power/action/reset" // Return the task @@ -344,9 +381,10 @@ func (vapp *VApp) Reset() (Task, error) { "", "error resetting vApp: %s", nil) } +// Suspend suspends a vApp func (vapp *VApp) Suspend() (Task, error) { - apiEndpoint, _ := url.ParseRequestURI(vapp.VApp.HREF) + apiEndpoint := urlParseRequestURI(vapp.VApp.HREF) apiEndpoint.Path += "/power/action/suspend" // Return the task @@ -354,9 +392,27 @@ func (vapp *VApp) Suspend() (Task, error) { "", "error suspending vApp: %s", nil) } +// DiscardSuspendedState takes back a vApp from suspension +func (vapp *VApp) DiscardSuspendedState() error { + // Status 3 means that the vApp is suspended + if vapp.VApp.Status != 3 { + return nil + } + apiEndpoint := urlParseRequestURI(vapp.VApp.HREF) + apiEndpoint.Path += "/action/discardSuspendedState" + + // Return the task + task, err := vapp.client.ExecuteTaskRequest(apiEndpoint.String(), http.MethodPost, + "", "error discarding suspended state for vApp: %s", nil) + if err != nil { + return err + } + return task.WaitTaskCompletion() +} + func (vapp *VApp) Shutdown() (Task, error) { - apiEndpoint, _ := url.ParseRequestURI(vapp.VApp.HREF) + apiEndpoint := urlParseRequestURI(vapp.VApp.HREF) apiEndpoint.Path += "/power/action/shutdown" // Return the task @@ -371,7 +427,7 @@ func (vapp *VApp) Undeploy() (Task, error) { UndeployPowerAction: "powerOff", } - apiEndpoint, _ := url.ParseRequestURI(vapp.VApp.HREF) + apiEndpoint := urlParseRequestURI(vapp.VApp.HREF) apiEndpoint.Path += "/action/undeploy" // Return the task @@ -386,7 +442,7 @@ func (vapp *VApp) Deploy() (Task, error) { PowerOn: false, } - apiEndpoint, _ := url.ParseRequestURI(vapp.VApp.HREF) + apiEndpoint := urlParseRequestURI(vapp.VApp.HREF) apiEndpoint.Path += "/action/deploy" // Return the task @@ -427,13 +483,13 @@ func (vapp *VApp) Customize(computername, script string, changeSid bool) (Task, HREF: vapp.VApp.Children.VM[0].HREF, Type: types.MimeGuestCustomizationSection, Info: "Specifies Guest OS Customization Settings", - Enabled: takeBoolPointer(true), + Enabled: addrOf(true), ComputerName: computername, CustomizationScript: script, - ChangeSid: takeBoolPointer(changeSid), + ChangeSid: &changeSid, } - apiEndpoint, _ := url.ParseRequestURI(vapp.VApp.Children.VM[0].HREF) + apiEndpoint := urlParseRequestURI(vapp.VApp.Children.VM[0].HREF) apiEndpoint.Path += "/guestCustomizationSection/" // Return the task @@ -546,7 +602,7 @@ func (vapp *VApp) ChangeCPUCountWithCore(virtualCpuCount int, coresPerSocket *in }, } - apiEndpoint, _ := url.ParseRequestURI(vapp.VApp.Children.VM[0].HREF) + apiEndpoint := urlParseRequestURI(vapp.VApp.Children.VM[0].HREF) apiEndpoint.Path += "/virtualHardwareSection/cpu" // Return the task @@ -564,7 +620,7 @@ func (vapp *VApp) ChangeStorageProfile(name string) (Task, error) { return Task{}, fmt.Errorf("vApp doesn't contain any children, interrupting customization") } - vdc, err := vapp.getParentVDC() + vdc, err := vapp.GetParentVDC() if err != nil { return Task{}, fmt.Errorf("error retrieving parent VDC for vApp %s", vapp.VApp.Name) } @@ -637,7 +693,7 @@ func (vapp *VApp) SetOvf(parameters map[string]string) (Task, error) { ProductSection: vapp.VApp.Children.VM[0].ProductSection, } - apiEndpoint, _ := url.ParseRequestURI(vapp.VApp.Children.VM[0].HREF) + apiEndpoint := urlParseRequestURI(vapp.VApp.Children.VM[0].HREF) apiEndpoint.Path += "/productSections" // Return the task @@ -695,7 +751,7 @@ func (vapp *VApp) ChangeNetworkConfig(networks []map[string]interface{}, ip stri } - apiEndpoint, _ := url.ParseRequestURI(vapp.VApp.Children.VM[0].HREF) + apiEndpoint := urlParseRequestURI(vapp.VApp.Children.VM[0].HREF) apiEndpoint.Path += "/networkConnectionSection/" // Return the task @@ -737,7 +793,7 @@ func (vapp *VApp) ChangeMemorySize(size int) (Task, error) { }, } - apiEndpoint, _ := url.ParseRequestURI(vapp.VApp.Children.VM[0].HREF) + apiEndpoint := urlParseRequestURI(vapp.VApp.Children.VM[0].HREF) apiEndpoint.Path += "/virtualHardwareSection/memory" // Return the task @@ -886,14 +942,22 @@ func (vapp *VApp) CreateVappNetworkAsync(newNetworkSettings *VappNetworkSettings FenceMode: types.FenceModeIsolated, GuestVlanAllowed: newNetworkSettings.GuestVLANAllowed, Features: networkFeatures, - IPScopes: &types.IPScopes{IPScope: []*types.IPScope{&types.IPScope{IsInherited: false, Gateway: newNetworkSettings.Gateway, - Netmask: newNetworkSettings.NetMask, DNS1: newNetworkSettings.DNS1, - DNS2: newNetworkSettings.DNS2, DNSSuffix: newNetworkSettings.DNSSuffix, IsEnabled: true, - IPRanges: &types.IPRanges{IPRange: newNetworkSettings.StaticIPRanges}}}}, + IPScopes: &types.IPScopes{ + IPScope: []*types.IPScope{{ + IsInherited: false, + Gateway: newNetworkSettings.Gateway, + Netmask: newNetworkSettings.NetMask, + SubnetPrefixLength: newNetworkSettings.SubnetPrefixLength, + DNS1: newNetworkSettings.DNS1, + DNS2: newNetworkSettings.DNS2, + DNSSuffix: newNetworkSettings.DNSSuffix, + IsEnabled: true, + IPRanges: &types.IPRanges{IPRange: newNetworkSettings.StaticIPRanges}}}}, RetainNetInfoAcrossDeployments: newNetworkSettings.RetainIpMacEnabled, }, IsDeployed: false, } + if orgNetwork != nil { vappConfiguration.Configuration.ParentNetwork = &types.Reference{ HREF: orgNetwork.HREF, @@ -1005,6 +1069,9 @@ func (vapp *VApp) UpdateNetworkAsync(networkSettingsToUpdate *VappNetworkSetting if networkToUpdate == (types.VAppNetworkConfiguration{}) { return Task{}, fmt.Errorf("not found network to update with Id %s", networkSettingsToUpdate.ID) } + if networkToUpdate.Configuration == nil { + networkToUpdate.Configuration = &types.NetworkConfiguration{} + } networkToUpdate.Configuration.RetainNetInfoAcrossDeployments = networkSettingsToUpdate.RetainIpMacEnabled // new network to connect if networkToUpdate.Configuration.ParentNetwork == nil && orgNetwork != nil { @@ -1035,6 +1102,10 @@ func (vapp *VApp) UpdateNetworkAsync(networkSettingsToUpdate *VappNetworkSetting networkSettingsToUpdate.DhcpSettings.IPRange.EndAddress = networkSettingsToUpdate.DhcpSettings.IPRange.StartAddress } + if networkToUpdate.Configuration.Features == nil { + networkToUpdate.Configuration.Features = &types.NetworkFeatures{} + } + // remove DHCP config if networkSettingsToUpdate.DhcpSettings == nil { networkToUpdate.Configuration.Features.DhcpService = nil @@ -1117,6 +1188,9 @@ func (vapp *VApp) UpdateOrgNetworkAsync(networkSettingsToUpdate *VappNetworkSett fenceMode = types.FenceModeNAT } + if networkToUpdate.Configuration == nil { + networkToUpdate.Configuration = &types.NetworkConfiguration{} + } networkToUpdate.Configuration.RetainNetInfoAcrossDeployments = networkSettingsToUpdate.RetainIpMacEnabled networkToUpdate.Configuration.FenceMode = fenceMode @@ -1134,12 +1208,12 @@ func validateNetworkConfigSettings(networkSettings *VappNetworkSettings) error { return errors.New("network gateway IP is missing") } - if networkSettings.NetMask == "" { - return errors.New("network mask config is missing") + if networkSettings.NetMask == "" && networkSettings.SubnetPrefixLength == "" { + return errors.New("network mask and subnet prefix length config is missing, exactly one is required") } - if networkSettings.NetMask == "" { - return errors.New("network mask config is missing") + if networkSettings.NetMask != "" && networkSettings.SubnetPrefixLength != "" { + return errors.New("exactly one of netmask and prefix length can be supplied") } if networkSettings.DhcpSettings != nil && networkSettings.DhcpSettings.IPRange == nil { @@ -1241,7 +1315,7 @@ func updateNetworkConfigurations(vapp *VApp, networkConfigurations []types.VAppN NetworkConfig: networkConfigurations, } - apiEndpoint, _ := url.ParseRequestURI(vapp.VApp.HREF) + apiEndpoint := urlParseRequestURI(vapp.VApp.HREF) apiEndpoint.Path += "/networkConfigSection/" // Return the task @@ -1362,16 +1436,163 @@ func (client *Client) QueryVappList() ([]*types.QueryResultVAppRecordType, error } // getOrgInfo finds the organization to which the vApp belongs (through the VDC), and returns its name and ID -func (vapp *VApp) getOrgInfo() (orgInfoType, error) { +func (vapp *VApp) getOrgInfo() (*TenantContext, error) { previous, exists := orgInfoCache[vapp.VApp.ID] if exists { return previous, nil } - //var orgHref string var err error - vdc, err := vapp.getParentVDC() + vdc, err := vapp.GetParentVDC() if err != nil { - return orgInfoType{}, err + return nil, err + } + return vdc.getTenantContext() +} + +// UpdateNameDescription can change the name and the description of a vApp +// If name is empty, it is left unchanged. +func (vapp *VApp) UpdateNameDescription(newName, newDescription string) error { + if vapp == nil || vapp.VApp.HREF == "" { + return fmt.Errorf("vApp or href cannot be empty") + } + + // Skip update if we are using the original values + if (newName == vapp.VApp.Name || newName == "") && (newDescription == vapp.VApp.Description) { + return nil + } + + opType := types.MimeRecomposeVappParams + + href := "" + for _, link := range vapp.VApp.Link { + if link.Type == opType && link.Rel == "recompose" { + href = link.HREF + break + } + } + + if href == "" { + return fmt.Errorf("no appropriate link for update found for vApp %s", vapp.VApp.Name) + } + + if newName == "" { + newName = vapp.VApp.Name + } + + recomposeParams := &types.SmallRecomposeVappParams{ + XMLName: xml.Name{}, + Ovf: types.XMLNamespaceOVF, + Xsi: types.XMLNamespaceXSI, + Xmlns: types.XMLNamespaceVCloud, + Name: newName, + Description: newDescription, + Deploy: vapp.VApp.Deployed, + } + + task, err := vapp.client.ExecuteTaskRequest(href, http.MethodPost, + opType, "error updating vapp: %s", recomposeParams) + + if err != nil { + return fmt.Errorf("unable to update vApp: %s", err) + } + + err = task.WaitTaskCompletion() + if err != nil { + return fmt.Errorf("task for updating vApp failed: %s", err) + } + return vapp.Refresh() +} + +// UpdateDescription changes the description of a vApp +func (vapp *VApp) UpdateDescription(newDescription string) error { + return vapp.UpdateNameDescription("", newDescription) +} + +// Rename changes the name of a vApp +func (vapp *VApp) Rename(newName string) error { + return vapp.UpdateNameDescription(newName, vapp.VApp.Description) +} + +func (vapp *VApp) getTenantContext() (*TenantContext, error) { + parentVdc, err := vapp.GetParentVDC() + if err != nil { + return nil, err + } + return parentVdc.getTenantContext() +} + +// RenewLease updates the lease terms for the vApp +func (vapp *VApp) RenewLease(deploymentLeaseInSeconds, storageLeaseInSeconds int) error { + + href := "" + if vapp.VApp.LeaseSettingsSection != nil { + if vapp.VApp.LeaseSettingsSection.DeploymentLeaseInSeconds == deploymentLeaseInSeconds && + vapp.VApp.LeaseSettingsSection.StorageLeaseInSeconds == storageLeaseInSeconds { + // Requested parameters are the same as existing parameters: exit without updating + return nil + } + href = vapp.VApp.LeaseSettingsSection.HREF + } + if href == "" { + for _, link := range vapp.VApp.Link { + if link.Rel == "edit" && link.Type == types.MimeLeaseSettingSection { + href = link.HREF + break + } + } + } + if href == "" { + return fmt.Errorf("link to update lease settings not found for vApp %s", vapp.VApp.Name) + } + + var leaseSettings = types.UpdateLeaseSettingsSection{ + HREF: href, + XmlnsOvf: types.XMLNamespaceOVF, + Xmlns: types.XMLNamespaceVCloud, + OVFInfo: "Lease section settings", + Type: types.MimeLeaseSettingSection, + DeploymentLeaseInSeconds: &deploymentLeaseInSeconds, + StorageLeaseInSeconds: &storageLeaseInSeconds, + } + + task, err := vapp.client.ExecuteTaskRequest(href, http.MethodPut, + types.MimeLeaseSettingSection, "error updating vapp lease : %s", &leaseSettings) + + if err != nil { + return fmt.Errorf("unable to update vApp lease: %s", err) + } + + err = task.WaitTaskCompletion() + if err != nil { + return fmt.Errorf("task for updating vApp lease failed: %s", err) + } + return vapp.Refresh() +} + +// GetLease retrieves the lease terms for a vApp +func (vapp *VApp) GetLease() (*types.LeaseSettingsSection, error) { + + href := "" + if vapp.VApp.LeaseSettingsSection != nil { + href = vapp.VApp.LeaseSettingsSection.HREF + } + if href == "" { + for _, link := range vapp.VApp.Link { + if link.Type == types.MimeLeaseSettingSection { + href = link.HREF + break + } + } + } + if href == "" { + return nil, fmt.Errorf("link to retrieve lease settings not found for vApp %s", vapp.VApp.Name) + } + var leaseSettings types.LeaseSettingsSection + + _, err := vapp.client.ExecuteRequest(href, http.MethodGet, "", "error getting vApp lease info: %s", nil, &leaseSettings) + + if err != nil { + return nil, err } - return getOrgInfo(vapp.client, vdc.Vdc.Link, vapp.VApp.ID, vapp.VApp.Name, "vApp") + return &leaseSettings, nil } diff --git a/govcd/vapp_clone_test.go b/govcd/vapp_clone_test.go new file mode 100644 index 000000000..d0f05aaed --- /dev/null +++ b/govcd/vapp_clone_test.go @@ -0,0 +1,148 @@ +//go:build vapp || functional || ALL +// +build vapp functional ALL + +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" + "time" +) + +// TestVappfromTemplateAndClone creates a vApp with multiple VMs at once, then clones such vApp into a new one +func (vcd *TestVCD) TestVappfromTemplateAndClone(check *C) { + org, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(org, NotNil) + + vdc, err := org.GetVDCByName(vcd.config.VCD.Nsxt.Vdc, false) + check.Assert(err, IsNil) + check.Assert(vdc, NotNil) + + name := check.TestName() + description := "test compose raw vApp with template" + + catalog, err := org.GetCatalogByName(vcd.config.VCD.Catalog.NsxtBackedCatalogName, false) + check.Assert(err, IsNil) + vappTemplateName := vcd.config.VCD.Catalog.CatalogItemWithMultiVms + if vappTemplateName == "" { + check.Skip(fmt.Sprintf("vApp template missing in configuration - Make sure there is such template in catalog %s -"+ + " Using test_resources/vapp_with_3_vms.ova", + vcd.config.VCD.Catalog.NsxtBackedCatalogName)) + } + vappTemplate, err := catalog.GetVAppTemplateByName(vappTemplateName) + if err != nil { + if ContainsNotFound(err) { + check.Skip(fmt.Sprintf("vApp template %s not found - Make sure there is such template in catalog %s -"+ + " Using test_resources/vapp_with_3_vms.ova", + vappTemplateName, vcd.config.VCD.Catalog.NsxtBackedCatalogName)) + } + } + check.Assert(err, IsNil) + check.Assert(vappTemplate.VAppTemplate.Children, NotNil) + check.Assert(vappTemplate.VAppTemplate.Children.VM, NotNil) + + var def = types.InstantiateVAppTemplateParams{ + Name: name, + Deploy: true, + PowerOn: true, + Description: description, + Source: &types.Reference{ + HREF: vappTemplate.VAppTemplate.HREF, + ID: vappTemplate.VAppTemplate.ID, + }, + IsSourceDelete: false, + AllEULAsAccepted: true, + } + + start := time.Now() + printVerbose("creating vapp '%s' from template '%s'\n", name, vappTemplateName) + vapp, err := vdc.CreateVappFromTemplate(&def) + check.Assert(err, IsNil) + printVerbose("** created in %s\n", time.Since(start)) + + AddToCleanupList(name, "vapp", vdc.Vdc.Name, name) + + check.Assert(vapp.VApp.Name, Equals, name) + check.Assert(vapp.VApp.Description, Equals, description) + + check.Assert(vapp.VApp.Children, NotNil) + check.Assert(vapp.VApp.Children.VM, NotNil) + + cloneName := name + "-clone" + cloneDescription := description + " clone" + var defClone = types.CloneVAppParams{ + Name: cloneName, + Deploy: true, + PowerOn: true, + Description: cloneDescription, + Source: &types.Reference{ + HREF: vapp.VApp.HREF, + Type: vapp.VApp.Type, + }, + IsSourceDelete: addrOf(false), + } + + start = time.Now() + printVerbose("cloning vapp '%s' from vapp '%s'\n", cloneName, name) + vapp2, err := vdc.CloneVapp(&defClone) + check.Assert(err, IsNil) + printVerbose("** cloned in %s\n", time.Since(start)) + + AddToCleanupList(cloneName, "vapp", vdc.Vdc.Name, name) + + status, err := vapp2.GetStatus() + check.Assert(err, IsNil) + if status == "SUSPENDED" { + printVerbose("\t discarding suspended state for vApp %s\n", vapp2.VApp.Name) + err = vapp2.DiscardSuspendedState() + check.Assert(err, IsNil) + status, err = vapp2.GetStatus() + check.Assert(err, IsNil) + if status != "POWERED_ON" { + printVerbose("\t powering on vApp %s\n", vapp2.VApp.Name) + task, err := vapp2.PowerOn() + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) + } + } + + check.Assert(vapp2.VApp.Name, Equals, cloneName) + check.Assert(vapp2.VApp.Description, Equals, cloneDescription) + check.Assert(vapp.VApp.HREF, Not(Equals), vapp2.VApp.HREF) + + vappRemove(vapp, check) + vappRemove(vapp2, check) +} + +func vappRemove(vapp *VApp, check *C) { + var task Task + var err error + status, err := vapp.GetStatus() + check.Assert(err, IsNil) + if status == "POWERED_ON" { + printVerbose("powering off vApp '%s'\n", vapp.VApp.Name) + task, err = vapp.Undeploy() + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) + } + + printVerbose("removing networks from vApp '%s'\n", vapp.VApp.Name) + task, err = vapp.RemoveAllNetworks() + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) + + printVerbose("removing vApp '%s'\n", vapp.VApp.Name) + task, err = vapp.Delete() + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) +} diff --git a/govcd/vapp_concurrent_test.go b/govcd/vapp_concurrent_test.go index a539c629c..6f1e49b24 100644 --- a/govcd/vapp_concurrent_test.go +++ b/govcd/vapp_concurrent_test.go @@ -1,4 +1,4 @@ -// +build concurrent +//go:build concurrent /* * Copyright 2020 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. diff --git a/govcd/vapp_network.go b/govcd/vapp_network.go index 5f3acb6cc..bd95458e0 100644 --- a/govcd/vapp_network.go +++ b/govcd/vapp_network.go @@ -9,6 +9,7 @@ import ( "github.com/vmware/go-vcloud-director/v2/types/v56" "github.com/vmware/go-vcloud-director/v2/util" "net/http" + "strings" ) // UpdateNetworkFirewallRules updates vApp networks firewall rules. It will overwrite existing ones as there is @@ -300,3 +301,78 @@ func (vapp *VApp) RemoveAllNetworkStaticRoutes(networkId string) error { } return nil } + +// queryVappNetworks returns a list of vApp networks with an optional filter +func queryVappNetworks(client *Client, values map[string]string) ([]*types.QueryResultVappNetworkRecordType, error) { + + vAppNetworkType := types.QtVappNetwork + if client.IsSysAdmin { + vAppNetworkType = types.QtAdminVappNetwork + } + + params := map[string]string{ + "type": vAppNetworkType, + } + filterValue := "" + if len(values) > 0 { + var filterElements []string + for k, v := range values { + item := fmt.Sprintf("%s==%s", k, v) + filterElements = append(filterElements, item) + } + filterValue = strings.Join(filterElements, ";") + } + if filterValue != "" { + params["filter"] = filterValue + } + results, err := client.cumulativeQuery(vAppNetworkType, nil, params) + if err != nil { + return nil, fmt.Errorf("error retrieving vApp networks %s", err) + } + + if client.IsSysAdmin { + return results.Results.AdminVappNetworkRecord, nil + } + return results.Results.VappNetworkRecord, nil +} + +// QueryVappNetworks returns all vApp networks visible to the client +func (client *Client) QueryVappNetworks(values map[string]string) ([]*types.QueryResultVappNetworkRecordType, error) { + return queryVappNetworks(client, values) +} + +// QueryAllVappNetworks returns all vApp networks and vApp Org Networks belonging to the current vApp +func (vapp *VApp) QueryAllVappNetworks(values map[string]string) ([]*types.QueryResultVappNetworkRecordType, error) { + // Note: when querying a field that contains a UUID, the system compares only the UUIDs, even if the full field contains more than that. + allValues := map[string]string{"vApp": extractUuid(vapp.VApp.ID)} + for k, v := range values { + allValues[k] = v + } + return queryVappNetworks(vapp.client, allValues) +} + +// QueryVappNetworks returns all vApp networks belonging to the current vApp +func (vapp *VApp) QueryVappNetworks(values map[string]string) ([]*types.QueryResultVappNetworkRecordType, error) { + // Note: when querying a field that contains a UUID, the system compares only the UUIDs, even if the full field contains more than that. + allValues := map[string]string{ + "vApp": extractUuid(vapp.VApp.ID), + "isLinked": "false", + } + for k, v := range values { + allValues[k] = v + } + return queryVappNetworks(vapp.client, allValues) +} + +// QueryVappOrgNetworks returns all vApp networks belonging to the current vApp +func (vapp *VApp) QueryVappOrgNetworks(values map[string]string) ([]*types.QueryResultVappNetworkRecordType, error) { + // Note: when querying a field that contains a UUID, the system compares only the UUIDs, even if the full field contains more than that. + allValues := map[string]string{ + "vApp": extractUuid(vapp.VApp.ID), + "isLinked": "true", + } + for k, v := range values { + allValues[k] = v + } + return queryVappNetworks(vapp.client, allValues) +} diff --git a/govcd/vapp_network_test.go b/govcd/vapp_network_test.go index 104e099b1..5f85c511a 100644 --- a/govcd/vapp_network_test.go +++ b/govcd/vapp_network_test.go @@ -1,4 +1,4 @@ -// +build vapp functional ALL +//go:build vapp || functional || ALL /* * Copyright 2020 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -8,7 +8,6 @@ package govcd import ( "fmt" - . "gopkg.in/check.v1" "github.com/vmware/go-vcloud-director/v2/types/v56" @@ -83,7 +82,7 @@ func (vcd *TestVCD) Test_UpdateNetworkFirewallRules(check *C) { func (vcd *TestVCD) prepareVappWithVappNetwork(check *C, vappName, orgVdcNetworkName string) (*VApp, string, *types.NetworkConfigSection, error) { fmt.Printf("Running: %s\n", check.TestName()) - vapp, err := createVappForTest(vcd, vappName) + vapp, err := deployVappForTest(vcd, vappName) check.Assert(err, IsNil) check.Assert(vapp, NotNil) @@ -226,7 +225,7 @@ func (vcd *TestVCD) Test_UpdateNetworkNatRules(check *C) { &types.NatRule{OneToOneVMRule: &types.NatOneToOneVMRule{ MappingMode: "manual", VMNicID: 0, VAppScopedVMID: vm2.VM.VAppScopedLocalID, - ExternalIPAddress: takeStringPointer("192.168.100.1")}}}, + ExternalIPAddress: addrOf("192.168.100.1")}}}, false, "ipTranslation", "allowTrafficIn") check.Assert(err, IsNil) check.Assert(result, NotNil) @@ -314,13 +313,14 @@ func createRoutedNetwork(vcd *TestVCD, check *C, networkName string) { } func (vcd *TestVCD) Test_UpdateNetworkStaticRoutes(check *C) { - createRoutedNetwork(vcd, check, "Test_UpdateNetworkStaticRoutes") - vapp, networkName, vappNetworkConfig, err := vcd.prepareVappWithVappNetwork(check, "Test_UpdateNetworkStaticRoutes", "Test_UpdateNetworkStaticRoutes") + testName := check.TestName() + createRoutedNetwork(vcd, check, testName) + vapp, vappNetworkName, vappNetworkConfig, err := vcd.prepareVappWithVappNetwork(check, testName, testName) check.Assert(err, IsNil) networkFound := types.VAppNetworkConfiguration{} for _, networkConfig := range vappNetworkConfig.NetworkConfig { - if networkConfig.NetworkName == networkName { + if networkConfig.NetworkName == vappNetworkName { networkFound = networkConfig } } @@ -332,7 +332,7 @@ func (vcd *TestVCD) Test_UpdateNetworkStaticRoutes(check *C) { &types.NetworkConnection{ IsConnected: true, IPAddressAllocationMode: types.IPAllocationModePool, - Network: "Test_UpdateNetworkStaticRoutes", + Network: vappNetworkName, NetworkConnectionIndex: 0, }) @@ -374,4 +374,10 @@ func (vcd *TestVCD) Test_UpdateNetworkStaticRoutes(check *C) { err = task.WaitTaskCompletion() check.Assert(err, IsNil) check.Assert(task.Task.Status, Equals, "success") + network, err := vcd.vdc.GetOrgVdcNetworkByName(testName, true) + check.Assert(err, IsNil) + task, err = network.Delete() + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) } diff --git a/govcd/vapp_test.go b/govcd/vapp_test.go index c27478737..91f507ddc 100644 --- a/govcd/vapp_test.go +++ b/govcd/vapp_test.go @@ -1,14 +1,16 @@ -// +build vapp functional ALL +//go:build vapp || functional || ALL /* - * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + * Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. */ package govcd import ( "fmt" + "github.com/kr/pretty" "regexp" + "time" . "gopkg.in/check.v1" @@ -28,12 +30,31 @@ func (vcd *TestVCD) TestGetParentVDC(check *C) { vapp, err := vcd.vdc.GetVAppByName(vcd.vapp.VApp.Name, false) check.Assert(err, IsNil) - vdc, err := vapp.getParentVDC() + vdc, err := vapp.GetParentVDC() check.Assert(err, IsNil) check.Assert(vdc.Vdc.Name, Equals, vcd.vdc.Vdc.Name) } +func (vcd *TestVCD) TestGetVappByHref(check *C) { + if vcd.skipVappTests { + check.Skip("Skipping test because vApp was not successfully created at setup") + } + vapp, err := vcd.vdc.GetVAppByName(vcd.vapp.VApp.Name, false) + check.Assert(err, IsNil) + + vdc, err := vapp.GetParentVDC() + check.Assert(err, IsNil) + + orgVappByHref, err := vcd.org.GetVAppByHref(vapp.VApp.HREF) + check.Assert(err, IsNil) + check.Assert(orgVappByHref.VApp, DeepEquals, vapp.VApp) + + vdcVappByHref, err := vdc.GetVAppByHref(vapp.VApp.HREF) + check.Assert(err, IsNil) + check.Assert(vdcVappByHref.VApp, DeepEquals, vapp.VApp) +} + // Tests Powering On and Powering Off a VApp. Also tests Deletion // of a VApp func (vcd *TestVCD) Test_PowerOn(check *C) { @@ -454,7 +475,7 @@ func (vcd *TestVCD) Test_AddNewVMNilNIC(check *C) { vapptemplate, err := catitem.GetVAppTemplate() check.Assert(err, IsNil) - vapp, err := createVappForTest(vcd, "Test_AddNewVMNilNIC") + vapp, err := deployVappForTest(vcd, "Test_AddNewVMNilNIC") check.Assert(err, IsNil) check.Assert(vapp, NotNil) task, err := vapp.AddNewVM(check.TestName(), vapptemplate, nil, true) @@ -509,7 +530,7 @@ func (vcd *TestVCD) Test_AddNewVMMultiNIC(check *C) { vapptemplate, err := catitem.GetVAppTemplate() check.Assert(err, IsNil) - vapp, err := createVappForTest(vcd, "Test_AddNewVMMultiNIC") + vapp, err := deployVappForTest(vcd, "Test_AddNewVMMultiNIC") check.Assert(err, IsNil) check.Assert(vapp, NotNil) @@ -590,6 +611,20 @@ func (vcd *TestVCD) Test_AddNewVMMultiNIC(check *C) { verifyNetworkConnectionSection(check, actualNetConfig, desiredNetConfig) + allVappNetworks, err := vapp.QueryAllVappNetworks(nil) + check.Assert(err, IsNil) + printVerbose("%# v\n", pretty.Formatter(allVappNetworks)) + check.Assert(len(allVappNetworks), Equals, 2) + + vappNetworks, err := vapp.QueryVappNetworks(nil) + check.Assert(err, IsNil) + printVerbose("%# v\n", pretty.Formatter(vappNetworks)) + check.Assert(len(vappNetworks), Equals, 0) + vappOrgNetworks, err := vapp.QueryVappOrgNetworks(nil) + check.Assert(err, IsNil) + printVerbose("%# v\n", pretty.Formatter(vappOrgNetworks)) + check.Assert(len(vappOrgNetworks), Equals, 2) + // Cleanup err = vapp.RemoveVM(*vm) check.Assert(err, IsNil) @@ -662,6 +697,36 @@ func (vcd *TestVCD) Test_RemoveAllNetworks(check *C) { check.Assert(len(networkConfig.NetworkConfig), Equals, 2) + // Network removal requires for the vApp to be down therefore attempt to power off vApp before + // network removal, but ignore error as it might already be powered off + vappStatus, err := vcd.vapp.GetStatus() + check.Assert(err, IsNil) + + allVappNetworks, err := vcd.vapp.QueryAllVappNetworks(nil) + check.Assert(err, IsNil) + printVerbose("%# v\n", pretty.Formatter(allVappNetworks)) + check.Assert(len(allVappNetworks), Equals, 2) + + vappNetworks, err := vcd.vapp.QueryVappNetworks(nil) + check.Assert(err, IsNil) + printVerbose("%# v\n", pretty.Formatter(vappNetworks)) + check.Assert(len(vappNetworks), Equals, 1) + vappOrgNetworks, err := vcd.vapp.QueryVappOrgNetworks(nil) + check.Assert(err, IsNil) + printVerbose("%# v\n", pretty.Formatter(vappOrgNetworks)) + check.Assert(len(vappOrgNetworks), Equals, 1) + + if vappStatus != "POWERED_OFF" { + task, err := vcd.vapp.Undeploy() + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) + } + + vappStatus, err = vcd.vapp.GetStatus() + check.Assert(err, IsNil) + printVerbose("vApp status before network removal: %s\n", vappStatus) + task, err := vcd.vapp.RemoveAllNetworks() check.Assert(err, IsNil) err = task.WaitTaskCompletion() @@ -681,6 +746,12 @@ func (vcd *TestVCD) Test_RemoveAllNetworks(check *C) { } check.Assert(hasNetworks, Equals, false) + + // Power on shared vApp for other tests + task, err = vcd.vapp.PowerOn() + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) } // Test_VappSetProductSectionList sets vApp product section, retrieves it and deeply matches if @@ -749,7 +820,7 @@ func (vcd *TestVCD) Test_GetVM(check *C) { func (vcd *TestVCD) Test_AddAndRemoveIsolatedVappNetwork(check *C) { fmt.Printf("Running: %s\n", check.TestName()) - vapp, err := createVappForTest(vcd, "Test_AddAndRemoveIsolatedVappNetwork") + vapp, err := deployVappForTest(vcd, "Test_AddAndRemoveIsolatedVappNetwork") check.Assert(err, IsNil) check.Assert(vapp, NotNil) @@ -829,6 +900,98 @@ func (vcd *TestVCD) Test_AddAndRemoveIsolatedVappNetwork(check *C) { check.Assert(task.Task.Status, Equals, "success") } +// Test_AddAndRemoveIsolatedVappNetworkIpv6 is identical to Test_AddAndRemoveIsolatedVappNetwork, +// but it uses ipv6 values for network specification. +func (vcd *TestVCD) Test_AddAndRemoveIsolatedVappNetworkIpv6(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + + vapp, err := deployVappForTest(vcd, "Test_AddAndRemoveIsolatedVappNetwork") + check.Assert(err, IsNil) + check.Assert(vapp, NotNil) + + // Add Metadata + networkName := check.TestName() + description := "Created in test" + const gateway = "fe80:0:0:0:0:0:0:aaaa" + const prefixlength = "100" + // VCD API returns ipv6 addresses in expanded format, so this is + // needed to compare values properly. + const dns1 = "2001:4860:4860:0:0:0:0:8844" + const dns2 = "2001:4860:4860:0:0:0:0:8844" + const dnsSuffix = "biz.biz" + const startAddress = "fe80:0:0:0:0:0:0:aaab" + const endAddress = "fe80:0:0:0:0:0:0:bbbb" + const dhcpStartAddress = "fe80:0:0:0:0:0:0:cccc" + const dhcpEndAddress = "fe80:0:0:0:0:0:0:dddd" + const maxLeaseTime = 3500 + const defaultLeaseTime = 2400 + var guestVlanAllowed = true + + vappNetworkSettings := &VappNetworkSettings{ + Name: networkName, + Gateway: gateway, + SubnetPrefixLength: prefixlength, + DNS1: dns1, + DNS2: dns2, + DNSSuffix: dnsSuffix, + StaticIPRanges: []*types.IPRange{{StartAddress: startAddress, EndAddress: endAddress}}, + DhcpSettings: &DhcpSettings{IsEnabled: true, MaxLeaseTime: maxLeaseTime, DefaultLeaseTime: defaultLeaseTime, IPRange: &types.IPRange{StartAddress: dhcpStartAddress, EndAddress: dhcpEndAddress}}, + GuestVLANAllowed: &guestVlanAllowed, + Description: description, + } + + vappNetworkConfig, err := vapp.CreateVappNetwork(vappNetworkSettings, nil) + check.Assert(err, IsNil) + check.Assert(vappNetworkConfig, NotNil) + + vappNetworkSettings.NetMask = "255.255.255.0" + vappNetworkConfig2, err := vapp.CreateVappNetwork(vappNetworkSettings, nil) + check.Assert(err, NotNil) + check.Assert(vappNetworkConfig2, IsNil) + + networkFound := types.VAppNetworkConfiguration{} + for _, networkConfig := range vappNetworkConfig.NetworkConfig { + if networkConfig.NetworkName == networkName { + networkFound = networkConfig + } + } + + check.Assert(networkFound.Description, Equals, description) + check.Assert(networkFound.Configuration.IPScopes.IPScope[0].Gateway, Equals, gateway) + check.Assert(networkFound.Configuration.IPScopes.IPScope[0].SubnetPrefixLength, Equals, prefixlength) + check.Assert(networkFound.Configuration.IPScopes.IPScope[0].DNS1, Equals, dns1) + check.Assert(networkFound.Configuration.IPScopes.IPScope[0].DNS2, Equals, dns2) + check.Assert(networkFound.Configuration.IPScopes.IPScope[0].DNSSuffix, Equals, dnsSuffix) + check.Assert(networkFound.Configuration.IPScopes.IPScope[0].IPRanges.IPRange[0].StartAddress, Equals, startAddress) + check.Assert(networkFound.Configuration.IPScopes.IPScope[0].IPRanges.IPRange[0].EndAddress, Equals, endAddress) + + check.Assert(networkFound.Configuration.Features.DhcpService.IsEnabled, Equals, true) + check.Assert(networkFound.Configuration.Features.DhcpService.MaxLeaseTime, Equals, maxLeaseTime) + check.Assert(networkFound.Configuration.Features.DhcpService.DefaultLeaseTime, Equals, defaultLeaseTime) + check.Assert(networkFound.Configuration.Features.DhcpService.IPRange.StartAddress, Equals, dhcpStartAddress) + check.Assert(networkFound.Configuration.Features.DhcpService.IPRange.EndAddress, Equals, dhcpEndAddress) + + err = vapp.Refresh() + check.Assert(err, IsNil) + vappNetworkConfig, err = vapp.RemoveNetwork(networkName) + check.Assert(err, IsNil) + check.Assert(vappNetworkConfig, NotNil) + + isExist := false + for _, networkConfig := range vappNetworkConfig.NetworkConfig { + if networkConfig.NetworkName == networkName { + isExist = true + } + } + check.Assert(isExist, Equals, false) + + task, err := vapp.Delete() + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) + check.Assert(task.Task.Status, Equals, "success") +} + func (vcd *TestVCD) Test_AddAndRemoveNatVappNetwork(check *C) { fmt.Printf("Running: %s\n", check.TestName()) @@ -836,7 +999,7 @@ func (vcd *TestVCD) Test_AddAndRemoveNatVappNetwork(check *C) { check.Skip("Skipping test because no network was given") } - vapp, err := createVappForTest(vcd, "Test_AddAndRemoveNatVappNetwork") + vapp, err := deployVappForTest(vcd, "Test_AddAndRemoveNatVappNetwork") check.Assert(err, IsNil) check.Assert(vapp, NotNil) @@ -933,7 +1096,7 @@ func (vcd *TestVCD) Test_UpdateVappNetwork(check *C) { check.Skip("Skipping test because no network was given") } - vapp, err := createVappForTest(vcd, "Test_UpdateVappNetwork") + vapp, err := deployVappForTest(vcd, "Test_UpdateVappNetwork") check.Assert(err, IsNil) check.Assert(vapp, NotNil) @@ -1092,7 +1255,7 @@ func (vcd *TestVCD) Test_UpdateVappNetwork(check *C) { func (vcd *TestVCD) Test_AddAndRemoveVappNetworkWithMinimumValues(check *C) { fmt.Printf("Running: %s\n", check.TestName()) - vapp, err := createVappForTest(vcd, "Test_AddAndRemoveVappNetworkWithMinimumValues") + vapp, err := deployVappForTest(vcd, "Test_AddAndRemoveVappNetworkWithMinimumValues") check.Assert(err, IsNil) check.Assert(vapp, NotNil) @@ -1159,14 +1322,16 @@ func (vcd *TestVCD) Test_AddAndRemoveOrgVappNetworkWithMinimumValues(check *C) { fmt.Printf("Running: %s\n", check.TestName()) if vcd.config.VCD.Network.Net1 == "" { - check.Skip("Skipping test because no network was given") + check.Skip("Skipping test because no first network was given") } - - vapp, err := createVappForTest(vcd, "Test_AddAndRemoveOrgVappNetworkWithMinimumValues") + if vcd.config.VCD.Network.Net2 == "" { + check.Skip("Skipping test because no second network was given") + } + vapp, err := deployVappForTest(vcd, "Test_AddAndRemoveOrgVappNetworkWithMinimumValues") check.Assert(err, IsNil) check.Assert(vapp, NotNil) - orgVdcNetwork, err := vcd.vdc.GetOrgVdcNetworkByName(vcd.config.VCD.Network.Net1, false) + orgVdcNetwork, err := vcd.vdc.GetOrgVdcNetworkByName(vcd.config.VCD.Network.Net2, false) check.Assert(err, IsNil) check.Assert(orgVdcNetwork, NotNil) @@ -1178,7 +1343,7 @@ func (vcd *TestVCD) Test_AddAndRemoveOrgVappNetworkWithMinimumValues(check *C) { networkFound := types.VAppNetworkConfiguration{} for _, networkConfig := range vappNetworkConfig.NetworkConfig { - if networkConfig.NetworkName == vcd.config.VCD.Network.Net1 { + if networkConfig.NetworkName == vcd.config.VCD.Network.Net2 { networkFound = networkConfig } } @@ -1194,17 +1359,17 @@ func (vcd *TestVCD) Test_AddAndRemoveOrgVappNetworkWithMinimumValues(check *C) { check.Assert(*networkFound.Configuration.RetainNetInfoAcrossDeployments, Equals, false) - check.Assert(networkFound.Configuration.ParentNetwork.Name, Equals, vcd.config.VCD.Network.Net1) + check.Assert(networkFound.Configuration.ParentNetwork.Name, Equals, vcd.config.VCD.Network.Net2) - err = vcd.vapp.Refresh() + err = vapp.Refresh() check.Assert(err, IsNil) - vappNetworkConfig, err = vapp.RemoveNetwork(vcd.config.VCD.Network.Net1) + vappNetworkConfig, err = vapp.RemoveNetwork(vcd.config.VCD.Network.Net2) check.Assert(err, IsNil) check.Assert(vappNetworkConfig, NotNil) isExist := false for _, networkConfig := range vappNetworkConfig.NetworkConfig { - if networkConfig.NetworkName == vcd.config.VCD.Network.Net1 { + if networkConfig.NetworkName == vcd.config.VCD.Network.Net2 { isExist = true } } @@ -1221,14 +1386,17 @@ func (vcd *TestVCD) Test_AddAndRemoveOrgVappNetwork(check *C) { fmt.Printf("Running: %s\n", check.TestName()) if vcd.config.VCD.Network.Net1 == "" { - check.Skip("Skipping test because no network was given") + check.Skip("Skipping test because no first network was given") + } + if vcd.config.VCD.Network.Net2 == "" { + check.Skip("Skipping test because no second network was given") } - vapp, err := createVappForTest(vcd, "Test_AddAndRemoveOrgVappNetwork") + vapp, err := deployVappForTest(vcd, "Test_AddAndRemoveOrgVappNetwork") check.Assert(err, IsNil) check.Assert(vapp, NotNil) - orgVdcNetwork, err := vcd.vdc.GetOrgVdcNetworkByName(vcd.config.VCD.Network.Net1, false) + orgVdcNetwork, err := vcd.vdc.GetOrgVdcNetworkByName(vcd.config.VCD.Network.Net2, false) check.Assert(err, IsNil) check.Assert(orgVdcNetwork, NotNil) @@ -1244,7 +1412,7 @@ func (vcd *TestVCD) Test_AddAndRemoveOrgVappNetwork(check *C) { networkFound := types.VAppNetworkConfiguration{} for _, networkConfig := range vappNetworkConfig.NetworkConfig { - if networkConfig.NetworkName == vcd.config.VCD.Network.Net1 { + if networkConfig.NetworkName == vcd.config.VCD.Network.Net2 { networkFound = networkConfig } } @@ -1257,17 +1425,18 @@ func (vcd *TestVCD) Test_AddAndRemoveOrgVappNetwork(check *C) { check.Assert(*networkFound.Configuration.RetainNetInfoAcrossDeployments, Equals, retainIpMacEnabled) - check.Assert(networkFound.Configuration.ParentNetwork.Name, Equals, vcd.config.VCD.Network.Net1) + check.Assert(networkFound.Configuration.ParentNetwork.Name, Equals, vcd.config.VCD.Network.Net2) - err = vcd.vapp.Refresh() + err = vapp.Refresh() check.Assert(err, IsNil) - vappNetworkConfig, err = vapp.RemoveNetwork(vcd.config.VCD.Network.Net1) + check.Assert(len(vapp.VApp.NetworkConfigSection.NetworkConfig), Equals, 2) + vappNetworkConfig, err = vapp.RemoveNetwork(vcd.config.VCD.Network.Net2) check.Assert(err, IsNil) check.Assert(vappNetworkConfig, NotNil) isExist := false for _, networkConfig := range vappNetworkConfig.NetworkConfig { - if networkConfig.NetworkName == vcd.config.VCD.Network.Net1 { + if networkConfig.NetworkName == vcd.config.VCD.Network.Net2 { isExist = true } } @@ -1284,14 +1453,17 @@ func (vcd *TestVCD) Test_UpdateOrgVappNetwork(check *C) { fmt.Printf("Running: %s\n", check.TestName()) if vcd.config.VCD.Network.Net1 == "" { - check.Skip("Skipping test because no network was given") + check.Skip("Skipping test because no first network was given") + } + if vcd.config.VCD.Network.Net2 == "" { + check.Skip("Skipping test because no second network was given") } - vapp, err := createVappForTest(vcd, "Test_UpdateOrgVappNetwork") + vapp, err := deployVappForTest(vcd, "Test_UpdateOrgVappNetwork") check.Assert(err, IsNil) check.Assert(vapp, NotNil) - orgVdcNetwork, err := vcd.vdc.GetOrgVdcNetworkByName(vcd.config.VCD.Network.Net1, false) + orgVdcNetwork, err := vcd.vdc.GetOrgVdcNetworkByName(vcd.config.VCD.Network.Net2, false) check.Assert(err, IsNil) check.Assert(orgVdcNetwork, NotNil) @@ -1307,7 +1479,7 @@ func (vcd *TestVCD) Test_UpdateOrgVappNetwork(check *C) { networkFound := types.VAppNetworkConfiguration{} for _, networkConfig := range vappNetworkConfig.NetworkConfig { - if networkConfig.NetworkName == vcd.config.VCD.Network.Net1 { + if networkConfig.NetworkName == vcd.config.VCD.Network.Net2 { networkFound = networkConfig } } @@ -1320,7 +1492,7 @@ func (vcd *TestVCD) Test_UpdateOrgVappNetwork(check *C) { check.Assert(*networkFound.Configuration.RetainNetInfoAcrossDeployments, Equals, retainIpMacEnabled) - check.Assert(networkFound.Configuration.ParentNetwork.Name, Equals, vcd.config.VCD.Network.Net1) + check.Assert(networkFound.Configuration.ParentNetwork.Name, Equals, vcd.config.VCD.Network.Net2) uuid, err := GetUuidFromHref(networkFound.Link.HREF, false) check.Assert(err, IsNil) @@ -1338,7 +1510,7 @@ func (vcd *TestVCD) Test_UpdateOrgVappNetwork(check *C) { check.Assert(vappNetworkConfig, NotNil) for _, networkConfig := range vappNetworkConfig.NetworkConfig { - if networkConfig.NetworkName == vcd.config.VCD.Network.Net1 { + if networkConfig.NetworkName == vcd.config.VCD.Network.Net2 { networkFound = networkConfig } } @@ -1354,17 +1526,18 @@ func (vcd *TestVCD) Test_UpdateOrgVappNetwork(check *C) { check.Assert(networkFound.Configuration.Features, Equals, emptyFirewallFeatures) check.Assert(*networkFound.Configuration.RetainNetInfoAcrossDeployments, Equals, updateRetainIpMacEnabled) - check.Assert(networkFound.Configuration.ParentNetwork.Name, Equals, vcd.config.VCD.Network.Net1) + check.Assert(networkFound.Configuration.ParentNetwork.Name, Equals, vcd.config.VCD.Network.Net2) - err = vcd.vapp.Refresh() + err = vapp.Refresh() check.Assert(err, IsNil) - vappNetworkConfig, err = vapp.RemoveNetwork(vcd.config.VCD.Network.Net1) + check.Assert(len(vapp.VApp.NetworkConfigSection.NetworkConfig), Equals, 2) + vappNetworkConfig, err = vapp.RemoveNetwork(vcd.config.VCD.Network.Net2) check.Assert(err, IsNil) check.Assert(vappNetworkConfig, NotNil) isExist := false for _, networkConfig := range vappNetworkConfig.NetworkConfig { - if networkConfig.NetworkName == vcd.config.VCD.Network.Net1 { + if networkConfig.NetworkName == vcd.config.VCD.Network.Net2 { isExist = true } } @@ -1393,32 +1566,34 @@ func (vcd *TestVCD) Test_AddNewVMFromMultiVmTemplate(check *C) { } // Populate Catalog - catalog, err := vcd.org.GetCatalogByName(vcd.config.VCD.Catalog.Name, false) + catalog, err := vcd.org.GetCatalogByName(vcd.config.VCD.Catalog.NsxtBackedCatalogName, false) check.Assert(err, IsNil) check.Assert(catalog, NotNil) + uploadedItem := false itemName := vcd.config.VCD.Catalog.CatalogItemWithMultiVms if itemName == "" { - check.Log("Using `OvaMultiVmPath` for test. Will upload to use it.") + check.Logf("Using `OvaMultiVmPath` '%s' for test. Will upload to use it.", vcd.config.OVA.OvaMultiVmPath) itemName = check.TestName() uploadTask, err := catalog.UploadOvf(vcd.config.OVA.OvaMultiVmPath, itemName, "upload from test", 1024) check.Assert(err, IsNil) err = uploadTask.WaitTaskCompletion() check.Assert(err, IsNil) - AddToCleanupList(itemName, "catalogItem", vcd.org.Org.Name+"|"+vcd.config.VCD.Catalog.Name, check.TestName()) + uploadedItem = true + AddToCleanupList(itemName, "catalogItem", vcd.org.Org.Name+"|"+vcd.config.VCD.Catalog.NsxtBackedCatalogName, check.TestName()) } else { - check.Log("Using `CatalogItemWithMultiVms` for test") + check.Logf("Using pre-loaded `CatalogItemWithMultiVms` '%s' for test", itemName) } - vmInTemplateRecord, err := vcd.vdc.QueryVappVmTemplate(vcd.config.VCD.Catalog.Name, itemName, vcd.config.VCD.Catalog.VmNameInMultiVmItem) + vmInTemplateRecord, err := vcd.nsxtVdc.QueryVappSynchronizedVmTemplate(vcd.config.VCD.Catalog.NsxtBackedCatalogName, itemName, vcd.config.VCD.Catalog.VmNameInMultiVmItem) check.Assert(err, IsNil) // Get VAppTemplate returnedVappTemplate, err := catalog.GetVappTemplateByHref(vmInTemplateRecord.HREF) check.Assert(err, IsNil) - vapp, err := createVappForTest(vcd, "Test_AddNewVMFromMultiVmTemplate") + vapp, err := deployVappForTest(vcd, "Test_AddNewVMFromMultiVmTemplate") check.Assert(err, IsNil) check.Assert(vapp, NotNil) task, err := vapp.AddNewVM(check.TestName(), *returnedVappTemplate, nil, true) @@ -1440,15 +1615,20 @@ func (vcd *TestVCD) Test_AddNewVMFromMultiVmTemplate(check *C) { err = task.WaitTaskCompletion() check.Assert(err, IsNil) check.Assert(task.Task.Status, Equals, "success") -} -// Test_AddNewVMWithComputeCapacity creates a new VM in vApp with VM using compute capacity -func (vcd *TestVCD) Test_AddNewVMWithComputeCapacity(check *C) { + // Remove uploaded catalog item + if uploadedItem { + catalogItem, err := catalog.GetCatalogItemByName(itemName, true) + check.Assert(err, IsNil) - if vcd.client.Client.APIVCDMaxVersionIs("< 33.0") { - check.Skip(fmt.Sprintf("Test %s requires VCD 10.0 (API version 33) or higher", check.TestName())) + err = catalogItem.Delete() + check.Assert(err, IsNil) } +} +// Test_AddNewVMWithComputeCapacity creates a new VM in vApp with VM using compute capacity +func (vcd *TestVCD) Test_AddNewVMWithComputeCapacity(check *C) { + vcd.skipIfNotSysAdmin(check) if vcd.skipVappTests { check.Skip("Skipping test because vApp was not successfully created at setup") } @@ -1472,7 +1652,7 @@ func (vcd *TestVCD) Test_AddNewVMWithComputeCapacity(check *C) { vapptemplate, err := catitem.GetVAppTemplate() check.Assert(err, IsNil) - vapp, err := createVappForTest(vcd, "Test_AddNewVMWithComputeCapacity") + vapp, err := deployVappForTest(vcd, "Test_AddNewVMWithComputeCapacity") check.Assert(err, IsNil) check.Assert(vapp, NotNil) @@ -1481,7 +1661,7 @@ func (vcd *TestVCD) Test_AddNewVMWithComputeCapacity(check *C) { client: vcd.org.client, VdcComputePolicy: &types.VdcComputePolicy{ Name: check.TestName() + "_empty", - Description: "Empty policy created by test", + Description: addrOf("Empty policy created by test"), }, } @@ -1557,3 +1737,174 @@ func (vcd *TestVCD) Test_AddNewVMWithComputeCapacity(check *C) { _, err = adminVdc.SetAssignedComputePolicies(types.VdcComputePolicyReferences{VdcComputePolicyReference: beforeTestPolicyReferences}) check.Assert(err, IsNil) } + +func (vcd *TestVCD) testUpdateVapp(op string, check *C, vapp *VApp, name, description string, vms []string) { + + var err error + switch op { + case "update_desc", "remove_desc": + printVerbose("[%s] testing vapp.UpdateDescription(\"%s\")\n", op, description) + err = vapp.UpdateDescription(description) + check.Assert(err, IsNil) + case "update_both": + printVerbose("[%s] testing vapp.UpdateNameDescription(\"%s\", \"%s\")\n", op, name, description) + err = vapp.UpdateNameDescription(name, description) + check.Assert(err, IsNil) + case "rename": + printVerbose("[%s] testing vapp.Rename(\"%s\")\n", op, name) + err = vapp.Rename(name) + check.Assert(err, IsNil) + default: + check.Assert("unhandled operation", Equals, "true") + } + + if name == "" { + name = vapp.VApp.Name + } + + // Get a fresh copy of the vApp + vapp, err = vcd.vdc.GetVAppByName(name, true) + check.Assert(err, IsNil) + + check.Assert(vapp.VApp.Name, Equals, name) + check.Assert(vapp.VApp.Description, Equals, description) + // check that the VMs still exist after vApp update + for _, vm := range vms { + printVerbose("checking VM %s\n", vm) + _, err = vapp.GetVMByName(vm, true) + check.Assert(err, IsNil) + } +} + +func (vcd *TestVCD) Test_UpdateVappNameDescription(check *C) { + + fmt.Printf("Running: %s\n", check.TestName()) + + vappName := check.TestName() + vappDescription := vappName + " description" + newVappName := vappName + "_new" + + newVappDescription := vappName + " desc" + // Compose VApp + vapp, err := makeEmptyVapp(vcd.vdc, vappName, vappDescription) + check.Assert(err, IsNil) + AddToCleanupList(vappName, "vapp", "", "Test_RenameVapp") + + check.Assert(vapp.VApp.Name, Equals, vappName) + check.Assert(vapp.VApp.Description, Equals, vappDescription) + + // Need a slight delay for the vApp to get the links that are needed for renaming + time.Sleep(time.Second) + + // change description + vcd.testUpdateVapp("update_desc", check, vapp, "", newVappDescription, nil) + + // remove description + vcd.testUpdateVapp("remove_desc", check, vapp, vappName, "", nil) + + // restore original + vcd.testUpdateVapp("update_both", check, vapp, vappName, vappDescription, nil) + + // change name + vcd.testUpdateVapp("rename", check, vapp, newVappName, vappDescription, nil) + AddToCleanupList(newVappName, "vapp", "", "Test_RenameVapp") + // restore original + vcd.testUpdateVapp("update_both", check, vapp, vappName, vappDescription, nil) + + // Add two VMs + _, err = makeEmptyVm(vapp, "vm1") + check.Assert(err, IsNil) + _, err = makeEmptyVm(vapp, "vm2") + check.Assert(err, IsNil) + + vms := []string{"vm1", "vm2"} + // change description after adding VMs + vcd.testUpdateVapp("update_desc", check, vapp, "", newVappDescription, vms) + vcd.testUpdateVapp("remove_desc", check, vapp, vappName, "", nil) + // restore original + vcd.testUpdateVapp("update_both", check, vapp, vappName, vappDescription, vms) + + // change name after adding VMs + vcd.testUpdateVapp("rename", check, vapp, newVappName, vappDescription, vms) + // restore original + vcd.testUpdateVapp("update_both", check, vapp, vappName, vappDescription, vms) + + // Remove vApp + err = deleteVapp(vcd, vappName) + check.Assert(err, IsNil) +} + +func (vcd *TestVCD) Test_Vapp_LeaseUpdate(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + + if vcd.config.VCD.Org == "" { + check.Skip("Organization not set in configuration") + } + org, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + orgVappLease := org.AdminOrg.OrgSettings.OrgVAppLeaseSettings + + vappName := check.TestName() + vappDescription := vappName + " description" + + vapp, err := makeEmptyVapp(vcd.vdc, vappName, vappDescription) + check.Assert(err, IsNil) + AddToCleanupList(vappName, "vapp", "", "Test_Vapp_GetLease") + + lease, err := vapp.GetLease() + check.Assert(err, IsNil) + check.Assert(lease, NotNil) + + // Check that lease in vApp is the same as the default lease in the organization + check.Assert(lease.StorageLeaseInSeconds, Equals, *orgVappLease.StorageLeaseSeconds) + check.Assert(lease.DeploymentLeaseInSeconds, Equals, *orgVappLease.DeploymentLeaseSeconds) + if testVerbose { + fmt.Printf("lease deployment at Org level: %d\n", *orgVappLease.DeploymentLeaseSeconds) + fmt.Printf("lease storage at Org level: %d\n", *orgVappLease.StorageLeaseSeconds) + fmt.Printf("lease deployment in vApp before: %d\n", lease.DeploymentLeaseInSeconds) + fmt.Printf("lease storage in vApp before: %d\n", lease.StorageLeaseInSeconds) + } + secondsInDay := 60 * 60 * 24 + + // Set lease to 90 days deployment, 7 days storage + err = vapp.RenewLease(secondsInDay*90, secondsInDay*7) + check.Assert(err, IsNil) + + // Make sure the vApp internal values were updated + check.Assert(vapp.VApp.LeaseSettingsSection.DeploymentLeaseInSeconds, Equals, secondsInDay*90) + check.Assert(vapp.VApp.LeaseSettingsSection.StorageLeaseInSeconds, Equals, secondsInDay*7) + + newLease, err := vapp.GetLease() + check.Assert(err, IsNil) + check.Assert(newLease, NotNil) + check.Assert(newLease.DeploymentLeaseInSeconds, Equals, secondsInDay*90) + check.Assert(newLease.StorageLeaseInSeconds, Equals, secondsInDay*7) + + if testVerbose { + fmt.Printf("lease deployment in vApp after: %d\n", newLease.DeploymentLeaseInSeconds) + fmt.Printf("lease storage in vApp after: %d\n", newLease.StorageLeaseInSeconds) + } + + // Set lease to "never expires", which defaults to the Org maximum lease if the Org itself has lower limits + err = vapp.RenewLease(0, 0) + check.Assert(err, IsNil) + + check.Assert(vapp.VApp.LeaseSettingsSection.DeploymentLeaseInSeconds, Equals, *orgVappLease.DeploymentLeaseSeconds) + + check.Assert(vapp.VApp.LeaseSettingsSection.StorageLeaseInSeconds, Equals, *orgVappLease.StorageLeaseSeconds) + + if *orgVappLease.DeploymentLeaseSeconds != 0 { + // Check that setting a lease higher than allowed by the Org settings results in the defaults lease being set + err = vapp.RenewLease(*orgVappLease.DeploymentLeaseSeconds+3600, + *orgVappLease.StorageLeaseSeconds+3600) + check.Assert(err, IsNil) + + check.Assert(vapp.VApp.LeaseSettingsSection.DeploymentLeaseInSeconds, Equals, *orgVappLease.DeploymentLeaseSeconds) + check.Assert(vapp.VApp.LeaseSettingsSection.StorageLeaseInSeconds, Equals, *orgVappLease.StorageLeaseSeconds) + } + + task, err := vapp.Delete() + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) +} diff --git a/govcd/vapp_vm_test.go b/govcd/vapp_vm_test.go index 13853fa3c..e56977667 100644 --- a/govcd/vapp_vm_test.go +++ b/govcd/vapp_vm_test.go @@ -1,4 +1,4 @@ -// +build vapp vm functional ALL +//go:build vapp || vm || functional || ALL /* * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -103,10 +103,10 @@ type getGuestCustomizationSectionGetSetter interface { // out settings on all objects implementing such interface func guestCustomizationPropertyTester(vcd *TestVCD, check *C, object getGuestCustomizationSectionGetSetter) { setupedGuestCustomizationSection := &types.GuestCustomizationSection{ - Enabled: takeBoolPointer(true), JoinDomainEnabled: takeBoolPointer(false), UseOrgSettings: takeBoolPointer(false), + Enabled: addrOf(true), JoinDomainEnabled: addrOf(false), UseOrgSettings: addrOf(false), DomainUserName: "", DomainName: "", DomainUserPassword: "", - AdminPasswordEnabled: takeBoolPointer(true), AdminPassword: "adminPass", AdminPasswordAuto: takeBoolPointer(false), - AdminAutoLogonEnabled: takeBoolPointer(true), AdminAutoLogonCount: 15, ResetPasswordRequired: takeBoolPointer(true), + AdminPasswordEnabled: addrOf(true), AdminPassword: "adminPass", AdminPasswordAuto: addrOf(false), + AdminAutoLogonEnabled: addrOf(true), AdminAutoLogonCount: 15, ResetPasswordRequired: addrOf(true), CustomizationScript: "ls", ComputerName: "Cname18"} guestCustomizationSection, err := object.SetGuestCustomizationSection(setupedGuestCustomizationSection) diff --git a/govcd/vapptemplate.go b/govcd/vapptemplate.go index be2f2d02a..2044cde24 100644 --- a/govcd/vapptemplate.go +++ b/govcd/vapptemplate.go @@ -10,6 +10,7 @@ import ( "net/url" "github.com/vmware/go-vcloud-director/v2/types/v56" + "github.com/vmware/go-vcloud-director/v2/util" ) type VAppTemplate struct { @@ -24,6 +25,8 @@ func NewVAppTemplate(cli *Client) *VAppTemplate { } } +// Deprecated: wrong implementation and result +// Use vdc.CreateVappFromTemplate instead func (vdc *Vdc) InstantiateVAppTemplate(template *types.InstantiateVAppTemplateParams) error { vdcHref, err := url.ParseRequestURI(vdc.Vdc.HREF) if err != nil { @@ -34,7 +37,7 @@ func (vdc *Vdc) InstantiateVAppTemplate(template *types.InstantiateVAppTemplateP vapptemplate := NewVAppTemplate(vdc.client) _, err = vdc.client.ExecuteRequest(vdcHref.String(), http.MethodPut, - types.MimeInstantiateVappTemplateParams, "error instantiating a new template: %s", template, vapptemplate) + types.MimeInstantiateVappTemplateParams, "error instantiating a new vApp Template: %s", template, vapptemplate) if err != nil { return err } @@ -65,7 +68,319 @@ func (vAppTemplate *VAppTemplate) Refresh() error { vAppTemplate.VAppTemplate = &types.VAppTemplate{} _, err := vAppTemplate.client.ExecuteRequest(url, http.MethodGet, - "", "error retrieving vApp template item: %s", nil, vAppTemplate.VAppTemplate) + "", "error retrieving vApp Template: %s", nil, vAppTemplate.VAppTemplate) return err } + +// GetCatalogName gets the catalog name to which the receiver vApp Template belongs +func (vAppTemplate *VAppTemplate) GetCatalogName() (string, error) { + queriedVappTemplates, err := queryVappTemplateListWithFilter(vAppTemplate.client, map[string]string{ + "id": vAppTemplate.VAppTemplate.ID, + }) + if err != nil { + return "", err + } + if len(queriedVappTemplates) != 1 { + return "", fmt.Errorf("found %d vApp Templates with ID %s", len(queriedVappTemplates), vAppTemplate.VAppTemplate.ID) + } + return queriedVappTemplates[0].CatalogName, nil +} + +// GetVdcName gets the VDC name to which the receiver vApp Template belongs +func (vAppTemplate *VAppTemplate) GetVdcName() (string, error) { + queriedVappTemplates, err := queryVappTemplateListWithFilter(vAppTemplate.client, map[string]string{ + "id": vAppTemplate.VAppTemplate.ID, + }) + if err != nil { + return "", err + } + if len(queriedVappTemplates) != 1 { + return "", fmt.Errorf("found %d vApp Templates with ID %s", len(queriedVappTemplates), vAppTemplate.VAppTemplate.ID) + } + return queriedVappTemplates[0].VdcName, nil +} + +// GetVappTemplateRecord gets the corresponding vApp template record +func (vAppTemplate *VAppTemplate) GetVappTemplateRecord() (*types.QueryResultVappTemplateType, error) { + queriedVappTemplates, err := queryVappTemplateListWithFilter(vAppTemplate.client, map[string]string{ + "id": vAppTemplate.VAppTemplate.ID, + }) + if err != nil { + return nil, err + } + if len(queriedVappTemplates) != 1 { + return nil, fmt.Errorf("found %d vApp Templates with ID %s", len(queriedVappTemplates), vAppTemplate.VAppTemplate.ID) + } + return queriedVappTemplates[0], nil +} + +// Update updates the vApp template item information. +// VCD also updates the associated Catalog Item, in order to be in sync with the receiver vApp Template entity. +// For example, updating a vApp Template name "A" to "B" will make VCD to also update the Catalog Item to be renamed to "B". +// Returns vApp template and error. +func (vAppTemplate *VAppTemplate) Update() (*VAppTemplate, error) { + if vAppTemplate.VAppTemplate == nil { + return nil, fmt.Errorf("cannot update, Object is empty") + } + + url := vAppTemplate.VAppTemplate.HREF + if url == "nil" { + return nil, fmt.Errorf("cannot update, HREF is empty") + } + + task, err := vAppTemplate.UpdateAsync() + if err != nil { + return nil, err + } + err = task.WaitTaskCompletion() + if err != nil { + return nil, fmt.Errorf("error waiting for task completion after updating vApp Template %s: %s", vAppTemplate.VAppTemplate.Name, err) + } + err = vAppTemplate.Refresh() + if err != nil { + return nil, fmt.Errorf("error refreshing vApp Template %s: %s", vAppTemplate.VAppTemplate.Name, err) + } + return vAppTemplate, nil +} + +// UpdateAsync updates the vApp template item information +// Returns Task and error. +func (vAppTemplate *VAppTemplate) UpdateAsync() (Task, error) { + + if vAppTemplate.VAppTemplate == nil { + return Task{}, fmt.Errorf("cannot update, Object is empty") + } + + url := vAppTemplate.VAppTemplate.HREF + if url == "nil" { + return Task{}, fmt.Errorf("cannot update, HREF is empty") + } + + vappTemplatePayload := types.VAppTemplateForUpdate{ + Xmlns: types.XMLNamespaceVCloud, + HREF: vAppTemplate.VAppTemplate.HREF, + ID: vAppTemplate.VAppTemplate.ID, + Name: vAppTemplate.VAppTemplate.Name, + GoldMaster: vAppTemplate.VAppTemplate.GoldMaster, + Description: vAppTemplate.VAppTemplate.Description, + Link: vAppTemplate.VAppTemplate.Link, + } + + return vAppTemplate.client.ExecuteTaskRequest(url, http.MethodPut, + types.MimeVAppTemplate, "error updating vApp Template: %s", vappTemplatePayload) +} + +// DeleteAsync deletes the VAppTemplate, returning the Task that monitors the deletion process, or an error +// if something wrong happened. +func (vAppTemplate *VAppTemplate) DeleteAsync() (Task, error) { + util.Logger.Printf("[TRACE] Deleting vApp Template: %#v", vAppTemplate.VAppTemplate) + + vappTemplateHref := vAppTemplate.client.VCDHREF + vappTemplateHref.Path += "/vAppTemplate/vappTemplate-" + extractUuid(vAppTemplate.VAppTemplate.ID) + + util.Logger.Printf("[TRACE] Url for deleting vApp Template: %#v and name: %s", vappTemplateHref, vAppTemplate.VAppTemplate.Name) + + return vAppTemplate.client.ExecuteTaskRequest(vappTemplateHref.String(), http.MethodDelete, + "", "error deleting vApp Template: %s", nil) +} + +// Delete deletes the VAppTemplate and waits for the deletion to finish, returning an error if something wrong happened. +func (vAppTemplate *VAppTemplate) Delete() error { + task, err := vAppTemplate.DeleteAsync() + if err != nil { + return err + } + err = task.WaitTaskCompletion() + if err != nil { + return fmt.Errorf("error waiting for task completion after deleting vApp Template %s: %s", vAppTemplate.VAppTemplate.Name, err) + } + return nil +} + +// GetVAppTemplateByHref finds a vApp template by HREF +// On success, returns a pointer to the vApp template structure and a nil error +// On failure, returns a nil pointer and an error +func (vcdClient *VCDClient) GetVAppTemplateByHref(href string) (*VAppTemplate, error) { + return getVAppTemplateByHref(&vcdClient.Client, href) +} + +// GetVAppTemplateById finds a vApp Template by ID. +// On success, returns a pointer to the VAppTemplate structure and a nil error. +// On failure, returns a nil pointer and an error. +func (vcdClient *VCDClient) GetVAppTemplateById(vAppTemplateId string) (*VAppTemplate, error) { + return getVAppTemplateById(&vcdClient.Client, vAppTemplateId) +} + +// QuerySynchronizedVAppTemplateById Finds a vApp Template by its URN that is synchronized in the catalog. +// Returns types.QueryResultVMRecordType if it is found, returns ErrorEntityNotFound if not found, or an error if many are +// found. +func (vcdClient *VCDClient) QuerySynchronizedVAppTemplateById(vAppTemplateId string) (*types.QueryResultVappTemplateType, error) { + queryType := types.QtVappTemplate + if vcdClient.Client.IsSysAdmin { + queryType = types.QtAdminVappTemplate + } + + // this allows to query deployed and not deployed templates + results, err := vcdClient.QueryWithNotEncodedParams(nil, map[string]string{ + "type": queryType, + "filter": "id==" + url.QueryEscape(extractUuid(vAppTemplateId)) + + ";status!=FAILED_CREATION;status!=UNKNOWN;status!=UNRECOGNIZED;status!=UNRESOLVED;status!=LOCAL_COPY_UNAVAILABLE&links=true", + "filterEncoded": "true"}) + if err != nil { + return nil, fmt.Errorf("[QueryVAppTemplateById] error quering vApp templates with ID %s: %s", vAppTemplateId, err) + } + + vAppTemplateRecords := results.Results.VappTemplateRecord + if vcdClient.Client.IsSysAdmin { + vAppTemplateRecords = results.Results.AdminVappTemplateRecord + } + if len(vAppTemplateRecords) == 0 { + return nil, ErrorEntityNotFound + } + + if len(vAppTemplateRecords) > 1 { + return nil, fmt.Errorf("[QueryVmInVAppTemplateByHref] found %d results with with ID: %s", len(vAppTemplateRecords), vAppTemplateId) + } + + return vAppTemplateRecords[0], nil +} + +// QueryVmInVAppTemplateByHref Finds a VM inside a vApp Template using the latter HREF. +// Returns types.QueryResultVMRecordType if it is found, returns ErrorEntityNotFound if not found, or an error if many are +// found. +func (vcdClient *VCDClient) QueryVmInVAppTemplateByHref(vAppTemplateHref, vmNameInTemplate string) (*types.QueryResultVMRecordType, error) { + queryType := types.QtVm + if vcdClient.Client.IsSysAdmin { + queryType = types.QtAdminVm + } + + // this allows to query deployed and not deployed templates + results, err := vcdClient.QueryWithNotEncodedParams(nil, map[string]string{ + "type": queryType, + "filter": "container==" + url.QueryEscape(vAppTemplateHref) + ";name==" + url.QueryEscape(vmNameInTemplate) + + ";isVAppTemplate==true;status!=FAILED_CREATION;status!=UNKNOWN;status!=UNRECOGNIZED;status!=UNRESOLVED&links=true;", + "filterEncoded": "true"}) + if err != nil { + return nil, fmt.Errorf("[QueryVmInVAppTemplateByHref] error quering vApp templates with HREF %s:, VM name: %s: Error: %s", vAppTemplateHref, vmNameInTemplate, err) + } + + vmResults := results.Results.VMRecord + if vcdClient.Client.IsSysAdmin { + vmResults = results.Results.AdminVMRecord + } + + if len(vmResults) == 0 { + return nil, ErrorEntityNotFound + } + + if len(vmResults) > 1 { + return nil, fmt.Errorf("[QueryVmInVAppTemplateByHref] found %d results with with HREF: %s, VM name: %s", len(vmResults), vAppTemplateHref, vmNameInTemplate) + } + + return vmResults[0], nil +} + +// QuerySynchronizedVmInVAppTemplateByHref Finds a catalog-synchronized VM inside a vApp Template using the latter HREF. +// Returns types.QueryResultVMRecordType if it is found and it's synchronized in the catalog. +// Returns ErrorEntityNotFound if not found, or an error if many are found. +func (vcdClient *VCDClient) QuerySynchronizedVmInVAppTemplateByHref(vAppTemplateHref, vmNameInTemplate string) (*types.QueryResultVMRecordType, error) { + vmRecord, err := vcdClient.QueryVmInVAppTemplateByHref(vAppTemplateHref, vmNameInTemplate) + if err != nil { + return nil, err + } + if vmRecord.Status == "LOCAL_COPY_UNAVAILABLE" { + return nil, fmt.Errorf("vApp template %s is not synchronized", extractUuid(vAppTemplateHref)) + } + return vmRecord, nil +} + +// RenewLease updates the lease terms for the vAppTemplate +func (vAppTemplate *VAppTemplate) RenewLease(storageLeaseInSeconds int) error { + + href := "" + if vAppTemplate.VAppTemplate.LeaseSettingsSection != nil { + if vAppTemplate.VAppTemplate.LeaseSettingsSection.StorageLeaseInSeconds == storageLeaseInSeconds { + // Requested parameters are the same as existing parameters: exit without updating + return nil + } + href = vAppTemplate.VAppTemplate.LeaseSettingsSection.HREF + } + if href == "" { + href = getUrlFromLink(vAppTemplate.VAppTemplate.Link, "edit", types.MimeLeaseSettingSection) + } + + if href == "" { + return fmt.Errorf("link to update lease settings not found for vAppTemplate %s", vAppTemplate.VAppTemplate.Name) + } + + var leaseSettings = types.UpdateLeaseSettingsSection{ + HREF: href, + XmlnsOvf: types.XMLNamespaceOVF, + Xmlns: types.XMLNamespaceVCloud, + OVFInfo: "Lease section settings", + Type: types.MimeLeaseSettingSection, + StorageLeaseInSeconds: &storageLeaseInSeconds, + } + + task, err := vAppTemplate.client.ExecuteTaskRequest(href, http.MethodPut, + types.MimeLeaseSettingSection, "error updating vAppTemplate lease : %s", &leaseSettings) + + if err != nil { + return fmt.Errorf("unable to update vAppTemplate lease: %s", err) + } + + err = task.WaitTaskCompletion() + if err != nil { + return fmt.Errorf("task for updating vAppTemplate lease failed: %s", err) + } + return vAppTemplate.Refresh() +} + +// GetLease retrieves the lease terms for a vAppTemplate +func (vAppTemplate *VAppTemplate) GetLease() (*types.LeaseSettingsSection, error) { + + href := "" + if vAppTemplate.VAppTemplate.LeaseSettingsSection != nil { + href = vAppTemplate.VAppTemplate.LeaseSettingsSection.HREF + } + if href == "" { + for _, link := range vAppTemplate.VAppTemplate.Link { + if link.Type == types.MimeLeaseSettingSection { + href = link.HREF + break + } + } + } + if href == "" { + return nil, fmt.Errorf("link to retrieve lease settings not found for vApp %s", vAppTemplate.VAppTemplate.Name) + } + var leaseSettings types.LeaseSettingsSection + + _, err := vAppTemplate.client.ExecuteRequest(href, http.MethodGet, "", "error getting vAppTemplate lease info: %s", nil, &leaseSettings) + + if err != nil { + return nil, err + } + return &leaseSettings, nil +} + +// GetCatalogItemHref looks up Href for catalog item in vApp template +func (vAppTemplate *VAppTemplate) GetCatalogItemHref() (string, error) { + for _, link := range vAppTemplate.VAppTemplate.Link { + if link.Rel == "catalogItem" && link.Type == types.MimeCatalogItem { + return link.HREF, nil + } + } + return "", fmt.Errorf("error finding Catalog Item link in vApp template %s", vAppTemplate.VAppTemplate.ID) +} + +// GetCatalogItemId returns ID for catalog item in vApp template +func (vAppTemplate *VAppTemplate) GetCatalogItemId() (string, error) { + href, err := vAppTemplate.GetCatalogItemHref() + if err != nil { + return "", err + } + + return fmt.Sprintf("urn:vcloud:catalogitem:%s", extractUuid(href)), nil +} diff --git a/govcd/vapptemplate_test.go b/govcd/vapptemplate_test.go index 7e177645e..0186adb1f 100644 --- a/govcd/vapptemplate_test.go +++ b/govcd/vapptemplate_test.go @@ -1,4 +1,4 @@ -// +build vapp functional ALL +//go:build vapp || functional || ALL /* * Copyright 2018 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -46,4 +46,184 @@ func (vcd *TestVCD) Test_RefreshVAppTemplate(check *C) { check.Assert(oldVAppTemplate.VAppTemplate.ID, Equals, vAppTemplate.VAppTemplate.ID) check.Assert(oldVAppTemplate.VAppTemplate.Name, Equals, vAppTemplate.VAppTemplate.Name) check.Assert(oldVAppTemplate.VAppTemplate.HREF, Equals, vAppTemplate.VAppTemplate.HREF) + + catalogItemHref, err := vAppTemplate.GetCatalogItemHref() + check.Assert(err, IsNil) + check.Assert(catalogItemHref, Not(Equals), "") + + catalogItemId, err := vAppTemplate.GetCatalogItemId() + check.Assert(err, IsNil) + check.Assert(catalogItemId, Not(Equals), "") +} + +func (vcd *TestVCD) Test_UpdateAndDeleteVAppTemplateFromOvaFile(check *C) { + testUploadAndDeleteVAppTemplate(vcd, check, false) +} + +func (vcd *TestVCD) Test_UpdateAndDeleteVAppTemplateFromUrl(check *C) { + testUploadAndDeleteVAppTemplate(vcd, check, true) +} + +func (vcd *TestVCD) Test_GetInformationFromVAppTemplate(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + if vcd.config.VCD.Catalog.Name == "" { + check.Skip(check.TestName() + ": Catalog not given in testing configuration. Test can't proceed") + } + + if vcd.config.VCD.Catalog.CatalogItem == "" { + check.Skip(check.TestName() + ": Catalog Item not given in testing configuration. Test can't proceed") + } + + catalog, err := vcd.org.GetCatalogByName(vcd.config.VCD.Catalog.Name, false) + check.Assert(err, IsNil) + check.Assert(catalog, NotNil) + vAppTemplate, err := catalog.GetVAppTemplateByName(vcd.config.VCD.Catalog.CatalogItem) + check.Assert(err, IsNil) + check.Assert(vAppTemplate, NotNil) + + catalogName, err := vAppTemplate.GetCatalogName() + check.Assert(err, IsNil) + check.Assert(catalogName, Equals, catalog.Catalog.Name) + + vdcId, err := vAppTemplate.GetVdcName() + check.Assert(err, IsNil) + check.Assert(vdcId, Equals, vcd.vdc.Vdc.Name) +} + +func testUploadAndDeleteVAppTemplate(vcd *TestVCD, check *C, isOvfLink bool) { + fmt.Printf("Running: %s\n", check.TestName()) + catalog, err := vcd.org.GetCatalogByName(vcd.config.VCD.Catalog.Name, false) + if err != nil { + check.Skip(check.TestName() + ": Catalog not found. Test can't proceed") + return + } + check.Assert(catalog, NotNil) + + itemName := check.TestName() + + description := "upload from test" + + if isOvfLink { + uploadTask, err := catalog.UploadOvfByLink(vcd.config.OVA.OvfUrl, itemName, description) + check.Assert(err, IsNil) + err = uploadTask.WaitTaskCompletion() + check.Assert(err, IsNil) + } else { + task, err := catalog.UploadOvf(vcd.config.OVA.OvaPath, itemName, description, 1024) + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) + } + + AddToCleanupList(itemName, "catalogItem", vcd.org.Org.Name+"|"+vcd.config.VCD.Catalog.Name, check.TestName()) + + vAppTemplate, err := catalog.GetVAppTemplateByName(itemName) + check.Assert(err, IsNil) + check.Assert(vAppTemplate, NotNil) + check.Assert(vAppTemplate.VAppTemplate.Name, Equals, itemName) + + // FIXME: Due to bug in OVF Link upload in VCD, this assert is skipped + if !isOvfLink { + check.Assert(vAppTemplate.VAppTemplate.Description, Equals, description) + } + + nameForUpdate := itemName + "updated" + descriptionForUpdate := description + "updated" + + AddToCleanupList(nameForUpdate, "catalogItem", vcd.org.Org.Name+"|"+vcd.config.VCD.Catalog.Name, check.TestName()) + + vAppTemplate.VAppTemplate.Name = nameForUpdate + vAppTemplate.VAppTemplate.Description = descriptionForUpdate + vAppTemplate.VAppTemplate.GoldMaster = true + + _, err = vAppTemplate.Update() + check.Assert(err, IsNil) + err = vAppTemplate.Refresh() + check.Assert(err, IsNil) + check.Assert(vAppTemplate.VAppTemplate.Name, Equals, nameForUpdate) + check.Assert(vAppTemplate.VAppTemplate.Description, Equals, descriptionForUpdate) + check.Assert(vAppTemplate.VAppTemplate.GoldMaster, Equals, true) + + err = vAppTemplate.Delete() + check.Assert(err, IsNil) + vAppTemplate, err = catalog.GetVAppTemplateByName(itemName) + check.Assert(err, NotNil) + check.Assert(vAppTemplate, IsNil) +} + +func (vcd *TestVCD) Test_VappTemplateLeaseUpdate(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + + if vcd.config.VCD.Org == "" { + check.Skip("Organization not set in configuration") + } + org, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + orgVappTemplateLease := org.AdminOrg.OrgSettings.OrgVAppTemplateSettings + + catalog, err := vcd.org.GetCatalogByName(vcd.config.VCD.Catalog.NsxtBackedCatalogName, false) + if err != nil { + check.Skip("Test_GetVAppTemplate: Catalog not found. Test can't proceed") + return + } + check.Assert(catalog, NotNil) + + itemName := check.TestName() + description := "upload from test" + + task, err := catalog.UploadOvf(vcd.config.OVA.OvaPath, itemName, description, 1024) + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) + + AddToCleanupList(itemName, "catalogItem", vcd.org.Org.Name+"|"+vcd.config.VCD.Catalog.NsxtBackedCatalogName, check.TestName()) + + vAppTemplate, err := catalog.GetVAppTemplateByName(itemName) + check.Assert(err, IsNil) + check.Assert(vAppTemplate, NotNil) + check.Assert(vAppTemplate.VAppTemplate.Name, Equals, itemName) + + lease, err := vAppTemplate.GetLease() + check.Assert(err, IsNil) + check.Assert(lease, NotNil) + + // Check that lease in vAppTemplate is the same as the default lease in the organization + check.Assert(lease.StorageLeaseInSeconds, Equals, *orgVappTemplateLease.StorageLeaseSeconds) + printVerbose("lease storage at Org level: %6d\n", *orgVappTemplateLease.StorageLeaseSeconds) + printVerbose("lease storage in vApp Template before: %6d\n", lease.StorageLeaseInSeconds) + secondsInDay := 60 * 60 * 24 + + // Set lease to 7 days storage + err = vAppTemplate.RenewLease(secondsInDay * 7) + check.Assert(err, IsNil) + + // Make sure the vAppTemplate internal values were updated + check.Assert(vAppTemplate.VAppTemplate.LeaseSettingsSection.StorageLeaseInSeconds, Equals, secondsInDay*7) + + newLease, err := vAppTemplate.GetLease() + check.Assert(err, IsNil) + check.Assert(newLease, NotNil) + check.Assert(newLease.StorageLeaseInSeconds, Equals, secondsInDay*7) + + printVerbose("lease storage in vAppTemplate after: %6d\n", newLease.StorageLeaseInSeconds) + + // Set lease to "never expires", which defaults to the Org maximum lease if the Org itself has lower limits + err = vAppTemplate.RenewLease(0) + check.Assert(err, IsNil) + check.Assert(vAppTemplate.VAppTemplate.LeaseSettingsSection.StorageLeaseInSeconds, Equals, *orgVappTemplateLease.StorageLeaseSeconds) + + newLease, err = vAppTemplate.GetLease() + check.Assert(err, IsNil) + check.Assert(newLease, NotNil) + printVerbose("lease storage in vAppTemplate (reset): %d\n", newLease.StorageLeaseInSeconds) + + if *orgVappTemplateLease.StorageLeaseSeconds != 0 { + // Check that setting a lease higher than allowed by the Org settings results in an error + err = vAppTemplate.RenewLease(*orgVappTemplateLease.StorageLeaseSeconds + 3600) + check.Assert(err, NotNil) + // Note: the same operation in a vApp results with the lease settings silently going back to the Organization defaults + } + + err = vAppTemplate.Delete() + check.Assert(err, IsNil) } diff --git a/govcd/vcd_test_help.go b/govcd/vcd_test_help.go new file mode 100644 index 000000000..58649b5b3 --- /dev/null +++ b/govcd/vcd_test_help.go @@ -0,0 +1,22 @@ +package govcd + +import "strings" + +// Gets the two or three components of a "parent" string, as passed to AddToCleanupList +// If the number of split strings is not 2 or 3 it return 3 empty strings +// Example input parent: my-org|my-vdc|my-edge-gw, separator: | +// Output : first: my-org, second: my-vdc, third: my-edge-gw +func splitParent(parent string, separator string) (first, second, third string) { + strList := strings.Split(parent, separator) + if len(strList) < 2 || len(strList) > 3 { + return "", "", "" + } + first = strList[0] + second = strList[1] + + if len(strList) == 3 { + third = strList[2] + } + + return +} diff --git a/govcd/vdc.go b/govcd/vdc.go index dccef99fc..05cffacb9 100644 --- a/govcd/vdc.go +++ b/govcd/vdc.go @@ -1,5 +1,5 @@ /* - * Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. */ package govcd @@ -20,6 +20,7 @@ import ( type Vdc struct { Vdc *types.Vdc client *Client + parent organization } func NewVdc(cli *Client) *Vdc { @@ -277,6 +278,8 @@ func (vdc *Vdc) FindStorageProfileReference(name string) (types.Reference, error return types.Reference{}, fmt.Errorf("can't find any VDC Storage_profiles") } +// GetDefaultStorageProfileReference should find the default storage profile for a VDC +// Deprecated: unused and implemented in the wrong way. Use adminVdc.GetDefaultStorageProfileReference instead func (vdc *Vdc) GetDefaultStorageProfileReference(storageprofiles *types.QueryResultRecordsType) (types.Reference, error) { err := vdc.Refresh() @@ -386,6 +389,14 @@ func (vdc *Vdc) GetEdgeGatewayByHref(href string) (*EdgeGateway, error) { if err != nil { return nil, err } + + // Edge gateways can sometimes come without any configured services which + // lead to nil pointer dereference when adding e.g a DNAT rule + // https://github.com/vmware/go-vcloud-director/issues/585 + if edge.EdgeGateway.Configuration.EdgeGatewayServiceConfiguration == nil { + edge.EdgeGateway.Configuration.EdgeGatewayServiceConfiguration = &types.GatewayFeatures{} + } + return edge, nil } @@ -464,14 +475,17 @@ func (vdc *Vdc) GetEdgeGatewayByNameOrId(identifier string, refresh bool) (*Edge return entity.(*EdgeGateway), err } -func (vdc *Vdc) ComposeRawVApp(name string) error { +// ComposeRawVApp creates an empty vApp +// Deprecated: use CreateRawVApp instead +func (vdc *Vdc) ComposeRawVApp(name string, description string) error { vcomp := &types.ComposeVAppParams{ - Ovf: types.XMLNamespaceOVF, - Xsi: types.XMLNamespaceXSI, - Xmlns: types.XMLNamespaceVCloud, - Deploy: false, - Name: name, - PowerOn: false, + Ovf: types.XMLNamespaceOVF, + Xsi: types.XMLNamespaceXSI, + Xmlns: types.XMLNamespaceVCloud, + Deploy: false, + Name: name, + PowerOn: false, + Description: description, } vdcHref, err := url.ParseRequestURI(vdc.Vdc.HREF) @@ -480,6 +494,7 @@ func (vdc *Vdc) ComposeRawVApp(name string) error { } vdcHref.Path += "/action/composeVApp" + // This call is wrong: /action/composeVApp returns a vApp, not a task task, err := vdc.client.ExecuteTaskRequest(vdcHref.String(), http.MethodPost, types.MimeComposeVappParams, "error instantiating a new vApp:: %s", vcomp) if err != nil { @@ -494,10 +509,65 @@ func (vdc *Vdc) ComposeRawVApp(name string) error { return nil } +// CreateRawVApp creates an empty vApp +func (vdc *Vdc) CreateRawVApp(name string, description string) (*VApp, error) { + vcomp := &types.ComposeVAppParams{ + Ovf: types.XMLNamespaceOVF, + Xsi: types.XMLNamespaceXSI, + Xmlns: types.XMLNamespaceVCloud, + Deploy: false, + Name: name, + PowerOn: false, + Description: description, + } + + vdcHref, err := url.ParseRequestURI(vdc.Vdc.HREF) + if err != nil { + return nil, fmt.Errorf("error getting vdc href: %s", err) + } + vdcHref.Path += "/action/composeVApp" + + var vAppContents types.VApp + + _, err = vdc.client.ExecuteRequest(vdcHref.String(), http.MethodPost, + types.MimeComposeVappParams, "error instantiating a new vApp:: %s", vcomp, &vAppContents) + if err != nil { + return nil, fmt.Errorf("error executing task request: %s", err) + } + + if vAppContents.Tasks != nil { + for _, innerTask := range vAppContents.Tasks.Task { + if innerTask != nil { + task := NewTask(vdc.client) + task.Task = innerTask + err = task.WaitTaskCompletion() + if err != nil { + return nil, fmt.Errorf("error performing task: %s", err) + } + } + } + } + + vapp := NewVApp(vdc.client) + vapp.VApp = &vAppContents + + err = vapp.Refresh() + if err != nil { + return nil, err + } + + err = vdc.Refresh() + if err != nil { + return nil, err + } + return vapp, nil +} + // ComposeVApp creates a vapp with the given template, name, and description // that uses the storageprofile and networks given. If you want all eulas // to be accepted set acceptalleulas to true. Returns a successful task // if completed successfully, otherwise returns an error and an empty task. +// Deprecated: bad implementation func (vdc *Vdc) ComposeVApp(orgvdcnetworks []*types.OrgVDCNetwork, vapptemplate VAppTemplate, storageprofileref types.Reference, name string, description string, acceptalleulas bool) (Task, error) { if vapptemplate.VAppTemplate.Children == nil || orgvdcnetworks == nil { return Task{}, fmt.Errorf("can't compose a new vApp, objects passed are not valid") @@ -579,6 +649,9 @@ func (vdc *Vdc) ComposeVApp(orgvdcnetworks []*types.OrgVDCNetwork, vapptemplate } vdcHref.Path += "/action/composeVApp" + // Like ComposeRawVApp, this function returns a task, while it should be returning a vApp + // Since we don't use this function in terraform-provider-vcd, we are not going to + // replace it. return vdc.client.ExecuteTaskRequest(vdcHref.String(), http.MethodPost, types.MimeComposeVappParams, "error instantiating a new vApp: %s", vcomp) } @@ -844,7 +917,9 @@ func (vdc *Vdc) QueryMediaList() ([]*types.MediaRecordType, error) { return getExistingMedia(vdc) } -// QueryVappVmTemplate Finds VM template using catalog name, vApp template name, VN name in template. Returns types.QueryResultVMRecordType +// QueryVappVmTemplate Finds VM template using catalog name, vApp template name, VN name in template. +// Returns types.QueryResultVMRecordType if it finds the VM. Returns ErrorEntityNotFound +// if it's not found. Returns other error if it finds more than one or the search fails. func (vdc *Vdc) QueryVappVmTemplate(catalogName, vappTemplateName, vmNameInTemplate string) (*types.QueryResultVMRecordType, error) { queryType := "vm" @@ -879,6 +954,44 @@ func (vdc *Vdc) QueryVappVmTemplate(catalogName, vappTemplateName, vmNameInTempl return vmResults[0], nil } +// QueryVappSynchronizedVmTemplate Finds a catalog-synchronized VM inside a vApp Template using catalog name, vApp template name, VN name in template. +// Returns types.QueryResultVMRecordType if it finds the VM and it's synchronized in the catalog. Returns ErrorEntityNotFound +// if it's not found. Returns other error if it finds more than one or the search fails. +func (vdc *Vdc) QueryVappSynchronizedVmTemplate(catalogName, vappTemplateName, vmNameInTemplate string) (*types.QueryResultVMRecordType, error) { + vmRecord, err := vdc.QueryVappVmTemplate(catalogName, vappTemplateName, vmNameInTemplate) + if err != nil { + return nil, err + } + if vmRecord.Status == "LOCAL_COPY_UNAVAILABLE" { + return nil, ErrorEntityNotFound + } + return vmRecord, nil +} + +// GetVAppTemplateByName finds a VAppTemplate by Name +// On success, returns a pointer to the VAppTemplate structure and a nil error +// On failure, returns a nil pointer and an error +func (vdc *Vdc) GetVAppTemplateByName(vAppTemplateName string) (*VAppTemplate, error) { + vAppTemplateQueryResult, err := vdc.QueryVappTemplateWithName(vAppTemplateName) + if err != nil { + return nil, err + } + return getVAppTemplateByHref(vdc.client, vAppTemplateQueryResult.HREF) +} + +// GetVAppTemplateByNameOrId finds a vApp Template by Name or ID. +// On success, returns a pointer to the VAppTemplate structure and a nil error +// On failure, returns a nil pointer and an error +func (vdc *Vdc) GetVAppTemplateByNameOrId(identifier string, refresh bool) (*VAppTemplate, error) { + getByName := func(name string, refresh bool) (interface{}, error) { return vdc.GetVAppTemplateByName(name) } + getById := func(id string, refresh bool) (interface{}, error) { return getVAppTemplateById(vdc.client, id) } + entity, err := getEntityByNameOrIdSkipNonId(getByName, getById, identifier, refresh) + if entity == nil { + return nil, err + } + return entity.(*VAppTemplate), err +} + // getLinkHref returns a link HREF for a wanted combination of rel and type func (vdc *Vdc) getLinkHref(rel, linkType string) string { for _, link := range vdc.Vdc.Link { @@ -925,7 +1038,10 @@ func (vdc *Vdc) CreateStandaloneVmAsync(params *types.CreateVmParams) (Task, err } params.XmlnsOvf = types.XMLNamespaceOVF - return vdc.client.ExecuteTaskRequest(href, http.MethodPost, types.MimeCreateVmParams, "error creating standalone VM: %s", params) + // 37.1 Introduced new parameters to VM configuration + return vdc.client.ExecuteTaskRequestWithApiVersion(href, http.MethodPost, + types.MimeCreateVmParams, "error creating standalone VM: %s", params, + vdc.client.GetSpecificApiVersionOnCondition(">=37.1", "37.1")) } // getVmFromTask finds a VM from a running standalone VM creation task @@ -992,10 +1108,25 @@ func (vdc *Vdc) QueryVmByName(name string) (*VM, error) { return vdc.client.GetVMByHref(foundVM[0].HREF) } -// QueryVmById retrieves a standalone VM by ID +// QueryVmById retrieves a standalone VM by ID in an Org +// It can also retrieve a standard VM (created from vApp) +func (org *Org) QueryVmById(id string) (*VM, error) { + return queryVmById(id, org.client, org.QueryVmList) +} + +// QueryVmById retrieves a standalone VM by ID in a Vdc // It can also retrieve a standard VM (created from vApp) func (vdc *Vdc) QueryVmById(id string) (*VM, error) { - vmList, err := vdc.QueryVmList(types.VmQueryFilterOnlyDeployed) + return queryVmById(id, vdc.client, vdc.QueryVmList) +} + +// queryVmListFunc +type queryVmListFunc func(filter types.VmQueryFilter) ([]*types.QueryResultVMRecordType, error) + +// queryVmById is shared between org.QueryVmById and vdc.QueryVmById which allow to search for VM +// in different scope (Org or VDC) +func queryVmById(id string, client *Client, queryFunc queryVmListFunc) (*VM, error) { + vmList, err := queryFunc(types.VmQueryFilterOnlyDeployed) if err != nil { return nil, err } @@ -1011,7 +1142,7 @@ func (vdc *Vdc) QueryVmById(id string) (*VM, error) { if len(foundVM) > 1 { return nil, fmt.Errorf("more than one VM found with ID %s", id) } - return vdc.client.GetVMByHref(foundVM[0].HREF) + return client.GetVMByHref(foundVM[0].HREF) } // CreateStandaloneVMFromTemplateAsync starts a standalone VM creation using a template @@ -1085,7 +1216,7 @@ func (vdc *Vdc) GetCapabilities() ([]types.VdcCapability, error) { } capabilities := make([]types.VdcCapability, 0) - err = vdc.client.OpenApiGetAllItems(minimumApiVersion, urlRef, nil, &capabilities) + err = vdc.client.OpenApiGetAllItems(minimumApiVersion, urlRef, nil, &capabilities, nil) if err != nil { return nil, err } @@ -1126,3 +1257,166 @@ func getCapabilityValue(capabilities []types.VdcCapability, fieldName string) st return "" } + +func (vdc *Vdc) getParentOrg() (organization, error) { + for _, vdcLink := range vdc.Vdc.Link { + if vdcLink.Rel != "up" { + continue + } + switch vdcLink.Type { + case types.MimeOrg: + org, err := getOrgByHref(vdc.client, vdcLink.HREF) + if err != nil { + return nil, err + } + return org, nil + case types.MimeAdminOrg: + adminOrg, err := getAdminOrgByHref(vdc.client, vdcLink.HREF) + if err != nil { + return nil, err + } + return adminOrg, nil + + default: + continue + } + } + return nil, fmt.Errorf("no parent found for VDC %s", vdc.Vdc.Name) +} + +// CreateVappFromTemplate instantiates a new vApp from a vApp template +// The template argument must contain at least: +// * Name +// * Source (a reference to the source vApp template) +func (vdc *Vdc) CreateVappFromTemplate(template *types.InstantiateVAppTemplateParams) (*VApp, error) { + vdcHref, err := url.ParseRequestURI(vdc.Vdc.HREF) + if err != nil { + return nil, fmt.Errorf("error getting VDC href: %s", err) + } + vdcHref.Path += "/action/instantiateVAppTemplate" + + vapp := NewVApp(vdc.client) + + template.Xmlns = types.XMLNamespaceVCloud + template.Ovf = types.XMLNamespaceOVF + template.Deploy = true + + _, err = vdc.client.ExecuteRequest(vdcHref.String(), http.MethodPost, + types.MimeInstantiateVappTemplateParams, "error instantiating a new vApp from Template: %s", template, vapp.VApp) + if err != nil { + return nil, err + } + + task := NewTask(vdc.client) + for _, taskItem := range vapp.VApp.Tasks.Task { + task.Task = taskItem + err = task.WaitTaskCompletion() + if err != nil { + return nil, fmt.Errorf("error performing task: %s", err) + } + } + err = vapp.Refresh() + return vapp, err +} + +// CloneVapp makes a copy of a vApp into a new one +// The sourceVapp argument must contain at least: +// * Name +// * Source (a reference to the source vApp) +func (vdc *Vdc) CloneVapp(sourceVapp *types.CloneVAppParams) (*VApp, error) { + vdcHref, err := url.ParseRequestURI(vdc.Vdc.HREF) + if err != nil { + return nil, fmt.Errorf("error getting VDC href: %s", err) + } + vdcHref.Path += "/action/cloneVApp" + + vapp := NewVApp(vdc.client) + + sourceVapp.Xmlns = types.XMLNamespaceVCloud + sourceVapp.Ovf = types.XMLNamespaceOVF + sourceVapp.Deploy = true + + _, err = vdc.client.ExecuteRequest(vdcHref.String(), http.MethodPost, + types.MimeCloneVapp, "error cloning a vApp : %s", sourceVapp, vapp.VApp) + if err != nil { + return nil, err + } + + task := NewTask(vdc.client) + for _, taskItem := range vapp.VApp.Tasks.Task { + task.Task = taskItem + err = task.WaitTaskCompletion() + if err != nil { + return nil, fmt.Errorf("error performing task: %s", err) + } + } + err = vapp.Refresh() + return vapp, err +} + +// Get the details of a hardware version +func (vdc *Vdc) GetHardwareVersion(name string) (*types.VirtualHardwareVersion, error) { + if len(vdc.Vdc.Capabilities) == 0 { + return nil, fmt.Errorf("VDC doesn't have any virtual hardware version support information stored") + } + + found := false + for _, hwVersion := range vdc.Vdc.Capabilities[0].SupportedHardwareVersions.SupportedHardwareVersion { + if hwVersion.Name == name { + found = true + } + } + if !found { + return nil, fmt.Errorf("hardware version %s not found or not supported", name) + } + + vdcHref, err := url.ParseRequestURI(vdc.Vdc.HREF) + if err != nil { + return nil, fmt.Errorf("error getting VDC href: %s", err) + } + vdcHref.Path += "/hwv/" + name + + hardwareVersion := &types.VirtualHardwareVersion{} + + _, err = vdc.client.ExecuteRequest(vdcHref.String(), http.MethodGet, types.MimeVirtualHardwareVersion, "error getting hardware version: %s", nil, hardwareVersion) + if err != nil { + return nil, err + } + + return hardwareVersion, nil +} + +// Get highest supported hardware version of a VDC +func (vdc *Vdc) GetHighestHardwareVersion() (*types.VirtualHardwareVersion, error) { + err := vdc.Refresh() + if err != nil { + return nil, err + } + + if len(vdc.Vdc.Capabilities) == 0 { + return nil, fmt.Errorf("VDC doesn't have any virtual hardware version support information stored") + } + + hardwareVersions := vdc.Vdc.Capabilities[0].SupportedHardwareVersions.SupportedHardwareVersion + // Get last item (highest version) of SupportedHardwareVersions + highestVersion := hardwareVersions[len(hardwareVersions)-1].Name + + hardwareVersion, err := vdc.GetHardwareVersion(highestVersion) + if err != nil { + return nil, err + } + return hardwareVersion, nil +} + +// FindOsFromId attempts to find a OS by ID using the given hardware version +func (vdc *Vdc) FindOsFromId(hardwareVersion *types.VirtualHardwareVersion, osId string) (*types.OperatingSystemInfoType, error) { + for _, osFamily := range hardwareVersion.SupportedOperatingSystems.OperatingSystemFamilyInfo { + for _, os := range osFamily.OperatingSystems { + if osId == os.InternalName { + return os, nil + } + } + } + + return nil, fmt.Errorf("no OS found with ID: %s", osId) +} diff --git a/govcd/vdc_group.go b/govcd/vdc_group.go new file mode 100644 index 000000000..a071f6c0a --- /dev/null +++ b/govcd/vdc_group.go @@ -0,0 +1,623 @@ +/* + * Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "net/url" + "strings" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +// VdcGroup is a structure defining a VdcGroup in Organization +type VdcGroup struct { + VdcGroup *types.VdcGroup + Href string + client *Client + parent organization +} + +// CreateNsxtVdcGroup create NSX-T VDC group with provided VDC IDs. +// More generic creation method available also - CreateVdcGroup +func (adminOrg *AdminOrg) CreateNsxtVdcGroup(name, description, startingVdcId string, participatingVdcIds []string) (*VdcGroup, error) { + participatingVdcs, err := composeParticipatingOrgVdcs(adminOrg, startingVdcId, participatingVdcIds) + if err != nil { + return nil, err + } + + vdcGroupConfig := &types.VdcGroup{} + vdcGroupConfig.OrgId = adminOrg.orgId() + vdcGroupConfig.Name = name + vdcGroupConfig.Description = description + vdcGroupConfig.ParticipatingOrgVdcs = participatingVdcs + vdcGroupConfig.LocalEgress = false + vdcGroupConfig.UniversalNetworkingEnabled = false + vdcGroupConfig.NetworkProviderType = "NSX_T" + vdcGroupConfig.Type = "LOCAL" + vdcGroupConfig.ParticipatingOrgVdcs = participatingVdcs + return adminOrg.CreateVdcGroup(vdcGroupConfig) +} + +// composeParticipatingOrgVdcs converts fetched candidate VDCs to []types.ParticipatingOrgVdcs +// returns error also in case participatingVdcId not found as candidate VDC. +func composeParticipatingOrgVdcs(adminOrg *AdminOrg, startingVdcId string, participatingVdcIds []string) ([]types.ParticipatingOrgVdcs, error) { + candidateVdcs, err := adminOrg.GetAllNsxtVdcGroupCandidates(startingVdcId, nil) + if err != nil { + return nil, err + } + participatingVdcs := []types.ParticipatingOrgVdcs{} + var foundParticipatingVdcsIds []string + for _, candidateVdc := range candidateVdcs { + if contains(candidateVdc.Id, participatingVdcIds) { + participatingVdcs = append(participatingVdcs, types.ParticipatingOrgVdcs{ + OrgRef: candidateVdc.OrgRef, + SiteRef: candidateVdc.SiteRef, + VdcRef: types.OpenApiReference{ + ID: candidateVdc.Id, + }, + FaultDomainTag: candidateVdc.FaultDomainTag, + NetworkProviderScope: candidateVdc.NetworkProviderScope, + }) + foundParticipatingVdcsIds = append(foundParticipatingVdcsIds, candidateVdc.Id) + } + } + + if len(participatingVdcs) != len(participatingVdcIds) { + var notFoundVdcs []string + for _, participatingVdcId := range participatingVdcIds { + if !contains(participatingVdcId, foundParticipatingVdcsIds) { + notFoundVdcs = append(notFoundVdcs, participatingVdcId) + } + } + return nil, fmt.Errorf("VDC IDs are not found as Candidate VDCs: %s", notFoundVdcs) + } + + return participatingVdcs, nil +} + +// contains tells whether slice of string contains item. +func contains(item string, slice []string) bool { + for _, n := range slice { + if item == n { + return true + } + } + return false +} + +// CreateVdcGroup create VDC group with provided VDC ref. +// Only supports NSX-T VDCs. +func (adminOrg *AdminOrg) CreateVdcGroup(vdcGroup *types.VdcGroup) (*VdcGroup, error) { + tenantContext, err := adminOrg.getTenantContext() + if err != nil { + return nil, err + } + return createVdcGroup(adminOrg, vdcGroup, getTenantContextHeader(tenantContext)) +} + +// createVdcGroup create VDC group with provided VDC ref. +// Only supports NSX-T VDCs. +func createVdcGroup(adminOrg *AdminOrg, vdcGroup *types.VdcGroup, + additionalHeader map[string]string) (*VdcGroup, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroups + apiVersion, err := adminOrg.client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := adminOrg.client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + typeResponse := &VdcGroup{ + VdcGroup: &types.VdcGroup{}, + client: adminOrg.client, + Href: urlRef.String(), + parent: adminOrg, + } + + err = adminOrg.client.OpenApiPostItem(apiVersion, urlRef, nil, + vdcGroup, typeResponse.VdcGroup, additionalHeader) + if err != nil { + return nil, err + } + + return typeResponse, nil +} + +// GetAllNsxtVdcGroupCandidates returns NSXT candidate VDCs for VDC group +func (adminOrg *AdminOrg) GetAllNsxtVdcGroupCandidates(startingVdcId string, queryParameters url.Values) ([]*types.CandidateVdc, error) { + queryParams := copyOrNewUrlValues(queryParameters) + queryParams = queryParameterFilterAnd("_context==LOCAL", queryParams) + queryParams = queryParameterFilterAnd(fmt.Sprintf("_context==%s", startingVdcId), queryParams) + queryParams.Add("filterEncoded", "true") + queryParams.Add("links", "true") + return adminOrg.GetAllVdcGroupCandidates(queryParams) +} + +// GetAllVdcGroupCandidates returns candidate VDCs for VDC group +func (adminOrg *AdminOrg) GetAllVdcGroupCandidates(queryParameters url.Values) ([]*types.CandidateVdc, error) { + tenantContext, err := adminOrg.getTenantContext() + if err != nil { + return nil, err + } + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroupsCandidateVdcs + minimumApiVersion, err := adminOrg.client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := adminOrg.client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + responses := []*types.CandidateVdc{} + err = adminOrg.client.OpenApiGetAllItems(minimumApiVersion, urlRef, queryParameters, &responses, getTenantContextHeader(tenantContext)) + if err != nil { + return nil, err + } + + return responses, nil +} + +// Delete deletes VDC group +func (vdcGroup *VdcGroup) Delete() error { + return vdcGroup.ForceDelete(false) +} + +// ForceDelete deletes VDC group with force parameter if enabled +func (vdcGroup *VdcGroup) ForceDelete(force bool) error { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroups + minimumApiVersion, err := vdcGroup.client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return err + } + + if vdcGroup.VdcGroup.Id == "" { + return fmt.Errorf("cannot delete VDC group without id") + } + + urlRef, err := vdcGroup.client.OpenApiBuildEndpoint(endpoint, vdcGroup.VdcGroup.Id) + if err != nil { + return err + } + + params := copyOrNewUrlValues(nil) + if force { + params.Add("force", "true") + } + + err = vdcGroup.client.OpenApiDeleteItem(minimumApiVersion, urlRef, params, nil) + if err != nil { + return fmt.Errorf("error deleting VDC group (force %t): %s", force, err) + } + + return nil +} + +// GetAllVdcGroups retrieves all VDC groups. Query parameters can be supplied to perform additional filtering +func (adminOrg *AdminOrg) GetAllVdcGroups(queryParameters url.Values) ([]*VdcGroup, error) { + tenantContext, err := adminOrg.getTenantContext() + if err != nil { + return nil, err + } + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroups + minimumApiVersion, err := adminOrg.client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := adminOrg.client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + responses := []*types.VdcGroup{} + err = adminOrg.client.OpenApiGetAllItems(minimumApiVersion, urlRef, queryParameters, &responses, getTenantContextHeader(tenantContext)) + if err != nil { + return nil, err + } + + var wrappedVdcGroups []*VdcGroup + for _, response := range responses { + urlRef, err := adminOrg.client.OpenApiBuildEndpoint(endpoint, response.Id) + if err != nil { + return nil, err + } + wrappedVdcGroup := &VdcGroup{ + VdcGroup: response, + client: adminOrg.client, + Href: urlRef.String(), + parent: adminOrg, + } + wrappedVdcGroups = append(wrappedVdcGroups, wrappedVdcGroup) + } + + return wrappedVdcGroups, nil +} + +// GetVdcGroupByName retrieves VDC group by given name +// When the name contains commas, semicolons or asterisks, the encoding is rejected by the API in VCD. +// For this reason, when one or more commas, semicolons or asterisks are present we run the search brute force, +// by fetching all VDC groups and comparing the names. +// Also, url.QueryEscape as well as url.Values.Encode() both encode the space as a + character. So we use +// search brute force too. Reference to issue: +// https://github.com/golang/go/issues/4013 +// https://github.com/czos/goamz/pull/11/files +func (adminOrg *AdminOrg) GetVdcGroupByName(name string) (*VdcGroup, error) { + slowSearch, params := shouldDoSlowSearch("name", name) + + var foundVdcGroups []*VdcGroup + vdcGroups, err := adminOrg.GetAllVdcGroups(params) + if err != nil { + return nil, err + } + if len(vdcGroups) == 0 { + return nil, ErrorEntityNotFound + } + foundVdcGroups = append(foundVdcGroups, vdcGroups[0]) + + if slowSearch { + foundVdcGroups = nil + for _, vdcGroup := range vdcGroups { + if vdcGroup.VdcGroup.Name == name { + foundVdcGroups = append(foundVdcGroups, vdcGroup) + } + } + if len(foundVdcGroups) == 0 { + return nil, ErrorEntityNotFound + } + if len(foundVdcGroups) > 1 { + return nil, fmt.Errorf("more than one VDC group found with name '%s'", name) + } + } + + if len(vdcGroups) > 1 && !slowSearch { + return nil, fmt.Errorf("more than one VDC group found with name '%s'", name) + } + + return foundVdcGroups[0], nil +} + +// GetVdcGroupById Returns VDC group using provided ID +func (adminOrg *AdminOrg) GetVdcGroupById(id string) (*VdcGroup, error) { + tenantContext, err := adminOrg.getTenantContext() + if err != nil { + return nil, err + } + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroups + minimumApiVersion, err := adminOrg.client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + if id == "" { + return nil, fmt.Errorf("empty VDC group ID") + } + + urlRef, err := adminOrg.client.OpenApiBuildEndpoint(endpoint, id) + if err != nil { + return nil, err + } + + vdcGroup := &VdcGroup{ + VdcGroup: &types.VdcGroup{}, + client: adminOrg.client, + Href: urlRef.String(), + parent: adminOrg, + } + + err = adminOrg.client.OpenApiGetItem(minimumApiVersion, urlRef, nil, vdcGroup.VdcGroup, getTenantContextHeader(tenantContext)) + if err != nil { + return nil, err + } + + return vdcGroup, nil +} + +// GetVdcGroupById Returns VDC group using provided ID +func (org *Org) GetVdcGroupById(id string) (*VdcGroup, error) { + if id == "" { + return nil, fmt.Errorf("empty VDC group ID") + } + + tenantContext, err := org.getTenantContext() + if err != nil { + return nil, err + } + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroups + minimumApiVersion, err := org.client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := org.client.OpenApiBuildEndpoint(endpoint, id) + if err != nil { + return nil, err + } + + vdcGroup := &VdcGroup{ + VdcGroup: &types.VdcGroup{}, + client: org.client, + Href: urlRef.String(), + parent: org, + } + + err = org.client.OpenApiGetItem(minimumApiVersion, urlRef, nil, vdcGroup.VdcGroup, getTenantContextHeader(tenantContext)) + if err != nil { + return nil, err + } + + return vdcGroup, nil +} + +// Update updates existing Vdc group. Allows changing only name and description and participating VCDs +// Not restrictive update method also available - GenericUpdate +func (vdcGroup *VdcGroup) Update(name, description string, participatingOrgVddIs []string) (*VdcGroup, error) { + + vdcGroup.VdcGroup.Name = name + vdcGroup.VdcGroup.Description = description + + participatingOrgVdcs, err := composeParticipatingOrgVdcs(vdcGroup.parent.fullObject().(*AdminOrg), vdcGroup.VdcGroup.Id, participatingOrgVddIs) + if err != nil { + return nil, err + } + vdcGroup.VdcGroup.ParticipatingOrgVdcs = participatingOrgVdcs + + return vdcGroup.GenericUpdate() +} + +// GenericUpdate updates existing Vdc group. API allows changing only name and description and participating VCDs +func (vdcGroup *VdcGroup) GenericUpdate() (*VdcGroup, error) { + tenantContext, err := vdcGroup.getTenantContext() + if err != nil { + return nil, err + } + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroups + minimumApiVersion, err := vdcGroup.client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + if vdcGroup.VdcGroup.Id == "" { + return nil, fmt.Errorf("cannot update VDC group without id") + } + + urlRef, err := vdcGroup.client.OpenApiBuildEndpoint(endpoint, vdcGroup.VdcGroup.Id) + if err != nil { + return nil, err + } + + returnVdcGroup := &VdcGroup{ + VdcGroup: &types.VdcGroup{}, + client: vdcGroup.client, + Href: vdcGroup.Href, + parent: vdcGroup.parent, + } + + err = vdcGroup.client.OpenApiPutItem(minimumApiVersion, urlRef, nil, vdcGroup.VdcGroup, + returnVdcGroup.VdcGroup, getTenantContextHeader(tenantContext)) + if err != nil { + return nil, fmt.Errorf("error updating VDC group: %s", err) + } + + return returnVdcGroup, nil +} + +// UpdateDfwPolicies updates distributed firewall policies +func (vdcGroup *VdcGroup) UpdateDfwPolicies(dfwPolicies types.DfwPolicies) (*VdcGroup, error) { + tenantContext, err := vdcGroup.getTenantContext() + if err != nil { + return nil, err + } + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroupsDfwPolicies + minimumApiVersion, err := vdcGroup.client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + if vdcGroup.VdcGroup.Id == "" { + return nil, fmt.Errorf("cannot update VDC group Dfw policies without id") + } + + urlRef, err := vdcGroup.client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, vdcGroup.VdcGroup.Id)) + if err != nil { + return nil, err + } + + err = vdcGroup.client.OpenApiPutItem(minimumApiVersion, urlRef, nil, dfwPolicies, + nil, getTenantContextHeader(tenantContext)) + if err != nil { + return nil, fmt.Errorf("error updating VDC group Dfw policies: %s", err) + } + + adminOrg := vdcGroup.parent.fullObject().(*AdminOrg) + return adminOrg.GetVdcGroupById(vdcGroup.VdcGroup.Id) +} + +// UpdateDefaultDfwPolicies updates distributed firewall default policies +func (vdcGroup *VdcGroup) UpdateDefaultDfwPolicies(defaultDfwPolicies types.DefaultPolicy) (*VdcGroup, error) { + tenantContext, err := vdcGroup.getTenantContext() + if err != nil { + return nil, err + } + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroupsDfwDefaultPolicies + minimumApiVersion, err := vdcGroup.client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + if vdcGroup.VdcGroup.Id == "" { + return nil, fmt.Errorf("cannot update VDC group default DFW policies without id") + } + + urlRef, err := vdcGroup.client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, vdcGroup.VdcGroup.Id)) + if err != nil { + return nil, err + } + + err = vdcGroup.client.OpenApiPutItem(minimumApiVersion, urlRef, nil, defaultDfwPolicies, + nil, getTenantContextHeader(tenantContext)) + if err != nil { + return nil, fmt.Errorf("error updating VDC group default DFW policies: %s", err) + } + + adminOrg := vdcGroup.parent.fullObject().(*AdminOrg) + return adminOrg.GetVdcGroupById(vdcGroup.VdcGroup.Id) +} + +// ActivateDfw activates distributed firewall +func (vdcGroup *VdcGroup) ActivateDfw() (*VdcGroup, error) { + return vdcGroup.UpdateDfwPolicies(types.DfwPolicies{ + Enabled: true, + }) +} + +// DeactivateDfw deactivates distributed firewall +func (vdcGroup *VdcGroup) DeactivateDfw() (*VdcGroup, error) { + return vdcGroup.UpdateDfwPolicies(types.DfwPolicies{ + Enabled: false, + }) +} + +// GetDfwPolicies retrieves all distributed firewall policies +func (vdcGroup *VdcGroup) GetDfwPolicies() (*types.DfwPolicies, error) { + tenantContext, err := vdcGroup.getTenantContext() + if err != nil { + return nil, err + } + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroupsDfwPolicies + minimumApiVersion, err := vdcGroup.client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := vdcGroup.client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, vdcGroup.VdcGroup.Id)) + if err != nil { + return nil, err + } + + response := types.DfwPolicies{} + err = vdcGroup.client.OpenApiGetItem(minimumApiVersion, urlRef, nil, &response, getTenantContextHeader(tenantContext)) + if err != nil { + return nil, err + } + + return &response, nil +} + +// EnableDefaultPolicy activates default dfw policy +func (vdcGroup *VdcGroup) EnableDefaultPolicy() (*VdcGroup, error) { + dfwPolicies, err := vdcGroup.GetDfwPolicies() + if err != nil { + return nil, err + } + + if dfwPolicies.DefaultPolicy == nil { + return nil, fmt.Errorf("DFW has to be enabled before changing Default policy") + } + dfwPolicies.DefaultPolicy.Enabled = addrOf(true) + return vdcGroup.UpdateDefaultDfwPolicies(*dfwPolicies.DefaultPolicy) +} + +// DisableDefaultPolicy deactivates default dfw policy +func (vdcGroup *VdcGroup) DisableDefaultPolicy() (*VdcGroup, error) { + dfwPolicies, err := vdcGroup.GetDfwPolicies() + if err != nil { + return nil, err + } + + if dfwPolicies.DefaultPolicy == nil { + return nil, fmt.Errorf("DFW has to be enabled before changing Default policy") + } + dfwPolicies.DefaultPolicy.Enabled = addrOf(false) + return vdcGroup.UpdateDefaultDfwPolicies(*dfwPolicies.DefaultPolicy) +} + +func getOwnerTypeFromUrn(urn string) (string, error) { + if !isUrn(urn) { + return "", fmt.Errorf("supplied ID is not URN: %s", urn) + } + + ss := strings.Split(urn, ":") + return ss[2], nil +} + +// OwnerIsVdcGroup evaluates given URN and returns true if it is a VDC Group +func OwnerIsVdcGroup(urn string) bool { + ownerType, err := getOwnerTypeFromUrn(urn) + if err != nil { + return false + } + + if strings.EqualFold(ownerType, types.UrnTypeVdcGroup) { + return true + } + + return false +} + +// OwnerIsVdc evaluates a given URN and returns true if it is a VDC +func OwnerIsVdc(urn string) bool { + ownerType, err := getOwnerTypeFromUrn(urn) + if err != nil { + return false + } + + if strings.EqualFold(ownerType, types.UrnTypeVdc) { + return true + } + + return false +} + +// GetCapabilities allows to retrieve a list of VDC capabilities. It has a list of values. Some particularly useful are: +// * networkProvider - overlay stack responsible for providing network functionality. (NSX_V or NSX_T) +// * crossVdc - supports cross vDC network creation +func (vdcGroup *VdcGroup) GetCapabilities() ([]types.VdcCapability, error) { + if vdcGroup.VdcGroup.Id == "" { + return nil, fmt.Errorf("VDC ID must be set to get capabilities") + } + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcCapabilities + minimumApiVersion, err := vdcGroup.client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := vdcGroup.client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, url.QueryEscape(vdcGroup.VdcGroup.Id))) + if err != nil { + return nil, err + } + + capabilities := make([]types.VdcCapability, 0) + err = vdcGroup.client.OpenApiGetAllItems(minimumApiVersion, urlRef, nil, &capabilities, nil) + if err != nil { + return nil, err + } + return capabilities, nil +} + +// IsNsxt is a convenience function to check if VDC is backed by NSX-T pVdc +// If error occurs - it returns false +func (vdcGroup *VdcGroup) IsNsxt() bool { + vdcCapabilities, err := vdcGroup.GetCapabilities() + if err != nil { + return false + } + + networkProviderCapability := getCapabilityValue(vdcCapabilities, "networkProvider") + return networkProviderCapability == types.VdcCapabilityNetworkProviderNsxt +} diff --git a/govcd/vdc_group_common_test.go b/govcd/vdc_group_common_test.go new file mode 100644 index 000000000..3508409d6 --- /dev/null +++ b/govcd/vdc_group_common_test.go @@ -0,0 +1,383 @@ +//go:build network || functional || openapi || vdcGroup || nsxt || gateway || ALL + +/* + * Copyright 2022 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" + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_NsxtVdcGroupOrgNetworks(check *C) { + skipNoNsxtConfiguration(vcd, check) + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointEdgeGateways) + vcd.skipIfNotSysAdmin(check) + + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(adminOrg, NotNil) + check.Assert(err, IsNil) + + org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(org, NotNil) + check.Assert(err, IsNil) + + nsxtExternalNetwork, err := GetExternalNetworkV2ByName(vcd.client, vcd.config.VCD.Nsxt.ExternalNetwork) + check.Assert(err, IsNil) + check.Assert(nsxtExternalNetwork, NotNil) + + vdc, vdcGroup := test_CreateVdcGroup(check, adminOrg, vcd) + check.Assert(vdc, NotNil) + check.Assert(vdcGroup, NotNil) + + egwDefinition := &types.OpenAPIEdgeGateway{ + Name: "nsx-t-edge", + Description: "nsx-t-edge-description", + OwnerRef: &types.OpenApiReference{ + ID: vdc.Vdc.ID, + }, + EdgeGatewayUplinks: []types.EdgeGatewayUplinks{{ + UplinkID: nsxtExternalNetwork.ExternalNetwork.ID, + Subnets: types.OpenAPIEdgeGatewaySubnets{Values: []types.OpenAPIEdgeGatewaySubnetValue{{ + Gateway: "1.1.1.1", + PrefixLength: 24, + Enabled: true, + }}}, + Connected: true, + Dedicated: false, + }}, + } + + // Create Edge Gateway in VDC + createdEdge, err := adminOrg.CreateNsxtEdgeGateway(egwDefinition) + check.Assert(err, IsNil) + check.Assert(createdEdge, NotNil) + check.Assert(createdEdge.EdgeGateway.OwnerRef.ID, Matches, `^urn:vcloud:vdc:.*`) + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGateways + createdEdge.EdgeGateway.ID + PrependToCleanupListOpenApi(createdEdge.EdgeGateway.Name, check.TestName(), openApiEndpoint) + + // Move Edge Gateway to VDC Group + movedGateway, err := createdEdge.MoveToVdcOrVdcGroup(vdcGroup.VdcGroup.Id) + check.Assert(err, IsNil) + check.Assert(movedGateway, NotNil) + check.Assert(movedGateway.EdgeGateway.OwnerRef.ID, Equals, vdcGroup.VdcGroup.Id) + check.Assert(movedGateway.EdgeGateway.OwnerRef.ID, Matches, `^urn:vcloud:vdcGroup:.*`) + + mapOfNetworkConfigs := make(map[string]*types.OpenApiOrgVdcNetwork, 3) + mapOfNetworkConfigs["isolated"] = buildIsolatedOrgVdcNetworkConfig(check, vcd, vdcGroup.VdcGroup.Id) + mapOfNetworkConfigs["imported"] = buildImportedOrgVdcNetworkConfig(check, vcd, vdcGroup.VdcGroup.Id) + mapOfNetworkConfigs["routed"] = buildRoutedOrgVdcNetworkConfig(check, vcd, movedGateway, vdcGroup.VdcGroup.Id) + + sliceOfCreatedNetworkConfigs := make(map[string]*OpenApiOrgVdcNetwork, 3) + for index, orgVdcNetworkConfig := range mapOfNetworkConfigs { + orgVdcNet, err := org.CreateOpenApiOrgVdcNetwork(orgVdcNetworkConfig) + check.Assert(err, IsNil) + check.Assert(orgVdcNet, NotNil) + check.Assert(orgVdcNet.OpenApiOrgVdcNetwork.OwnerRef.ID, Equals, vdcGroup.VdcGroup.Id) + + sliceOfCreatedNetworkConfigs[index] = orgVdcNet + + // Use generic "OpenApiEntity" resource cleanup type + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointOrgVdcNetworks + orgVdcNet.OpenApiOrgVdcNetwork.ID + PrependToCleanupListOpenApi(orgVdcNet.OpenApiOrgVdcNetwork.Name, check.TestName(), openApiEndpoint) + + check.Assert(orgVdcNet.GetType(), Equals, orgVdcNetworkConfig.NetworkType) + } + + // Move Edge Gateway back to VDC + movedBackToVdcEdge, err := movedGateway.MoveToVdcOrVdcGroup(vdc.Vdc.ID) + check.Assert(err, IsNil) + check.Assert(movedBackToVdcEdge, NotNil) + check.Assert(movedBackToVdcEdge.EdgeGateway.OwnerRef.ID, Matches, `^urn:vcloud:vdc:.*`) + + // Routed networks migrate to/from VDC Groups together with Edge Gateway therefore we need to + // check that routed network owner ID is the same as Edge Gateway. Routed network must be + // retrieved again so that it reflects latest information. + routedOrgNetwork, err := org.GetOpenApiOrgVdcNetworkById(sliceOfCreatedNetworkConfigs["routed"].OpenApiOrgVdcNetwork.ID) + check.Assert(err, IsNil) + check.Assert(routedOrgNetwork, NotNil) + check.Assert(routedOrgNetwork.OpenApiOrgVdcNetwork.OwnerRef.ID, Equals, movedBackToVdcEdge.EdgeGateway.OwnerRef.ID) + + // Remove all created networks + for _, network := range sliceOfCreatedNetworkConfigs { + err = network.Delete() + check.Assert(err, IsNil) + } + + // Remove Edge Gateway + err = movedGateway.Delete() + check.Assert(err, IsNil) + + // Remove VDC group and VDC + err = vdcGroup.Delete() + check.Assert(err, IsNil) + task, err := vdc.Delete(true, true) + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) +} + +func buildIsolatedOrgVdcNetworkConfig(check *C, vcd *TestVCD, ownerId string) *types.OpenApiOrgVdcNetwork { + isolatedOrgVdcNetworkConfig := &types.OpenApiOrgVdcNetwork{ + Name: check.TestName() + "-isolated", + Description: check.TestName() + "-description", + + OwnerRef: &types.OpenApiReference{ID: ownerId}, + + NetworkType: types.OrgVdcNetworkTypeIsolated, + Subnets: types.OrgVdcNetworkSubnets{ + Values: []types.OrgVdcNetworkSubnetValues{ + { + Gateway: "4.1.1.1", + PrefixLength: 25, + DNSServer1: "8.8.8.8", + DNSServer2: "8.8.4.4", + DNSSuffix: "bar.foo", + IPRanges: types.OrgVdcNetworkSubnetIPRanges{ + Values: []types.OrgVdcNetworkSubnetIPRangeValues{ + { + StartAddress: "4.1.1.20", + EndAddress: "4.1.1.30", + }, + { + StartAddress: "4.1.1.40", + EndAddress: "4.1.1.50", + }, + { + StartAddress: "4.1.1.88", + EndAddress: "4.1.1.92", + }, + }}, + }, + }, + }, + } + + return isolatedOrgVdcNetworkConfig +} + +func buildImportedOrgVdcNetworkConfig(check *C, vcd *TestVCD, ownerId string) *types.OpenApiOrgVdcNetwork { + logicalSwitch, err := vcd.nsxtVdc.GetNsxtImportableSwitchByName(vcd.config.VCD.Nsxt.NsxtImportSegment) + check.Assert(err, IsNil) + + importedOrgVdcNetworkConfig := &types.OpenApiOrgVdcNetwork{ + Name: check.TestName() + "-imported", + Description: check.TestName() + "-description", + + OwnerRef: &types.OpenApiReference{ID: ownerId}, + + NetworkType: types.OrgVdcNetworkTypeOpaque, + // BackingNetworkId contains NSX-T logical switch ID for Imported networks + BackingNetworkId: logicalSwitch.NsxtImportableSwitch.ID, + + 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", + }, + }}, + }, + }, + }, + } + + return importedOrgVdcNetworkConfig +} + +func buildRoutedOrgVdcNetworkConfig(check *C, vcd *TestVCD, edgeGateway *NsxtEdgeGateway, ownerId string) *types.OpenApiOrgVdcNetwork { + routedOrgVdcNetworkConfig := &types.OpenApiOrgVdcNetwork{ + Name: check.TestName() + "-routed", + Description: check.TestName() + "-description", + + OwnerRef: &types.OpenApiReference{ID: ownerId}, + + NetworkType: types.OrgVdcNetworkTypeRouted, + + // Connection is used for "routed" network + Connection: &types.Connection{ + RouterRef: types.OpenApiReference{ + ID: edgeGateway.EdgeGateway.ID, + }, + ConnectionType: "INTERNAL", + }, + Subnets: types.OrgVdcNetworkSubnets{ + Values: []types.OrgVdcNetworkSubnetValues{ + { + Gateway: "3.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: "3.1.1.20", + EndAddress: "3.1.1.30", + }, + { + StartAddress: "3.1.1.40", + EndAddress: "3.1.1.50", + }, + { + StartAddress: "3.1.1.60", + EndAddress: "3.1.1.62", + }, { + StartAddress: "3.1.1.72", + EndAddress: "3.1.1.74", + }, { + StartAddress: "3.1.1.84", + EndAddress: "3.1.1.85", + }, + }}, + }, + }, + }, + } + + return routedOrgVdcNetworkConfig +} + +func test_CreateVdcGroup(check *C, adminOrg *AdminOrg, vcd *TestVCD) (*Vdc, *VdcGroup) { + createdVdc := createNewVdc(vcd, check, check.TestName()) + + createdVdcAsCandidate, err := adminOrg.GetAllNsxtVdcGroupCandidates(createdVdc.vdcId(), + map[string][]string{"filter": []string{fmt.Sprintf("name==%s", url.QueryEscape(createdVdc.vdcName()))}}) + check.Assert(err, IsNil) + check.Assert(createdVdcAsCandidate, NotNil) + check.Assert(len(createdVdcAsCandidate) == 1, Equals, true) + + existingVdcAsCandidate, err := adminOrg.GetAllNsxtVdcGroupCandidates(createdVdc.vdcId(), + map[string][]string{"filter": []string{fmt.Sprintf("name==%s", url.QueryEscape(vcd.nsxtVdc.vdcName()))}}) + check.Assert(err, IsNil) + check.Assert(existingVdcAsCandidate, NotNil) + check.Assert(len(existingVdcAsCandidate) == 1, Equals, true) + + vdcGroupConfig := &types.VdcGroup{ + Name: check.TestName() + "Group", + OrgId: adminOrg.orgId(), + ParticipatingOrgVdcs: []types.ParticipatingOrgVdcs{ + types.ParticipatingOrgVdcs{ + VdcRef: types.OpenApiReference{ + ID: createdVdc.vdcId(), + }, + SiteRef: (createdVdcAsCandidate)[0].SiteRef, + OrgRef: (createdVdcAsCandidate)[0].OrgRef, + }, + types.ParticipatingOrgVdcs{ + VdcRef: types.OpenApiReference{ + ID: vcd.nsxtVdc.vdcId(), + }, + SiteRef: (existingVdcAsCandidate)[0].SiteRef, + OrgRef: (existingVdcAsCandidate)[0].OrgRef, + }, + }, + LocalEgress: false, + UniversalNetworkingEnabled: false, + NetworkProviderType: "NSX_T", + Type: "LOCAL", + //DfwEnabled: true, // ignored by API + } + + vdcGroup, err := adminOrg.CreateVdcGroup(vdcGroupConfig) + check.Assert(err, IsNil) + check.Assert(vdcGroup, NotNil) + check.Assert(vdcGroup.IsNsxt(), Equals, true) + + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroups + vdcGroup.VdcGroup.Id + PrependToCleanupListOpenApi(vdcGroup.VdcGroup.Name, check.TestName(), openApiEndpoint) + + return createdVdc, vdcGroup +} + +func createNewVdc(vcd *TestVCD, check *C, vdcName string) *Vdc { + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.org.Org.Name) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + + pVdcs, err := QueryProviderVdcByName(vcd.client, vcd.config.VCD.NsxtProviderVdc.Name) + check.Assert(err, IsNil) + + if len(pVdcs) == 0 { + check.Skip(fmt.Sprintf("No NSX-T Provider VDC found with name '%s'", vcd.config.VCD.NsxtProviderVdc.Name)) + } + providerVdcHref := pVdcs[0].HREF + pvdcStorageProfile, err := vcd.client.QueryProviderVdcStorageProfileByName(vcd.config.VCD.NsxtProviderVdc.StorageProfile, providerVdcHref) + check.Assert(err, IsNil) + check.Assert(pvdcStorageProfile, NotNil) + providerVdcStorageProfileHref := pvdcStorageProfile.HREF + + networkPools, err := QueryNetworkPoolByName(vcd.client, vcd.config.VCD.NsxtProviderVdc.NetworkPool) + check.Assert(err, IsNil) + if len(networkPools) == 0 { + check.Skip(fmt.Sprintf("No network pool found with name '%s'", vcd.config.VCD.NsxtProviderVdc.NetworkPool)) + } + + networkPoolHref := networkPools[0].HREF + trueValue := true + vdcConfiguration := &types.VdcConfiguration{ + Name: vdcName, + Xmlns: types.XMLNamespaceVCloud, + AllocationModel: "Flex", + ComputeCapacity: []*types.ComputeCapacity{ + &types.ComputeCapacity{ + CPU: &types.CapacityWithUsage{ + Units: "MHz", + Allocated: 1024, + Limit: 1024, + }, + Memory: &types.CapacityWithUsage{ + Allocated: 1024, + Limit: 1024, + Units: "MB", + }, + }, + }, + VdcStorageProfile: []*types.VdcStorageProfileConfiguration{&types.VdcStorageProfileConfiguration{ + Enabled: addrOf(true), + Units: "MB", + Limit: 1024, + Default: true, + ProviderVdcStorageProfile: &types.Reference{ + HREF: providerVdcStorageProfileHref, + }, + }, + }, + NetworkPoolReference: &types.Reference{ + HREF: networkPoolHref, + }, + ProviderVdcReference: &types.Reference{ + HREF: providerVdcHref, + }, + IsEnabled: true, + IsThinProvision: true, + UsesFastProvisioning: true, + IsElastic: &trueValue, + IncludeMemoryOverhead: &trueValue, + ResourceGuaranteedMemory: addrOf(1.00), + } + + vdc, err := adminOrg.CreateOrgVdc(vdcConfiguration) + check.Assert(err, IsNil) + check.Assert(vdc, NotNil) + + AddToCleanupList(vdcConfiguration.Name, "vdc", vcd.org.Org.Name, check.TestName()) + return vdc +} diff --git a/govcd/vdc_group_test.go b/govcd/vdc_group_test.go new file mode 100644 index 000000000..c7b9461b6 --- /dev/null +++ b/govcd/vdc_group_test.go @@ -0,0 +1,327 @@ +//go:build functional || openapi || vdcGroup || nsxt || ALL + +/* + * Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "strings" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +// tests creation of NSX-T VDCs group +func (vcd *TestVCD) Test_CreateVdcGroup(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + if vcd.config.VCD.Nsxt.Vdc == "" { + check.Skip("Missing NSX-T config: No NSX-T VDC specified") + } + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointVdcGroups) + + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.org.Org.Name) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + + vdc, vdcGroup := test_CreateVdcGroup(check, adminOrg, vcd) + err = vdcGroup.Delete() + check.Assert(err, IsNil) + task, err := vdc.Delete(true, true) + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) +} + +// tests creation of NSX-T VDCs group +func (vcd *TestVCD) Test_NsxtVdcGroup(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + if vcd.config.VCD.Nsxt.Vdc == "" { + check.Skip("Missing NSX-T config: No NSX-T VDC specified") + } + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointVdcGroups) + + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.org.Org.Name) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + vdcGroup := test_NsxtVdcGroup(check, adminOrg, vcd) + + err = vdcGroup.Delete() + check.Assert(err, IsNil) +} + +func (vcd *TestVCD) Test_NsxtVdcGroupForceDelete(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + if vcd.config.VCD.Nsxt.Vdc == "" { + check.Skip("Missing NSX-T config: No NSX-T VDC specified") + } + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointVdcGroups) + + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.org.Org.Name) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + + // Create VDC Group + vdcGroup, err := adminOrg.CreateNsxtVdcGroup(check.TestName(), "", vcd.nsxtVdc.vdcId(), []string{vcd.nsxtVdc.vdcId()}) + check.Assert(err, IsNil) + check.Assert(vdcGroup, NotNil) + + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroups + vdcGroup.VdcGroup.Id + PrependToCleanupListOpenApi(vdcGroup.VdcGroup.Name, check.TestName(), openApiEndpoint) + + // Create an IP Set within a VDC Group to ensure that force deletion of a VDC Group works later + // (it would return an error without forcing it) + ipSetDefinition := &types.NsxtFirewallGroup{ + Name: check.TestName(), + Description: check.TestName() + "-Description", + Type: types.FirewallGroupTypeIpSet, + OwnerRef: &types.OpenApiReference{ID: vdcGroup.VdcGroup.Id}, + IpAddresses: []string{"12.12.12.1"}, + } + + // Create IP Set and add to cleanup if it was created + _, err = vdcGroup.CreateNsxtFirewallGroup(ipSetDefinition) + check.Assert(err, IsNil) + + // Force delete VDC Group + err = vdcGroup.ForceDelete(true) + check.Assert(err, IsNil) + + _, err = adminOrg.GetVdcGroupById(vdcGroup.VdcGroup.Id) + check.Assert(ContainsNotFound(err), Equals, true) +} + +func test_NsxtVdcGroup(check *C, adminOrg *AdminOrg, vcd *TestVCD) *VdcGroup { + description := "vdc group created by test" + + _, err := adminOrg.CreateNsxtVdcGroup(check.TestName(), description, vcd.nsxtVdc.vdcId(), []string{vcd.vdc.vdcId()}) + check.Assert(err, NotNil) + + vdcGroup, err := adminOrg.CreateNsxtVdcGroup(check.TestName(), description, vcd.nsxtVdc.vdcId(), []string{vcd.nsxtVdc.vdcId()}) + check.Assert(err, IsNil) + check.Assert(vdcGroup, NotNil) + + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroups + vdcGroup.VdcGroup.Id + PrependToCleanupListOpenApi(vdcGroup.VdcGroup.Name, check.TestName(), openApiEndpoint) + + check.Assert(vdcGroup.VdcGroup.Description, Equals, description) + check.Assert(vdcGroup.VdcGroup.DfwEnabled, Equals, false) + check.Assert(len(vdcGroup.VdcGroup.ParticipatingOrgVdcs), Equals, 1) + check.Assert(vdcGroup.VdcGroup.OrgId, Equals, adminOrg.AdminOrg.ID) + check.Assert(vdcGroup.VdcGroup.Name, Equals, check.TestName()) + check.Assert(vdcGroup.VdcGroup.LocalEgress, Equals, false) + check.Assert(vdcGroup.VdcGroup.UniversalNetworkingEnabled, Equals, false) + check.Assert(vdcGroup.VdcGroup.NetworkProviderType, Equals, "NSX_T") + check.Assert(vdcGroup.VdcGroup.Type, Equals, "LOCAL") + + // check fetching by ID + foundVdcGroup, err := adminOrg.GetVdcGroupById(vdcGroup.VdcGroup.Id) + check.Assert(err, IsNil) + check.Assert(foundVdcGroup, NotNil) + check.Assert(foundVdcGroup.VdcGroup.Name, Equals, vdcGroup.VdcGroup.Name) + check.Assert(foundVdcGroup.VdcGroup.Description, Equals, vdcGroup.VdcGroup.Description) + check.Assert(len(foundVdcGroup.VdcGroup.ParticipatingOrgVdcs), Equals, len(vdcGroup.VdcGroup.ParticipatingOrgVdcs)) + + // check fetching all VDC groups + allVdcGroups, err := adminOrg.GetAllVdcGroups(nil) + check.Assert(err, IsNil) + check.Assert(allVdcGroups, NotNil) + + if testVerbose { + fmt.Printf("(org) how many VDC groups: %d\n", len(allVdcGroups)) + for i, oneVdcGroup := range allVdcGroups { + fmt.Printf("%3d %-20s %-53s %s\n", i, oneVdcGroup.VdcGroup.Name, oneVdcGroup.VdcGroup.Id, + oneVdcGroup.VdcGroup.Description) + } + } + + // check fetching VDC group by Name + createdVdc := createNewVdc(vcd, check, check.TestName()+"_forUpdate") + check.Assert(err, IsNil) + check.Assert(createdVdc, NotNil) + + foundVdcGroup, err = adminOrg.GetVdcGroupByName(check.TestName()) + check.Assert(err, IsNil) + check.Assert(foundVdcGroup, NotNil) + check.Assert(foundVdcGroup.VdcGroup.Name, Equals, check.TestName()) + + // check update + newDescription := "newDescription" + newName := check.TestName() + "newName" + updatedVdcGroup, err := foundVdcGroup.Update(newName, newDescription, []string{createdVdc.vdcId()}) + check.Assert(err, IsNil) + check.Assert(updatedVdcGroup, NotNil) + check.Assert(updatedVdcGroup.VdcGroup.Name, Equals, newName) + check.Assert(updatedVdcGroup.VdcGroup.Description, Equals, newDescription) + check.Assert(updatedVdcGroup.VdcGroup.Id, Not(Equals), "") + check.Assert(len(updatedVdcGroup.VdcGroup.ParticipatingOrgVdcs), Equals, 1) + + // activate and deactivate DFW + enabledVdcGroup, err := updatedVdcGroup.ActivateDfw() + check.Assert(err, IsNil) + check.Assert(enabledVdcGroup, NotNil) + check.Assert(enabledVdcGroup.VdcGroup.DfwEnabled, Equals, true) + + // disable default policy, otherwise deactivation of Dfw fails + _, err = enabledVdcGroup.DisableDefaultPolicy() + check.Assert(err, IsNil) + defaultPolicy, err := enabledVdcGroup.GetDfwPolicies() + check.Assert(err, IsNil) + check.Assert(defaultPolicy, NotNil) + check.Assert(*defaultPolicy.DefaultPolicy.Enabled, Equals, false) + + // also validate enable default policy + _, err = enabledVdcGroup.EnableDefaultPolicy() + check.Assert(err, IsNil) + defaultPolicy, err = enabledVdcGroup.GetDfwPolicies() + check.Assert(err, IsNil) + check.Assert(defaultPolicy, NotNil) + check.Assert(*defaultPolicy.DefaultPolicy.Enabled, Equals, true) + + _, err = enabledVdcGroup.DisableDefaultPolicy() + check.Assert(err, IsNil) + defaultPolicy, err = enabledVdcGroup.GetDfwPolicies() + check.Assert(err, IsNil) + check.Assert(defaultPolicy, NotNil) + check.Assert(*defaultPolicy.DefaultPolicy.Enabled, Equals, false) + + disabledVdcGroup, err := updatedVdcGroup.DeactivateDfw() + check.Assert(err, IsNil) + check.Assert(disabledVdcGroup, NotNil) + check.Assert(disabledVdcGroup.VdcGroup.DfwEnabled, Equals, false) + return vdcGroup +} + +func (vcd *TestVCD) Test_GetVdcGroupByName_ValidatesSymbolsInName(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + if vcd.config.VCD.Nsxt.Vdc == "" { + check.Skip("Missing NSX-T config: No NSX-T VDC specified") + } + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointVdcGroups) + + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.org.Org.Name) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + test_GetVdcGroupByName_ValidatesSymbolsInName(check, adminOrg, vcd.nsxtVdc.vdcId()) +} + +func test_GetVdcGroupByName_ValidatesSymbolsInName(check *C, adminOrg *AdminOrg, vdcId string) { + // When alias contains commas, semicolons, stars, or plus signs, the encoding may reject by the API when we try to Query it + // Also, spaces present their own issues + for _, symbol := range []string{";", ",", "+", " ", "*"} { + + name := fmt.Sprintf("Test%sVdcGroup", symbol) + + createdVdcGroup, err := adminOrg.CreateNsxtVdcGroup(name, "", vdcId, []string{vdcId}) + check.Assert(err, IsNil) + check.Assert(createdVdcGroup, NotNil) + + openApiEndpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcGroups + createdVdcGroup.VdcGroup.Id + PrependToCleanupListOpenApi(createdVdcGroup.VdcGroup.Name, check.TestName(), openApiEndpoint) + + check.Assert(createdVdcGroup, NotNil) + check.Assert(createdVdcGroup.VdcGroup.Id, Not(Equals), "") + check.Assert(createdVdcGroup.VdcGroup.Name, Equals, name) + check.Assert(len(createdVdcGroup.VdcGroup.ParticipatingOrgVdcs), Equals, 1) + + foundVdcGroup, err := adminOrg.GetVdcGroupByName(name) + check.Assert(err, IsNil) + check.Assert(foundVdcGroup, NotNil) + check.Assert(foundVdcGroup.VdcGroup.Name, Equals, name) + + err = foundVdcGroup.Delete() + check.Assert(err, IsNil) + } +} + +// Test_NsxtVdcGroupWithOrgAdmin additionally tests Test_CreateVdcGroup, Test_GetVdcGroupByName_ValidatesSymbolsInName +// and Test_NsxtVdcGroup using an org amin user with added rights which allows working with VDC groups. +func (vcd *TestVCD) Test_NsxtVdcGroupWithOrgAdmin(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + if vcd.config.VCD.Nsxt.Vdc == "" { + check.Skip("Missing NSX-T config: No NSX-T VDC specified") + } + skipOpenApiEndpointTest(vcd, check, types.OpenApiPathVersion1_0_0+types.OpenApiEndpointVdcGroups) + + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.org.Org.Name) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + + skipIfNeededRightsMissing(check, adminOrg) + orgAdminClient, _, err := newOrgUserConnection(adminOrg, "test-user2", "CHANGE-ME", vcd.config.Provider.Url, true) + check.Assert(err, IsNil) + check.Assert(orgAdminClient, NotNil) + + orgAsOrgAdminUser, err := orgAdminClient.GetAdminOrgByName(vcd.org.Org.Name) + check.Assert(err, IsNil) + check.Assert(orgAsOrgAdminUser, NotNil) + + //run tests ad org Admin with needed rights + vdcGroup1 := test_NsxtVdcGroup(check, adminOrg, vcd) + vdc, vdcGroup := test_CreateVdcGroup(check, adminOrg, vcd) + test_GetVdcGroupByName_ValidatesSymbolsInName(check, orgAsOrgAdminUser, vcd.nsxtVdc.vdcId()) + + // Remove VDC group and VDC + err = vdcGroup1.Delete() + check.Assert(err, IsNil) + err = vdcGroup.Delete() + check.Assert(err, IsNil) + task, err := vdc.Delete(true, true) + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) +} + +// skipIfNeededRightsMissing checks if needed rights are configured +func skipIfNeededRightsMissing(check *C, adminOrg *AdminOrg) { + defaultRightsBundle, err := adminOrg.client.GetRightsBundleByName("Default Rights Bundle") + check.Assert(err, IsNil) + check.Assert(defaultRightsBundle, NotNil) + + // add new rights to bundle + var missingRights []string + + rightsBeforeChange, err := defaultRightsBundle.GetRights(nil) + check.Assert(err, IsNil) + for _, rightName := range []string{ + "vDC Group: Configure", + "vDC Group: Configure Logging", + "vDC Group: View", + "Organization vDC Distributed Firewall: Enable/Disable", + //"Security Tag Edit", 10.2 doesn't have it and for this kind testing not needed + } { + newRight, err := adminOrg.client.GetRightByName(rightName) + check.Assert(err, IsNil) + check.Assert(newRight, NotNil) + foundRight := false + for _, old := range rightsBeforeChange { + if old.Name == rightName { + foundRight = true + } + } + if !foundRight { + missingRights = append(missingRights, newRight.Name) + } + } + + if len(missingRights) > 0 { + check.Skip(check.TestName() + "missing rights to run test: " + strings.Join(missingRights, ", ")) + } +} diff --git a/govcd/vdc_network_profile.go b/govcd/vdc_network_profile.go new file mode 100644 index 000000000..150639e55 --- /dev/null +++ b/govcd/vdc_network_profile.go @@ -0,0 +1,104 @@ +/* + * Copyright 2022 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +const labelVdcNetworkProfile = "VDC Network Profile" + +// VDC Network profiles have 1:1 mapping with VDC - each VDC has an option to configure VDC Network +// Profiles. types.VdcNetworkProfile holds more information about possible configurations + +// GetVdcNetworkProfile retrieves VDC Network Profile configuration +// vdc.Vdc.ID must be set and valid present +func (vdc *Vdc) GetVdcNetworkProfile() (*types.VdcNetworkProfile, error) { + if vdc == nil || vdc.Vdc == nil || vdc.Vdc.ID == "" { + return nil, fmt.Errorf("cannot lookup VDC Network Profile configuration without VDC ID") + } + + return getVdcNetworkProfile(vdc.client, vdc.Vdc.ID) +} + +// GetVdcNetworkProfile retrieves VDC Network Profile configuration +// vdc.Vdc.ID must be set and valid present +func (adminVdc *AdminVdc) GetVdcNetworkProfile() (*types.VdcNetworkProfile, error) { + if adminVdc == nil || adminVdc.AdminVdc == nil || adminVdc.AdminVdc.ID == "" { + return nil, fmt.Errorf("cannot lookup VDC Network Profile configuration without VDC ID") + } + + return getVdcNetworkProfile(adminVdc.client, adminVdc.AdminVdc.ID) +} + +// UpdateVdcNetworkProfile updates the VDC Network Profile configuration +// +// Note. Whenever updating VDC Network Profile it is required to send all fields (not only the +// changed ones) as VCD will remove other configuration. Best practice is to fetch current +// configuration of VDC Network Profile using GetVdcNetworkProfile, alter it with new values and +// submit it to UpdateVdcNetworkProfile. +func (vdc *Vdc) UpdateVdcNetworkProfile(vdcNetworkProfileConfig *types.VdcNetworkProfile) (*types.VdcNetworkProfile, error) { + if vdc == nil || vdc.Vdc == nil || vdc.Vdc.ID == "" { + return nil, fmt.Errorf("cannot update VDC Network Profile configuration without ID") + } + + return updateVdcNetworkProfile(vdc.client, vdc.Vdc.ID, vdcNetworkProfileConfig) +} + +// UpdateVdcNetworkProfile updates the VDC Network Profile configuration +func (adminVdc *AdminVdc) UpdateVdcNetworkProfile(vdcNetworkProfileConfig *types.VdcNetworkProfile) (*types.VdcNetworkProfile, error) { + if adminVdc == nil || adminVdc.AdminVdc == nil || adminVdc.AdminVdc.ID == "" { + return nil, fmt.Errorf("cannot update VDC Network Profile configuration without ID") + } + + return updateVdcNetworkProfile(adminVdc.client, adminVdc.AdminVdc.ID, vdcNetworkProfileConfig) +} + +// DeleteVdcNetworkProfile deletes VDC Network Profile Configuration +func (vdc *Vdc) DeleteVdcNetworkProfile() error { + if vdc == nil || vdc.Vdc == nil || vdc.Vdc.ID == "" { + return fmt.Errorf("cannot lookup VDC Network Profile without VDC ID") + } + + return deleteVdcNetworkProfile(vdc.client, vdc.Vdc.ID) +} + +// DeleteVdcNetworkProfile deletes VDC Network Profile Configuration +func (adminVdc *AdminVdc) DeleteVdcNetworkProfile() error { + if adminVdc == nil || adminVdc.AdminVdc == nil || adminVdc.AdminVdc.ID == "" { + return fmt.Errorf("cannot lookup VDC Network Profile without VDC ID") + } + + return deleteVdcNetworkProfile(adminVdc.client, adminVdc.AdminVdc.ID) +} + +func getVdcNetworkProfile(client *Client, vdcId string) (*types.VdcNetworkProfile, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcNetworkProfile, + endpointParams: []string{vdcId}, + entityLabel: labelVdcNetworkProfile, + } + return getInnerEntity[types.VdcNetworkProfile](client, c) +} + +func updateVdcNetworkProfile(client *Client, vdcId string, vdcNetworkProfileConfig *types.VdcNetworkProfile) (*types.VdcNetworkProfile, error) { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcNetworkProfile, + endpointParams: []string{vdcId}, + entityLabel: labelVdcNetworkProfile, + } + return updateInnerEntity(client, c, vdcNetworkProfileConfig) +} + +func deleteVdcNetworkProfile(client *Client, vdcId string) error { + c := crudConfig{ + endpoint: types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcNetworkProfile, + endpointParams: []string{vdcId}, + entityLabel: labelVdcNetworkProfile, + } + return deleteEntityById(client, c) +} diff --git a/govcd/vdc_network_profile_test.go b/govcd/vdc_network_profile_test.go new file mode 100644 index 000000000..a45854a4c --- /dev/null +++ b/govcd/vdc_network_profile_test.go @@ -0,0 +1,55 @@ +//go:build network || nsxt || functional || openapi || ALL + +package govcd + +import ( + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_VdcNetworkProfile(check *C) { + vcd.skipIfNotSysAdmin(check) + skipNoNsxtConfiguration(vcd, check) + if vcd.config.VCD.Nsxt.NsxtEdgeCluster == "" { + check.Skip("missing value for vcd.config.VCD.Nsxt.NsxtEdgeCluster") + } + + org, err := vcd.client.GetOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + nsxtVdc, err := org.GetVDCByName(vcd.config.VCD.Nsxt.Vdc, false) + check.Assert(err, IsNil) + + existingVdcNetworkProfile, err := nsxtVdc.GetVdcNetworkProfile() + check.Assert(err, IsNil) + check.Assert(existingVdcNetworkProfile, NotNil) + + // Lookup Edge available Edge Cluster + edgeCluster, err := nsxtVdc.GetNsxtEdgeClusterByName(vcd.config.VCD.Nsxt.NsxtEdgeCluster) + check.Assert(err, IsNil) + check.Assert(edgeCluster, NotNil) + + networkProfileConfig := &types.VdcNetworkProfile{ + ServicesEdgeCluster: &types.VdcNetworkProfileServicesEdgeCluster{ + BackingID: edgeCluster.NsxtEdgeCluster.ID, + }, + } + + newVdcNetworkProfile, err := nsxtVdc.UpdateVdcNetworkProfile(networkProfileConfig) + check.Assert(err, IsNil) + check.Assert(newVdcNetworkProfile, NotNil) + check.Assert(newVdcNetworkProfile.ServicesEdgeCluster.BackingID, Equals, edgeCluster.NsxtEdgeCluster.ID) + + // Unset Edge Cluster (and other values) by sending empty structure + unsetNetworkProfileConfig := &types.VdcNetworkProfile{} + unsetVdcNetworkProfile, err := nsxtVdc.UpdateVdcNetworkProfile(unsetNetworkProfileConfig) + check.Assert(err, IsNil) + check.Assert(unsetVdcNetworkProfile, NotNil) + + networkProfileAfterCleanup, err := nsxtVdc.GetVdcNetworkProfile() + check.Assert(err, IsNil) + check.Assert(networkProfileAfterCleanup.ServicesEdgeCluster, IsNil) + // Cleanup + + err = nsxtVdc.DeleteVdcNetworkProfile() + check.Assert(err, IsNil) +} diff --git a/govcd/vdc_template.go b/govcd/vdc_template.go new file mode 100644 index 000000000..d6c725a51 --- /dev/null +++ b/govcd/vdc_template.go @@ -0,0 +1,262 @@ +/* + * Copyright 2024 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "github.com/vmware/go-vcloud-director/v2/types/v56" + "net/http" + "net/url" + "strings" +) + +type VdcTemplate struct { + VdcTemplate *types.VMWVdcTemplate + client *Client +} + +// CreateVdcTemplate creates a VDC Template with the given settings. +func (vcdClient *VCDClient) CreateVdcTemplate(input types.VMWVdcTemplate) (*VdcTemplate, error) { + href := vcdClient.Client.VCDHREF + href.Path += "/admin/extension/vdcTemplates" + + return genericVdcTemplateRequest(&vcdClient.Client, input, &href, http.MethodPost) +} + +// Update updates an existing VDC Template with the given settings. +// Returns the updated VDC Template. +func (vdcTemplate *VdcTemplate) Update(input types.VMWVdcTemplate) (*VdcTemplate, error) { + href := vdcTemplate.client.VCDHREF + href.Path += fmt.Sprintf("/admin/extension/vdcTemplate/%s", extractUuid(vdcTemplate.VdcTemplate.ID)) + + return genericVdcTemplateRequest(vdcTemplate.client, input, &href, http.MethodPut) +} + +// genericVdcTemplateRequest creates or updates a VDC Template with the given settings +func genericVdcTemplateRequest(client *Client, input types.VMWVdcTemplate, href *url.URL, method string) (*VdcTemplate, error) { + if !client.IsSysAdmin { + return nil, fmt.Errorf("functionality requires System Administrator privileges") + } + + result := &types.VMWVdcTemplate{} + + resp, err := client.executeJsonRequest(href.String(), method, input, "error when performing a "+method+" for VDC Template: %s") + if err != nil { + return nil, err + } + defer closeBody(resp) + + vdcTemplate := VdcTemplate{ + VdcTemplate: result, + client: client, + } + + err = decodeBody(types.BodyTypeJSON, resp, vdcTemplate.VdcTemplate) + if err != nil { + return nil, err + } + + return &vdcTemplate, nil +} + +// GetVdcTemplateById retrieves the VDC Template with the given ID +func (vcdClient *VCDClient) GetVdcTemplateById(id string) (*VdcTemplate, error) { + href := vcdClient.Client.VCDHREF + href.Path += "/admin/extension/vdcTemplate/" + extractUuid(id) + + result := &types.VMWVdcTemplate{} + resp, err := vcdClient.Client.executeJsonRequest(href.String(), http.MethodGet, nil, "error getting VDC Template: %s") + if err != nil { + if strings.Contains(err.Error(), "RESOURCE_NOT_FOUND") || strings.Contains(err.Error(), "not exist") { + return nil, fmt.Errorf("%s: %s", ErrorEntityNotFound, err) + } + return nil, err + } + defer closeBody(resp) + + vdcTemplate := VdcTemplate{ + VdcTemplate: result, + client: &vcdClient.Client, + } + + err = decodeBody(types.BodyTypeJSON, resp, vdcTemplate.VdcTemplate) + if err != nil { + return nil, err + } + + return &vdcTemplate, nil +} + +// GetVdcTemplateByName retrieves the VDC Template with the given name. +// NOTE: System administrators must use the name as seen by System administrators (VMWVdcTemplate.Name), while Tenants must use the +// name as seen by tenants (VMWVdcTemplate.TenantName) +func (vcdClient *VCDClient) GetVdcTemplateByName(name string) (*VdcTemplate, error) { + queryType := types.QtOrgVdcTemplate + if vcdClient.Client.IsSysAdmin { + queryType = types.QtAdminOrgVdcTemplate + } + results, err := vcdClient.QueryWithNotEncodedParams(nil, map[string]string{ + "type": queryType, + "filter": fmt.Sprintf("name==%s", url.QueryEscape(name)), + "filterEncode": "true", + }) + if err != nil { + return nil, err + } + if vcdClient.Client.IsSysAdmin { + if len(results.Results.AdminOrgVdcTemplateRecord) == 0 { + return nil, fmt.Errorf("could not find any VDC Template with name '%s': %s", name, ErrorEntityNotFound) + } + if len(results.Results.AdminOrgVdcTemplateRecord) > 1 { + return nil, fmt.Errorf("expected one VDC Template with name '%s', but got %d", name, len(results.Results.AdminOrgVdcTemplateRecord)) + } + return vcdClient.GetVdcTemplateById(results.Results.AdminOrgVdcTemplateRecord[0].HREF) + } else { + if len(results.Results.OrgVdcTemplateRecord) == 0 { + return nil, fmt.Errorf("could not find any VDC Template with name '%s': %s", name, ErrorEntityNotFound) + } + if len(results.Results.OrgVdcTemplateRecord) > 1 { + return nil, fmt.Errorf("expected one VDC Template with name '%s', but got %d", name, len(results.Results.OrgVdcTemplateRecord)) + } + return vcdClient.GetVdcTemplateById(results.Results.OrgVdcTemplateRecord[0].HREF) + } +} + +// QueryAdminVdcTemplates gets the list of VDC Templates as System Administrator +func (vcdClient *VCDClient) QueryAdminVdcTemplates() ([]*types.QueryResultAdminOrgVdcTemplateRecordType, error) { + if !vcdClient.Client.IsSysAdmin { + return nil, fmt.Errorf("querying %s requires System administrator privileges", types.QtAdminOrgVdcTemplate) + } + + results, err := vcdClient.Client.cumulativeQuery(types.QtAdminOrgVdcTemplate, nil, nil) + if err != nil { + return nil, err + } + return results.Results.AdminOrgVdcTemplateRecord, nil +} + +// QueryVdcTemplates gets the list of VDC Templates from the receiver Org, as a tenant +func (org *Org) QueryVdcTemplates() ([]*types.QueryResultOrgVdcTemplateRecordType, error) { + results, err := org.client.cumulativeQueryWithHeaders(types.QtOrgVdcTemplate, nil, nil, getTenantContextHeader(&TenantContext{ + OrgId: org.Org.ID, + OrgName: org.Org.Name, + })) + if err != nil { + return nil, err + } + return results.Results.OrgVdcTemplateRecord, nil +} + +// Delete deletes the receiver VDC Template +func (vdcTemplate *VdcTemplate) Delete() error { + if !vdcTemplate.client.IsSysAdmin { + return fmt.Errorf("functionality requires System Administrator privileges") + } + if vdcTemplate.VdcTemplate.HREF == "" { + return fmt.Errorf("cannot delete the VDC Template, its HREF is empty") + } + + _, err := vdcTemplate.client.ExecuteRequest(vdcTemplate.VdcTemplate.HREF, http.MethodDelete, "", "error deleting VDC Template: %s", nil, nil) + if err != nil { + return err + } + return nil +} + +// SetAccessControl sets the Access control configuration for the receiver VDC Template, +// which specifies which Organizations can read it. +func (vdcTemplate *VdcTemplate) SetAccessControl(organizationIds []string) error { + if !vdcTemplate.client.IsSysAdmin { + return fmt.Errorf("functionality requires System Administrator privileges") + } + if vdcTemplate.VdcTemplate.HREF == "" { + return fmt.Errorf("cannot set the Access control for the VDC Template, its HREF is empty") + } + accessSettings := make([]*types.AccessSetting, len(organizationIds)) + for i, organizationId := range organizationIds { + accessSettings[i] = &types.AccessSetting{ + Subject: &types.LocalSubject{ + HREF: fmt.Sprintf("%s/org/%s", vdcTemplate.client.VCDHREF.String(), extractUuid(organizationId))}, + AccessLevel: types.ControlAccessReadOnly, + } + } + payload := &types.ControlAccessParams{AccessSettings: &types.AccessSettingList{AccessSetting: accessSettings}} + + return vdcTemplate.client.setAccessControlWithHttpMethod(http.MethodPut, payload, vdcTemplate.VdcTemplate.HREF, "VDC Template", vdcTemplate.VdcTemplate.Name, nil) +} + +// GetAccessControl retrieves the Access control configuration for the receiver VDC Template, which +// contains the Organizations that can read it. +func (vdcTemplate *VdcTemplate) GetAccessControl() (*types.ControlAccessParams, error) { + if !vdcTemplate.client.IsSysAdmin { + return nil, fmt.Errorf("functionality requires System Administrator privileges") + } + if vdcTemplate.VdcTemplate.HREF == "" { + return nil, fmt.Errorf("cannot get the Access control for the VDC Template, its HREF is empty") + } + result := &types.ControlAccessParams{} + href := fmt.Sprintf("%s/controlAccess", vdcTemplate.VdcTemplate.HREF) + _, err := vdcTemplate.client.ExecuteRequest(href, http.MethodGet, types.AnyXMLMime, "error getting the Access control of VDC Template: %s", nil, result) + if err != nil { + return nil, err + } + return result, nil +} + +// InstantiateVdcAsync creates a new VDC by instantiating the receiver VDC Template. This method finishes immediately after +// requesting the VDC instance, by returning the Task associated to the VDC instantiation process. If there's any error +// during the process, returns a nil Task and an error. +func (vdcTemplate *VdcTemplate) InstantiateVdcAsync(vdcName, description, organizationId string) (*Task, error) { + if vdcName == "" { + return nil, fmt.Errorf("the VDC name is required to instantiate VDC Template '%s'", vdcTemplate.VdcTemplate.Name) + } + if organizationId == "" { + return nil, fmt.Errorf("the Organization ID is required to instantiate VDC Template '%s'", vdcTemplate.VdcTemplate.Name) + } + + payload := &types.InstantiateVdcTemplateParams{ + Xmlns: types.XMLNamespaceVCloud, + Name: vdcName, + Source: &types.Reference{ + HREF: vdcTemplate.VdcTemplate.HREF, + Type: types.MimeVdcTemplateInstantiateType, + }, + } + if description != "" { + payload.Description = description + } + + href := vdcTemplate.client.VCDHREF + href.Path += fmt.Sprintf("/org/%s/action/instantiate", extractUuid(organizationId)) + task, err := vdcTemplate.client.ExecuteTaskRequest(href.String(), http.MethodPost, types.MimeVdcTemplateInstantiate, "error instantiating the VDC Template: %s", payload) + if err != nil { + return nil, err + } + return &task, nil +} + +// InstantiateVdc creates a new VDC by instantiating the receiver VDC Template. This method waits for the associated Task +// to complete and returns the instantiated VDC. If there's any error during the process or in the Task, returns a nil VDC and an error. +func (vdcTemplate *VdcTemplate) InstantiateVdc(vdcName, description, organizationId string) (*Vdc, error) { + task, err := vdcTemplate.InstantiateVdcAsync(vdcName, description, organizationId) + if err != nil { + return nil, err + } + err = task.WaitTaskCompletion() + if err != nil { + return nil, fmt.Errorf("failed instantiating the VDC Template: %s", err) + } + if task.Task.Owner == nil || task.Task.Owner.HREF == "" { + return nil, fmt.Errorf("the VDC was instantiated but could not retrieve its ID from the finished task") + } + vdc, err := getVDCByHref(vdcTemplate.client, task.Task.Owner.HREF) + if err != nil { + return nil, fmt.Errorf("could not retrieve the VDC from Task's HREF '%s': %s", task.Task.Owner.HREF, err) + } + return &Vdc{ + Vdc: vdc, + client: vdcTemplate.client, + }, nil +} diff --git a/govcd/vdc_template_test.go b/govcd/vdc_template_test.go new file mode 100644 index 000000000..f66cc847e --- /dev/null +++ b/govcd/vdc_template_test.go @@ -0,0 +1,417 @@ +//go:build vdc || functional || ALL + +/* + * Copyright 2024 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_VdcTemplateCRUD(check *C) { + if !vcd.client.Client.IsSysAdmin { + check.Skip("test requires system administrator privileges") + } + + // Pre-requisites: We need information such as Provider VDC, External networks (Provider Gateways) + // and Edge Clusters. + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + + vdc, err := adminOrg.GetVDCByName(vcd.config.VCD.Nsxt.Vdc, false) + check.Assert(err, IsNil) + check.Assert(vdc, NotNil) + + edgeCluster, err := vdc.GetNsxtEdgeClusterByName(vcd.config.VCD.Nsxt.NsxtEdgeCluster) + check.Assert(err, IsNil) + check.Assert(edgeCluster, NotNil) + + providerVdc, err := vcd.client.GetProviderVdcByName(vcd.config.VCD.NsxtProviderVdc.Name) + check.Assert(err, IsNil) + check.Assert(providerVdc, NotNil) + check.Assert(providerVdc.ProviderVdc.AvailableNetworks, NotNil) + check.Assert(providerVdc.ProviderVdc.NetworkPoolReferences, NotNil) + + var networkRef *types.Reference + for _, netRef := range providerVdc.ProviderVdc.AvailableNetworks.Network { + if netRef.Name == vcd.config.VCD.Nsxt.ExternalNetwork { + networkRef = netRef + break + } + } + check.Assert(networkRef, NotNil) + + var networkPoolRef *types.Reference + for _, netPoolRef := range providerVdc.ProviderVdc.NetworkPoolReferences.NetworkPoolReference { + if netPoolRef.Name == vcd.config.VCD.NsxtProviderVdc.NetworkPool { + networkPoolRef = netPoolRef + break + } + } + check.Assert(networkPoolRef, NotNil) + + // Bindings must be random UUIDs generated manually + externalNetworkBindingId := "urn:vcloud:binding:b871f699-50e6-4a65-ab8b-428324735ff2" + gatewayEdgeClusterBindingId := "urn:vcloud:binding:36038940-dbbf-4346-94d9-e99e54c8e43a" + servicesEdgeClusterBindingId := "urn:vcloud:binding:8e7e2480-ba77-4dd0-a7d4-2d1155e4d087" + + settings := types.VMWVdcTemplate{ + NetworkBackingType: "NSX_T", + ProviderVdcReference: []*types.VMWVdcTemplateProviderVdcSpecification{{ + HREF: providerVdc.ProviderVdc.HREF, + Binding: []*types.VMWVdcTemplateBinding{ + { + Name: gatewayEdgeClusterBindingId, + Value: &types.Reference{ + ID: fmt.Sprintf("urn:vcloud:backingEdgeCluster:%s", edgeCluster.NsxtEdgeCluster.ID), + HREF: fmt.Sprintf("urn:vcloud:backingEdgeCluster:%s", edgeCluster.NsxtEdgeCluster.ID), + Type: "application/json", + }, + }, + { + Name: servicesEdgeClusterBindingId, + Value: &types.Reference{ + ID: fmt.Sprintf("urn:vcloud:backingEdgeCluster:%s", edgeCluster.NsxtEdgeCluster.ID), + HREF: fmt.Sprintf("urn:vcloud:backingEdgeCluster:%s", edgeCluster.NsxtEdgeCluster.ID), + Type: "application/json", + }, + }, + { + Name: externalNetworkBindingId, + Value: &types.Reference{ + ID: networkRef.ID, + HREF: networkRef.HREF, + Type: networkRef.Type, + }, + }, + }, + }}, + Name: check.TestName(), + Description: check.TestName(), + TenantName: check.TestName() + "_Tenant", + TenantDescription: check.TestName() + "_Tenant", + + VdcTemplateSpecification: &types.VMWVdcTemplateSpecification{ + Type: types.VdcTemplateFlexType, + NicQuota: 100, + VmQuota: 100, + ProvisionedNetworkQuota: 1000, + GatewayConfiguration: &types.VdcTemplateSpecificationGatewayConfiguration{ + Gateway: &types.EdgeGateway{ + Name: check.TestName(), + Description: check.TestName(), + Configuration: &types.GatewayConfiguration{ + GatewayInterfaces: &types.GatewayInterfaces{GatewayInterface: []*types.GatewayInterface{ + { + Name: gatewayEdgeClusterBindingId, + DisplayName: gatewayEdgeClusterBindingId, + Connected: true, + InterfaceType: "UPLINK", + Network: &types.Reference{ + HREF: gatewayEdgeClusterBindingId, + }, + }, + }}, + EdgeClusterConfiguration: &types.EdgeClusterConfiguration{PrimaryEdgeCluster: &types.Reference{HREF: gatewayEdgeClusterBindingId}}, + }, + }, + Network: &types.OrgVDCNetwork{ + Name: check.TestName() + "_Net", + Description: check.TestName() + "_Net", + Configuration: &types.NetworkConfiguration{ + IPScopes: &types.IPScopes{IPScope: []*types.IPScope{ + { + IsInherited: false, + Gateway: "1.1.1.1", + Netmask: "255.255.240.0", + SubnetPrefixLengthInt: addrOf(20), + IPRanges: &types.IPRanges{IPRange: []*types.IPRange{ + { + StartAddress: "1.1.1.1", + EndAddress: "1.1.1.1", + }, + }}, + }, + }}, + FenceMode: "natRouted", + }, + IsShared: false, + }, + }, + StorageProfile: []*types.VdcStorageProfile{ + { + Name: vcd.config.VCD.NsxtProviderVdc.StorageProfile2, + Enabled: addrOf(true), + Units: "MB", + Limit: 1024, + Default: true, + }, + }, + IsElastic: addrOf(false), + IncludeMemoryOverhead: addrOf(true), + ThinProvision: true, + FastProvisioningEnabled: true, + NetworkPoolReference: networkPoolRef, + NetworkProfileConfiguration: &types.VdcTemplateNetworkProfile{ + ServicesEdgeCluster: &types.Reference{HREF: servicesEdgeClusterBindingId}, + }, + CpuAllocationMhz: 256, + CpuLimitMhzPerVcpu: 1000, + CpuLimitMhz: 256, + MemoryAllocationMB: 1024, + MemoryLimitMb: 1024, + CpuGuaranteedPercentage: 20, + MemoryGuaranteedPercentage: 30, + }, + } + + template, err := vcd.client.CreateVdcTemplate(settings) + check.Assert(err, IsNil) + check.Assert(template, NotNil) + + defer func() { + err = template.Delete() + check.Assert(err, IsNil) + }() + + templateById, err := vcd.client.GetVdcTemplateById(template.VdcTemplate.ID) + check.Assert(err, IsNil) + check.Assert(templateById, NotNil) + check.Assert(templateById, DeepEquals, template) + + _, err = vcd.client.GetVdcTemplateById("urn:vcloud:vdctemplate:00000000-0000-0000-00000-000000000000") + check.Assert(err, NotNil) + check.Assert(ContainsNotFound(err), Equals, true) + + templateByName, err := vcd.client.GetVdcTemplateByName(template.VdcTemplate.Name) + check.Assert(err, IsNil) + check.Assert(templateByName, NotNil) + check.Assert(templateByName, DeepEquals, templateById) + + _, err = vcd.client.GetVdcTemplateByName("IDoNotExist") + check.Assert(err, NotNil) + check.Assert(ContainsNotFound(err), Equals, true) + + org, err := vcd.client.GetOrgByName(adminOrg.AdminOrg.Name) + check.Assert(err, IsNil) + check.Assert(org, NotNil) + + adminTemplates, err := vcd.client.QueryAdminVdcTemplates() + check.Assert(err, IsNil) + check.Assert(true, Equals, len(adminTemplates) > 0) + found := false + for _, adminTemplate := range adminTemplates { + if extractUuid(adminTemplate.HREF) == extractUuid(templateByName.VdcTemplate.HREF) { + found = true + break + } + } + check.Assert(found, Equals, true) + + settings.Description = "Updated" + settings.VdcTemplateSpecification.CpuLimitMhz = 500 + settings.VdcTemplateSpecification.NicQuota = 500 + template, err = template.Update(settings) + check.Assert(err, IsNil) + check.Assert(template, NotNil) + check.Assert(template.VdcTemplate.Description, Equals, "Updated") + check.Assert(template.VdcTemplate.VdcTemplateSpecification.CpuLimitMhz, Equals, 500) + check.Assert(template.VdcTemplate.VdcTemplateSpecification.NicQuota, Equals, 500) + + access, err := template.GetAccessControl() + check.Assert(err, IsNil) + check.Assert(access, NotNil) + check.Assert(access.AccessSettings, IsNil) + + err = template.SetAccessControl([]string{adminOrg.AdminOrg.ID}) + check.Assert(err, IsNil) + + access, err = template.GetAccessControl() + check.Assert(err, IsNil) + check.Assert(access, NotNil) + check.Assert(access.AccessSettings, NotNil) + check.Assert(len(access.AccessSettings.AccessSetting), Equals, 1) + check.Assert(access.AccessSettings.AccessSetting[0].Subject, NotNil) + check.Assert(access.AccessSettings.AccessSetting[0].Subject.HREF, Equals, adminOrg.AdminOrg.HREF) + + // Now that the tenant has permissions, the query should return it + templates, err := org.QueryVdcTemplates() + check.Assert(err, IsNil) + check.Assert(true, Equals, len(adminTemplates) > 0) + found = false + for _, t := range templates { + if extractUuid(t.HREF) == extractUuid(templateByName.VdcTemplate.HREF) { + found = true + break + } + } + check.Assert(found, Equals, true) +} + +func (vcd *TestVCD) Test_VdcTemplateInstantiate(check *C) { + if !vcd.client.Client.IsSysAdmin { + check.Skip("test requires system administrator privileges") + } + + // Pre-requisites: We need information such as Provider VDC and External networks (Provider Gateways) + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + + providerVdc, err := vcd.client.GetProviderVdcByName(vcd.config.VCD.NsxtProviderVdc.Name) + check.Assert(err, IsNil) + check.Assert(providerVdc, NotNil) + check.Assert(providerVdc.ProviderVdc.AvailableNetworks, NotNil) + check.Assert(providerVdc.ProviderVdc.NetworkPoolReferences, NotNil) + + var networkRef *types.Reference + for _, netRef := range providerVdc.ProviderVdc.AvailableNetworks.Network { + if netRef.Name == vcd.config.VCD.Nsxt.ExternalNetwork { + networkRef = netRef + break + } + } + check.Assert(networkRef, NotNil) + + var networkPoolRef *types.Reference + for _, netPoolRef := range providerVdc.ProviderVdc.NetworkPoolReferences.NetworkPoolReference { + if netPoolRef.Name == vcd.config.VCD.NsxtProviderVdc.NetworkPool { + networkPoolRef = netPoolRef + break + } + } + check.Assert(networkPoolRef, NotNil) + + // Bindings must be random UUIDs generated manually + externalNetworkBindingId := "urn:vcloud:binding:10d82a8a-f0a9-4c98-a462-d6a1b65ae210" + + template, err := vcd.client.CreateVdcTemplate(types.VMWVdcTemplate{ + NetworkBackingType: "NSX_T", + ProviderVdcReference: []*types.VMWVdcTemplateProviderVdcSpecification{{ + HREF: providerVdc.ProviderVdc.HREF, + Binding: []*types.VMWVdcTemplateBinding{ + { + Name: externalNetworkBindingId, + Value: &types.Reference{ + ID: networkRef.ID, + HREF: networkRef.HREF, + Type: networkRef.Type, + }, + }, + }, + }}, + Name: check.TestName(), + Description: check.TestName(), + TenantName: check.TestName() + "_Tenant", + TenantDescription: check.TestName() + "_Tenant", + + VdcTemplateSpecification: &types.VMWVdcTemplateSpecification{ + Type: types.VdcTemplateFlexType, + NicQuota: 100, + VmQuota: 100, + ProvisionedNetworkQuota: 1000, + StorageProfile: []*types.VdcStorageProfile{ + { + Name: vcd.config.VCD.NsxtProviderVdc.StorageProfile2, + Enabled: addrOf(true), + Units: "MB", + Limit: 1024, + Default: true, + }, + }, + IsElastic: addrOf(false), + IncludeMemoryOverhead: addrOf(true), + ThinProvision: true, + FastProvisioningEnabled: true, + CpuAllocationMhz: 256, + CpuLimitMhzPerVcpu: 1000, + CpuLimitMhz: 256, + MemoryAllocationMB: 1024, + MemoryLimitMb: 1024, + CpuGuaranteedPercentage: 20, + MemoryGuaranteedPercentage: 30, + }, + }) + check.Assert(err, IsNil) + check.Assert(template, NotNil) + + defer func() { + err = template.Delete() + check.Assert(err, IsNil) + }() + + err = template.SetAccessControl([]string{adminOrg.AdminOrg.ID}) + check.Assert(err, IsNil) + + // Instantiate the VDC Template as System administrator + vdc, err := template.InstantiateVdc(check.TestName(), check.TestName(), adminOrg.AdminOrg.ID) + check.Assert(err, IsNil) + check.Assert(vdc, NotNil) + defer func() { + // Delete the instantiated VDC even on test errors + err = vdc.DeleteWait(true, true) + check.Assert(err, IsNil) + }() + check.Assert(vdc.Vdc.Name, Equals, check.TestName()) + check.Assert(vdc.Vdc.Description, Equals, check.TestName()) + + org, err := vdc.getParentOrg() + check.Assert(err, IsNil) + check.Assert(adminOrg.AdminOrg.ID, Equals, org.orgId()) + + // Instantiate the VDC Template as a Tenant + if len(vcd.config.Tenants) > 0 { + orgName := vcd.config.Tenants[0].SysOrg + userName := vcd.config.Tenants[0].User + password := vcd.config.Tenants[0].Password + + vcdClient := NewVCDClient(vcd.client.Client.VCDHREF, true) + err := vcdClient.Authenticate(userName, password, orgName) + check.Assert(err, IsNil) + + templateAsTenant, err := vcdClient.GetVdcTemplateByName(template.VdcTemplate.TenantName) // Careful, we must use the Tenant name now + check.Assert(err, IsNil) + check.Assert(templateAsTenant, NotNil) + + _, err = vcdClient.QueryAdminVdcTemplates() + check.Assert(err, NotNil) + + org, err := vcdClient.GetOrgByName(orgName) + check.Assert(err, IsNil) + check.Assert(org, NotNil) + + tenantTemplates, err := org.QueryVdcTemplates() + check.Assert(err, IsNil) + check.Assert(true, Equals, len(tenantTemplates) > 0) + found := false + for _, tenantTemplate := range tenantTemplates { + if extractUuid(tenantTemplate.HREF) == extractUuid(templateAsTenant.VdcTemplate.HREF) { + found = true + break + } + } + check.Assert(found, Equals, true) + + vdc2, err := templateAsTenant.InstantiateVdc(check.TestName()+"2", check.TestName()+"2", adminOrg.AdminOrg.ID) + check.Assert(err, IsNil) + check.Assert(vdc2, NotNil) + defer func() { + // Also delete the second instantiated VDC even on test errors. We need to retrieve it as Admin for that. + adminVdc2, err := adminOrg.GetVDCById(vdc2.Vdc.ID, true) + check.Assert(err, IsNil) + err = adminVdc2.DeleteWait(true, true) + check.Assert(err, IsNil) + }() + check.Assert(vdc2.Vdc.Name, Equals, check.TestName()+"2") + check.Assert(vdc2.Vdc.Description, Equals, check.TestName()+"2") + + vdcOrg, err := vdc.getParentOrg() + check.Assert(err, IsNil) + check.Assert(adminOrg.AdminOrg.ID, Equals, vdcOrg.orgId()) + } +} diff --git a/govcd/vdc_test.go b/govcd/vdc_test.go index b0c0b42d8..967630f28 100644 --- a/govcd/vdc_test.go +++ b/govcd/vdc_test.go @@ -1,13 +1,15 @@ -// +build vdc functional ALL +//go:build vdc || functional || ALL /* - * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + * Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. */ package govcd import ( "fmt" + "reflect" + "strings" . "gopkg.in/check.v1" @@ -143,6 +145,26 @@ func (vcd *TestVCD) Test_NewVdc(check *C) { } +// Test_GetVDCHardwareVersion tests hardware version fetching functionality +func (vcd *TestVCD) Test_GetVDCHardwareVersion(check *C) { + err := vcd.vdc.Refresh() + check.Assert(err, IsNil) + + // vmx-18 is the latest version supported by 10.3.0, the oldest version we support. + hwVersion, err := vcd.vdc.GetHardwareVersion("vmx-18") + check.Assert(err, IsNil) + check.Assert(hwVersion, NotNil) + + check.Assert(hwVersion.Name, Equals, "vmx-18") + + os, err := vcd.vdc.FindOsFromId(hwVersion, "sles10_64Guest") + check.Assert(err, IsNil) + check.Assert(os, NotNil) + + check.Assert(os.InternalName, Equals, "sles10_64Guest") + check.Assert(os.Name, Equals, "SUSE Linux Enterprise 10 (64-bit)") +} + // Tests ComposeVApp with given parameters in the config file. // Throws an error if networks, catalog, catalog item, and // storage preference are omitted from the config file. @@ -178,6 +200,8 @@ func (vcd *TestVCD) Test_ComposeVApp(check *C) { // Compose VApp task, err := vcd.vdc.ComposeVApp(networks, vapptemplate, storageprofileref, TestComposeVapp, TestComposeVappDesc, true) check.Assert(err, IsNil) + check.Assert(task.Task.Tasks, NotNil) + check.Assert(len(task.Task.Tasks.Task) > 0, Equals, true) check.Assert(task.Task.Tasks.Task[0].OperationName, Equals, "vdcComposeVapp") // Get VApp vapp, err := vcd.vdc.GetVAppByName(TestComposeVapp, true) @@ -260,6 +284,11 @@ func (vcd *TestVCD) Test_QueryVM(check *C) { check.Assert(err, IsNil) check.Assert(vm.VM.Name, Equals, vmName) + + if vcd.client.Client.IsSysAdmin { + check.Assert(vm.VM.Moref, Not(Equals), "") + check.Assert(strings.HasPrefix(vm.VM.Moref, "vm-"), Equals, true) + } } func init() { @@ -424,7 +453,8 @@ func (vcd *TestVCD) TestGetVappList(check *C) { // Use the search engine to find the known vApp criteria := NewFilterDef() - criteria.AddFilter(types.FilterNameRegex, TestSetUpSuite) + err = criteria.AddFilter(types.FilterNameRegex, TestSetUpSuite) + check.Assert(err, IsNil) queryType := vcd.client.Client.GetQueryType(types.QtVapp) queryItems, _, err := vcd.client.Client.SearchByFilter(queryType, criteria) check.Assert(err, IsNil) @@ -437,8 +467,10 @@ func (vcd *TestVCD) TestGetVappList(check *C) { check.Assert(vmName, Not(Equals), "") check.Assert(vm.HREF, Not(Equals), "") criteria = NewFilterDef() - criteria.AddFilter(types.FilterNameRegex, vmName) - criteria.AddFilter(types.FilterParent, vapp.VApp.Name) + err = criteria.AddFilter(types.FilterNameRegex, vmName) + check.Assert(err, IsNil) + err = criteria.AddFilter(types.FilterParent, vapp.VApp.Name) + check.Assert(err, IsNil) queryType = vcd.client.Client.GetQueryType(types.QtVm) queryItems, _, err = vcd.client.Client.SearchByFilter(queryType, criteria) check.Assert(err, IsNil) @@ -458,8 +490,214 @@ func (vcd *TestVCD) TestGetVdcCapabilities(check *C) { func (vcd *TestVCD) TestVdcIsNsxt(check *C) { skipNoNsxtConfiguration(vcd, check) check.Assert(vcd.nsxtVdc.IsNsxt(), Equals, true) + if vcd.vdc != nil { + check.Assert(vcd.vdc.IsNsxt(), Equals, false) + } } func (vcd *TestVCD) TestVdcIsNsxv(check *C) { check.Assert(vcd.vdc.IsNsxv(), Equals, true) + // retrieve the same VDC as AdminVdc, to test the corresponding function + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + adminVdc, err := adminOrg.GetVDCByName(vcd.vdc.Vdc.Name, false) + check.Assert(err, IsNil) + check.Assert(adminVdc.IsNsxv(), Equals, true) + // if NSX-T is configured, we also check a NSX-T VDC + if vcd.nsxtVdc != nil { + check.Assert(vcd.nsxtVdc.IsNsxv(), Equals, false) + nsxtAdminVdc, err := adminOrg.GetAdminVDCByName(vcd.config.VCD.Nsxt.Vdc, false) + check.Assert(err, IsNil) + check.Assert(nsxtAdminVdc.IsNsxv(), Equals, false) + } +} + +func (vcd *TestVCD) TestCreateRawVapp(check *C) { + org, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(org, NotNil) + + vdc, err := org.GetVDCByName(vcd.config.VCD.Vdc, false) + check.Assert(err, IsNil) + check.Assert(vdc, NotNil) + + name := check.TestName() + description := "test compose raw app" + vapp, err := vdc.CreateRawVApp(name, description) + check.Assert(err, IsNil) + AddToCleanupList(name, "vapp", vdc.Vdc.Name, name) + + check.Assert(vapp.VApp.Name, Equals, name) + check.Assert(vapp.VApp.Description, Equals, description) + task, err := vapp.Delete() + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) +} + +func (vcd *TestVCD) TestSetControlAccess(check *C) { + // Set VDC sharing to everyone + org, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(org, NotNil) + + vdc, err := org.GetVDCByName(vcd.config.VCD.Vdc, false) + check.Assert(err, IsNil) + check.Assert(vdc, NotNil) + + readControlAccessParams, err := vdc.SetControlAccess(true, "ReadOnly", nil, true) + check.Assert(err, IsNil) + check.Assert(readControlAccessParams, NotNil) + check.Assert(readControlAccessParams.IsSharedToEveryone, Equals, true) + check.Assert(*readControlAccessParams.EveryoneAccessLevel, Equals, "ReadOnly") + check.Assert(readControlAccessParams.AccessSettings, IsNil) // If not shared with users/groups, this will be nil + + // Set VDC sharing to one user + orgUserRef := org.AdminOrg.Users.User[0] + user, err := org.GetUserByName(orgUserRef.Name, false) + check.Assert(err, IsNil) + check.Assert(user, NotNil) + + accessSettings := []*types.AccessSetting{ + { + AccessLevel: "ReadOnly", + Subject: &types.LocalSubject{ + HREF: user.User.Href, + Name: user.User.Name, + Type: user.User.Type, + }, + }, + } + + readControlAccessParams, err = vdc.SetControlAccess(false, "", accessSettings, true) + check.Assert(err, IsNil) + check.Assert(readControlAccessParams, NotNil) + check.Assert(len(readControlAccessParams.AccessSettings.AccessSetting) > 0, Equals, true) + check.Assert(assertVDCAccessSettings(accessSettings, readControlAccessParams.AccessSettings.AccessSetting), IsNil) + + // Check that fail if both isSharedToEveryone and accessSettings is passed + readControlAccessParams, err = vdc.SetControlAccess(true, "ReadOnly", accessSettings, true) + check.Assert(err, NotNil) + check.Assert(readControlAccessParams, IsNil) + + // Check DeleteControlAccess + readControlAccessParams, err = vdc.DeleteControlAccess(true) + check.Assert(err, IsNil) + check.Assert(readControlAccessParams.IsSharedToEveryone, Equals, false) + check.Assert(readControlAccessParams.AccessSettings, IsNil) +} + +func assertVDCAccessSettings(wanted, received []*types.AccessSetting) error { + if len(wanted) != len(received) { + return fmt.Errorf("wanted and received access settings are not the same length") + } + for _, receivedAccessSetting := range received { + for i, wantedAccessSetting := range wanted { + if reflect.DeepEqual(*wantedAccessSetting.Subject, *receivedAccessSetting.Subject) && (wantedAccessSetting.AccessLevel == receivedAccessSetting.AccessLevel) { + break + } + if i == len(wanted)-1 { + return fmt.Errorf("access settings for user %s were not found or are not correct", wantedAccessSetting.Subject.Name) + } + } + } + return nil +} + +// TestVAppTemplateRetrieval tests that VDC receiver objects can search vApp Templates successfully. +func (vcd *TestVCD) TestVAppTemplateRetrieval(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + + if vcd.config.VCD.Catalog.NsxtCatalogItem == "" { + check.Skip(fmt.Sprintf("%s: Catalog Item not given. Test can't proceed", check.TestName())) + } + + org, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(org, NotNil) + + vdc, err := org.GetVDCByName(vcd.config.VCD.Nsxt.Vdc, false) + check.Assert(err, IsNil) + check.Assert(vdc, NotNil) + + // Test cases + vAppTemplate, err := vdc.GetVAppTemplateByName(vcd.config.VCD.Catalog.NsxtCatalogItem) + check.Assert(err, IsNil) + check.Assert(vAppTemplate.VAppTemplate.Name, Equals, vcd.config.VCD.Catalog.NsxtCatalogItem) + if vcd.config.VCD.Catalog.CatalogItemDescription != "" { + check.Assert(strings.Contains(vAppTemplate.VAppTemplate.Description, vcd.config.VCD.Catalog.CatalogItemDescription), Equals, true) + } + + vAppTemplate, err = vcd.client.GetVAppTemplateById(vAppTemplate.VAppTemplate.ID) + check.Assert(err, IsNil) + check.Assert(vAppTemplate.VAppTemplate.Name, Equals, vcd.config.VCD.Catalog.NsxtCatalogItem) + if vcd.config.VCD.Catalog.CatalogItemDescription != "" { + check.Assert(strings.Contains(vAppTemplate.VAppTemplate.Description, vcd.config.VCD.Catalog.CatalogItemDescription), Equals, true) + } + + vAppTemplate, err = vdc.GetVAppTemplateByNameOrId(vAppTemplate.VAppTemplate.ID, false) + check.Assert(err, IsNil) + check.Assert(vAppTemplate.VAppTemplate.Name, Equals, vcd.config.VCD.Catalog.NsxtCatalogItem) + if vcd.config.VCD.Catalog.CatalogItemDescription != "" { + check.Assert(strings.Contains(vAppTemplate.VAppTemplate.Description, vcd.config.VCD.Catalog.CatalogItemDescription), Equals, true) + } + + vAppTemplate, err = vdc.GetVAppTemplateByNameOrId(vcd.config.VCD.Catalog.NsxtCatalogItem, false) + check.Assert(err, IsNil) + check.Assert(vAppTemplate.VAppTemplate.Name, Equals, vcd.config.VCD.Catalog.NsxtCatalogItem) + if vcd.config.VCD.Catalog.CatalogItemDescription != "" { + check.Assert(strings.Contains(vAppTemplate.VAppTemplate.Description, vcd.config.VCD.Catalog.CatalogItemDescription), Equals, true) + } + + vAppTemplateRecord, err := vcd.client.QuerySynchronizedVAppTemplateById(vAppTemplate.VAppTemplate.ID) + check.Assert(err, IsNil) + check.Assert(vAppTemplateRecord.Name, Equals, vAppTemplate.VAppTemplate.Name) + check.Assert(vAppTemplateRecord.HREF, Equals, vAppTemplate.VAppTemplate.HREF) + + vmTemplateRecord, err := vcd.client.QuerySynchronizedVmInVAppTemplateByHref(vAppTemplate.VAppTemplate.HREF, "**") + check.Assert(err, IsNil) + check.Assert(vmTemplateRecord, NotNil) + + // Test non-existent vApp Template + _, err = vdc.GetVAppTemplateByName("INVALID") + check.Assert(err, NotNil) + + _, err = vcd.client.QuerySynchronizedVmInVAppTemplateByHref(vAppTemplate.VAppTemplate.HREF, "INVALID") + check.Assert(err, Equals, ErrorEntityNotFound) +} + +// TestMediaRetrieval tests that VDC receiver objects can search Media items successfully. +func (vcd *TestVCD) TestMediaRetrieval(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + + if vcd.config.Media.NsxtMedia == "" { + check.Skip(fmt.Sprintf("%s: NSX-T Media item not given. Test can't proceed", check.TestName())) + } + + org, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + check.Assert(org, NotNil) + + catalog, err := org.GetCatalogByName(vcd.config.VCD.Catalog.NsxtBackedCatalogName, false) + check.Assert(err, IsNil) + check.Assert(catalog, NotNil) + + vdc, err := org.GetVDCByName(vcd.config.VCD.Nsxt.Vdc, false) + check.Assert(err, IsNil) + check.Assert(vdc, NotNil) + + mediaFromCatalog, err := catalog.GetMediaByName(vcd.config.Media.NsxtMedia, false) + check.Assert(err, IsNil) + check.Assert(mediaFromCatalog, NotNil) + + // Test cases + mediaFromVdc, err := vcd.client.QueryMediaById(mediaFromCatalog.Media.ID) + check.Assert(err, IsNil) + check.Assert(mediaFromCatalog.Media.HREF, Equals, mediaFromVdc.MediaRecord.HREF) + check.Assert(mediaFromCatalog.Media.Name, Equals, mediaFromVdc.MediaRecord.Name) + + // Test non-existent Media item + mediaFromVdc, err = vcd.client.QueryMediaById("INVALID") + check.Assert(err, NotNil) + check.Assert(mediaFromVdc, IsNil) } diff --git a/govcd/vdccomputepolicy.go b/govcd/vdccomputepolicy.go index ddd5db134..623035b8b 100644 --- a/govcd/vdccomputepolicy.go +++ b/govcd/vdccomputepolicy.go @@ -1,18 +1,18 @@ package govcd /* - * Copyright 2020 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + * Copyright 2022 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. */ import ( "fmt" - "github.com/vmware/go-vcloud-director/v2/types/v56" - "github.com/vmware/go-vcloud-director/v2/util" - "net/http" "net/url" + + "github.com/vmware/go-vcloud-director/v2/types/v56" ) -// In UI called VM sizing policy. In API VDC compute policy +// VdcComputePolicy defines a VDC Compute Policy, which can be a VM Sizing Policy, a VM Placement Policy or a vGPU Policy. +// Deprecated: Use VdcComputePolicyV2 instead type VdcComputePolicy struct { VdcComputePolicy *types.VdcComputePolicy Href string @@ -20,16 +20,25 @@ type VdcComputePolicy struct { } // GetVdcComputePolicyById retrieves VDC compute policy by given ID +// Deprecated: Use VCDClient.GetVdcComputePolicyV2ById instead +func (client *Client) GetVdcComputePolicyById(id string) (*VdcComputePolicy, error) { + return getVdcComputePolicyById(client, id) +} + +// GetVdcComputePolicyById retrieves VDC compute policy by given ID +// Deprecated: use VCDClient.GetVdcComputePolicyV2ById func (org *AdminOrg) GetVdcComputePolicyById(id string) (*VdcComputePolicy, error) { return getVdcComputePolicyById(org.client, id) } // GetVdcComputePolicyById retrieves VDC compute policy by given ID +// Deprecated: use VCDClient.GetVdcComputePolicyV2ById func (org *Org) GetVdcComputePolicyById(id string) (*VdcComputePolicy, error) { return getVdcComputePolicyById(org.client, id) } // getVdcComputePolicyById retrieves VDC compute policy by given ID +// Deprecated: Use getVdcComputePolicyV2ById instead func getVdcComputePolicyById(client *Client, id string) (*VdcComputePolicy, error) { endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcComputePolicies minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) @@ -53,7 +62,7 @@ func getVdcComputePolicyById(client *Client, id string) (*VdcComputePolicy, erro client: client, } - err = client.OpenApiGetItem(minimumApiVersion, urlRef, nil, vdcComputePolicy.VdcComputePolicy) + err = client.OpenApiGetItem(minimumApiVersion, urlRef, nil, vdcComputePolicy.VdcComputePolicy, nil) if err != nil { return nil, err } @@ -63,18 +72,28 @@ func getVdcComputePolicyById(client *Client, id string) (*VdcComputePolicy, erro // GetAllVdcComputePolicies retrieves all VDC compute policies using OpenAPI endpoint. Query parameters can be supplied to perform additional // filtering +// Deprecated: use VCDClient.GetAllVdcComputePoliciesV2 +func (client *Client) GetAllVdcComputePolicies(queryParameters url.Values) ([]*VdcComputePolicy, error) { + return getAllVdcComputePolicies(client, queryParameters) +} + +// GetAllVdcComputePolicies retrieves all VDC compute policies using OpenAPI endpoint. Query parameters can be supplied to perform additional +// filtering +// Deprecated: use VCDClient.GetAllVdcComputePoliciesV2 func (org *AdminOrg) GetAllVdcComputePolicies(queryParameters url.Values) ([]*VdcComputePolicy, error) { return getAllVdcComputePolicies(org.client, queryParameters) } // GetAllVdcComputePolicies retrieves all VDC compute policies using OpenAPI endpoint. Query parameters can be supplied to perform additional // filtering +// Deprecated: use VCDClient.GetAllVdcComputePoliciesV2 func (org *Org) GetAllVdcComputePolicies(queryParameters url.Values) ([]*VdcComputePolicy, error) { return getAllVdcComputePolicies(org.client, queryParameters) } // getAllVdcComputePolicies retrieves all VDC compute policies using OpenAPI endpoint. Query parameters can be supplied to perform additional // filtering +// Deprecated: use getAllVdcComputePoliciesV2 func getAllVdcComputePolicies(client *Client, queryParameters url.Values) ([]*VdcComputePolicy, error) { endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcComputePolicies minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) @@ -89,7 +108,7 @@ func getAllVdcComputePolicies(client *Client, queryParameters url.Values) ([]*Vd responses := []*types.VdcComputePolicy{{}} - err = client.OpenApiGetAllItems(minimumApiVersion, urlRef, queryParameters, &responses) + err = client.OpenApiGetAllItems(minimumApiVersion, urlRef, queryParameters, &responses, nil) if err != nil { return nil, err } @@ -107,24 +126,31 @@ func getAllVdcComputePolicies(client *Client, queryParameters url.Values) ([]*Vd } // CreateVdcComputePolicy creates a new VDC Compute Policy using OpenAPI endpoint +// Deprecated: use VCDClient.CreateVdcComputePolicyV2 func (org *AdminOrg) CreateVdcComputePolicy(newVdcComputePolicy *types.VdcComputePolicy) (*VdcComputePolicy, error) { + return org.client.CreateVdcComputePolicy(newVdcComputePolicy) +} + +// CreateVdcComputePolicy creates a new VDC Compute Policy using OpenAPI endpoint +// Deprecated: use VCDClient.CreateVdcComputePolicyV2 +func (client *Client) CreateVdcComputePolicy(newVdcComputePolicy *types.VdcComputePolicy) (*VdcComputePolicy, error) { endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcComputePolicies - minimumApiVersion, err := org.client.checkOpenApiEndpointCompatibility(endpoint) + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) if err != nil { return nil, err } - urlRef, err := org.client.OpenApiBuildEndpoint(endpoint) + urlRef, err := client.OpenApiBuildEndpoint(endpoint) if err != nil { return nil, err } returnVdcComputePolicy := &VdcComputePolicy{ VdcComputePolicy: &types.VdcComputePolicy{}, - client: org.client, + client: client, } - err = org.client.OpenApiPostItem(minimumApiVersion, urlRef, nil, newVdcComputePolicy, returnVdcComputePolicy.VdcComputePolicy) + err = client.OpenApiPostItem(minimumApiVersion, urlRef, nil, newVdcComputePolicy, returnVdcComputePolicy.VdcComputePolicy, nil) if err != nil { return nil, fmt.Errorf("error creating VDC compute policy: %s", err) } @@ -133,6 +159,7 @@ func (org *AdminOrg) CreateVdcComputePolicy(newVdcComputePolicy *types.VdcComput } // Update existing VDC compute policy +// Deprecated: use VdcComputePolicyV2.Update func (vdcComputePolicy *VdcComputePolicy) Update() (*VdcComputePolicy, error) { endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcComputePolicies minimumApiVersion, err := vdcComputePolicy.client.checkOpenApiEndpointCompatibility(endpoint) @@ -154,7 +181,7 @@ func (vdcComputePolicy *VdcComputePolicy) Update() (*VdcComputePolicy, error) { client: vdcComputePolicy.client, } - err = vdcComputePolicy.client.OpenApiPutItem(minimumApiVersion, urlRef, nil, vdcComputePolicy.VdcComputePolicy, returnVdcComputePolicy.VdcComputePolicy) + err = vdcComputePolicy.client.OpenApiPutItem(minimumApiVersion, urlRef, nil, vdcComputePolicy.VdcComputePolicy, returnVdcComputePolicy.VdcComputePolicy, nil) if err != nil { return nil, fmt.Errorf("error updating VDC compute policy: %s", err) } @@ -163,6 +190,7 @@ func (vdcComputePolicy *VdcComputePolicy) Update() (*VdcComputePolicy, error) { } // Delete deletes VDC compute policy +// Deprecated: use VdcComputePolicyV2.Delete func (vdcComputePolicy *VdcComputePolicy) Delete() error { endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcComputePolicies minimumApiVersion, err := vdcComputePolicy.client.checkOpenApiEndpointCompatibility(endpoint) @@ -179,7 +207,7 @@ func (vdcComputePolicy *VdcComputePolicy) Delete() error { return err } - err = vdcComputePolicy.client.OpenApiDeleteItem(minimumApiVersion, urlRef, nil) + err = vdcComputePolicy.client.OpenApiDeleteItem(minimumApiVersion, urlRef, nil, nil) if err != nil { return fmt.Errorf("error deleting VDC compute policy: %s", err) @@ -190,6 +218,7 @@ func (vdcComputePolicy *VdcComputePolicy) Delete() error { // GetAllAssignedVdcComputePolicies retrieves all VDC assigned compute policies using OpenAPI endpoint. Query parameters can be supplied to perform additional // filtering +// Deprecated: use AdminVdc.GetAllAssignedVdcComputePoliciesV2 func (vdc *AdminVdc) GetAllAssignedVdcComputePolicies(queryParameters url.Values) ([]*VdcComputePolicy, error) { endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVdcAssignedComputePolicies minimumApiVersion, err := vdc.client.checkOpenApiEndpointCompatibility(endpoint) @@ -204,7 +233,7 @@ func (vdc *AdminVdc) GetAllAssignedVdcComputePolicies(queryParameters url.Values responses := []*types.VdcComputePolicy{{}} - err = vdc.client.OpenApiGetAllItems(minimumApiVersion, urlRef, queryParameters, &responses) + err = vdc.client.OpenApiGetAllItems(minimumApiVersion, urlRef, queryParameters, &responses, nil) if err != nil { return nil, err } @@ -220,34 +249,3 @@ func (vdc *AdminVdc) GetAllAssignedVdcComputePolicies(queryParameters url.Values return wrappedVdcComputePolicies, nil } - -// SetAssignedComputePolicies assign(set) compute policies. -func (vdc *AdminVdc) SetAssignedComputePolicies(computePolicyReferences types.VdcComputePolicyReferences) (*types.VdcComputePolicyReferences, error) { - util.Logger.Printf("[TRACE] Set Compute Policies started") - - if !vdc.client.IsSysAdmin { - return nil, fmt.Errorf("functionality requires System Administrator privileges") - } - - adminVdcPolicyHREF, err := url.ParseRequestURI(vdc.AdminVdc.HREF) - if err != nil { - return nil, fmt.Errorf("error parsing VDC URL: %s", err) - } - - vdcId, err := GetUuidFromHref(vdc.AdminVdc.HREF, true) - if err != nil { - return nil, fmt.Errorf("unable to get vdc ID from HREF: %s", err) - } - adminVdcPolicyHREF.Path = "/api/admin/vdc/" + vdcId + "/computePolicies" - - returnedVdcComputePolicies := &types.VdcComputePolicyReferences{} - computePolicyReferences.Xmlns = types.XMLNamespaceVCloud - - _, err = vdc.client.ExecuteRequest(adminVdcPolicyHREF.String(), http.MethodPut, - types.MimeVdcComputePolicyReferences, "error setting compute policies for VDC: %s", computePolicyReferences, returnedVdcComputePolicies) - if err != nil { - return nil, err - } - - return returnedVdcComputePolicies, nil -} diff --git a/govcd/vdccomputepolicy_test.go b/govcd/vdccomputepolicy_test.go index a80b0c8d4..40f58801f 100644 --- a/govcd/vdccomputepolicy_test.go +++ b/govcd/vdccomputepolicy_test.go @@ -1,7 +1,7 @@ -// +build vdc functional openapi ALL +//go:build vdc || functional || openapi || ALL /* - * Copyright 2020 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + * Copyright 2022 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. */ package govcd @@ -20,53 +20,46 @@ func (vcd *TestVCD) Test_VdcComputePolicies(check *C) { check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) } - if vcd.client.Client.APIVCDMaxVersionIs("< 33.0") { - check.Skip(fmt.Sprintf("Test %s requires VCD 10.0 (API version 33) or higher", check.TestName())) - } - - org, err := vcd.client.GetAdminOrgByName(vcd.org.Org.Name) - check.Assert(err, IsNil) - check.Assert(org, NotNil) - + client := &vcd.client.Client // Step 1 - Create a new VDC compute policies newComputePolicy := &VdcComputePolicy{ - client: org.client, + client: client, VdcComputePolicy: &types.VdcComputePolicy{ Name: check.TestName() + "_empty", - Description: "Empty policy created by test", + Description: addrOf("Empty policy created by test"), }, } - createdPolicy, err := org.CreateVdcComputePolicy(newComputePolicy.VdcComputePolicy) + createdPolicy, err := client.CreateVdcComputePolicy(newComputePolicy.VdcComputePolicy) check.Assert(err, IsNil) - AddToCleanupList(createdPolicy.VdcComputePolicy.ID, "vdcComputePolicy", vcd.org.Org.Name, "Test_VdcComputePolicies") + AddToCleanupList(createdPolicy.VdcComputePolicy.ID, "vdcComputePolicy", "", check.TestName()) check.Assert(createdPolicy.VdcComputePolicy.Name, Equals, newComputePolicy.VdcComputePolicy.Name) - check.Assert(createdPolicy.VdcComputePolicy.Description, Equals, newComputePolicy.VdcComputePolicy.Description) + check.Assert(*createdPolicy.VdcComputePolicy.Description, Equals, *newComputePolicy.VdcComputePolicy.Description) newComputePolicy2 := &VdcComputePolicy{ - client: org.client, + client: client, VdcComputePolicy: &types.VdcComputePolicy{ Name: check.TestName(), - Description: "Not Empty policy created by test", - CPUSpeed: takeIntAddress(100), - CPUCount: takeIntAddress(2), - CoresPerSocket: takeIntAddress(1), + Description: addrOf("Not Empty policy created by test"), + CPUSpeed: addrOf(100), + CPUCount: addrOf(2), + CoresPerSocket: addrOf(1), CPUReservationGuarantee: takeFloatAddress(0.26), - CPULimit: takeIntAddress(200), - CPUShares: takeIntAddress(5), - Memory: takeIntAddress(1600), + CPULimit: addrOf(200), + CPUShares: addrOf(5), + Memory: addrOf(1600), MemoryReservationGuarantee: takeFloatAddress(0.5), - MemoryLimit: takeIntAddress(1200), - MemoryShares: takeIntAddress(500), + MemoryLimit: addrOf(1200), + MemoryShares: addrOf(500), }, } - createdPolicy2, err := org.CreateVdcComputePolicy(newComputePolicy2.VdcComputePolicy) + createdPolicy2, err := client.CreateVdcComputePolicy(newComputePolicy2.VdcComputePolicy) check.Assert(err, IsNil) - AddToCleanupList(createdPolicy2.VdcComputePolicy.ID, "vdcComputePolicy", vcd.org.Org.Name, "Test_VdcComputePolicies") + AddToCleanupList(createdPolicy2.VdcComputePolicy.ID, "vdcComputePolicy", "", check.TestName()) check.Assert(createdPolicy2.VdcComputePolicy.Name, Equals, newComputePolicy2.VdcComputePolicy.Name) check.Assert(*createdPolicy2.VdcComputePolicy.CPUSpeed, Equals, 100) @@ -81,13 +74,13 @@ func (vcd *TestVCD) Test_VdcComputePolicies(check *C) { check.Assert(*createdPolicy2.VdcComputePolicy.MemoryShares, Equals, 500) // Step 2 - update - createdPolicy2.VdcComputePolicy.Description = "Updated description" + createdPolicy2.VdcComputePolicy.Description = addrOf("Updated description") updatedPolicy, err := createdPolicy2.Update() check.Assert(err, IsNil) check.Assert(updatedPolicy.VdcComputePolicy, DeepEquals, createdPolicy2.VdcComputePolicy) // Step 3 - Get all VDC compute policies - allExistingPolicies, err := org.GetAllVdcComputePolicies(nil) + allExistingPolicies, err := client.GetAllVdcComputePolicies(nil) check.Assert(err, IsNil) check.Assert(allExistingPolicies, NotNil) @@ -98,12 +91,12 @@ func (vcd *TestVCD) Test_VdcComputePolicies(check *C) { queryParams := url.Values{} queryParams.Add("filter", "id=="+onePolicy.VdcComputePolicy.ID) - expectOnePolicyResultById, err := org.GetAllVdcComputePolicies(queryParams) + expectOnePolicyResultById, err := client.GetAllVdcComputePolicies(queryParams) check.Assert(err, IsNil) check.Assert(len(expectOnePolicyResultById) == 1, Equals, true) // Step 2.2 - retrieve - exactItem, err := org.GetVdcComputePolicyById(onePolicy.VdcComputePolicy.ID) + exactItem, err := client.GetVdcComputePolicyById(onePolicy.VdcComputePolicy.ID) check.Assert(err, IsNil) check.Assert(err, IsNil) @@ -118,13 +111,13 @@ func (vcd *TestVCD) Test_VdcComputePolicies(check *C) { err = createdPolicy.Delete() check.Assert(err, IsNil) // Step 5 - try to read deleted VDC computed policy should end up with error 'ErrorEntityNotFound' - deletedPolicy, err := org.GetVdcComputePolicyById(createdPolicy.VdcComputePolicy.ID) + deletedPolicy, err := client.GetVdcComputePolicyById(createdPolicy.VdcComputePolicy.ID) check.Assert(ContainsNotFound(err), Equals, true) check.Assert(deletedPolicy, IsNil) err = createdPolicy2.Delete() check.Assert(err, IsNil) - deletedPolicy2, err := org.GetVdcComputePolicyById(createdPolicy2.VdcComputePolicy.ID) + deletedPolicy2, err := client.GetVdcComputePolicyById(createdPolicy2.VdcComputePolicy.ID) check.Assert(ContainsNotFound(err), Equals, true) check.Assert(deletedPolicy2, IsNil) } @@ -134,10 +127,7 @@ func (vcd *TestVCD) Test_SetAssignedComputePolicies(check *C) { check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) } - if vcd.client.Client.APIVCDMaxVersionIs("< 33.0") { - check.Skip(fmt.Sprintf("Test %s requires VCD 10.0 (API version 33) or higher", check.TestName())) - } - + client := &vcd.client.Client org, err := vcd.client.GetAdminOrgByName(vcd.org.Org.Name) check.Assert(err, IsNil) check.Assert(org, NotNil) @@ -152,29 +142,29 @@ func (vcd *TestVCD) Test_SetAssignedComputePolicies(check *C) { client: org.client, VdcComputePolicy: &types.VdcComputePolicy{ Name: check.TestName() + "1", - Description: "Policy created by Test_SetVdcComputePolicies", - CoresPerSocket: takeIntAddress(1), + Description: addrOf("Policy created by Test_SetAssignedComputePolicies"), + CoresPerSocket: addrOf(1), CPUReservationGuarantee: takeFloatAddress(0.26), - CPULimit: takeIntAddress(200), + CPULimit: addrOf(200), }, } - createdPolicy, err := org.CreateVdcComputePolicy(newComputePolicy.VdcComputePolicy) + createdPolicy, err := client.CreateVdcComputePolicy(newComputePolicy.VdcComputePolicy) check.Assert(err, IsNil) - AddToCleanupList(createdPolicy.VdcComputePolicy.ID, "vdcComputePolicy", vcd.org.Org.Name, "Test_VdcComputePolicies") + AddToCleanupList(createdPolicy.VdcComputePolicy.ID, "vdcComputePolicy", "", check.TestName()) newComputePolicy2 := &VdcComputePolicy{ client: org.client, VdcComputePolicy: &types.VdcComputePolicy{ Name: check.TestName() + "2", - Description: "Policy created by Test_SetVdcComputePolicies", - CoresPerSocket: takeIntAddress(2), + Description: addrOf("Policy created by Test_SetAssignedComputePolicies"), + CoresPerSocket: addrOf(2), CPUReservationGuarantee: takeFloatAddress(0.52), - CPULimit: takeIntAddress(400), + CPULimit: addrOf(400), }, } - createdPolicy2, err := org.CreateVdcComputePolicy(newComputePolicy2.VdcComputePolicy) + createdPolicy2, err := client.CreateVdcComputePolicy(newComputePolicy2.VdcComputePolicy) check.Assert(err, IsNil) - AddToCleanupList(createdPolicy2.VdcComputePolicy.ID, "vdcComputePolicy", vcd.org.Org.Name, "Test_VdcComputePolicies") + AddToCleanupList(createdPolicy2.VdcComputePolicy.ID, "vdcComputePolicy", "", check.TestName()) // Get default compute policy allAssignedComputePolicies, err := adminVdc.GetAllAssignedVdcComputePolicies(nil) @@ -207,4 +197,9 @@ func (vcd *TestVCD) Test_SetAssignedComputePolicies(check *C) { _, err = adminVdc.SetAssignedComputePolicies(policyReferences) check.Assert(err, IsNil) + + err = createdPolicy.Delete() + check.Assert(err, IsNil) + err = createdPolicy2.Delete() + check.Assert(err, IsNil) } diff --git a/govcd/vdccomputepolicy_v2.go b/govcd/vdccomputepolicy_v2.go new file mode 100644 index 000000000..68cc47a9b --- /dev/null +++ b/govcd/vdccomputepolicy_v2.go @@ -0,0 +1,271 @@ +package govcd + +/* + * Copyright 2022 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +import ( + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + "github.com/vmware/go-vcloud-director/v2/util" +) + +// VdcComputePolicyV2 defines a VDC Compute Policy, which can be a VM Sizing Policy, a VM Placement Policy or a vGPU Policy. +type VdcComputePolicyV2 struct { + VdcComputePolicyV2 *types.VdcComputePolicyV2 + Href string + client *Client +} + +// GetVdcComputePolicyV2ById retrieves VDC Compute Policy (V2) by given ID +func (client *VCDClient) GetVdcComputePolicyV2ById(id string) (*VdcComputePolicyV2, error) { + return getVdcComputePolicyV2ById(&client.Client, id) +} + +// getVdcComputePolicyV2ById retrieves VDC Compute Policy (V2) by given ID +func getVdcComputePolicyV2ById(client *Client, id string) (*VdcComputePolicyV2, error) { + endpoint := types.OpenApiPathVersion2_0_0 + types.OpenApiEndpointVdcComputePolicies + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + if id == "" { + return nil, fmt.Errorf("empty VDC id") + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, id) + + if err != nil { + return nil, err + } + + vdcComputePolicy := &VdcComputePolicyV2{ + VdcComputePolicyV2: &types.VdcComputePolicyV2{}, + Href: urlRef.String(), + client: client, + } + + err = client.OpenApiGetItem(minimumApiVersion, urlRef, nil, vdcComputePolicy.VdcComputePolicyV2, nil) + if err != nil { + return nil, err + } + + return vdcComputePolicy, nil +} + +// GetAllVdcComputePoliciesV2 retrieves all VDC Compute Policies (V2) using OpenAPI endpoint. Query parameters can be supplied to perform additional +// filtering +func (client *VCDClient) GetAllVdcComputePoliciesV2(queryParameters url.Values) ([]*VdcComputePolicyV2, error) { + return getAllVdcComputePoliciesV2(&client.Client, queryParameters) +} + +// getAllVdcComputePolicies retrieves all VDC Compute Policies (V2) using OpenAPI endpoint. Query parameters can be supplied to perform additional +// filtering +func getAllVdcComputePoliciesV2(client *Client, queryParameters url.Values) ([]*VdcComputePolicyV2, error) { + endpoint := types.OpenApiPathVersion2_0_0 + types.OpenApiEndpointVdcComputePolicies + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + responses := []*types.VdcComputePolicyV2{{}} + + err = client.OpenApiGetAllItems(minimumApiVersion, urlRef, queryParameters, &responses, nil) + if err != nil { + return nil, err + } + + var wrappedVdcComputePolicies []*VdcComputePolicyV2 + for _, response := range responses { + wrappedVdcComputePolicy := &VdcComputePolicyV2{ + client: client, + VdcComputePolicyV2: response, + } + wrappedVdcComputePolicies = append(wrappedVdcComputePolicies, wrappedVdcComputePolicy) + } + + return wrappedVdcComputePolicies, nil +} + +// CreateVdcComputePolicyV2 creates a new VDC Compute Policy (V2) using OpenAPI endpoint +func (client *VCDClient) CreateVdcComputePolicyV2(newVdcComputePolicy *types.VdcComputePolicyV2) (*VdcComputePolicyV2, error) { + endpoint := types.OpenApiPathVersion2_0_0 + types.OpenApiEndpointVdcComputePolicies + minimumApiVersion, err := client.Client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.Client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + returnVdcComputePolicy := &VdcComputePolicyV2{ + VdcComputePolicyV2: &types.VdcComputePolicyV2{}, + client: &client.Client, + } + + err = client.Client.OpenApiPostItem(minimumApiVersion, urlRef, nil, newVdcComputePolicy, returnVdcComputePolicy.VdcComputePolicyV2, nil) + if err != nil { + return nil, fmt.Errorf("error creating VDC Compute Policy: %s", getFriendlyErrorIfVmPlacementPolicyAlreadyExists(newVdcComputePolicy.Name, err)) + } + + return returnVdcComputePolicy, nil +} + +// Update existing VDC Compute Policy (V2) +func (vdcComputePolicy *VdcComputePolicyV2) Update() (*VdcComputePolicyV2, error) { + endpoint := types.OpenApiPathVersion2_0_0 + types.OpenApiEndpointVdcComputePolicies + minimumApiVersion, err := vdcComputePolicy.client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + if vdcComputePolicy.VdcComputePolicyV2.ID == "" { + return nil, fmt.Errorf("cannot update VDC Compute Policy without ID") + } + + urlRef, err := vdcComputePolicy.client.OpenApiBuildEndpoint(endpoint, vdcComputePolicy.VdcComputePolicyV2.ID) + if err != nil { + return nil, err + } + + returnVdcComputePolicy := &VdcComputePolicyV2{ + VdcComputePolicyV2: &types.VdcComputePolicyV2{}, + client: vdcComputePolicy.client, + } + + err = vdcComputePolicy.client.OpenApiPutItem(minimumApiVersion, urlRef, nil, vdcComputePolicy.VdcComputePolicyV2, returnVdcComputePolicy.VdcComputePolicyV2, nil) + if err != nil { + return nil, fmt.Errorf("error updating VDC Compute Policy: %s", err) + } + + return returnVdcComputePolicy, nil +} + +// Delete deletes VDC Compute Policy (V2) +func (vdcComputePolicy *VdcComputePolicyV2) Delete() error { + endpoint := types.OpenApiPathVersion2_0_0 + types.OpenApiEndpointVdcComputePolicies + minimumApiVersion, err := vdcComputePolicy.client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return err + } + + if vdcComputePolicy.VdcComputePolicyV2.ID == "" { + return fmt.Errorf("cannot delete VDC Compute Policy without id") + } + + urlRef, err := vdcComputePolicy.client.OpenApiBuildEndpoint(endpoint, vdcComputePolicy.VdcComputePolicyV2.ID) + if err != nil { + return err + } + + err = vdcComputePolicy.client.OpenApiDeleteItem(minimumApiVersion, urlRef, nil, nil) + + if err != nil { + return fmt.Errorf("error deleting VDC Compute Policy: %s", err) + } + + return nil +} + +// GetAllAssignedVdcComputePoliciesV2 retrieves all VDC assigned Compute Policies (V2) using OpenAPI endpoint. Query parameters can be supplied to perform additional +// filtering +func (vdc *AdminVdc) GetAllAssignedVdcComputePoliciesV2(queryParameters url.Values) ([]*VdcComputePolicyV2, error) { + return getAllAssignedVdcComputePoliciesV2(vdc.client, vdc.AdminVdc.ID, queryParameters) +} + +// GetAllAssignedVdcComputePoliciesV2 retrieves all VDC assigned Compute Policies (V2) using OpenAPI endpoint and the mandatory VDC identifier. +// Query parameters can be supplied to perform additional filtering +func (vcdClient *VCDClient) GetAllAssignedVdcComputePoliciesV2(vdcId string, queryParameters url.Values) ([]*VdcComputePolicyV2, error) { + return getAllAssignedVdcComputePoliciesV2(&vcdClient.Client, vdcId, queryParameters) +} + +// getAllAssignedVdcComputePoliciesV2 retrieves all VDC assigned Compute Policies (V2) using OpenAPI endpoint and the mandatory VDC identifier. +// Query parameters can be supplied to perform additional filtering +func getAllAssignedVdcComputePoliciesV2(client *Client, vdcId string, queryParameters url.Values) ([]*VdcComputePolicyV2, error) { + if strings.TrimSpace(vdcId) == "" { + return nil, fmt.Errorf("VDC ID is mandatory to retrieve its assigned VDC Compute Policies") + } + + endpoint := types.OpenApiPathVersion2_0_0 + types.OpenApiEndpointVdcAssignedComputePolicies + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, vdcId)) + if err != nil { + return nil, err + } + + responses := []*types.VdcComputePolicyV2{{}} + + err = client.OpenApiGetAllItems(minimumApiVersion, urlRef, queryParameters, &responses, nil) + if err != nil { + return nil, err + } + + var wrappedVdcComputePolicies []*VdcComputePolicyV2 + for _, response := range responses { + wrappedVdcComputePolicy := &VdcComputePolicyV2{ + client: client, + VdcComputePolicyV2: response, + } + wrappedVdcComputePolicies = append(wrappedVdcComputePolicies, wrappedVdcComputePolicy) + } + + return wrappedVdcComputePolicies, nil +} + +// SetAssignedComputePolicies assign(set) Compute Policies to the receiver VDC. +func (vdc *AdminVdc) SetAssignedComputePolicies(computePolicyReferences types.VdcComputePolicyReferences) (*types.VdcComputePolicyReferences, error) { + util.Logger.Printf("[TRACE] Set Compute Policies started") + + if !vdc.client.IsSysAdmin { + return nil, fmt.Errorf("functionality requires System Administrator privileges") + } + + adminVdcPolicyHREF, err := url.ParseRequestURI(vdc.AdminVdc.HREF) + if err != nil { + return nil, fmt.Errorf("error parsing VDC URL: %s", err) + } + + vdcId, err := GetUuidFromHref(vdc.AdminVdc.HREF, true) + if err != nil { + return nil, fmt.Errorf("unable to get vdc ID from HREF: %s", err) + } + adminVdcPolicyHREF.Path = "/api/admin/vdc/" + vdcId + "/computePolicies" + + returnedVdcComputePolicies := &types.VdcComputePolicyReferences{} + computePolicyReferences.Xmlns = types.XMLNamespaceVCloud + + _, err = vdc.client.ExecuteRequest(adminVdcPolicyHREF.String(), http.MethodPut, + types.MimeVdcComputePolicyReferences, "error setting Compute Policies for VDC: %s", computePolicyReferences, returnedVdcComputePolicies) + if err != nil { + return nil, err + } + + return returnedVdcComputePolicies, nil +} + +// getFriendlyErrorIfVmPlacementPolicyAlreadyExists is intended to be used when a VM Placement Policy already exists, and +// we try to create another one with the same name. When this happens, VCD discloses a lot of unnecessary information to the user that is +// hard to read and understand, so this function simplifies the message. +// Note: This function should not be needed anymore once VCD 10.4.0 is discontinued (this issue is fixed in 10.4.1). +func getFriendlyErrorIfVmPlacementPolicyAlreadyExists(vmPlacementPolicyName string, err error) error { + if err != nil && strings.Contains(err.Error(), "already exists") && strings.Contains(err.Error(), "duplicate key") { + return fmt.Errorf("VM Placement Policy with name '%s' already exists", vmPlacementPolicyName) + } + return err +} diff --git a/govcd/vdccomputepolicy_v2_test.go b/govcd/vdccomputepolicy_v2_test.go new file mode 100644 index 000000000..3a1e14c6d --- /dev/null +++ b/govcd/vdccomputepolicy_v2_test.go @@ -0,0 +1,390 @@ +//go:build vdc || functional || openapi || ALL + +/* + * Copyright 2022 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "net/url" + "strings" + + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_VdcComputePoliciesV2(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + + // Step 1 - Create a new VDC Compute Policy + newComputePolicy := &VdcComputePolicyV2{ + client: &vcd.client.Client, + VdcComputePolicyV2: &types.VdcComputePolicyV2{ + VdcComputePolicy: types.VdcComputePolicy{ + Name: check.TestName() + "_empty", + Description: addrOf("Empty policy created by test"), + }, + PolicyType: "VdcVmPolicy", + }, + } + + createdPolicy, err := vcd.client.CreateVdcComputePolicyV2(newComputePolicy.VdcComputePolicyV2) + check.Assert(err, IsNil) + + AddToCleanupList(createdPolicy.VdcComputePolicyV2.ID, "vdcComputePolicy", "", check.TestName()) + + check.Assert(createdPolicy.VdcComputePolicyV2.Name, Equals, newComputePolicy.VdcComputePolicyV2.Name) + check.Assert(*createdPolicy.VdcComputePolicyV2.Description, Equals, *newComputePolicy.VdcComputePolicyV2.Description) + + newComputePolicy2 := &VdcComputePolicyV2{ + client: &vcd.client.Client, + VdcComputePolicyV2: &types.VdcComputePolicyV2{ + VdcComputePolicy: types.VdcComputePolicy{ + Name: check.TestName(), + Description: addrOf("Not Empty policy created by test"), + CPUSpeed: addrOf(100), + CPUCount: addrOf(2), + CoresPerSocket: addrOf(1), + CPUReservationGuarantee: takeFloatAddress(0.26), + CPULimit: addrOf(200), + CPUShares: addrOf(5), + Memory: addrOf(1600), + MemoryReservationGuarantee: takeFloatAddress(0.5), + MemoryLimit: addrOf(1200), + MemoryShares: addrOf(500), + }, + PolicyType: "VdcVmPolicy", + }, + } + + createdPolicy2, err := vcd.client.CreateVdcComputePolicyV2(newComputePolicy2.VdcComputePolicyV2) + check.Assert(err, IsNil) + + AddToCleanupList(createdPolicy2.VdcComputePolicyV2.ID, "vdcComputePolicy", "", check.TestName()) + + check.Assert(createdPolicy2.VdcComputePolicyV2.Name, Equals, newComputePolicy2.VdcComputePolicyV2.Name) + check.Assert(*createdPolicy2.VdcComputePolicyV2.CPUSpeed, Equals, 100) + check.Assert(*createdPolicy2.VdcComputePolicyV2.CPUCount, Equals, 2) + check.Assert(*createdPolicy2.VdcComputePolicyV2.CoresPerSocket, Equals, 1) + check.Assert(*createdPolicy2.VdcComputePolicyV2.CPUReservationGuarantee, Equals, 0.26) + check.Assert(*createdPolicy2.VdcComputePolicyV2.CPULimit, Equals, 200) + check.Assert(*createdPolicy2.VdcComputePolicyV2.CPUShares, Equals, 5) + check.Assert(*createdPolicy2.VdcComputePolicyV2.Memory, Equals, 1600) + check.Assert(*createdPolicy2.VdcComputePolicyV2.MemoryReservationGuarantee, Equals, 0.5) + check.Assert(*createdPolicy2.VdcComputePolicyV2.MemoryLimit, Equals, 1200) + check.Assert(*createdPolicy2.VdcComputePolicyV2.MemoryShares, Equals, 500) + + // Step 2 - Update + createdPolicy2.VdcComputePolicyV2.Description = addrOf("Updated description") + updatedPolicy, err := createdPolicy2.Update() + check.Assert(err, IsNil) + check.Assert(updatedPolicy.VdcComputePolicyV2, DeepEquals, createdPolicy2.VdcComputePolicyV2) + + // Step 3 - Get all VDC compute policies + allExistingPolicies, err := vcd.client.GetAllVdcComputePoliciesV2(nil) + check.Assert(err, IsNil) + check.Assert(allExistingPolicies, NotNil) + + // Step 4 - Get all VDC compute policies using query filters + for _, onePolicy := range allExistingPolicies { + + // Step 3.1 - Retrieve using FIQL filter + queryParams := url.Values{} + queryParams.Add("filter", "id=="+onePolicy.VdcComputePolicyV2.ID) + + expectOnePolicyResultById, err := vcd.client.GetAllVdcComputePoliciesV2(queryParams) + check.Assert(err, IsNil) + check.Assert(len(expectOnePolicyResultById) == 1, Equals, true) + + // Step 2.2 - Retrieve + exactItem, err := vcd.client.GetVdcComputePolicyV2ById(onePolicy.VdcComputePolicyV2.ID) + check.Assert(err, IsNil) + + check.Assert(err, IsNil) + check.Assert(exactItem, NotNil) + + // Step 2.3 - Compare struct retrieved by using filter and the one retrieved by exact ID + check.Assert(onePolicy, DeepEquals, expectOnePolicyResultById[0]) + + } + + // Step 5 - Delete + err = createdPolicy.Delete() + check.Assert(err, IsNil) + // Step 5 - Try to read deleted VDC computed policy should end up with error 'ErrorEntityNotFound' + deletedPolicy, err := vcd.client.GetVdcComputePolicyV2ById(createdPolicy.VdcComputePolicyV2.ID) + check.Assert(ContainsNotFound(err), Equals, true) + check.Assert(deletedPolicy, IsNil) + + err = createdPolicy2.Delete() + check.Assert(err, IsNil) + deletedPolicy2, err := vcd.client.GetVdcComputePolicyV2ById(createdPolicy2.VdcComputePolicyV2.ID) + check.Assert(ContainsNotFound(err), Equals, true) + check.Assert(deletedPolicy2, IsNil) +} + +func (vcd *TestVCD) Test_SetAssignedComputePoliciesV2(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + + org, err := vcd.client.GetAdminOrgByName(vcd.org.Org.Name) + check.Assert(err, IsNil) + check.Assert(org, NotNil) + + adminVdc, err := org.GetAdminVDCByName(vcd.vdc.Vdc.Name, false) + check.Assert(err, IsNil) + check.Assert(adminVdc, NotNil) + + // Create a new VDC compute policies + newComputePolicy := &VdcComputePolicyV2{ + client: &vcd.client.Client, + VdcComputePolicyV2: &types.VdcComputePolicyV2{ + VdcComputePolicy: types.VdcComputePolicy{ + Name: check.TestName() + "1", + Description: addrOf("Policy created by Test_SetAssignedComputePolicies"), + CoresPerSocket: addrOf(1), + CPUReservationGuarantee: takeFloatAddress(0.26), + CPULimit: addrOf(200), + }, + PolicyType: "VdcVmPolicy", + }, + } + createdPolicy, err := vcd.client.CreateVdcComputePolicyV2(newComputePolicy.VdcComputePolicyV2) + check.Assert(err, IsNil) + AddToCleanupList(createdPolicy.VdcComputePolicyV2.ID, "vdcComputePolicy", "", check.TestName()) + + newComputePolicy2 := &VdcComputePolicyV2{ + client: &vcd.client.Client, + VdcComputePolicyV2: &types.VdcComputePolicyV2{ + VdcComputePolicy: types.VdcComputePolicy{ + Name: check.TestName() + "2", + Description: addrOf("Policy created by Test_SetAssignedComputePolicies"), + CoresPerSocket: addrOf(2), + CPUReservationGuarantee: takeFloatAddress(0.52), + CPULimit: addrOf(400), + }, + PolicyType: "VdcVmPolicy", + }, + } + createdPolicy2, err := vcd.client.CreateVdcComputePolicyV2(newComputePolicy2.VdcComputePolicyV2) + check.Assert(err, IsNil) + AddToCleanupList(createdPolicy2.VdcComputePolicyV2.ID, "vdcComputePolicy", "", check.TestName()) + + // Get default compute policy + allAssignedComputePolicies, err := adminVdc.GetAllAssignedVdcComputePoliciesV2(nil) + check.Assert(err, IsNil) + var defaultPolicyId string + for _, assignedPolicy := range allAssignedComputePolicies { + if assignedPolicy.VdcComputePolicyV2.ID == vcd.vdc.Vdc.DefaultComputePolicy.ID { + defaultPolicyId = assignedPolicy.VdcComputePolicyV2.ID + } + } + allAssignedComputePolicies, err = vcd.client.GetAllAssignedVdcComputePoliciesV2(adminVdc.AdminVdc.ID, nil) + check.Assert(err, IsNil) + for _, assignedPolicy := range allAssignedComputePolicies { + if assignedPolicy.VdcComputePolicyV2.ID == vcd.vdc.Vdc.DefaultComputePolicy.ID { + defaultPolicyId = assignedPolicy.VdcComputePolicyV2.ID + } + } + + vdcComputePolicyHref, err := org.client.OpenApiBuildEndpoint(types.OpenApiPathVersion2_0_0, types.OpenApiEndpointVdcComputePolicies) + check.Assert(err, IsNil) + + // Assign compute policies to VDC + policyReferences := types.VdcComputePolicyReferences{VdcComputePolicyReference: []*types.Reference{ + {HREF: vdcComputePolicyHref.String() + createdPolicy.VdcComputePolicyV2.ID}, + {HREF: vdcComputePolicyHref.String() + createdPolicy2.VdcComputePolicyV2.ID}, + {HREF: vdcComputePolicyHref.String() + defaultPolicyId}}} + + assignedVdcComputePolicies, err := adminVdc.SetAssignedComputePolicies(policyReferences) + check.Assert(err, IsNil) + check.Assert(strings.SplitAfter(policyReferences.VdcComputePolicyReference[0].HREF, "vdcComputePolicy:")[1], Equals, + strings.SplitAfter(assignedVdcComputePolicies.VdcComputePolicyReference[0].HREF, "vdcComputePolicy:")[1]) + check.Assert(strings.SplitAfter(policyReferences.VdcComputePolicyReference[1].HREF, "vdcComputePolicy:")[1], Equals, + strings.SplitAfter(assignedVdcComputePolicies.VdcComputePolicyReference[1].HREF, "vdcComputePolicy:")[1]) + + // Cleanup assigned compute policies + policyReferences = types.VdcComputePolicyReferences{VdcComputePolicyReference: []*types.Reference{ + {HREF: vdcComputePolicyHref.String() + defaultPolicyId}}} + + _, err = adminVdc.SetAssignedComputePolicies(policyReferences) + check.Assert(err, IsNil) + + err = createdPolicy.Delete() + check.Assert(err, IsNil) + err = createdPolicy2.Delete() + check.Assert(err, IsNil) +} + +// Test_VdcVmPlacementPoliciesV2 is similar to Test_VdcComputePoliciesV2 but focused on VM Placement Policies +func (vcd *TestVCD) Test_VdcVmPlacementPoliciesV2(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + + if vcd.config.VCD.NsxtProviderVdc.PlacementPolicyVmGroup == "" { + check.Skip("The configuration entry vcd.nsxt_provider_vdc.placementPolicyVmGroup is needed") + } + + // We need the Provider VDC URN + pVdc, err := vcd.client.GetProviderVdcByName(vcd.config.VCD.NsxtProviderVdc.Name) + check.Assert(err, IsNil) + + // We also need the VM Group to create a VM Placement Policy + vmGroup, err := vcd.client.GetVmGroupByNameAndProviderVdcUrn(vcd.config.VCD.NsxtProviderVdc.PlacementPolicyVmGroup, pVdc.ProviderVdc.ID) + check.Assert(err, IsNil) + check.Assert(vmGroup.VmGroup.Name, Equals, vcd.config.VCD.NsxtProviderVdc.PlacementPolicyVmGroup) + + // We'll also use a Logical VM Group to create the VM Placement Policy + logicalVmGroup, err := vcd.client.CreateLogicalVmGroup(types.LogicalVmGroup{ + Name: check.TestName(), + NamedVmGroupReferences: types.OpenApiReferences{ + types.OpenApiReference{ + ID: fmt.Sprintf("%s:%s", vmGroupUrnPrefix, vmGroup.VmGroup.NamedVmGroupId), + Name: vmGroup.VmGroup.Name}, + }, + PvdcID: pVdc.ProviderVdc.ID, + }) + check.Assert(err, IsNil) + AddToCleanupList(logicalVmGroup.LogicalVmGroup.ID, "logicalVmGroup", "", check.TestName()) + + // Create a new VDC Compute Policy (VM Placement Policy) + newComputePolicy := &VdcComputePolicyV2{ + client: &vcd.client.Client, + VdcComputePolicyV2: &types.VdcComputePolicyV2{ + VdcComputePolicy: types.VdcComputePolicy{ + Name: check.TestName() + "_empty", + Description: addrOf("VM Placement Policy created by " + check.TestName()), + }, + PolicyType: "VdcVmPolicy", + PvdcNamedVmGroupsMap: []types.PvdcNamedVmGroupsMap{ + { + NamedVmGroups: []types.OpenApiReferences{ + { + types.OpenApiReference{ + Name: vmGroup.VmGroup.Name, + ID: fmt.Sprintf("%s:%s", vmGroupUrnPrefix, vmGroup.VmGroup.NamedVmGroupId), + }, + }, + }, + Pvdc: types.OpenApiReference{ + Name: pVdc.ProviderVdc.Name, + ID: pVdc.ProviderVdc.ID, + }, + }, + }, + PvdcLogicalVmGroupsMap: []types.PvdcLogicalVmGroupsMap{ + { + LogicalVmGroups: types.OpenApiReferences{ + types.OpenApiReference{ + Name: logicalVmGroup.LogicalVmGroup.Name, + ID: logicalVmGroup.LogicalVmGroup.ID, + }, + }, + Pvdc: types.OpenApiReference{ + Name: pVdc.ProviderVdc.Name, + ID: pVdc.ProviderVdc.ID, + }, + }, + }, + }, + } + + createdPolicy, err := vcd.client.CreateVdcComputePolicyV2(newComputePolicy.VdcComputePolicyV2) + check.Assert(err, IsNil) + + AddToCleanupList(createdPolicy.VdcComputePolicyV2.ID, "vdcComputePolicy", "", check.TestName()) + + check.Assert(createdPolicy.VdcComputePolicyV2.Name, Equals, newComputePolicy.VdcComputePolicyV2.Name) + check.Assert(*createdPolicy.VdcComputePolicyV2.Description, Equals, *newComputePolicy.VdcComputePolicyV2.Description) + check.Assert(createdPolicy.VdcComputePolicyV2.PvdcLogicalVmGroupsMap, DeepEquals, newComputePolicy.VdcComputePolicyV2.PvdcLogicalVmGroupsMap) + check.Assert(createdPolicy.VdcComputePolicyV2.PvdcNamedVmGroupsMap, DeepEquals, newComputePolicy.VdcComputePolicyV2.PvdcNamedVmGroupsMap) + + // Update the VM Placement Policy + createdPolicy.VdcComputePolicyV2.Description = addrOf("Updated description") + updatedPolicy, err := createdPolicy.Update() + check.Assert(err, IsNil) + check.Assert(updatedPolicy.VdcComputePolicyV2, DeepEquals, createdPolicy.VdcComputePolicyV2) + + // Delete the VM Placement Policy and check it doesn't exist anymore + err = createdPolicy.Delete() + check.Assert(err, IsNil) + deletedPolicy, err := vcd.client.GetVdcComputePolicyV2ById(createdPolicy.VdcComputePolicyV2.ID) + check.Assert(ContainsNotFound(err), Equals, true) + check.Assert(deletedPolicy, IsNil) + + // Clean up + err = logicalVmGroup.Delete() + check.Assert(err, IsNil) +} + +// Test_VdcDuplicatedVmPlacementPolicyGetsACleanError checks that when creating a duplicated VM Placement Policy, consumers +// of the SDK get a nicely formatted error. +// This test should not be needed once function `getFriendlyErrorIfVmPlacementPolicyAlreadyExists` is removed. +func (vcd *TestVCD) Test_VdcDuplicatedVmPlacementPolicyGetsACleanError(check *C) { + if vcd.client.Client.APIVCDMaxVersionIs(">= 37.2") { + check.Skip("The bug that this test checks for is fixed in 10.4.2") + } + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + + if vcd.config.VCD.NsxtProviderVdc.PlacementPolicyVmGroup == "" { + check.Skip("The configuration entry vcd.nsxt_provider_vdc.placementPolicyVmGroup is needed") + } + + // We need the Provider VDC URN + pVdc, err := vcd.client.GetProviderVdcByName(vcd.config.VCD.NsxtProviderVdc.Name) + check.Assert(err, IsNil) + + // We also need the VM Group to create a VM Placement Policy + vmGroup, err := vcd.client.GetVmGroupByNameAndProviderVdcUrn(vcd.config.VCD.NsxtProviderVdc.PlacementPolicyVmGroup, pVdc.ProviderVdc.ID) + check.Assert(err, IsNil) + check.Assert(vmGroup.VmGroup.Name, Equals, vcd.config.VCD.NsxtProviderVdc.PlacementPolicyVmGroup) + + // Create a new VDC Compute Policy (VM Placement Policy) + newComputePolicy := &VdcComputePolicyV2{ + client: &vcd.client.Client, + VdcComputePolicyV2: &types.VdcComputePolicyV2{ + VdcComputePolicy: types.VdcComputePolicy{ + Name: check.TestName(), + Description: addrOf("VM Placement Policy created by " + check.TestName()), + }, + PolicyType: "VdcVmPolicy", + PvdcNamedVmGroupsMap: []types.PvdcNamedVmGroupsMap{ + { + NamedVmGroups: []types.OpenApiReferences{ + { + types.OpenApiReference{ + Name: vmGroup.VmGroup.Name, + ID: fmt.Sprintf("%s:%s", vmGroupUrnPrefix, vmGroup.VmGroup.NamedVmGroupId), + }, + }, + }, + Pvdc: types.OpenApiReference{ + Name: pVdc.ProviderVdc.Name, + ID: pVdc.ProviderVdc.ID, + }, + }, + }, + }, + } + + createdPolicy, err := vcd.client.CreateVdcComputePolicyV2(newComputePolicy.VdcComputePolicyV2) + check.Assert(err, IsNil) + check.Assert(createdPolicy, NotNil) + + AddToCleanupList(createdPolicy.VdcComputePolicyV2.ID, "vdcComputePolicy", "", check.TestName()) + + _, err = vcd.client.CreateVdcComputePolicyV2(newComputePolicy.VdcComputePolicyV2) + check.Assert(err, NotNil) + check.Assert(true, Equals, strings.Contains(err.Error(), "VM Placement Policy with name '"+check.TestName()+"' already exists")) + + err = createdPolicy.Delete() + check.Assert(err, IsNil) +} diff --git a/govcd/vgpu_profile.go b/govcd/vgpu_profile.go new file mode 100644 index 000000000..b42e23d0f --- /dev/null +++ b/govcd/vgpu_profile.go @@ -0,0 +1,155 @@ +package govcd + +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +import ( + "fmt" + "net/url" + + "github.com/vmware/go-vcloud-director/v2/types/v56" +) + +// VgpuProfile defines a vGPU profile which is fetched from vCenter +type VgpuProfile struct { + VgpuProfile *types.VgpuProfile + client *Client +} + +// GetAllVgpuProfiles gets all vGPU profiles that are available to VCD +func (client *VCDClient) GetAllVgpuProfiles(queryParameters url.Values) ([]*VgpuProfile, error) { + return getAllVgpuProfiles(queryParameters, &client.Client) +} + +// GetVgpuProfilesByProviderVdc gets all vGPU profiles that are available to a specific provider VDC +func (client *VCDClient) GetVgpuProfilesByProviderVdc(providerVdcUrn string) ([]*VgpuProfile, error) { + queryParameters := url.Values{} + queryParameters = queryParameterFilterAnd(fmt.Sprintf("pvdcId==%s", providerVdcUrn), queryParameters) + return client.GetAllVgpuProfiles(queryParameters) +} + +// GetVgpuProfileById gets a vGPU profile by ID +func (client *VCDClient) GetVgpuProfileById(vgpuProfileId string) (*VgpuProfile, error) { + return getVgpuProfileById(vgpuProfileId, &client.Client) +} + +// GetVgpuProfileByName gets a vGPU profile by name +func (client *VCDClient) GetVgpuProfileByName(vgpuProfileName string) (*VgpuProfile, error) { + return getVgpuProfileByFilter("name", vgpuProfileName, &client.Client) +} + +// GetVgpuProfileByTenantFacingName gets a vGPU profile by its tenant facing name +func (client *VCDClient) GetVgpuProfileByTenantFacingName(tenantFacingName string) (*VgpuProfile, error) { + return getVgpuProfileByFilter("tenantFacingName", tenantFacingName, &client.Client) +} + +// Update updates a vGPU profile with new parameters +func (profile *VgpuProfile) Update(newProfile *types.VgpuProfile) error { + client := profile.client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVgpuProfile + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, "/", profile.VgpuProfile.Id) + if err != nil { + return err + } + + err = client.OpenApiPutItemSync(minimumApiVersion, urlRef, nil, newProfile, nil, nil) + if err != nil { + return err + } + + // We need to refresh here, as PUT returns the original struct instead of the updated one + err = profile.Refresh() + if err != nil { + return err + } + + return nil +} + +// Refresh updates the current state of the vGPU profile +func (profile *VgpuProfile) Refresh() error { + var err error + newProfile, err := getVgpuProfileById(profile.VgpuProfile.Id, profile.client) + if err != nil { + return err + } + profile.VgpuProfile = newProfile.VgpuProfile + + return nil +} + +func getVgpuProfileByFilter(filter, filterValue string, client *Client) (*VgpuProfile, error) { + queryParameters := url.Values{} + queryParameters = queryParameterFilterAnd(fmt.Sprintf("%s==%s", filter, filterValue), queryParameters) + vgpuProfiles, err := getAllVgpuProfiles(queryParameters, client) + if err != nil { + return nil, err + } + + vgpuProfile, err := oneOrError(filter, filterValue, vgpuProfiles) + if err != nil { + return nil, err + } + + return vgpuProfile, nil +} + +func getVgpuProfileById(vgpuProfileId string, client *Client) (*VgpuProfile, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVgpuProfile + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint, "/", vgpuProfileId) + if err != nil { + return nil, err + } + + profile := &VgpuProfile{ + client: client, + } + err = client.OpenApiGetItem(minimumApiVersion, urlRef, nil, &profile.VgpuProfile, nil) + if err != nil { + return nil, err + } + + return profile, nil +} + +func getAllVgpuProfiles(queryParameters url.Values, client *Client) ([]*VgpuProfile, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVgpuProfile + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + responses := []*types.VgpuProfile{{}} + + err = client.OpenApiGetAllItems(minimumApiVersion, urlRef, queryParameters, &responses, nil) + if err != nil { + return nil, err + } + + wrappedVgpuProfiles := make([]*VgpuProfile, len(responses)) + for index, response := range responses { + wrappedVgpuProfile := &VgpuProfile{ + client: client, + VgpuProfile: response, + } + wrappedVgpuProfiles[index] = wrappedVgpuProfile + } + + return wrappedVgpuProfiles, nil +} diff --git a/govcd/vm.go b/govcd/vm.go index 25669438e..f2017d250 100644 --- a/govcd/vm.go +++ b/govcd/vm.go @@ -5,11 +5,9 @@ package govcd import ( - "errors" "fmt" "net" "net/http" - "net/url" "strconv" "strings" "time" @@ -35,7 +33,7 @@ func NewVM(cli *Client) *VM { } } -// create instance with reference to types.QueryResultVMRecordType +// NewVMRecord creates an instance with reference to types.QueryResultVMRecordType func NewVMRecord(cli *Client) *VMRecord { return &VMRecord{ VM: new(types.QueryResultVMRecordType), @@ -72,9 +70,8 @@ func (vm *VM) Refresh() error { // elements in slices. vm.VM = &types.Vm{} - _, err := vm.client.ExecuteRequestWithApiVersion(refreshUrl, http.MethodGet, - "", "error refreshing VM: %s", nil, vm.VM, - vm.client.GetSpecificApiVersionOnCondition(">= 33.0", "33.0")) + // 37.1 Introduced BootOptions and Firmware parameters of a VM + _, err := vm.client.ExecuteRequestWithApiVersion(refreshUrl, http.MethodGet, "", "error refreshing VM: %s", nil, vm.VM, vm.client.GetSpecificApiVersionOnCondition(">=37.1", "37.1")) // The request was successful return err @@ -129,6 +126,7 @@ func (vm *VM) UpdateNetworkConnectionSection(networks *types.NetworkConnectionSe updateNetwork.PrimaryNetworkConnectionIndex = networks.PrimaryNetworkConnectionIndex updateNetwork.NetworkConnection = networks.NetworkConnection updateNetwork.Ovf = types.XMLNamespaceOVF + updateNetwork.Xmlns = types.XMLNamespaceVCloud task, err := vm.client.ExecuteTaskRequest(vm.VM.HREF+"/networkConnectionSection/", http.MethodPut, types.MimeNetworkConnectionSection, "error updating network connection: %s", updateNetwork) @@ -144,11 +142,11 @@ func (vm *VM) UpdateNetworkConnectionSection(networks *types.NetworkConnectionSe } // Deprecated: use client.GetVMByHref instead -func (cli *Client) FindVMByHREF(vmHREF string) (VM, error) { +func (client *Client) FindVMByHREF(vmHREF string) (VM, error) { - newVm := NewVM(cli) + newVm := NewVM(client) - _, err := cli.ExecuteRequest(vmHREF, http.MethodGet, + _, err := client.ExecuteRequest(vmHREF, http.MethodGet, "", "error retrieving VM: %s", nil, newVm.VM) return *newVm, err @@ -157,7 +155,7 @@ func (cli *Client) FindVMByHREF(vmHREF string) (VM, error) { func (vm *VM) PowerOn() (Task, error) { - apiEndpoint, _ := url.ParseRequestURI(vm.VM.HREF) + apiEndpoint := urlParseRequestURI(vm.VM.HREF) apiEndpoint.Path += "/power/action/powerOn" // Return the task @@ -182,7 +180,7 @@ func (vm *VM) PowerOnAndForceCustomization() error { return fmt.Errorf("VM %s must be undeployed before forcing customization", vm.VM.Name) } - apiEndpoint, _ := url.ParseRequestURI(vm.VM.HREF) + apiEndpoint := urlParseRequestURI(vm.VM.HREF) apiEndpoint.Path += "/action/deploy" powerOnAndCustomize := &types.DeployVAppParams{ @@ -208,7 +206,7 @@ func (vm *VM) PowerOnAndForceCustomization() error { func (vm *VM) PowerOff() (Task, error) { - apiEndpoint, _ := url.ParseRequestURI(vm.VM.HREF) + apiEndpoint := urlParseRequestURI(vm.VM.HREF) apiEndpoint.Path += "/power/action/powerOff" // Return the task @@ -216,18 +214,20 @@ func (vm *VM) PowerOff() (Task, error) { "", "error powering off VM: %s", nil) } -// Sets number of available virtual logical processors +// ChangeCPUCount sets number of available virtual logical processors // (i.e. CPUs x cores per socket) // Cpu cores count is inherited from template. // https://communities.vmware.com/thread/576209 +// Deprecated: use vm.ChangeCPU instead func (vm *VM) ChangeCPUCount(virtualCpuCount int) (Task, error) { return vm.ChangeCPUCountWithCore(virtualCpuCount, nil) } -// Sets number of available virtual logical processors +// ChangeCPUCountWithCore sets number of available virtual logical processors // (i.e. CPUs x cores per socket) and cores per socket. // Socket count is a result of: virtual logical processors/cores per socket // https://communities.vmware.com/thread/576209 +// Deprecated: use vm.ChangeCPU instead func (vm *VM) ChangeCPUCountWithCore(virtualCpuCount int, coresPerSocket *int) (Task, error) { err := vm.Refresh() @@ -249,7 +249,6 @@ func (vm *VM) ChangeCPUCountWithCore(virtualCpuCount int, coresPerSocket *int) ( Reservation: 0, ResourceType: types.ResourceTypeProcessor, VirtualQuantity: int64(virtualCpuCount), - Weight: 0, CoresPerSocket: coresPerSocket, Link: &types.Link{ HREF: vm.VM.HREF + "/virtualHardwareSection/cpu", @@ -258,7 +257,7 @@ func (vm *VM) ChangeCPUCountWithCore(virtualCpuCount int, coresPerSocket *int) ( }, } - apiEndpoint, _ := url.ParseRequestURI(vm.VM.HREF) + apiEndpoint := urlParseRequestURI(vm.VM.HREF) apiEndpoint.Path += "/virtualHardwareSection/cpu" // Return the task @@ -300,9 +299,10 @@ func (vm *VM) updateNicParameters(networks []map[string]interface{}, networkSect ipAllocationMode = types.IPAllocationModeDHCP // TODO v3.0 remove until here when deprecated `ip` and `network_name` attributes are removed - case ipIsSet && net.ParseIP(ipFieldString) != nil && (network["ip_allocation_mode"].(string) == types.IPAllocationModeManual): - ipAllocationMode = types.IPAllocationModeManual - ipAddress = ipFieldString + // Removed for Coverity warning: dead code - We can reinstate after removing above code + //case ipIsSet && net.ParseIP(ipFieldString) != nil && (network["ip_allocation_mode"].(string) == types.IPAllocationModeManual): + // ipAllocationMode = types.IPAllocationModeManual + // ipAddress = ipFieldString default: // New networks functionality. IP was not set and we're defaulting to provided ip_allocation_mode (only manual requires the IP) ipAllocationMode = network["ip_allocation_mode"].(string) } @@ -353,7 +353,7 @@ func (vm *VM) ChangeNetworkConfig(networks []map[string]interface{}) (Task, erro networkSection.Ovf = types.XMLNamespaceOVF networkSection.Info = "Specifies the available VM network connections" - apiEndpoint, _ := url.ParseRequestURI(vm.VM.HREF) + apiEndpoint := urlParseRequestURI(vm.VM.HREF) apiEndpoint.Path += "/networkConnectionSection/" // Return the task @@ -361,6 +361,7 @@ func (vm *VM) ChangeNetworkConfig(networks []map[string]interface{}) (Task, erro types.MimeNetworkConnectionSection, "error changing network config: %s", networkSection) } +// Deprecated: use vm.ChangeMemory instead func (vm *VM) ChangeMemorySize(size int) (Task, error) { err := vm.Refresh() @@ -375,7 +376,7 @@ func (vm *VM) ChangeMemorySize(size int) (Task, error) { VCloudHREF: vm.VM.HREF + "/virtualHardwareSection/memory", VCloudType: types.MimeRasdItem, AllocationUnits: "byte * 2^20", - Description: "Memory Size", + Description: "Memory SizeMb", ElementName: strconv.Itoa(size) + " MB of memory", InstanceID: 5, Reservation: 0, @@ -389,7 +390,7 @@ func (vm *VM) ChangeMemorySize(size int) (Task, error) { }, } - apiEndpoint, _ := url.ParseRequestURI(vm.VM.HREF) + apiEndpoint := urlParseRequestURI(vm.VM.HREF) apiEndpoint.Path += "/virtualHardwareSection/memory" // Return the task @@ -397,8 +398,8 @@ func (vm *VM) ChangeMemorySize(size int) (Task, error) { types.MimeRasdItem, "error changing memory size: %s", newMem) } -func (vm *VM) RunCustomizationScript(computername, script string) (Task, error) { - return vm.Customize(computername, script, false) +func (vm *VM) RunCustomizationScript(computerName, script string) (Task, error) { + return vm.Customize(computerName, script, false) } // GetGuestCustomizationStatus retrieves guest customization status. @@ -449,7 +450,7 @@ func (vm *VM) BlockWhileGuestCustomizationStatus(unwantedStatus string, timeOutA // Customize function allows to set ComputerName, apply customization script and enable or disable the changeSid option // // Deprecated: Use vm.SetGuestCustomizationSection() -func (vm *VM) Customize(computername, script string, changeSid bool) (Task, error) { +func (vm *VM) Customize(computerName, script string, changeSid bool) (Task, error) { err := vm.Refresh() if err != nil { return Task{}, fmt.Errorf("error refreshing VM before running customization: %s", err) @@ -463,13 +464,13 @@ func (vm *VM) Customize(computername, script string, changeSid bool) (Task, erro HREF: vm.VM.HREF, Type: types.MimeGuestCustomizationSection, Info: "Specifies Guest OS Customization Settings", - Enabled: takeBoolPointer(true), - ComputerName: computername, + Enabled: addrOf(true), + ComputerName: computerName, CustomizationScript: script, - ChangeSid: takeBoolPointer(changeSid), + ChangeSid: &changeSid, } - apiEndpoint, _ := url.ParseRequestURI(vm.VM.HREF) + apiEndpoint := urlParseRequestURI(vm.VM.HREF) apiEndpoint.Path += "/guestCustomizationSection/" // Return the task @@ -485,7 +486,26 @@ func (vm *VM) Undeploy() (Task, error) { UndeployPowerAction: "powerOff", } - apiEndpoint, _ := url.ParseRequestURI(vm.VM.HREF) + apiEndpoint := urlParseRequestURI(vm.VM.HREF) + apiEndpoint.Path += "/action/undeploy" + + // Return the task + return vm.client.ExecuteTaskRequest(apiEndpoint.String(), http.MethodPost, + types.MimeUndeployVappParams, "error undeploy VM: %s", vu) +} + +// Shutdown triggers a VM undeploy and shutdown action. "Shut Down Guest OS" action in UI behaves +// this way. +// +// Note. Success of this operation depends on the VM having Guest Tools installed. +func (vm *VM) Shutdown() (Task, error) { + + vu := &types.UndeployVAppParams{ + Xmlns: types.XMLNamespaceVCloud, + UndeployPowerAction: "shutdown", + } + + apiEndpoint := urlParseRequestURI(vm.VM.HREF) apiEndpoint.Path += "/action/undeploy" // Return the task @@ -525,7 +545,7 @@ func (vm *VM) attachOrDetachDisk(diskParams *types.DiskAttachOrDetachParams, rel attachOrDetachDiskLink.Type, "error attach or detach disk: %s", diskParams) } -// Attach an independent disk +// AttachDisk attaches an independent disk // Call attachOrDetachDisk with disk and types.RelDiskAttach to attach an independent disk. // Please verify the independent disk is not connected to any VM before calling this function. // If the independent disk is connected to a VM, the task will be failed. @@ -533,16 +553,15 @@ func (vm *VM) attachOrDetachDisk(diskParams *types.DiskAttachOrDetachParams, rel // https://vdc-download.vmware.com/vmwb-repository/dcr-public/1b6cf07d-adb3-4dba-8c47-9c1c92b04857/ // 241956dd-e128-4fcc-8131-bf66e1edd895/vcloud_sp_api_guide_30_0.pdf func (vm *VM) AttachDisk(diskParams *types.DiskAttachOrDetachParams) (Task, error) { - util.Logger.Printf("[TRACE] Attach disk, HREF: %s\n", diskParams.Disk.HREF) - - if diskParams.Disk == nil { + if diskParams == nil || diskParams.Disk == nil || diskParams.Disk.HREF == "" { return Task{}, fmt.Errorf("could not find disk info for attach") } + util.Logger.Printf("[TRACE] Attach disk, HREF: %s\n", diskParams.Disk.HREF) return vm.attachOrDetachDisk(diskParams, types.RelDiskAttach) } -// Detach an independent disk +// DetachDisk detaches an independent disk // Call attachOrDetachDisk with disk and types.RelDiskDetach to detach an independent disk. // Please verify the independent disk is connected the VM before calling this function. // If the independent disk is not connected to the VM, the task will be failed. @@ -550,16 +569,16 @@ func (vm *VM) AttachDisk(diskParams *types.DiskAttachOrDetachParams) (Task, erro // https://vdc-download.vmware.com/vmwb-repository/dcr-public/1b6cf07d-adb3-4dba-8c47-9c1c92b04857/ // 241956dd-e128-4fcc-8131-bf66e1edd895/vcloud_sp_api_guide_30_0.pdf func (vm *VM) DetachDisk(diskParams *types.DiskAttachOrDetachParams) (Task, error) { - util.Logger.Printf("[TRACE] Detach disk, HREF: %s\n", diskParams.Disk.HREF) - if diskParams.Disk == nil { + if diskParams == nil || diskParams.Disk == nil || diskParams.Disk.HREF == "" { return Task{}, fmt.Errorf("could not find disk info for detach") } + util.Logger.Printf("[TRACE] Detach disk, HREF: %s\n", diskParams.Disk.HREF) return vm.attachOrDetachDisk(diskParams, types.RelDiskDetach) } -// Helper function which finds media and calls InsertMedia +// HandleInsertMedia helper function finds media and calls InsertMedia func (vm *VM) HandleInsertMedia(org *Org, catalogName, mediaName string) (Task, error) { catalog, err := org.GetCatalogByName(catalogName, false) @@ -620,7 +639,7 @@ func isMediaInjected(items []*types.VirtualHardwareItem) bool { return false } -// Helper function which finds media and calls EjectMedia +// HandleEjectMedia is a helper function which finds media and calls EjectMedia func (vm *VM) HandleEjectMedia(org *Org, catalogName, mediaName string) (EjectTask, error) { catalog, err := org.GetCatalogByName(catalogName, false) if err != nil { @@ -641,7 +660,7 @@ func (vm *VM) HandleEjectMedia(org *Org, catalogName, mediaName string) (EjectTa return task, err } -// Insert media for VM +// InsertMedia insert media for a VM // Call insertOrEjectMedia with media and types.RelMediaInsertMedia to insert media from VM. func (vm *VM) InsertMedia(mediaParams *types.MediaInsertOrEjectParams) (Task, error) { util.Logger.Printf("[TRACE] Insert media, HREF: %s\n", mediaParams.Media.HREF) @@ -654,7 +673,7 @@ func (vm *VM) InsertMedia(mediaParams *types.MediaInsertOrEjectParams) (Task, er return vm.insertOrEjectMedia(mediaParams, types.RelMediaInsertMedia) } -// Eject media from VM +// EjectMedia ejects media from VM // Call insertOrEjectMedia with media and types.RelMediaEjectMedia to eject media from VM. // If media isn't inserted then task still will be successful. func (vm *VM) EjectMedia(mediaParams *types.MediaInsertOrEjectParams) (EjectTask, error) { @@ -712,17 +731,20 @@ func (vm *VM) insertOrEjectMedia(mediaParams *types.MediaInsertOrEjectParams, li insertOrEjectMediaLink.Type, "error insert or eject media: %s", mediaParams) } -// Use the get existing VM question for operation which need additional response +// GetQuestion uses the get existing VM question for operation which need additional response // Reference: // https://code.vmware.com/apis/287/vcloud#/doc/doc/operations/GET-VmPendingQuestion.html func (vm *VM) GetQuestion() (types.VmPendingQuestion, error) { - apiEndpoint, _ := url.ParseRequestURI(vm.VM.HREF) + apiEndpoint := urlParseRequestURI(vm.VM.HREF) apiEndpoint.Path += "/question" req := vm.client.NewRequest(map[string]string{}, http.MethodGet, *apiEndpoint, nil) resp, err := vm.client.Http.Do(req) + if err != nil { + return types.VmPendingQuestion{}, fmt.Errorf("error getting VM question: %s", err) + } // vCD security feature - on no question return 403 access error if http.StatusForbidden == resp.StatusCode { @@ -730,10 +752,6 @@ func (vm *VM) GetQuestion() (types.VmPendingQuestion, error) { return types.VmPendingQuestion{}, nil } - if err != nil { - return types.VmPendingQuestion{}, fmt.Errorf("error getting question: %s", err) - } - if http.StatusOK != resp.StatusCode { return types.VmPendingQuestion{}, fmt.Errorf("error getting question: %s", ParseErr(types.BodyTypeXML, resp, &types.Error{})) } @@ -749,7 +767,7 @@ func (vm *VM) GetQuestion() (types.VmPendingQuestion, error) { } -// Use the provide answer to existing VM question for operation which need additional response +// AnswerQuestion uses the provided answer to existing VM question for operation which need additional response // Reference: // https://code.vmware.com/apis/287/vcloud#/doc/doc/operations/POST-AnswerVmPendingQuestion.html func (vm *VM) AnswerQuestion(questionId string, choiceId int) error { @@ -765,7 +783,7 @@ func (vm *VM) AnswerQuestion(questionId string, choiceId int) error { ChoiceId: choiceId, } - apiEndpoint, _ := url.ParseRequestURI(vm.VM.HREF) + apiEndpoint := urlParseRequestURI(vm.VM.HREF) apiEndpoint.Path += "/question/action/answer" return vm.client.ExecuteRequestWithoutResponse(apiEndpoint.String(), http.MethodPost, @@ -780,11 +798,11 @@ func (vm *VM) ToggleHardwareVirtualization(isEnabled bool) (Task, error) { if err != nil { return Task{}, fmt.Errorf("unable to toggle hardware virtualization: %s", err) } - if vmStatus != "POWERED_OFF" { + if vmStatus != "POWERED_OFF" && vmStatus != "PARTIALLY_POWERED_OFF" { return Task{}, fmt.Errorf("hardware virtualization can be changed from powered off state, status: %s", vmStatus) } - apiEndpoint, _ := url.ParseRequestURI(vm.VM.HREF) + apiEndpoint := urlParseRequestURI(vm.VM.HREF) if isEnabled { apiEndpoint.Path += "/action/enableNestedHypervisor" } else { @@ -816,6 +834,20 @@ func (vm *VM) GetProductSectionList() (*types.ProductSectionList, error) { return getProductSectionList(vm.client, vm.VM.HREF) } +// GetEnvironment returns the OVF Environment. It's only available for poweredOn VM +func (vm *VM) GetEnvironment() (*types.OvfEnvironment, error) { + vmStatus, err := vm.GetStatus() + if err != nil { + return nil, fmt.Errorf("unable to get OVF environment: %s", err) + } + + if vmStatus != "POWERED_ON" { + return nil, fmt.Errorf("OVF environment is only available when VM is powered on") + } + + return vm.VM.Environment, nil +} + // GetGuestCustomizationSection retrieves guest customization section for a VM. It allows to read VM guest customization properties. func (vm *VM) GetGuestCustomizationSection() (*types.GuestCustomizationSection, error) { if vm == nil || vm.VM.HREF == "" { @@ -894,7 +926,7 @@ func (vm *VM) GetParentVdc() (*Vdc, error) { return nil, fmt.Errorf("could not find parent vApp for VM %s: %s", vm.VM.Name, err) } - vdc, err := vapp.getParentVDC() + vdc, err := vapp.GetParentVDC() if err != nil { return nil, fmt.Errorf("could not find parent vApp for VM %s: %s", vm.VM.Name, err) } @@ -925,6 +957,10 @@ func (vm *VM) getEdgeGatewaysForRoutedNics(nicDhcpConfigs []nicDhcpConfig) ([]ni } else { // Lookup edge gateway edgeGateway, err := vdc.GetEdgeGatewayByName(edgeGatewayName, false) + if ContainsNotFound(err) { + util.Logger.Printf("[TRACE] [DHCP IP Lookup] edge gateway not found: %s. Ignoring.", edgeGatewayName) + continue + } if err != nil { return nil, fmt.Errorf("could not lookup edge gateway for routed network on NIC %d: %s", nic.vmNicIndex, err) @@ -1227,8 +1263,8 @@ func (vm *VM) validateInternalDiskInput(diskData *types.DiskSettings, vmName, vm return fmt.Errorf("[VM %s Id %s] disk settings size MB has to be 0 or higher", vmName, vmId) } - if diskData.Iops != nil && *diskData.Iops < int64(0) { - return fmt.Errorf("[VM %s Id %s] disk settings iops has to be 0 or higher", vmName, vmId) + if diskData.IopsAllocation != nil && diskData.IopsAllocation.Reservation < int64(0) { + return fmt.Errorf("[VM %s Id %s] disk settings iops reservation has to be 0 or higher", vmName, vmId) } if diskData.ThinProvisioned == nil { @@ -1318,26 +1354,22 @@ func (vm *VM) UpdateInternalDisks(disksSettingToUpdate *types.VmSpecSection) (*t return nil, fmt.Errorf("cannot update internal disks - VM HREF is unset") } - task, err := vm.UpdateInternalDisksAsync(disksSettingToUpdate) + description := vm.VM.Description + vm, err := vm.UpdateVmSpecSection(disksSettingToUpdate, description) if err != nil { return nil, err } - err = task.WaitTaskCompletion() - if err != nil { - return nil, fmt.Errorf("error waiting for task completion after internal disks update for VM %s: %s", vm.VM.Name, err) - } - err = vm.Refresh() - if err != nil { - return nil, fmt.Errorf("error refreshing VM %s: %s", vm.VM.Name, err) - } + return vm.VM.VmSpecSection, nil } // UpdateInternalDisksAsync applies disks configuration for the VM. -// types.VmSpecSection has to have all internal disk state. Disks which don't match provided ones in types.VmSpecSection -// will be deleted. Matched internal disk will be updated. New internal disk description found -// in types.VmSpecSection will be created. +// types.VmSpecSection has to have all internal disk state. Disks which don't +// match provided ones in types.VmSpecSection will be deleted. +// Matched internal disk will be updated. New internal disk description found in types.VmSpecSection will be created. // Returns Task and error. +// +// Deprecated: use UpdateInternalDisks or UpdateVmSpecSectionAsync instead func (vm *VM) UpdateInternalDisksAsync(disksSettingToUpdate *types.VmSpecSection) (Task, error) { if vm.VM.HREF == "" { return Task{}, fmt.Errorf("cannot update disks, VM HREF is unset") @@ -1351,6 +1383,7 @@ func (vm *VM) UpdateInternalDisksAsync(disksSettingToUpdate *types.VmSpecSection Xmlns: types.XMLNamespaceVCloud, Ovf: types.XMLNamespaceOVF, Name: vm.VM.Name, + Description: vm.VM.Description, VmSpecSection: disksSettingToUpdate, }) } @@ -1446,6 +1479,11 @@ func (vm *VM) UpdateVmSpecSectionAsync(vmSettingsToUpdate *types.VmSpecSection, return Task{}, fmt.Errorf("cannot update VM spec section, VM HREF is unset") } + // Firmware field is unavailable on <37.1 API Versions + if vmSettingsToUpdate.Firmware != "" && vm.client.APIVCDMaxVersionIs("<37.1") { + return Task{}, fmt.Errorf("VM Firmware can only be set on VCD 10.4.1+ (API 37.1+)") + } + vmSpecSectionModified := true vmSettingsToUpdate.Modified = &vmSpecSectionModified @@ -1456,17 +1494,46 @@ func (vm *VM) UpdateVmSpecSectionAsync(vmSettingsToUpdate *types.VmSpecSection, // GuestCustomizationSection // Sections not included in the request body will not be updated. - return vm.client.ExecuteTaskRequest(vm.VM.HREF+"/action/reconfigureVm", http.MethodPost, - types.MimeVM, "error updating VM spec section: %s", &types.Vm{ - Xmlns: types.XMLNamespaceVCloud, - Ovf: types.XMLNamespaceOVF, - Name: vm.VM.Name, - Description: description, - VmSpecSection: vmSettingsToUpdate, - }) + vmPayload := &types.Vm{ + Xmlns: types.XMLNamespaceVCloud, + Ovf: types.XMLNamespaceOVF, + Name: vm.VM.Name, + Description: description, + VmSpecSection: vmSettingsToUpdate, + } + + // Since 37.1 there is a Firmware field in VmSpecSection + return vm.client.ExecuteTaskRequestWithApiVersion(vm.VM.HREF+"/action/reconfigureVm", + http.MethodPost, types.MimeVM, "error updating VM spec section: %s", vmPayload, + vm.client.GetSpecificApiVersionOnCondition(">=37.1", "37.1")) +} + +// UpdateComputePolicyV2 updates VM Compute policy with the given compute policies using v2.0.0 OpenAPI endpoint, +// and returns an error if something went wrong, or the refreshed VM if all went OK. +// Updating with an empty compute policy ID will remove it from the VM. Both policies can't be empty as the VM requires +// at least one policy. +func (vm *VM) UpdateComputePolicyV2(sizingPolicyId, placementPolicyId, vGpuPolicyId string) (*VM, error) { + task, err := vm.UpdateComputePolicyV2Async(sizingPolicyId, placementPolicyId, vGpuPolicyId) + if err != nil { + return nil, err + } + + err = task.WaitTaskCompletion() + if err != nil { + return nil, err + } + + err = vm.Refresh() + if err != nil { + return nil, err + } + + return vm, nil + } // UpdateComputePolicy updates VM compute policy and returns refreshed VM or error. +// Deprecated: Use VM.UpdateComputePolicyV2 instead func (vm *VM) UpdateComputePolicy(computePolicy *types.VdcComputePolicy) (*VM, error) { task, err := vm.UpdateComputePolicyAsync(computePolicy) if err != nil { @@ -1487,7 +1554,64 @@ func (vm *VM) UpdateComputePolicy(computePolicy *types.VdcComputePolicy) (*VM, e } +// UpdateComputePolicyV2Async updates VM Compute policy with the given compute policies using v2.0.0 OpenAPI endpoint, +// and returns a Task and an error. Updating with an empty compute policy ID will remove it from the VM. Both +// policies can't be empty as the VM requires at least one policy. +// WARNING: At the moment, vGPU Policies are not supported. Using one will return an error. +func (vm *VM) UpdateComputePolicyV2Async(sizingPolicyId, placementPolicyId, vGpuPolicyId string) (Task, error) { + if vm.VM.HREF == "" { + return Task{}, fmt.Errorf("cannot update VM compute policy, VM HREF is unset") + } + + sizingIsEmpty := strings.TrimSpace(sizingPolicyId) == "" + placementIsEmpty := strings.TrimSpace(placementPolicyId) == "" + vGpuPolicyIsEmpty := strings.TrimSpace(vGpuPolicyId) == "" + + if !vGpuPolicyIsEmpty { + return Task{}, fmt.Errorf("vGPU policies are not supported, hence %s should be empty", vGpuPolicyId) + } + + if sizingIsEmpty && placementIsEmpty { + return Task{}, fmt.Errorf("either sizing policy ID or placement policy ID is needed") + } + + // `reconfigureVm` updates VM name, Description, and any or all of the following sections. + // VirtualHardwareSection + // OperatingSystemSection + // NetworkConnectionSection + // GuestCustomizationSection + // Sections not included in the request body will not be updated. + + computePolicy := &types.ComputePolicy{} + + if !sizingIsEmpty { + vdcSizingPolicyHref, err := vm.client.OpenApiBuildEndpoint(types.OpenApiPathVersion2_0_0, types.OpenApiEndpointVdcComputePolicies, sizingPolicyId) + if err != nil { + return Task{}, fmt.Errorf("error constructing HREF for sizing policy") + } + computePolicy.VmSizingPolicy = &types.Reference{HREF: vdcSizingPolicyHref.String()} + } + + if !placementIsEmpty { + vdcPlacementPolicyHref, err := vm.client.OpenApiBuildEndpoint(types.OpenApiPathVersion2_0_0, types.OpenApiEndpointVdcComputePolicies, placementPolicyId) + if err != nil { + return Task{}, fmt.Errorf("error constructing HREF for placement policy") + } + computePolicy.VmPlacementPolicy = &types.Reference{HREF: vdcPlacementPolicyHref.String()} + } + + return vm.client.ExecuteTaskRequest(vm.VM.HREF+"/action/reconfigureVm", http.MethodPost, + types.MimeVM, "error updating VM spec section: %s", &types.Vm{ + Xmlns: types.XMLNamespaceVCloud, + Ovf: types.XMLNamespaceOVF, + Name: vm.VM.Name, + Description: vm.VM.Description, + ComputePolicy: computePolicy, + }) +} + // UpdateComputePolicyAsync updates VM Compute policy and returns Task and error. +// Deprecated: Use VM.UpdateComputePolicyV2Async instead func (vm *VM) UpdateComputePolicyAsync(computePolicy *types.VdcComputePolicy) (Task, error) { if vm.VM.HREF == "" { return Task{}, fmt.Errorf("cannot update VM compute policy, VM HREF is unset") @@ -1499,9 +1623,6 @@ func (vm *VM) UpdateComputePolicyAsync(computePolicy *types.VdcComputePolicy) (T // NetworkConnectionSection // GuestCustomizationSection // Sections not included in the request body will not be updated. - if computePolicy != nil && vm.client.APIVCDMaxVersionIs("< 33.0") { - return Task{}, errors.New("[Error] compute policy can't be used - VCD version doesn't support it") - } vcdComputePolicyHref, err := vm.client.OpenApiBuildEndpoint(types.OpenApiPathVersion1_0_0, types.OpenApiEndpointVdcComputePolicies, computePolicy.ID) if err != nil { @@ -1540,10 +1661,23 @@ func (client *Client) QueryVmList(filter types.VmQueryFilter) ([]*types.QueryRes return vmList, nil } +// QueryVmList returns a list of all VMs in a given Org +func (org *Org) QueryVmList(filter types.VmQueryFilter) ([]*types.QueryResultVMRecordType, error) { + if org.client.IsSysAdmin { + return queryVmList(filter, org.client, "org", org.Org.HREF) + } + return queryVmList(filter, org.client, "", "") +} + // QueryVmList returns a list of all VMs in a given VDC func (vdc *Vdc) QueryVmList(filter types.VmQueryFilter) ([]*types.QueryResultVMRecordType, error) { + return queryVmList(filter, vdc.client, "vdc", vdc.Vdc.HREF) +} + +// queryVmList is extracted and used by org.QueryVmList and vdc.QueryVmList to adjust filtering scope +func queryVmList(filter types.VmQueryFilter, client *Client, filterParent, filterParentHref string) ([]*types.QueryResultVMRecordType, error) { var vmList []*types.QueryResultVMRecordType - queryType := vdc.client.GetQueryType(types.QtVm) + queryType := client.GetQueryType(types.QtVm) params := map[string]string{ "type": queryType, "filterEncoded": "true", @@ -1552,18 +1686,49 @@ func (vdc *Vdc) QueryVmList(filter types.VmQueryFilter) ([]*types.QueryResultVMR if filter.String() != "" { filterText = filter.String() } - if filterText == "" { - filterText = fmt.Sprintf("vdc==%s", vdc.Vdc.HREF) - } else { - filterText = fmt.Sprintf("%s;vdc==%s", filterText, vdc.Vdc.HREF) + if filterParent != "" { + if filterText == "" { + filterText = fmt.Sprintf("%s==%s", filterParent, filterParentHref) + } else { + filterText = fmt.Sprintf("%s;%s==%s", filterText, filterParent, filterParentHref) + } + params["filter"] = filterText + } + vmResult, err := client.cumulativeQuery(queryType, nil, params) + if err != nil { + return nil, fmt.Errorf("error getting VM list : %s", err) + } + vmList = vmResult.Results.VMRecord + if client.IsSysAdmin { + vmList = vmResult.Results.AdminVMRecord } + return vmList, nil +} + +// QueryVmList retrieves a list of VMs across all VDC, using parameters defined in searchParams +func QueryVmList(vmType types.VmQueryFilter, client *Client, searchParams map[string]string) ([]*types.QueryResultVMRecordType, error) { + var vmList []*types.QueryResultVMRecordType + queryType := client.GetQueryType(types.QtVm) + params := map[string]string{ + "type": queryType, + "filterEncoded": "true", + } + filterText := "" + if vmType.String() != "" { + // The first filter will be the type of VM, i.e. deployed (inside a vApp) or not (inside a vApp template) + filterText = vmType.String() + } + for k, v := range searchParams { + filterText = fmt.Sprintf("%s;%s==%s", filterText, k, v) + } + params["filter"] = filterText - vmResult, err := vdc.client.cumulativeQuery(queryType, nil, params) + vmResult, err := client.cumulativeQuery(queryType, nil, params) if err != nil { return nil, fmt.Errorf("error getting VM list : %s", err) } vmList = vmResult.Results.VMRecord - if vdc.client.IsSysAdmin { + if client.IsSysAdmin { vmList = vmResult.Results.AdminVMRecord } return vmList, nil @@ -1620,21 +1785,11 @@ var vmVersionedFuncsV10 = vmVersionedFuncs{ AddEmptyVmAsync: addEmptyVmAsyncV10, } -// VM function mapping for API version 32.0 (from vCD 9.7) -var vmVersionedFuncsV97 = vmVersionedFuncs{ - SupportedVersion: "32.0", - GetVMByHref: getVMByHrefV97, - AddEmptyVm: addEmptyVmV97, - AddEmptyVmAsync: addEmptyVmAsyncV97, -} - // vmVersionedFuncsByVcdVersion is a map of VDC functions by vCD version var vmVersionedFuncsByVcdVersion = map[string]vmVersionedFuncs{ "vm10.2": vmVersionedFuncsV10, "vm10.1": vmVersionedFuncsV10, "vm10.0": vmVersionedFuncsV10, - "vm9.7": vmVersionedFuncsV97, - // If we add a new function to this list, we also need to update the "default" entry // The "default" entry will hold the highest currently available function "default": vmVersionedFuncsV10, @@ -1657,7 +1812,7 @@ func addEmptyVmAsyncV10(vapp *VApp, reComposeVAppParams *types.RecomposeVAppPara if err != nil { return Task{}, err } - apiEndpoint, _ := url.ParseRequestURI(vapp.VApp.HREF) + apiEndpoint := urlParseRequestURI(vapp.VApp.HREF) apiEndpoint.Path += "/action/recomposeVApp" reComposeVAppParams.XmlnsVcloud = types.XMLNamespaceVCloud @@ -1666,7 +1821,7 @@ func addEmptyVmAsyncV10(vapp *VApp, reComposeVAppParams *types.RecomposeVAppPara // Return the task return vapp.client.ExecuteTaskRequestWithApiVersion(apiEndpoint.String(), http.MethodPost, types.MimeRecomposeVappParams, "error instantiating a new VM: %s", reComposeVAppParams, - vapp.client.GetSpecificApiVersionOnCondition(">= 33.0", "33.0")) + vapp.client.GetSpecificApiVersionOnCondition(">=37.1", "37.1")) } // addEmptyVmV10 adds an empty VM (without template) to vApp and returns the new created VM or an error. @@ -1704,58 +1859,6 @@ func addEmptyVmV10(vapp *VApp, reComposeVAppParams *types.RecomposeVAppParamsFor return nil, ErrorEntityNotFound } -// addEmptyVmAsyncV97 adds an empty VM (without template) to the vApp and returns a Task and an error. -func addEmptyVmAsyncV97(vapp *VApp, reComposeVAppParams *types.RecomposeVAppParamsForEmptyVm) (Task, error) { - err := validateEmptyVmParams(reComposeVAppParams) - if err != nil { - return Task{}, err - } - apiEndpoint, _ := url.ParseRequestURI(vapp.VApp.HREF) - apiEndpoint.Path += "/action/recomposeVApp" - - reComposeVAppParams.XmlnsVcloud = types.XMLNamespaceVCloud - reComposeVAppParams.XmlnsOvf = types.XMLNamespaceOVF - - // Return the task - return vapp.client.ExecuteTaskRequest(apiEndpoint.String(), http.MethodPost, - types.MimeRecomposeVappParams, "error instantiating a new VM: %s", reComposeVAppParams) -} - -// addEmptyVmV97 adds an empty VM (without template) to vApp and returns the new created VM or an error. -func addEmptyVmV97(vapp *VApp, reComposeVAppParams *types.RecomposeVAppParamsForEmptyVm) (*VM, error) { - task, err := addEmptyVmAsyncV97(vapp, reComposeVAppParams) - if err != nil { - return nil, err - } - - err = task.WaitTaskCompletion() - if err != nil { - return nil, err - } - - err = vapp.Refresh() - if err != nil { - return nil, fmt.Errorf("error refreshing vApp: %s", err) - } - - //vApp Might Not Have Any VMs - if vapp.VApp.Children == nil { - return nil, ErrorEntityNotFound - } - - util.Logger.Printf("[TRACE] Looking for VM: %s", reComposeVAppParams.CreateItem.Name) - for _, child := range vapp.VApp.Children.VM { - - util.Logger.Printf("[TRACE] Looking at: %s", child.Name) - if child.Name == reComposeVAppParams.CreateItem.Name { - return getVMByHrefV97(vapp.client, child.HREF) - } - - } - util.Logger.Printf("[TRACE] Couldn't find VM: %s", reComposeVAppParams.CreateItem.Name) - return nil, ErrorEntityNotFound -} - // getVMByHrefV10 returns a VM reference by running a vCD API call // If no valid VM is found, it returns a nil VM reference and an error // Note that the pointer receiver here is a Client instead of a VApp, because @@ -1766,27 +1869,7 @@ func getVMByHrefV10(client *Client, vmHref string) (*VM, error) { newVm := NewVM(client) _, err := client.ExecuteRequestWithApiVersion(vmHref, http.MethodGet, - "", "error retrieving vm: %s", nil, newVm.VM, - client.GetSpecificApiVersionOnCondition(">= 33.0", "33.0")) - - if err != nil { - - return nil, err - } - - return newVm, nil -} - -// getVMByHrefV97 returns a VM reference by running a vCD API call -// If no valid VM is found, it returns a nil VM reference and an error -// Note that the pointer receiver here is a Client instead of a VApp, because -// there are cases where we know the VM HREF but not which VApp it belongs to. -func getVMByHrefV97(client *Client, vmHref string) (*VM, error) { - - newVm := NewVM(client) - - _, err := client.ExecuteRequest(vmHref, http.MethodGet, - "", "error retrieving vm: %s", nil, newVm.VM) + "", "error retrieving vm: %s", nil, newVm.VM, client.GetSpecificApiVersionOnCondition(">=37.1", "37.1")) if err != nil { @@ -1852,7 +1935,7 @@ func (vm *VM) UpdateStorageProfileAsync(storageProfileHref string) (Task, error) // GuestCustomizationSection // Sections not included in the request body will not be updated. return vm.client.ExecuteTaskRequest(vm.VM.HREF+"/action/reconfigureVm", http.MethodPost, - types.MimeVM, "error updating VM spec section: %s", &types.Vm{ + types.MimeVM, "error updating VM storage profile: %s", &types.Vm{ Xmlns: types.XMLNamespaceVCloud, Ovf: types.XMLNamespaceOVF, Name: vm.VM.Name, @@ -1861,6 +1944,58 @@ func (vm *VM) UpdateStorageProfileAsync(storageProfileHref string) (Task, error) }) } +// UpdateBootOptions updates the Boot Options of a VM and returns the updated instance of the VM +func (vm *VM) UpdateBootOptions(bootOptions *types.BootOptions) (*VM, error) { + task, err := vm.UpdateBootOptionsAsync(bootOptions) + if err != nil { + return nil, err + } + + err = task.WaitTaskCompletion() + if err != nil { + return nil, err + } + + err = vm.Refresh() + if err != nil { + return nil, err + } + + return vm, nil +} + +// UpdateBootOptionsAsync updates the boot options of a VM +func (vm *VM) UpdateBootOptionsAsync(bootOptions *types.BootOptions) (Task, error) { + if vm.VM.HREF == "" { + return Task{}, fmt.Errorf("cannot update VM boot options, VM HREF is unset") + } + + if vm.client.APIVCDMaxVersionIs("<37.1") { + + if bootOptions.BootRetryEnabled != nil || bootOptions.BootRetryDelay != nil || + bootOptions.EfiSecureBootEnabled != nil || bootOptions.NetworkBootProtocol != "" { + return Task{}, fmt.Errorf("error: Boot retry, EFI Secure Boot and Boot Network Protocol options were introduced in VCD 10.4.1") + } + } + + if bootOptions == nil { + return Task{}, fmt.Errorf("cannot update VM boot options, none given") + } + + return vm.client.ExecuteTaskRequestWithApiVersion(vm.VM.HREF+"/action/reconfigureVm", http.MethodPost, + types.MimeVM, "error updating VM boot options: %s", &types.Vm{ + Xmlns: types.XMLNamespaceVCloud, + Ovf: types.XMLNamespaceOVF, + Name: vm.VM.Name, + Description: vm.VM.Description, + // We need to add ComputePolicy in the Request Body or settings will + // be set to default sizing policy set in the VDC if the VM is Not + // compliant with the current sizing policy + ComputePolicy: vm.VM.ComputePolicy, + BootOptions: bootOptions, + }, vm.client.GetSpecificApiVersionOnCondition(">=37.1", "37.1")) +} + // DeleteAsync starts a standalone VM deletion, returning a task func (vm *VM) DeleteAsync() (Task, error) { if vm.VM.HREF == "" { @@ -1886,3 +2021,201 @@ func (vm *VM) Delete() error { } return task.WaitTaskCompletion() } + +func (vm *VM) getTenantContext() (*TenantContext, error) { + parentVdc, err := vm.GetParentVdc() + if err != nil { + return nil, err + } + return parentVdc.getTenantContext() +} + +// ChangeMemory sets memory value. Size is MB +func (vm *VM) ChangeMemory(sizeInMb int64) error { + vmSpecSection := vm.VM.VmSpecSection + description := vm.VM.Description + // update treats same values as changes and fails, with no values provided - no changes are made for that section + vmSpecSection.DiskSection = nil + + vmSpecSection.MemoryResourceMb.Configured = sizeInMb + + _, err := vm.UpdateVmSpecSection(vmSpecSection, description) + if err != nil { + return fmt.Errorf("error changing memory size: %s", err) + } + return nil +} + +// ChangeCPUCount sets number of available virtual logical processors +// (i.e. CPUs x cores per socket) +// Cpu cores count is inherited from template. +// https://communities.vmware.com/thread/576209 +// Deprecated: use ChangeCPUAndCoreCount +func (vm *VM) ChangeCPU(cpus, cpuCores int) error { + vmSpecSection := vm.VM.VmSpecSection + description := vm.VM.Description + // update treats same values as changes and fails, with no values provided - no changes are made for that section + vmSpecSection.DiskSection = nil + + vmSpecSection.NumCpus = &cpus + // has to come together + vmSpecSection.NumCoresPerSocket = &cpuCores + + _, err := vm.UpdateVmSpecSection(vmSpecSection, description) + if err != nil { + return fmt.Errorf("error changing cpu size: %s", err) + } + return nil +} + +// ChangeCPUAndCoreCount sets CPU and CPU core counts +// Accepts values or `nil` for both parameters. +func (vm *VM) ChangeCPUAndCoreCount(cpus, cpuCores *int) error { + vmSpecSection := vm.VM.VmSpecSection + description := vm.VM.Description + // update treats same values as changes and fails, with no values provided - no changes are made for that section + vmSpecSection.DiskSection = nil + + vmSpecSection.NumCpus = cpus + // has to come together + vmSpecSection.NumCoresPerSocket = cpuCores + + _, err := vm.UpdateVmSpecSection(vmSpecSection, description) + if err != nil { + return fmt.Errorf("error changing CPU size: %s", err) + } + return nil +} + +// ConsolidateDisksAsync triggers VM disk consolidation task +func (vm *VM) ConsolidateDisksAsync() (Task, error) { + if vm.VM.HREF == "" { + return Task{}, fmt.Errorf("cannot consolidate disks, VM HREF is unset") + } + + return vm.client.ExecuteTaskRequest(vm.VM.HREF+"/action/consolidate", http.MethodPost, + types.AnyXMLMime, "error consolidating VM disks: %s", nil) +} + +// ConsolidateDisks triggers VM disk consolidation task and waits until it is completed +func (vm *VM) ConsolidateDisks() error { + task, err := vm.ConsolidateDisksAsync() + if err != nil { + return err + } + return task.WaitTaskCompletion() +} + +// GetExtraConfig retrieves the extra configuration items from a VM +func (vm *VM) GetExtraConfig() ([]*types.ExtraConfigMarshal, error) { + if vm.VM.HREF == "" { + return nil, fmt.Errorf("cannot update VM spec section, VM HREF is unset") + } + + virtualHardwareSection := &types.ResponseVirtualHardwareSection{} + _, err := vm.client.ExecuteRequest(vm.VM.HREF+"/virtualHardwareSection/", http.MethodGet, types.MimeVirtualHardwareSection, "error retrieving virtual hardware: %s", nil, virtualHardwareSection) + if err != nil { + return nil, err + } + + convertedExtraConfig := convertExtraConfig(virtualHardwareSection.ExtraConfigs) + + return convertedExtraConfig, nil +} + +// UpdateExtraConfig adds or changes items in the VM Extra Configuration set +// Returns the modified set +// Note: an item with an empty `Value` will be deleted. +func (vm *VM) UpdateExtraConfig(update []*types.ExtraConfigMarshal) ([]*types.ExtraConfigMarshal, error) { + return vm.updateExtraConfig(update, false) +} + +// DeleteExtraConfig removes items from the VM Extra Configuration set +// Returns the modified set +func (vm *VM) DeleteExtraConfig(deleteItems []*types.ExtraConfigMarshal) ([]*types.ExtraConfigMarshal, error) { + return vm.updateExtraConfig(deleteItems, true) +} + +// updateExtraConfig adds, changes, or delete items in the VM Extra Configuration set +func (vm *VM) updateExtraConfig(update []*types.ExtraConfigMarshal, wantDelete bool) ([]*types.ExtraConfigMarshal, error) { + if vm.VM.HREF == "" { + return nil, fmt.Errorf("cannot update VM spec section, VM HREF is unset") + } + + virtualHardwareSection := &types.ResponseVirtualHardwareSection{} + _, err := vm.client.ExecuteRequest(vm.VM.HREF+"/virtualHardwareSection/", http.MethodGet, types.MimeVirtualHardwareSection, "error retrieving virtual hardware: %s", nil, virtualHardwareSection) + if err != nil { + return nil, err + } + + var newExtraConfig []*types.ExtraConfigMarshal + + var invalidKeys []string + + if wantDelete { + for _, ec := range update { + newExtraConfig = append(newExtraConfig, &types.ExtraConfigMarshal{Key: ec.Key, Value: ""}) + } + + } else { + for _, ec := range update { + if strings.Contains(ec.Key, " ") { + invalidKeys = append(invalidKeys, ec.Key) + continue + } + newExtraConfig = append(newExtraConfig, ec) + } + if len(invalidKeys) > 0 { + return nil, fmt.Errorf("[vm.UpdateExtraConfig] invalid keys provided: [%s]", strings.Join(invalidKeys, ",")) + } + } + + requestVirtualHardwareSection := &types.RequestVirtualHardwareSection{ + Info: "Virtual hardware requirements", + Ovf: types.XMLNamespaceOVF, + Rasd: types.XMLNamespaceRASD, + Vssd: types.XMLNamespaceVSSD, + Ns2: types.XMLNamespaceVCloud, + Ns3: types.XMLNamespaceVCloud, + Ns4: types.XMLNamespaceVCloud, + Vmw: types.XMLNamespaceVMW, + Xmlns: types.XMLNamespaceVCloud, + + Type: virtualHardwareSection.Type, + System: virtualHardwareSection.System, + Item: virtualHardwareSection.Item, + + ExtraConfigs: newExtraConfig, + } + + task, err := vm.client.ExecuteTaskRequest(vm.VM.HREF+"/virtualHardwareSection/", http.MethodPut, + types.MimeVirtualHardwareSection, "error updating VM spec section: %s", requestVirtualHardwareSection) + if err != nil { + return nil, err + } + + err = task.WaitTaskCompletion() + if err != nil { + return nil, fmt.Errorf("error waiting task: %s", err) + } + + xtraCfg, err := vm.GetExtraConfig() + if err != nil { + return nil, fmt.Errorf("got error while retrieving extra config: %s", err) + } + + return xtraCfg, nil +} + +func convertExtraConfig(source []*types.ExtraConfig) []*types.ExtraConfigMarshal { + resp := make([]*types.ExtraConfigMarshal, len(source)) + for index, field := range source { + resp[index] = &types.ExtraConfigMarshal{ + Key: field.Key, + Value: field.Value, + Required: field.Required, + } + } + + return resp +} diff --git a/govcd/vm_affinity_rule.go b/govcd/vm_affinity_rule.go index 41693e20b..9cad9ab28 100644 --- a/govcd/vm_affinity_rule.go +++ b/govcd/vm_affinity_rule.go @@ -157,12 +157,12 @@ func (vdc *Vdc) GetVmAffinityRuleByNameOrId(identifier string) (*VmAffinityRule, // validateAffinityRule checks that a VM affinity rule has all the needed properties // If checkVMs is true, then the function checks that all VMs in the internal list exist. // The usual workflow is: -// 1. validation without VM checking -// 2. creation or update -// 3. if no error -> end -// 4. if error, validation with VM checks -// 4a. if validation error, it was a VM issue: return combined original error + validation error -// 4b. if no validation error, the failure was due to something else: return only original error +// 1. validation without VM checking +// 2. creation or update +// 3. if no error -> end +// 4. if error, validation with VM checks +// 4a. if validation error, it was a VM issue: return combined original error + validation error +// 4b. if no validation error, the failure was due to something else: return only original error func validateAffinityRule(client *Client, affinityRuleDef *types.VmAffinityRule, checkVMs bool) (*types.VmAffinityRule, error) { if affinityRuleDef == nil { return nil, fmt.Errorf("empty definition given for a VM affinity rule") @@ -390,7 +390,7 @@ func (vmar *VmAffinityRule) SetEnabled(value bool) error { return nil } } - vmar.VmAffinityRule.IsEnabled = takeBoolPointer(value) + vmar.VmAffinityRule.IsEnabled = &value return vmar.Update() } @@ -402,6 +402,6 @@ func (vmar *VmAffinityRule) SetMandatory(value bool) error { return nil } } - vmar.VmAffinityRule.IsMandatory = takeBoolPointer(value) + vmar.VmAffinityRule.IsMandatory = &value return vmar.Update() } diff --git a/govcd/vm_affinity_rule_test.go b/govcd/vm_affinity_rule_test.go index 99cc632a6..b6e5ef65f 100644 --- a/govcd/vm_affinity_rule_test.go +++ b/govcd/vm_affinity_rule_test.go @@ -1,4 +1,4 @@ -// +build vdc affinity functional ALL +//go:build vdc || affinity || functional || ALL /* * Copyright 2020 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -99,8 +99,8 @@ func (vcd *TestVCD) testCRUDVmAffinityRule(orgName string, vdc *Vdc, data affini } affinityRuleDef := &types.VmAffinityRule{ Name: data.name, - IsEnabled: takeBoolPointer(true), - IsMandatory: takeBoolPointer(true), + IsEnabled: addrOf(true), + IsMandatory: addrOf(true), Polarity: data.polarity, VmReferences: []*types.VMs{ &types.VMs{ @@ -271,58 +271,3 @@ func (vcd *TestVCD) Test_VmAffinityRule(check *C) { }, check) } - -// makeVappGroup creates multiple vApps, each with several VMs, -// as defined in `groupDefinition`. -// Returns a list of vApps -func makeVappGroup(label string, vdc *Vdc, groupDefinition map[string][]string) ([]*VApp, error) { - var vappList []*VApp - for vappName, vmNames := range groupDefinition { - existingVapp, err := vdc.GetVAppByName(vappName, false) - if err == nil { - - if existingVapp.VApp.Children == nil || len(existingVapp.VApp.Children.VM) == 0 { - return nil, fmt.Errorf("found vApp %s but without VMs", vappName) - } - foundVms := 0 - for _, vmName := range vmNames { - for _, existingVM := range existingVapp.VApp.Children.VM { - if existingVM.Name == vmName { - foundVms++ - } - } - } - if foundVms < 2 { - return nil, fmt.Errorf("found vApp %s but with %d VMs instead of 2 ", vappName, foundVms) - } - - vappList = append(vappList, existingVapp) - if testVerbose { - fmt.Printf("Using existing vApp %s\n", vappName) - } - continue - } - - if testVerbose { - fmt.Printf("Creating vApp %s\n", vappName) - } - vapp, err := makeEmptyVapp(vdc, vappName) - if err != nil { - return nil, err - } - if os.Getenv("GOVCD_KEEP_TEST_OBJECTS") == "" { - AddToCleanupList(vappName, "vapp", vdc.Vdc.Name, label) - } - for _, vmName := range vmNames { - if testVerbose { - fmt.Printf("\tCreating VM %s/%s\n", vappName, vmName) - } - _, err := makeEmptyVm(vapp, vmName) - if err != nil { - return nil, err - } - } - vappList = append(vappList, vapp) - } - return vappList, nil -} diff --git a/govcd/vm_concurrent_test.go b/govcd/vm_concurrent_test.go index 6d5b57be7..c54448f96 100644 --- a/govcd/vm_concurrent_test.go +++ b/govcd/vm_concurrent_test.go @@ -1,4 +1,4 @@ -// +build concurrent +//go:build concurrent /* * Copyright 2020 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. diff --git a/govcd/vm_dhcp_test.go b/govcd/vm_dhcp_test.go index 1a3b31315..a9d9f38f2 100644 --- a/govcd/vm_dhcp_test.go +++ b/govcd/vm_dhcp_test.go @@ -1,4 +1,4 @@ -// +build nsxv vm functional ALL +//go:build nsxv || vm || functional || ALL /* * Copyright 2020 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -28,7 +28,7 @@ func (vcd *TestVCD) Test_VMGetDhcpAddress(check *C) { } // Construct new VM for test - vapp, err := vcd.createTestVapp("GetDhcpAddress") + vapp, err := deployVappForTest(vcd, "GetDhcpAddress") check.Assert(err, IsNil) vmType, _ := vcd.findFirstVm(*vapp) vm := &VM{ @@ -45,7 +45,7 @@ func (vcd *TestVCD) Test_VMGetDhcpAddress(check *C) { network := makeOrgVdcNetworkWithDhcp(vcd, check, edgeGateway) // Attach Org network to vApp - _, err = vapp.AddOrgNetwork(&VappNetworkSettings{}, network, false) + _, err = vapp.AddOrgNetwork(&VappNetworkSettings{}, network.OrgVDCNetwork, false) check.Assert(err, IsNil) // Get network config and update it to use DHCP @@ -53,12 +53,12 @@ func (vcd *TestVCD) Test_VMGetDhcpAddress(check *C) { check.Assert(err, IsNil) check.Assert(netCfg, NotNil) - netCfg.NetworkConnection[0].Network = network.Name + netCfg.NetworkConnection[0].Network = network.OrgVDCNetwork.Name netCfg.NetworkConnection[0].IPAddressAllocationMode = types.IPAllocationModeDHCP netCfg.NetworkConnection[0].IsConnected = true secondNic := &types.NetworkConnection{ - Network: network.Name, + Network: network.OrgVDCNetwork.Name, IPAddressAllocationMode: types.IPAllocationModeDHCP, NetworkConnectionIndex: 1, IsConnected: true, @@ -163,12 +163,16 @@ func (vcd *TestVCD) Test_VMGetDhcpAddress(check *C) { // Cleanup vApp err = deleteVapp(vcd, vapp.VApp.Name) check.Assert(err, IsNil) + task, err = network.Delete() + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) } // makeOrgVdcNetworkWithDhcp is a helper that creates a routed Org network and a DHCP pool with // single IP address to be assigned. Org vDC network and IP address assigned to DHCP pool are // returned -func makeOrgVdcNetworkWithDhcp(vcd *TestVCD, check *C, edgeGateway *EdgeGateway) *types.OrgVDCNetwork { +func makeOrgVdcNetworkWithDhcp(vcd *TestVCD, check *C, edgeGateway *EdgeGateway) *OrgVDCNetwork { var networkConfig = types.OrgVDCNetwork{ Xmlns: types.XMLNamespaceVCloud, Name: TestCreateOrgVdcNetworkDhcp, @@ -225,5 +229,5 @@ func makeOrgVdcNetworkWithDhcp(vcd *TestVCD, check *C, edgeGateway *EdgeGateway) err = task.WaitTaskCompletion() check.Assert(err, IsNil) - return network.OrgVDCNetwork + return network } diff --git a/govcd/vm_groups.go b/govcd/vm_groups.go new file mode 100644 index 000000000..6345056d7 --- /dev/null +++ b/govcd/vm_groups.go @@ -0,0 +1,215 @@ +package govcd + +import ( + "fmt" + "github.com/vmware/go-vcloud-director/v2/types/v56" + "net/url" + "strings" +) + +// LogicalVmGroup is used to create VM Placement Policies. +type LogicalVmGroup struct { + LogicalVmGroup *types.LogicalVmGroup + client *Client +} + +// VmGroup is used to create VM Placement Policies. +type VmGroup struct { + VmGroup *types.QueryResultVmGroupsRecordType + client *Client +} + +// This constant is useful when managing Logical VM Groups by referencing VM Groups, as these are +// XML based and don't deal with IDs with full URNs, while Logical VM Groups are OpenAPI based and they do. +const vmGroupUrnPrefix = "urn:vcloud:namedVmGroup" + +// GetVmGroupById finds a VM Group by its ID. +// On success, returns a pointer to the VmGroup structure and a nil error +// On failure, returns a nil pointer and an error +func (vcdClient *VCDClient) GetVmGroupById(id string) (*VmGroup, error) { + return getVmGroupWithFilter(vcdClient, "vmGroupId=="+url.QueryEscape(extractUuid(id))) +} + +// GetVmGroupByNamedVmGroupIdAndProviderVdcUrn finds a VM Group by its Named VM Group ID and Provider VDC URN. +// On success, returns a pointer to the VmGroup structure and a nil error +// On failure, returns a nil pointer and an error +func (vcdClient *VCDClient) GetVmGroupByNamedVmGroupIdAndProviderVdcUrn(namedVmGroupId, pvdcUrn string) (*VmGroup, error) { + id := extractUuid(namedVmGroupId) + resourcePools, err := getResourcePools(vcdClient, pvdcUrn) + if err != nil { + return nil, fmt.Errorf("could not get VM Group with namedVmGroupId=%s: %s", id, err) + } + filter, err := buildFilterForVmGroups(resourcePools, "namedVmGroupId", id) + if err != nil { + return nil, fmt.Errorf("could not get VM Group with namedVmGroupId=%s: %s", id, err) + } + return getVmGroupWithFilter(vcdClient, filter) +} + +// GetVmGroupByNameAndProviderVdcUrn finds a VM Group by its name and associated Provider VDC URN. +// On success, returns a pointer to the VmGroup structure and a nil error +// On failure, returns a nil pointer and an error +func (vcdClient *VCDClient) GetVmGroupByNameAndProviderVdcUrn(name, pvdcUrn string) (*VmGroup, error) { + resourcePools, err := getResourcePools(vcdClient, pvdcUrn) + if err != nil { + return nil, fmt.Errorf("could not get VM Group with vmGroupName=%s: %s", name, err) + } + filter, err := buildFilterForVmGroups(resourcePools, "vmGroupName", name) + if err != nil { + return nil, fmt.Errorf("could not get VM Group with vmGroupName=%s: %s", name, err) + } + return getVmGroupWithFilter(vcdClient, filter) +} + +// buildFilterForVmGroups builds a filter to search for VM Groups based on the given resource pools and the desired +// identifier key and value. +func buildFilterForVmGroups(resourcePools []*types.QueryResultResourcePoolRecordType, idKey, idValue string) (string, error) { + if strings.TrimSpace(idKey) == "" || strings.TrimSpace(idValue) == "" { + return "", fmt.Errorf("identifier must have a key and value to be able to search") + } + clusterMorefs := "" + vCenters := "" + for _, resourcePool := range resourcePools { + if resourcePool.ClusterMoref != "" { + clusterMorefs += fmt.Sprintf("clusterMoref==%s,", url.QueryEscape(resourcePool.ClusterMoref)) + } + if resourcePool.VcenterHREF != "" { + vCenters += fmt.Sprintf("vcId==%s,", url.QueryEscape(extractUuid(resourcePool.VcenterHREF))) + } + } + + if len(clusterMorefs) == 0 || len(vCenters) == 0 { + return "", fmt.Errorf("could not retrieve Resource pools information to retrieve VM Group with %s=%s", idKey, idValue) + } + // Removes trailing "," + clusterMorefs = clusterMorefs[:len(clusterMorefs)-1] + vCenters = vCenters[:len(vCenters)-1] + + return fmt.Sprintf("(%s==%s;(%s);(%s))", url.QueryEscape(idKey), url.QueryEscape(idValue), clusterMorefs, vCenters), nil +} + +// GetLogicalVmGroupById finds a Logical VM Group by its URN. +// On success, returns a pointer to the LogicalVmGroup structure and a nil error +// On failure, returns a nil pointer and an error +func (vcdClient *VCDClient) GetLogicalVmGroupById(logicalVmGroupId string) (*LogicalVmGroup, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointLogicalVmGroups + + apiVersion, err := vcdClient.Client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + if logicalVmGroupId == "" { + return nil, fmt.Errorf("empty Logical VM Group id") + } + + urlRef, err := vcdClient.Client.OpenApiBuildEndpoint(endpoint, logicalVmGroupId) + if err != nil { + return nil, err + } + + result := &LogicalVmGroup{ + LogicalVmGroup: &types.LogicalVmGroup{}, + client: &vcdClient.Client, + } + + err = vcdClient.Client.OpenApiGetItem(apiVersion, urlRef, nil, result.LogicalVmGroup, nil) + if err != nil { + return nil, fmt.Errorf("error getting Logical VM Group: %s", err) + } + + return result, nil +} + +// CreateLogicalVmGroup creates a new Logical VM Group in VCD +func (vcdClient *VCDClient) CreateLogicalVmGroup(logicalVmGroup types.LogicalVmGroup) (*LogicalVmGroup, error) { + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointLogicalVmGroups + + apiVersion, err := vcdClient.Client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := vcdClient.Client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + result := &LogicalVmGroup{ + LogicalVmGroup: &types.LogicalVmGroup{}, + client: &vcdClient.Client, + } + + err = vcdClient.Client.OpenApiPostItem(apiVersion, urlRef, nil, logicalVmGroup, result.LogicalVmGroup, nil) + if err != nil { + return nil, fmt.Errorf("error creating the Logical VM Group: %s", err) + } + + return result, nil +} + +// Delete deletes the receiver Logical VM Group +func (logicalVmGroup *LogicalVmGroup) Delete() error { + if logicalVmGroup.LogicalVmGroup.ID == "" { + return fmt.Errorf("cannot delete Logical VM Group without id") + } + + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointLogicalVmGroups + + apiVersion, err := logicalVmGroup.client.getOpenApiHighestElevatedVersion(endpoint) + if err != nil { + return err + } + + urlRef, err := logicalVmGroup.client.OpenApiBuildEndpoint(endpoint, logicalVmGroup.LogicalVmGroup.ID) + if err != nil { + return err + } + + err = logicalVmGroup.client.OpenApiDeleteItem(apiVersion, urlRef, nil, nil) + if err != nil { + return fmt.Errorf("error deleting the Logical VM Group: %s", err) + } + return nil +} + +// getVmGroupWithFilter finds a VM Group by specifying a filter=(filterKey==filterValue). +// On success, returns a pointer to the VmGroup structure and a nil error +// On failure, returns a nil pointer and an error +func getVmGroupWithFilter(vcdClient *VCDClient, filter string) (*VmGroup, error) { + foundVmGroups, err := vcdClient.QueryWithNotEncodedParams(nil, map[string]string{ + "type": "vmGroups", + "filter": filter, + "filterEncoded": "true", + }) + if err != nil { + return nil, err + } + if len(foundVmGroups.Results.VmGroupsRecord) == 0 { + return nil, ErrorEntityNotFound + } + if len(foundVmGroups.Results.VmGroupsRecord) > 1 { + return nil, fmt.Errorf("more than one VM Group found with the filter: %v", filter) + } + vmGroup := &VmGroup{ + VmGroup: foundVmGroups.Results.VmGroupsRecord[0], + client: &vcdClient.Client, + } + return vmGroup, nil +} + +// getResourcePools returns the Resource Pool that can unequivocally identify a VM Group +func getResourcePools(vcdClient *VCDClient, pvdcUrn string) ([]*types.QueryResultResourcePoolRecordType, error) { + foundResourcePools, err := vcdClient.QueryWithNotEncodedParams(nil, map[string]string{ + "type": "resourcePool", + "filter": fmt.Sprintf("providerVdc==%s", url.QueryEscape(pvdcUrn)), + "filterEncoded": "true", + }) + if err != nil { + return nil, fmt.Errorf("could not get the Resource pool: %s", err) + } + if len(foundResourcePools.Results.ResourcePoolRecord) == 0 { + return nil, ErrorEntityNotFound + } + return foundResourcePools.Results.ResourcePoolRecord, nil +} diff --git a/govcd/vm_groups_test.go b/govcd/vm_groups_test.go new file mode 100644 index 000000000..fd3e7a9db --- /dev/null +++ b/govcd/vm_groups_test.go @@ -0,0 +1,62 @@ +//go:build functional || openapi || ALL + +/* + * Copyright 2022 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "github.com/vmware/go-vcloud-director/v2/types/v56" + . "gopkg.in/check.v1" +) + +// This test checks the correct behaviour of the read, create and delete operations for VM Groups and Logical VM Groups. +func (vcd *TestVCD) Test_VmGroupsCRUD(check *C) { + if vcd.skipAdminTests { + check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) + } + + if vcd.config.VCD.NsxtProviderVdc.PlacementPolicyVmGroup == "" { + check.Skip(fmt.Sprintf("%s test requires vcd.nsxt_provider_vdc.placementPolicyVmGroup configuration", check.TestName())) + } + if vcd.config.VCD.NsxtProviderVdc.Name == "" { + check.Skip(fmt.Sprintf("%s test requires vcd.nsxt_provider_vdc configuration", check.TestName())) + } + + // We need the Provider VDC URN + pVdc, err := vcd.client.GetProviderVdcByName(vcd.config.VCD.NsxtProviderVdc.Name) + check.Assert(err, IsNil) + + vmGroup, err := vcd.client.GetVmGroupByNameAndProviderVdcUrn(vcd.config.VCD.NsxtProviderVdc.PlacementPolicyVmGroup, pVdc.ProviderVdc.ID) + check.Assert(err, IsNil) + check.Assert(vmGroup.VmGroup.Name, Equals, vcd.config.VCD.NsxtProviderVdc.PlacementPolicyVmGroup) + + vmGroup2, err := vcd.client.GetVmGroupById(vmGroup.VmGroup.ID) + check.Assert(err, IsNil) + check.Assert(vmGroup2, DeepEquals, vmGroup) + + vmGroup3, err := vcd.client.GetVmGroupByNamedVmGroupIdAndProviderVdcUrn(vmGroup2.VmGroup.NamedVmGroupId, pVdc.ProviderVdc.ID) + check.Assert(err, IsNil) + check.Assert(vmGroup3, DeepEquals, vmGroup2) + + logicalVmGroup, err := vcd.client.CreateLogicalVmGroup(types.LogicalVmGroup{ + Name: check.TestName(), + NamedVmGroupReferences: types.OpenApiReferences{ + types.OpenApiReference{ + ID: fmt.Sprintf("%s:%s", vmGroupUrnPrefix, vmGroup.VmGroup.NamedVmGroupId), + Name: vmGroup.VmGroup.Name}, + }, + PvdcID: pVdc.ProviderVdc.ID, + }) + check.Assert(err, IsNil) + AddToCleanupList(logicalVmGroup.LogicalVmGroup.ID, "logicalVmGroup", "", check.TestName()) + + retrievedLogicalVmGroup, err := vcd.client.GetLogicalVmGroupById(logicalVmGroup.LogicalVmGroup.ID) + check.Assert(err, IsNil) + check.Assert(retrievedLogicalVmGroup.LogicalVmGroup, DeepEquals, logicalVmGroup.LogicalVmGroup) + + err = logicalVmGroup.Delete() + check.Assert(err, IsNil) +} diff --git a/govcd/vm_groups_unit_test.go b/govcd/vm_groups_unit_test.go new file mode 100644 index 000000000..8e40a0192 --- /dev/null +++ b/govcd/vm_groups_unit_test.go @@ -0,0 +1,119 @@ +//go:build unit || ALL + +/* + * Copyright 2022 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "github.com/vmware/go-vcloud-director/v2/types/v56" + "testing" +) + +// TestVmGroupFilterWithResourcePools tests that the filter for VM Groups works correctly, as it depends +// heavily on Resource Pool data type handling. +func TestVmGroupFilterWithResourcePools(t *testing.T) { + + // This function generates a few dummy Resource Pools + getDummyResourcePools := func(howMany int, generateErrors bool) []*types.QueryResultResourcePoolRecordType { + var resourcePools []*types.QueryResultResourcePoolRecordType + clusterMoref := "" + vCenterHREF := "" + for i := 0; i < howMany; i++ { + if !generateErrors { + clusterMoref = fmt.Sprintf("domain-%d", i%10) + vCenterHREF = fmt.Sprintf("https://my-company-vcd.com/api/admin/extension/vimServer/f583b76e-9e34-48e7-b90d-930653ee161%d", i%10) + } + resourcePools = append(resourcePools, &types.QueryResultResourcePoolRecordType{ + ClusterMoref: clusterMoref, + VcenterHREF: vCenterHREF, + }) + } + return resourcePools + } + + type testData struct { + name string + resourcePools []*types.QueryResultResourcePoolRecordType + idKey string + idValue string + expectedFilter string + expectedError string + } + var testItems = []testData{ + { + name: "create_filter_with_many_resource_pools_and_vm_group_id", + resourcePools: getDummyResourcePools(2, false), + idKey: "namedVmGroupId", + idValue: "12345678-9012-3456-7890-123456789012", + expectedFilter: "(namedVmGroupId==12345678-9012-3456-7890-123456789012;(clusterMoref==domain-0,clusterMoref==domain-1);(vcId==f583b76e-9e34-48e7-b90d-930653ee1610,vcId==f583b76e-9e34-48e7-b90d-930653ee1611))", + expectedError: "", + }, + { + name: "create_filter_with_one_resource_pool_and_vm_group_id", + resourcePools: getDummyResourcePools(1, false), + idKey: "namedVmGroupId", + idValue: "12345678-9012-3456-7890-123456789012", + expectedFilter: "(namedVmGroupId==12345678-9012-3456-7890-123456789012;(clusterMoref==domain-0);(vcId==f583b76e-9e34-48e7-b90d-930653ee1610))", + expectedError: "", + }, + { + name: "create_filter_with_many_resource_pools_and_vm_group_name", + resourcePools: getDummyResourcePools(2, false), + idKey: "vmGroupName", + idValue: "testVmGroup", + expectedFilter: "(vmGroupName==testVmGroup;(clusterMoref==domain-0,clusterMoref==domain-1);(vcId==f583b76e-9e34-48e7-b90d-930653ee1610,vcId==f583b76e-9e34-48e7-b90d-930653ee1611))", + expectedError: "", + }, + { + name: "create_filter_with_one_resource_pool_and_vm_group_name", + resourcePools: getDummyResourcePools(1, false), + idKey: "vmGroupName", + idValue: "testVmGroup", + expectedFilter: "(vmGroupName==testVmGroup;(clusterMoref==domain-0);(vcId==f583b76e-9e34-48e7-b90d-930653ee1610))", + expectedError: "", + }, + { + name: "create_filter_with_one_wrong_resource_pool_should_fail", + resourcePools: getDummyResourcePools(1, true), + idKey: "someKey", + idValue: "someValue", + expectedFilter: "", + expectedError: "could not retrieve Resource pools information to retrieve VM Group with someKey=someValue", + }, + { + name: "create_filter_with_no_identifier_nor_value_should_fail", + resourcePools: getDummyResourcePools(1, true), + idKey: "", + idValue: "", + expectedFilter: "someFilter", + expectedError: "identifier must have a key and value to be able to search", + }, + { + name: "create_filter_with_no_resource_pools_should_fail", + resourcePools: getDummyResourcePools(0, false), + idKey: "someKey", + idValue: "someValue", + expectedFilter: "", + expectedError: "could not retrieve Resource pools information to retrieve VM Group with someKey=someValue", + }, + } + for _, test := range testItems { + t.Run(test.name, func(t *testing.T) { + filter, err := buildFilterForVmGroups(test.resourcePools, test.idKey, test.idValue) + if test.expectedError == "" { + // Successful path + if filter != test.expectedFilter { + t.Errorf("Expected this filter: '%s' for %s=%s but got: %s", test.expectedFilter, test.idKey, test.idValue, filter) + } + } else { + // Error path + if err == nil || err.Error() != test.expectedError { + t.Errorf("Expected error for %s=%s but got: %s", test.idKey, test.idValue, err) + } + } + }) + } +} diff --git a/govcd/vm_test.go b/govcd/vm_test.go index 4313299c3..0a61b22e8 100644 --- a/govcd/vm_test.go +++ b/govcd/vm_test.go @@ -1,4 +1,4 @@ -// +build vm functional ALL +//go:build vm || functional || ALL /* * Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. @@ -9,6 +9,7 @@ package govcd import ( "fmt" + "slices" "strings" "time" @@ -46,13 +47,9 @@ func (vcd *TestVCD) Test_FindVMByHREF(check *C) { // Test attach disk to VM and detach disk from VM func (vcd *TestVCD) Test_VMAttachOrDetachDisk(check *C) { - if vcd.config.VCD.Disk.Size <= 0 { - check.Skip("skipping test because disk size is 0") - } - // Find VM - if vcd.vapp.VApp == nil { - check.Skip("skipping test because no vApp is found") + if skipVappCreation { + check.Skip("Skipping test because vapp was not successfully created at setup") } vapp := vcd.findFirstVapp() @@ -76,7 +73,7 @@ func (vcd *TestVCD) Test_VMAttachOrDetachDisk(check *C) { // Create disk diskCreateParamsDisk := &types.Disk{ Name: TestVMAttachOrDetachDisk, - Size: vcd.config.VCD.Disk.Size, + SizeMb: 1, Description: TestVMAttachOrDetachDisk, } @@ -101,7 +98,7 @@ func (vcd *TestVCD) Test_VMAttachOrDetachDisk(check *C) { disk, err := vcd.vdc.GetDiskByHref(diskHREF) check.Assert(err, IsNil) check.Assert(disk.Disk.Name, Equals, diskCreateParamsDisk.Name) - check.Assert(disk.Disk.Size, Equals, diskCreateParamsDisk.Size) + check.Assert(disk.Disk.SizeMb, Equals, diskCreateParamsDisk.SizeMb) check.Assert(disk.Disk.Description, Equals, diskCreateParamsDisk.Description) // Attach disk @@ -134,108 +131,8 @@ func (vcd *TestVCD) Test_VMAttachOrDetachDisk(check *C) { } -// Test attach disk to VM -func (vcd *TestVCD) Test_VMAttachDisk(check *C) { - - if vcd.skipVappTests { - check.Skip("Skipping test because vapp was not successfully created at setup") - } - - if vcd.config.VCD.Disk.Size <= 0 { - check.Skip("skipping test because disk size is 0") - } - - if vcd.skipVappTests { - check.Skip("skipping test because vApp wasn't properly created") - } - - // Find VM - vapp := vcd.findFirstVapp() - vmType, vmName := vcd.findFirstVm(vapp) - if vmName == "" { - check.Skip("skipping test because no VM is found") - } - - fmt.Printf("Running: %s\n", check.TestName()) - - vm := NewVM(&vcd.client.Client) - vm.VM = &vmType - - // Discard vApp suspension - // Disk attach and detach operations are not working if vApp is suspended - err := vcd.ensureVappIsSuitableForVMTest(vapp) - check.Assert(err, IsNil) - err = vcd.ensureVMIsSuitableForVMTest(vm) - check.Assert(err, IsNil) - - // Create disk - diskCreateParamsDisk := &types.Disk{ - Name: TestVMAttachDisk, - Size: vcd.config.VCD.Disk.Size, - Description: TestVMAttachDisk, - } - - diskCreateParams := &types.DiskCreateParams{ - Disk: diskCreateParamsDisk, - } - - task, err := vcd.vdc.CreateDisk(diskCreateParams) - check.Assert(err, IsNil) - - check.Assert(task.Task.Owner.Type, Equals, types.MimeDisk) - diskHREF := task.Task.Owner.HREF - - PrependToCleanupList(diskHREF, "disk", "", check.TestName()) - - // Wait for disk creation complete - err = task.WaitTaskCompletion() - check.Assert(err, IsNil) - - // Verify created disk - check.Assert(diskHREF, Not(Equals), "") - disk, err := vcd.vdc.GetDiskByHref(diskHREF) - check.Assert(err, IsNil) - check.Assert(disk.Disk.Name, Equals, diskCreateParamsDisk.Name) - check.Assert(disk.Disk.Size, Equals, diskCreateParamsDisk.Size) - check.Assert(disk.Disk.Description, Equals, diskCreateParamsDisk.Description) - - // Attach disk - attachDiskTask, err := vm.AttachDisk(&types.DiskAttachOrDetachParams{ - Disk: &types.Reference{ - HREF: disk.Disk.HREF, - }, - }) - check.Assert(err, IsNil) - - err = attachDiskTask.WaitTaskCompletion() - check.Assert(err, IsNil) - - // Get attached VM - vmRef, err := disk.AttachedVM() - check.Assert(err, IsNil) - check.Assert(vmRef, NotNil) - check.Assert(vmRef.Name, Equals, vm.VM.Name) - - // Cleanup: Detach disk - detachDiskTask, err := vm.attachOrDetachDisk(&types.DiskAttachOrDetachParams{ - Disk: &types.Reference{ - HREF: disk.Disk.HREF, - }, - }, types.RelDiskDetach) - check.Assert(err, IsNil) - - err = detachDiskTask.WaitTaskCompletion() - check.Assert(err, IsNil) - -} - -// Test detach disk from VM -func (vcd *TestVCD) Test_VMDetachDisk(check *C) { - - if vcd.config.VCD.Disk.Size <= 0 { - check.Skip("skipping test because disk size is 0") - } - +// Test attach/detach disk from VM +func (vcd *TestVCD) Test_VMAttachAndDetachDisk(check *C) { if vcd.skipVappTests { check.Skip("skipping test because vApp wasn't properly created") } @@ -262,7 +159,7 @@ func (vcd *TestVCD) Test_VMDetachDisk(check *C) { // Create disk diskCreateParamsDisk := &types.Disk{ Name: TestVMDetachDisk, - Size: vcd.config.VCD.Disk.Size, + SizeMb: 1, Description: TestVMDetachDisk, } @@ -288,7 +185,7 @@ func (vcd *TestVCD) Test_VMDetachDisk(check *C) { disk, err := vcd.vdc.GetDiskByHref(diskHREF) check.Assert(err, IsNil) check.Assert(disk.Disk.Name, Equals, diskCreateParamsDisk.Name) - check.Assert(disk.Disk.Size, Equals, diskCreateParamsDisk.Size) + check.Assert(disk.Disk.SizeMb, Equals, diskCreateParamsDisk.SizeMb) check.Assert(disk.Disk.Description, Equals, diskCreateParamsDisk.Description) // Attach disk @@ -328,10 +225,10 @@ func (vcd *TestVCD) Test_HandleInsertOrEjectMedia(check *C) { if vcd.skipVappTests { check.Skip("Skipping test because vapp was not successfully created at setup") } - itemName := "TestHandleInsertOrEjectMedia" + itemName := check.TestName() // Find VApp - if vcd.vapp.VApp == nil { + if vcd.vapp != nil && vcd.vapp.VApp == nil { check.Skip("skipping test because no vApp is found") } @@ -380,144 +277,8 @@ func (vcd *TestVCD) Test_HandleInsertOrEjectMedia(check *C) { //verify check.Assert(isMediaInjected(vm.VM.VirtualHardwareSection.Item), Equals, false) -} - -// Test Insert or Eject Media for VM -func (vcd *TestVCD) Test_InsertOrEjectMedia(check *C) { - fmt.Printf("Running: %s\n", check.TestName()) - - if vcd.skipVappTests { - check.Skip("Skipping test because vapp was not successfully created at setup") - } - - // Skipping this test due to a bug in vCD. VM refresh status returns old state, though eject task is finished. - if vcd.client.Client.APIVCDMaxVersionIs(">= 32.0, <= 33.0") { - check.Skip("Skipping test because this vCD version has a bug") - } - - itemName := "TestInsertOrEjectMedia" - - // Find VApp - if vcd.vapp.VApp == nil { - check.Skip("skipping test because no vApp is found") - } - - vapp := vcd.findFirstVapp() - vmType, vmName := vcd.findFirstVm(vapp) - if vmName == "" { - check.Skip("skipping test because no VM is found") - } - - fmt.Printf("Running: %s\n", check.TestName()) - - vm := NewVM(&vcd.client.Client) - vm.VM = &vmType - - // Upload Media - catalog, err := vcd.org.GetCatalogByName(vcd.config.VCD.Catalog.Name, false) - check.Assert(err, IsNil) - check.Assert(catalog, NotNil) - - uploadTask, err := catalog.UploadMediaImage(itemName, "upload from test", vcd.config.Media.MediaPath, 1024) - check.Assert(err, IsNil) - err = uploadTask.WaitTaskCompletion() - check.Assert(err, IsNil) - - AddToCleanupList(itemName, "mediaCatalogImage", vcd.org.Org.Name+"|"+vcd.config.VCD.Catalog.Name, "Test_InsertOrEjectMedia") - - catalog, err = vcd.org.GetCatalogByName(vcd.config.VCD.Catalog.Name, true) - check.Assert(err, IsNil) - check.Assert(catalog, NotNil) - - media, err := catalog.GetMediaByName(itemName, false) - check.Assert(err, IsNil) - check.Assert(media, NotNil) - - // Insert Media - insertMediaTask, err := vm.insertOrEjectMedia(&types.MediaInsertOrEjectParams{ - Media: &types.Reference{ - HREF: media.Media.HREF, - Name: media.Media.Name, - ID: media.Media.ID, - Type: media.Media.Type, - }, - }, types.RelMediaInsertMedia) - check.Assert(err, IsNil) - - err = insertMediaTask.WaitTaskCompletion() - check.Assert(err, IsNil) - - //verify - err = vm.Refresh() - check.Assert(err, IsNil) - - check.Assert(isMediaInjected(vm.VM.VirtualHardwareSection.Item), Equals, true) - - // Insert Media - ejectMediaTask, err := vm.insertOrEjectMedia(&types.MediaInsertOrEjectParams{ - Media: &types.Reference{ - HREF: media.Media.HREF, - }, - }, types.RelMediaEjectMedia) - check.Assert(err, IsNil) - - err = ejectMediaTask.WaitTaskCompletion() - check.Assert(err, IsNil) - - //verify - err = vm.Refresh() - check.Assert(err, IsNil) - check.Assert(isMediaInjected(vm.VM.VirtualHardwareSection.Item), Equals, false) -} - -// Test Insert or Eject Media for VM -func (vcd *TestVCD) Test_AnswerVmQuestion(check *C) { - fmt.Printf("Running: %s\n", check.TestName()) - - if vcd.skipVappTests { - check.Skip("Skipping test because vapp was not successfully created at setup") - } - - itemName := "TestAnswerVmQuestion" - - // Find VApp - if vcd.vapp.VApp == nil { - check.Skip("skipping test because no vApp is found") - } - - vapp := vcd.findFirstVapp() - vmType, vmName := vcd.findFirstVm(vapp) - if vmName == "" { - check.Skip("skipping test because no VM is found") - } - - vm := NewVM(&vcd.client.Client) - vm.VM = &vmType - - // Upload Media - catalog, err := vcd.org.GetCatalogByName(vcd.config.VCD.Catalog.Name, false) - check.Assert(err, IsNil) - check.Assert(catalog, NotNil) - - uploadTask, err := catalog.UploadMediaImage(itemName, "upload from test", vcd.config.Media.MediaPath, 1024) - check.Assert(err, IsNil) - err = uploadTask.WaitTaskCompletion() - check.Assert(err, IsNil) - - AddToCleanupList(itemName, "mediaCatalogImage", vcd.org.Org.Name+"|"+vcd.config.VCD.Catalog.Name, "Test_AnswerVmQuestion") - - catalog, err = vcd.org.GetCatalogByName(vcd.config.VCD.Catalog.Name, true) - check.Assert(err, IsNil) - check.Assert(catalog, NotNil) - - media, err := catalog.GetMediaByName(itemName, false) - check.Assert(err, IsNil) - check.Assert(media, NotNil) - - err = vm.Refresh() - check.Assert(err, IsNil) - insertMediaTask, err := vm.HandleInsertMedia(vcd.org, vcd.config.VCD.Catalog.Name, itemName) + insertMediaTask, err = vm.HandleInsertMedia(vcd.org, vcd.config.VCD.Catalog.Name, itemName) check.Assert(err, IsNil) err = insertMediaTask.WaitTaskCompletion() @@ -549,6 +310,12 @@ func (vcd *TestVCD) Test_AnswerVmQuestion(check *C) { err = vm.Refresh() check.Assert(err, IsNil) check.Assert(isMediaInjected(vm.VM.VirtualHardwareSection.Item), Equals, false) + + // Remove catalog item so far other tests don't fail + task, err := media.Delete() + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) } func (vcd *TestVCD) Test_VMChangeCPUCountWithCore(check *C) { @@ -618,17 +385,11 @@ func (vcd *TestVCD) Test_VMToggleHardwareVirtualization(check *C) { if vcd.skipVappTests { check.Skip("Skipping test because vapp was not successfully created at setup") } - vapp := vcd.findFirstVapp() - existingVm, vmName := vcd.findFirstVm(vapp) - if vmName == "" { - check.Skip("skipping test because no VM is found") - } - // Default nesting status should be false - nestingStatus := existingVm.NestedHypervisorEnabled - check.Assert(nestingStatus, Equals, false) - vm, err := vcd.client.Client.GetVMByHref(existingVm.HREF) - check.Assert(err, IsNil) + _, vm := createNsxtVAppAndVm(vcd, check) + + nestingStatus := vm.VM.NestedHypervisorEnabled + check.Assert(nestingStatus, Equals, false) // PowerOn task, err := vm.PowerOn() @@ -641,8 +402,8 @@ func (vcd *TestVCD) Test_VMToggleHardwareVirtualization(check *C) { _, err = vm.ToggleHardwareVirtualization(true) check.Assert(err, ErrorMatches, ".*hardware virtualization can be changed from powered off state.*") - // PowerOf - task, err = vm.PowerOff() + // Undeploy, so the VM goes to POWERED_OFF state instead of PARTIALLY_POWERED_OFF + task, err = vm.Undeploy() check.Assert(err, IsNil) err = task.WaitTaskCompletion() check.Assert(err, IsNil) @@ -668,24 +429,19 @@ func (vcd *TestVCD) Test_VMToggleHardwareVirtualization(check *C) { err = vm.Refresh() check.Assert(err, IsNil) check.Assert(vm.VM.NestedHypervisorEnabled, Equals, false) + + err = deleteNsxtVapp(vcd, check.TestName()) + check.Assert(err, IsNil) } func (vcd *TestVCD) Test_VMPowerOnPowerOff(check *C) { - if vcd.skipVappTests { - check.Skip("Skipping test because vapp was not successfully created at setup") - } - vapp := vcd.findFirstVapp() - existingVm, vmName := vcd.findFirstVm(vapp) - if vmName == "" { - check.Skip("skipping test because no VM is found") - } - vm, err := vcd.client.Client.GetVMByHref(existingVm.HREF) - check.Assert(err, IsNil) + _, vm := createNsxtVAppAndVm(vcd, check) // Ensure VM is not powered on vmStatus, err := vm.GetStatus() check.Assert(err, IsNil) - if vmStatus != "POWERED_OFF" { + if vmStatus != "POWERED_OFF" && vmStatus != "PARTIALLY_POWERED_OFF" { + fmt.Printf("VM status: %s, powering off", vmStatus) task, err := vm.PowerOff() check.Assert(err, IsNil) err = task.WaitTaskCompletion() @@ -704,45 +460,113 @@ func (vcd *TestVCD) Test_VMPowerOnPowerOff(check *C) { check.Assert(err, IsNil) check.Assert(vmStatus, Equals, "POWERED_ON") - // Power off again task, err = vm.PowerOff() check.Assert(err, IsNil) err = task.WaitTaskCompletion() check.Assert(err, IsNil) check.Assert(task.Task.Status, Equals, "success") - err = vm.Refresh() - check.Assert(err, IsNil) vmStatus, err = vm.GetStatus() check.Assert(err, IsNil) - check.Assert(vmStatus, Equals, "POWERED_OFF") -} - -func (vcd *TestVCD) Test_GetNetworkConnectionSection(check *C) { - if vcd.skipVappTests { - check.Skip("Skipping test because vapp was not successfully created at setup") - } - - vapp := vcd.findFirstVapp() - existingVm, vmName := vcd.findFirstVm(vapp) - if vmName == "" { - check.Skip("skipping test because no VM is found") - } + check.Assert(vmStatus == "POWERED_OFF" || vmStatus == "PARTIALLY_POWERED_OFF", Equals, true) - vm, err := vcd.client.Client.GetVMByHref(existingVm.HREF) + err = deleteNsxtVapp(vcd, check.TestName()) check.Assert(err, IsNil) +} - networkBefore, err := vm.GetNetworkConnectionSection() - check.Assert(err, IsNil) +func (vcd *TestVCD) Test_VmShutdown(check *C) { + vapp, vm := createNsxtVAppAndVm(vcd, check) - err = vm.UpdateNetworkConnectionSection(networkBefore) + vdc, err := vm.GetParentVdc() check.Assert(err, IsNil) - networkAfter, err := vm.GetNetworkConnectionSection() + // Ensure VM is not powered on + vmStatus, err := vm.GetStatus() check.Assert(err, IsNil) + fmt.Println("VM status: ", vmStatus) - // Filter out always differing fields and do deep comparison of objects - networkBefore.Link = &types.Link{} - networkAfter.Link = &types.Link{} + if vmStatus != "POWERED_ON" { + task, err := vm.PowerOn() + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) + check.Assert(task.Task.Status, Equals, "success") + err = vm.Refresh() + check.Assert(err, IsNil) + vmStatus, err = vm.GetStatus() + check.Assert(err, IsNil) + fmt.Println("VM status: ", vmStatus) + } + + timeout := time.Minute * 5 // Avoiding infinite loops + startTime := time.Now() + elapsed := time.Since(startTime) + gcStatus := "" + statusFound := false + // Wait until Guest Tools gets to `REBOOT_PENDING` or `GC_COMPLETE` as there is no real way to + // check if VM has Guest Tools operating + for elapsed < timeout { + err = vm.Refresh() + check.Assert(err, IsNil) + + vmQuery, err := vdc.QueryVM(vapp.VApp.Name, vm.VM.Name) + check.Assert(err, IsNil) + + gcStatus = vmQuery.VM.GcStatus + printVerbose("VM Tools Status: %s (%s)\n", vmQuery.VM.GcStatus, elapsed) + if vmQuery.VM.GcStatus == "GC_COMPLETE" || vmQuery.VM.GcStatus == "REBOOT_PENDING" { + statusFound = true + break + } + + time.Sleep(5 * time.Second) + elapsed = time.Since(startTime) + } + fmt.Printf("VM Tools Status: %s (%s)\n", gcStatus, elapsed) + check.Assert(statusFound, Equals, true) + + printVerbose("Shutting down VM:\n") + + task, err := vm.Shutdown() + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) + check.Assert(task.Task.Status, Equals, "success") + + newStatus, err := vm.GetStatus() + check.Assert(err, IsNil) + printVerbose("New VM status: %s\n", newStatus) + check.Assert(newStatus, Equals, "POWERED_OFF") + + err = deleteNsxtVapp(vcd, check.TestName()) + check.Assert(err, IsNil) +} + +func (vcd *TestVCD) Test_GetNetworkConnectionSection(check *C) { + if vcd.skipVappTests { + check.Skip("Skipping test because vapp was not successfully created at setup") + } + + vapp := vcd.findFirstVapp() + existingVm, vmName := vcd.findFirstVm(vapp) + if vmName == "" { + check.Skip("skipping test because no VM is found") + } + + vm, err := vcd.client.Client.GetVMByHref(existingVm.HREF) + check.Assert(err, IsNil) + + networkBefore, err := vm.GetNetworkConnectionSection() + check.Assert(err, IsNil) + + err = vm.UpdateNetworkConnectionSection(networkBefore) + check.Assert(err, IsNil) + + networkAfter, err := vm.GetNetworkConnectionSection() + check.Assert(err, IsNil) + + // Filter out always differing fields and do deep comparison of objects + networkBefore.Link = &types.Link{} + networkAfter.Link = &types.Link{} check.Assert(networkAfter, DeepEquals, networkBefore) } @@ -755,19 +579,10 @@ func (vcd *TestVCD) Test_GetNetworkConnectionSection(check *C) { // This test relies on longer timeouts in BlockWhileGuestCustomizationStatus because VMs take a lengthy time // to boot up and report customization done. func (vcd *TestVCD) Test_PowerOnAndForceCustomization(check *C) { - if vcd.skipVappTests { - check.Skip("Skipping test because vApp wasn't properly created") - } fmt.Printf("Running: %s\n", check.TestName()) - vapp := vcd.findFirstVapp() - vmType, vmName := vcd.findFirstVm(vapp) - if vmName == "" { - check.Skip("skipping test because no VM is found") - } - vm, err := vcd.client.Client.GetVMByHref(vmType.HREF) - check.Assert(err, IsNil) + _, vm := createNsxtVAppAndVm(vcd, check) // It may be that prebuilt VM was not booted before in the test vApp and it would still have // a guest customization status 'GC_PENDING'. This is because initially VM has this flag set @@ -776,9 +591,10 @@ func (vcd *TestVCD) Test_PowerOnAndForceCustomization(check *C) { // 'GC_PENDING' state. custStatus, err := vm.GetGuestCustomizationStatus() check.Assert(err, IsNil) + + vmStatus, err := vm.GetStatus() + check.Assert(err, IsNil) if custStatus == types.GuestCustStatusPending { - vmStatus, err := vm.GetStatus() - check.Assert(err, IsNil) // If VM is POWERED OFF - let's power it on before waiting for its status to change if vmStatus == "POWERED_OFF" { task, err := vm.PowerOn() @@ -831,6 +647,9 @@ func (vcd *TestVCD) Test_PowerOnAndForceCustomization(check *C) { // commands on guest VMs err = vm.BlockWhileGuestCustomizationStatus(types.GuestCustStatusPending, 300) check.Assert(err, IsNil) + + err = deleteNsxtVapp(vcd, check.TestName()) + check.Assert(err, IsNil) } func (vcd *TestVCD) Test_BlockWhileGuestCustomizationStatus(check *C) { @@ -901,51 +720,94 @@ func (vcd *TestVCD) Test_VMSetGetGuestCustomizationSection(check *C) { guestCustomizationPropertyTester(vcd, check, vm) } -// Test create internal disk For VM -func (vcd *TestVCD) Test_AddInternalDisk(check *C) { +// Test create/update/remove of Internal Disk +func (vcd *TestVCD) Test_InternalDisk(check *C) { fmt.Printf("Running: %s\n", check.TestName()) // In general VM internal disks works with Org users, but due we need change VDC fast provisioning value, we have to be sys admins if vcd.skipAdminTests { check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) } - - vmName := "Test_AddInternalDisk" - + vmName := check.TestName() vm, storageProfile, diskSettings, diskId, previousProvisioningValue, err := vcd.createInternalDisk(check, vmName, 1) check.Assert(err, IsNil) + description := check.TestName() + "_Description" + vm, err = vm.UpdateVmSpecSection(vm.VM.VmSpecSection, description) + check.Assert(err, IsNil) + //verify disk, err := vm.GetInternalDiskById(diskId, true) check.Assert(err, IsNil) + check.Assert(disk, NotNil) check.Assert(disk.StorageProfile.HREF, Equals, storageProfile.HREF) check.Assert(disk.StorageProfile.ID, Equals, storageProfile.ID) check.Assert(disk.AdapterType, Equals, diskSettings.AdapterType) check.Assert(*disk.ThinProvisioned, Equals, *diskSettings.ThinProvisioned) - check.Assert(*disk.Iops, Equals, *diskSettings.Iops) + check.Assert(disk.IopsAllocation, NotNil) + check.Assert(diskSettings.IopsAllocation, NotNil) + check.Assert(disk.IopsAllocation.Reservation, Equals, diskSettings.IopsAllocation.Reservation) check.Assert(disk.SizeMb, Equals, diskSettings.SizeMb) check.Assert(disk.UnitNumber, Equals, diskSettings.UnitNumber) check.Assert(disk.BusNumber, Equals, diskSettings.BusNumber) check.Assert(disk.AdapterType, Equals, diskSettings.AdapterType) + // increase new disk size + vmSpecSection := vm.VM.VmSpecSection + changeDiskSettings := vm.VM.VmSpecSection.DiskSection.DiskSettings + for _, diskSettings := range changeDiskSettings { + if diskSettings.DiskId == diskId { + diskSettings.SizeMb = 2048 + } + } + + vmSpecSection.DiskSection.DiskSettings = changeDiskSettings + + vmSpecSection, err = vm.UpdateInternalDisks(vmSpecSection) + check.Assert(err, IsNil) + check.Assert(vmSpecSection, NotNil) + + disk, err = vm.GetInternalDiskById(diskId, true) + check.Assert(err, IsNil) + check.Assert(disk, NotNil) + + //verify + check.Assert(disk.StorageProfile.HREF, Equals, storageProfile.HREF) + check.Assert(disk.StorageProfile.ID, Equals, storageProfile.ID) + check.Assert(disk.AdapterType, Equals, diskSettings.AdapterType) + check.Assert(*disk.ThinProvisioned, Equals, *diskSettings.ThinProvisioned) + check.Assert(disk.IopsAllocation.Reservation, Equals, diskSettings.IopsAllocation.Reservation) + check.Assert(disk.SizeMb, Equals, int64(2048)) + check.Assert(disk.UnitNumber, Equals, diskSettings.UnitNumber) + check.Assert(disk.BusNumber, Equals, diskSettings.BusNumber) + check.Assert(disk.AdapterType, Equals, diskSettings.AdapterType) + + // verify that VM description is still available - test for bugfix #418 + err = vm.Refresh() + check.Assert(err, IsNil) + check.Assert(vm.VM.Description, Equals, description) + + // attach independent disk + independentDisk, err := attachIndependentDisk(vcd, check, vm) + check.Assert(err, IsNil) + //cleanup err = vm.DeleteInternalDisk(diskId) check.Assert(err, IsNil) + detachIndependentDisk(vcd, check, independentDisk) // disable fast provisioning if needed updateVdcFastProvisioning(vcd, check, previousProvisioningValue) // delete Vapp early to avoid env capacity issue - deleteVapp(vcd, vmName) + err = deleteVapp(vcd, vmName) + check.Assert(err, IsNil) } // createInternalDisk Finds available VM and creates internal Disk in it. // returns VM, storage profile, disk settings, disk id and error. func (vcd *TestVCD) createInternalDisk(check *C, vmName string, busNumber int) (*VM, types.Reference, *types.DiskSettings, string, string, error) { - if vcd.skipVappTests { - check.Skip("Skipping test because vApp wasn't properly created") - } if vcd.config.VCD.StorageProfile.SP1 == "" { check.Skip("No Storage Profile given for VDC tests") } @@ -965,13 +827,12 @@ func (vcd *TestVCD) createInternalDisk(check *C, vmName string, busNumber int) ( vdc, _, vappTemplate, vapp, desiredNetConfig, err := vcd.createAndGetResourcesForVmCreation(check, vmName) check.Assert(err, IsNil) - vm, err := spawnVM("FirstNode", 512, *vdc, *vapp, desiredNetConfig, vappTemplate, check, "", true) + vm, err := spawnVM("FirstNode", 512, *vdc, *vapp, desiredNetConfig, vappTemplate, check, "", false) check.Assert(err, IsNil) storageProfile, err := vcd.vdc.FindStorageProfileReference(vcd.config.VCD.StorageProfile.SP1) check.Assert(err, IsNil) isThinProvisioned := true - iops := int64(0) diskSettings := &types.DiskSettings{ SizeMb: 1024, UnitNumber: 0, @@ -980,7 +841,12 @@ func (vcd *TestVCD) createInternalDisk(check *C, vmName string, busNumber int) ( ThinProvisioned: &isThinProvisioned, StorageProfile: &storageProfile, OverrideVmDefault: true, - Iops: &iops, + IopsAllocation: &types.IopsResource{ + Limit: 0, + Reservation: 0, + SharesLevel: "NORMAL", + Shares: 1000, + }, } diskId, err := vm.AddInternalDisk(diskSettings) @@ -1021,123 +887,15 @@ func updateVdcFastProvisioning(vcd *TestVCD, check *C, enable string) string { return vdcFastProvisioningValue } -// Test delete internal disk For VM -func (vcd *TestVCD) Test_DeleteInternalDisk(check *C) { - fmt.Printf("Running: %s\n", check.TestName()) - - // In general VM internal disks works with Org users, but due we need change VDC fast provisioning value, we have to be sys admins - if vcd.skipAdminTests { - check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) - } - - vmName := "Test_DeleteInternalDisk" - - vm, _, _, diskId, previousProvisioningValue, err := vcd.createInternalDisk(check, vmName, 2) - check.Assert(err, IsNil) - - //verify - err = vm.Refresh() - check.Assert(err, IsNil) - - err = vm.DeleteInternalDisk(diskId) - check.Assert(err, IsNil) - - disk, err := vm.GetInternalDiskById(diskId, true) - check.Assert(err, Equals, ErrorEntityNotFound) - check.Assert(disk, IsNil) - - // enable fast provisioning if needed - updateVdcFastProvisioning(vcd, check, previousProvisioningValue) - - // delete Vapp early to avoid env capacity issue - deleteVapp(vcd, vmName) -} - -// Test update internal disk for VM which has independent disk -func (vcd *TestVCD) Test_UpdateInternalDisk(check *C) { - fmt.Printf("Running: %s\n", check.TestName()) - - // In general VM internal disks works with Org users, but due we need change VDC fast provisioning value, we have to be sys admins - if vcd.skipAdminTests { - check.Skip(fmt.Sprintf(TestRequiresSysAdminPrivileges, check.TestName())) - } - vmName := "Test_UpdateInternalDisk" - vm, storageProfile, diskSettings, diskId, previousProvisioningValue, err := vcd.createInternalDisk(check, vmName, 1) - check.Assert(err, IsNil) - - //verify - disk, err := vm.GetInternalDiskById(diskId, true) - check.Assert(err, IsNil) - check.Assert(disk, NotNil) - - // increase new disk size - vmSpecSection := vm.VM.VmSpecSection - changeDiskSettings := vm.VM.VmSpecSection.DiskSection.DiskSettings - for _, diskSettings := range changeDiskSettings { - if diskSettings.DiskId == diskId { - diskSettings.SizeMb = 2048 - } - } - - vmSpecSection.DiskSection.DiskSettings = changeDiskSettings - - vmSpecSection, err = vm.UpdateInternalDisks(vmSpecSection) - check.Assert(err, IsNil) - check.Assert(vmSpecSection, NotNil) - - disk, err = vm.GetInternalDiskById(diskId, true) - check.Assert(err, IsNil) - check.Assert(disk, NotNil) - - //verify - check.Assert(disk.StorageProfile.HREF, Equals, storageProfile.HREF) - check.Assert(disk.StorageProfile.ID, Equals, storageProfile.ID) - check.Assert(disk.AdapterType, Equals, diskSettings.AdapterType) - check.Assert(*disk.ThinProvisioned, Equals, *diskSettings.ThinProvisioned) - check.Assert(*disk.Iops, Equals, *diskSettings.Iops) - check.Assert(disk.SizeMb, Equals, int64(2048)) - check.Assert(disk.UnitNumber, Equals, diskSettings.UnitNumber) - check.Assert(disk.BusNumber, Equals, diskSettings.BusNumber) - check.Assert(disk.AdapterType, Equals, diskSettings.AdapterType) - - // attach independent disk - independentDisk, err := attachIndependentDisk(vcd, check) - check.Assert(err, IsNil) - - //cleanup - err = vm.DeleteInternalDisk(diskId) - check.Assert(err, IsNil) - detachIndependentDisk(vcd, check, independentDisk) - - // disable fast provisioning if needed - updateVdcFastProvisioning(vcd, check, previousProvisioningValue) - - // delete Vapp early to avoid env capacity issue - deleteVapp(vcd, vmName) -} - -func attachIndependentDisk(vcd *TestVCD, check *C) (*Disk, error) { - // Find VM - vapp := vcd.findFirstVapp() - vmType, vmName := vcd.findFirstVm(vapp) - if vmName == "" { - check.Skip("skipping test because no VM is found") - } - - vm := NewVM(&vcd.client.Client) - vm.VM = &vmType - - // Ensure vApp and VM are suitable for this test +func attachIndependentDisk(vcd *TestVCD, check *C, vm *VM) (*Disk, error) { // Disk attach and detach operations are not working if VM is suspended - err := vcd.ensureVappIsSuitableForVMTest(vapp) - check.Assert(err, IsNil) - err = vcd.ensureVMIsSuitableForVMTest(vm) + err := vcd.ensureVMIsSuitableForVMTest(vm) check.Assert(err, IsNil) // Create disk diskCreateParamsDisk := &types.Disk{ Name: TestAttachedVMDisk, - Size: vcd.config.VCD.Disk.Size, + SizeMb: 1, Description: TestAttachedVMDisk, } @@ -1162,7 +920,7 @@ func attachIndependentDisk(vcd *TestVCD, check *C) (*Disk, error) { disk, err := vcd.vdc.GetDiskByHref(diskHREF) check.Assert(err, IsNil) check.Assert(disk.Disk.Name, Equals, diskCreateParamsDisk.Name) - check.Assert(disk.Disk.Size, Equals, diskCreateParamsDisk.Size) + check.Assert(disk.Disk.SizeMb, Equals, diskCreateParamsDisk.SizeMb) check.Assert(disk.Disk.Description, Equals, diskCreateParamsDisk.Description) // Attach disk @@ -1220,11 +978,11 @@ func (vcd *TestVCD) Test_AddNewEmptyVMMultiNIC(check *C) { } // Find VApp - if vcd.vapp.VApp == nil { + if vcd.vapp != nil && vcd.vapp.VApp == nil { check.Skip("skipping test because no vApp is found") } - vapp, err := createVappForTest(vcd, "Test_AddNewEmptyVMMultiNIC") + vapp, err := deployVappForTest(vcd, "Test_AddNewEmptyVMMultiNIC") check.Assert(err, IsNil) check.Assert(vapp, NotNil) @@ -1285,7 +1043,7 @@ func (vcd *TestVCD) Test_AddNewEmptyVMMultiNIC(check *C) { SizeMb: int64(16384), BusNumber: 0, UnitNumber: 0, - ThinProvisioned: takeBoolPointer(true), + ThinProvisioned: addrOf(true), OverrideVmDefault: true} requestDetails := &types.RecomposeVAppParamsForEmptyVm{ @@ -1295,11 +1053,11 @@ func (vcd *TestVCD) Test_AddNewEmptyVMMultiNIC(check *C) { Description: "created by Test_AddNewEmptyVMMultiNIC", GuestCustomizationSection: nil, VmSpecSection: &types.VmSpecSection{ - Modified: takeBoolPointer(true), + Modified: addrOf(true), Info: "Virtual Machine specification", OsType: "debian10Guest", - NumCpus: takeIntAddress(2), - NumCoresPerSocket: takeIntAddress(1), + NumCpus: addrOf(2), + NumCoresPerSocket: addrOf(1), CpuResourceMhz: &types.CpuResourceMhz{Configured: 1}, MemoryResourceMb: &types.MemoryResourceMb{Configured: 1024}, DiskSection: &types.DiskSection{DiskSettings: []*types.DiskSettings{&newDisk}}, @@ -1347,7 +1105,7 @@ func (vcd *TestVCD) Test_AddNewEmptyVMMultiNIC(check *C) { func (vcd *TestVCD) Test_UpdateVmSpecSection(check *C) { fmt.Printf("Running: %s\n", check.TestName()) - vmName := "Test_UpdateVmSpecSection" + vmName := check.TestName() if vcd.skipVappTests { check.Skip("Skipping test because vApp wasn't properly created") } @@ -1355,34 +1113,104 @@ func (vcd *TestVCD) Test_UpdateVmSpecSection(check *C) { vdc, _, vappTemplate, vapp, desiredNetConfig, err := vcd.createAndGetResourcesForVmCreation(check, vmName) check.Assert(err, IsNil) - vm, err := spawnVM("FirstNode", 512, *vdc, *vapp, desiredNetConfig, vappTemplate, check, "", true) - check.Assert(err, IsNil) - - task, err := vm.PowerOff() - check.Assert(err, IsNil) - err = task.WaitTaskCompletion() + vm, err := spawnVM("FirstNode", 512, *vdc, *vapp, desiredNetConfig, vappTemplate, check, "", false) check.Assert(err, IsNil) vmSpecSection := vm.VM.VmSpecSection - osType := "sles10_64Guest" - vmSpecSection.OsType = osType - vmSpecSection.NumCpus = takeIntAddress(4) - vmSpecSection.NumCoresPerSocket = takeIntAddress(2) + vmSpecSection.NumCpus = addrOf(4) + vmSpecSection.NumCoresPerSocket = addrOf(2) vmSpecSection.MemoryResourceMb = &types.MemoryResourceMb{Configured: 768} + if vcd.client.Client.APIVCDMaxVersionIs(">=37.1") { + vmSpecSection.Firmware = "efi" + } updatedVm, err := vm.UpdateVmSpecSection(vmSpecSection, "updateDescription") check.Assert(err, IsNil) check.Assert(updatedVm, NotNil) //verify - check.Assert(updatedVm.VM.VmSpecSection.OsType, Equals, osType) check.Assert(*updatedVm.VM.VmSpecSection.NumCpus, Equals, 4) check.Assert(*updatedVm.VM.VmSpecSection.NumCoresPerSocket, Equals, 2) check.Assert(updatedVm.VM.VmSpecSection.MemoryResourceMb.Configured, Equals, int64(768)) check.Assert(updatedVm.VM.Description, Equals, "updateDescription") + if vcd.client.Client.APIVCDMaxVersionIs(">=37.1") { + check.Assert(updatedVm.VM.VmSpecSection.Firmware, Equals, "efi") + } + + // delete Vapp early to avoid env capacity issue + err = deleteVapp(vcd, vmName) + check.Assert(err, IsNil) +} + +func (vcd *TestVCD) Test_VmBootOptions(check *C) { + fmt.Printf("Running: %s\n", check.TestName()) + + vmName := check.TestName() + org, err := vcd.client.GetAdminOrgByName(vcd.config.VCD.Org) + check.Assert(err, IsNil) + vdc, err := org.GetVDCByName(vcd.config.VCD.Vdc, false) + check.Assert(err, IsNil) + check.Assert(vdc, NotNil) + + _, vm := createNsxtVAppAndVmWithEfiSupport(vcd, check) + check.Assert(err, IsNil) + + hardwareVersion, err := vdc.GetHardwareVersion(vm.VM.VmSpecSection.HardwareVersion.Value) + check.Assert(err, IsNil) + check.Assert(hardwareVersion, NotNil) + + var updatedVm *VM + vmSpecSection := vm.VM.VmSpecSection + supportsExtendedBootOptions := vcd.client.Client.APIVCDMaxVersionIs(">=37.1") + if supportsExtendedBootOptions { + vmSpecSection.Firmware = "efi" + updatedVm, err = vm.UpdateVmSpecSection(vmSpecSection, "updateDescription") + check.Assert(err, IsNil) + check.Assert(updatedVm.VM.VmSpecSection.Firmware, Equals, "efi") + check.Assert(updatedVm, NotNil) + } + + bootOptions := &types.BootOptions{} + bootOptions.EnterBiosSetup = addrOf(true) + bootOptions.BootDelay = addrOf(1) + + if supportsExtendedBootOptions { + bootOptions.EfiSecureBootEnabled = addrOf(true) + bootOptions.BootRetryEnabled = addrOf(true) + bootOptions.BootRetryDelay = addrOf(200) + } + + updatedVm, err = vm.UpdateBootOptions(bootOptions) + check.Assert(err, IsNil) + + if supportsExtendedBootOptions { + check.Assert(updatedVm.VM.BootOptions.BootRetryEnabled, DeepEquals, addrOf(true)) + check.Assert(updatedVm.VM.BootOptions.BootRetryDelay, DeepEquals, addrOf(200)) + check.Assert(updatedVm.VM.BootOptions.EfiSecureBootEnabled, DeepEquals, addrOf(true)) + } + check.Assert(updatedVm.VM.BootOptions.EnterBiosSetup, DeepEquals, addrOf(true)) + check.Assert(updatedVm.VM.BootOptions.BootDelay, DeepEquals, addrOf(1)) + + task, err := updatedVm.PowerOn() + check.Assert(err, IsNil) + + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) + + task, err = updatedVm.PowerOff() + check.Assert(err, IsNil) + + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) + + err = updatedVm.Refresh() + check.Assert(err, IsNil) + + check.Assert(updatedVm.VM.BootOptions.EnterBiosSetup, DeepEquals, addrOf(false)) // delete Vapp early to avoid env capacity issue - deleteVapp(vcd, vmName) + err = deleteNsxtVapp(vcd, vmName) + check.Assert(err, IsNil) } func (vcd *TestVCD) Test_QueryVmList(check *C) { @@ -1434,12 +1262,7 @@ func (vcd *TestVCD) Test_UpdateVmCpuAndMemoryHotAdd(check *C) { vdc, _, vappTemplate, vapp, desiredNetConfig, err := vcd.createAndGetResourcesForVmCreation(check, vmName) check.Assert(err, IsNil) - vm, err := spawnVM("FirstNode", 512, *vdc, *vapp, desiredNetConfig, vappTemplate, check, "", true) - check.Assert(err, IsNil) - - task, err := vm.PowerOff() - check.Assert(err, IsNil) - err = task.WaitTaskCompletion() + vm, err := spawnVM("FirstNode", 512, *vdc, *vapp, desiredNetConfig, vappTemplate, check, "", false) check.Assert(err, IsNil) check.Assert(vm.VM.VMCapabilities.MemoryHotAddEnabled, Equals, false) @@ -1454,16 +1277,13 @@ func (vcd *TestVCD) Test_UpdateVmCpuAndMemoryHotAdd(check *C) { check.Assert(updatedVm.VM.VMCapabilities.CPUHotAddEnabled, Equals, true) // delete Vapp early to avoid env capacity issue - deleteVapp(vcd, vmName) + err = deleteVapp(vcd, vmName) + check.Assert(err, IsNil) } func (vcd *TestVCD) Test_AddNewEmptyVMWithVmComputePolicyAndUpdate(check *C) { - - if vcd.client.Client.APIVCDMaxVersionIs("< 33.0") { - check.Skip(fmt.Sprintf("Test %s requires VCD 10.0 (API version 33) or higher", check.TestName())) - } - - vapp, err := createVappForTest(vcd, "Test_AddNewEmptyVMWithVmComputePolicy") + vcd.skipIfNotSysAdmin(check) + vapp, err := deployVappForTest(vcd, "Test_AddNewEmptyVMWithVmComputePolicy") check.Assert(err, IsNil) check.Assert(vapp, NotNil) @@ -1475,7 +1295,7 @@ func (vcd *TestVCD) Test_AddNewEmptyVMWithVmComputePolicyAndUpdate(check *C) { client: vcd.org.client, VdcComputePolicy: &types.VdcComputePolicy{ Name: check.TestName() + "_empty", - Description: "Empty policy created by test", + Description: addrOf("Empty policy created by test"), }, } @@ -1483,8 +1303,8 @@ func (vcd *TestVCD) Test_AddNewEmptyVMWithVmComputePolicyAndUpdate(check *C) { client: vcd.org.client, VdcComputePolicy: &types.VdcComputePolicy{ Name: check.TestName() + "_memory", - Description: "Empty policy created by test 2", - Memory: takeIntAddress(2048), + Description: addrOf("Empty policy created by test 2"), + Memory: addrOf(2048), }, } @@ -1498,10 +1318,10 @@ func (vcd *TestVCD) Test_AddNewEmptyVMWithVmComputePolicyAndUpdate(check *C) { vcd.infoCleanup(notFoundMsg, "vdc", vcd.vdc.Vdc.Name) } - createdPolicy, err := adminOrg.CreateVdcComputePolicy(newComputePolicy.VdcComputePolicy) + createdPolicy, err := adminOrg.client.CreateVdcComputePolicy(newComputePolicy.VdcComputePolicy) check.Assert(err, IsNil) - createdPolicy2, err := adminOrg.CreateVdcComputePolicy(newComputePolicy2.VdcComputePolicy) + createdPolicy2, err := adminOrg.client.CreateVdcComputePolicy(newComputePolicy2.VdcComputePolicy) check.Assert(err, IsNil) AddToCleanupList(createdPolicy.VdcComputePolicy.ID, "vdcComputePolicy", vcd.org.Org.Name, "Test_AddNewEmptyVMWithVmComputePolicyAndUpdate") @@ -1538,7 +1358,7 @@ func (vcd *TestVCD) Test_AddNewEmptyVMWithVmComputePolicyAndUpdate(check *C) { SizeMb: int64(16384), BusNumber: 0, UnitNumber: 0, - ThinProvisioned: takeBoolPointer(true), + ThinProvisioned: addrOf(true), OverrideVmDefault: true} requestDetails := &types.RecomposeVAppParamsForEmptyVm{ @@ -1547,11 +1367,11 @@ func (vcd *TestVCD) Test_AddNewEmptyVMWithVmComputePolicyAndUpdate(check *C) { Description: "created by Test_AddNewEmptyVMWithVmComputePolicy", GuestCustomizationSection: nil, VmSpecSection: &types.VmSpecSection{ - Modified: takeBoolPointer(true), + Modified: addrOf(true), Info: "Virtual Machine specification", OsType: "debian10Guest", - NumCpus: takeIntAddress(2), - NumCoresPerSocket: takeIntAddress(1), + NumCpus: addrOf(2), + NumCoresPerSocket: addrOf(1), CpuResourceMhz: &types.CpuResourceMhz{Configured: 1}, MemoryResourceMb: &types.MemoryResourceMb{Configured: 1024}, MediaSection: nil, @@ -1617,7 +1437,7 @@ func (vcd *TestVCD) Test_VMUpdateStorageProfile(check *C) { check.Skip("Skipping test because both storage profiles have to be configured") } - vapp, err := createVappForTest(vcd, "Test_VMUpdateStorageProfile") + vapp, err := deployVappForTest(vcd, "Test_VMUpdateStorageProfile") check.Assert(err, IsNil) check.Assert(vapp, NotNil) @@ -1657,6 +1477,162 @@ func (vcd *TestVCD) Test_VMUpdateStorageProfile(check *C) { check.Assert(task.Task.Status, Equals, "success") } +func (vcd *TestVCD) Test_VMUpdateComputePolicies(check *C) { + vcd.skipIfNotSysAdmin(check) + providerVdc, err := vcd.client.GetProviderVdcByName(vcd.config.VCD.NsxtProviderVdc.Name) + check.Assert(err, IsNil) + check.Assert(providerVdc, NotNil) + + vmGroup, err := vcd.client.GetVmGroupByNameAndProviderVdcUrn(vcd.config.VCD.NsxtProviderVdc.PlacementPolicyVmGroup, providerVdc.ProviderVdc.ID) + check.Assert(err, IsNil) + check.Assert(vmGroup, NotNil) + + adminOrg, err := vcd.client.GetAdminOrgByName(vcd.org.Org.Name) + check.Assert(err, IsNil) + check.Assert(adminOrg, NotNil) + + adminVdc, err := adminOrg.GetAdminVDCByName(vcd.nsxtVdc.Vdc.Name, false) + if adminVdc == nil || err != nil { + vcd.infoCleanup(notFoundMsg, "vdc", vcd.nsxtVdc.Vdc.Name) + } + + // Create some Compute Policies + var placementPolicies []*VdcComputePolicyV2 + var sizingPolicies []*VdcComputePolicyV2 + numberOfPolicies := 2 + for i := 0; i < numberOfPolicies; i++ { + sizingPolicyName := fmt.Sprintf("%s_Sizing%d", check.TestName(), i+1) + placementPolicyName := fmt.Sprintf("%s_Placement%d", check.TestName(), i+1) + + sizingPolicies = append(sizingPolicies, &VdcComputePolicyV2{ + VdcComputePolicyV2: &types.VdcComputePolicyV2{ + VdcComputePolicy: types.VdcComputePolicy{ + Name: sizingPolicyName, + Description: addrOf("Empty sizing policy created by test"), + IsSizingOnly: true, + }, + PolicyType: "VdcVmPolicy", + }, + }) + + placementPolicies = append(placementPolicies, &VdcComputePolicyV2{ + VdcComputePolicyV2: &types.VdcComputePolicyV2{ + VdcComputePolicy: types.VdcComputePolicy{ + Name: placementPolicyName, + Description: addrOf("Empty placement policy created by test"), + IsSizingOnly: false, + }, + PolicyType: "VdcVmPolicy", + PvdcNamedVmGroupsMap: []types.PvdcNamedVmGroupsMap{ + { + NamedVmGroups: []types.OpenApiReferences{{ + { + Name: vmGroup.VmGroup.Name, + ID: fmt.Sprintf("urn:vcloud:namedVmGroup:%s", vmGroup.VmGroup.NamedVmGroupId), + }, + }}, + Pvdc: types.OpenApiReference{ + Name: providerVdc.ProviderVdc.Name, + ID: providerVdc.ProviderVdc.ID, + }, + }, + }, + }, + }) + + sizingPolicies[i], err = vcd.client.CreateVdcComputePolicyV2(sizingPolicies[i].VdcComputePolicyV2) + check.Assert(err, IsNil) + AddToCleanupList(sizingPolicies[i].VdcComputePolicyV2.ID, "vdcComputePolicy", vcd.org.Org.Name, sizingPolicyName) + + placementPolicies[i], err = vcd.client.CreateVdcComputePolicyV2(placementPolicies[i].VdcComputePolicyV2) + check.Assert(err, IsNil) + AddToCleanupList(placementPolicies[i].VdcComputePolicyV2.ID, "vdcComputePolicy", vcd.org.Org.Name, placementPolicyName) + } + + vdcComputePolicyHref, err := adminOrg.client.OpenApiBuildEndpoint(types.OpenApiPathVersion2_0_0, types.OpenApiEndpointVdcComputePolicies) + check.Assert(err, IsNil) + + // Add the created compute policies to the ones that the VDC has already assigned + alreadyAssignedPolicies, err := adminVdc.GetAllAssignedVdcComputePoliciesV2(nil) + check.Assert(err, IsNil) + var allComputePoliciesToAssign []*types.Reference + for _, alreadyAssignedPolicy := range alreadyAssignedPolicies { + allComputePoliciesToAssign = append(allComputePoliciesToAssign, &types.Reference{HREF: vdcComputePolicyHref.String() + alreadyAssignedPolicy.VdcComputePolicyV2.ID}) + } + for i := 0; i < numberOfPolicies; i++ { + allComputePoliciesToAssign = append(allComputePoliciesToAssign, &types.Reference{HREF: vdcComputePolicyHref.String() + sizingPolicies[i].VdcComputePolicyV2.ID}) + allComputePoliciesToAssign = append(allComputePoliciesToAssign, &types.Reference{HREF: vdcComputePolicyHref.String() + placementPolicies[i].VdcComputePolicyV2.ID}) + } + + assignedVdcComputePolicies, err := adminVdc.SetAssignedComputePolicies(types.VdcComputePolicyReferences{VdcComputePolicyReference: allComputePoliciesToAssign}) + check.Assert(err, IsNil) + check.Assert(len(alreadyAssignedPolicies)+numberOfPolicies*2, Equals, len(assignedVdcComputePolicies.VdcComputePolicyReference)) + + vapp, vm := createNsxtVAppAndVm(vcd, check) + check.Assert(vapp, NotNil) + check.Assert(vm, NotNil) + + // Update all Compute Policies: Sizing and Placement + check.Assert(err, IsNil) + vm, err = vm.UpdateComputePolicyV2(sizingPolicies[0].VdcComputePolicyV2.ID, placementPolicies[0].VdcComputePolicyV2.ID, "") + check.Assert(err, IsNil) + check.Assert(vm.VM.ComputePolicy.VmSizingPolicy.ID, Equals, sizingPolicies[0].VdcComputePolicyV2.ID) + check.Assert(vm.VM.ComputePolicy.VmPlacementPolicy.ID, Equals, placementPolicies[0].VdcComputePolicyV2.ID) + + // Update Sizing policy only + vm, err = vm.UpdateComputePolicyV2(sizingPolicies[0].VdcComputePolicyV2.ID, placementPolicies[1].VdcComputePolicyV2.ID, "") + check.Assert(err, IsNil) + check.Assert(vm.VM.ComputePolicy.VmSizingPolicy.ID, Equals, sizingPolicies[0].VdcComputePolicyV2.ID) + check.Assert(vm.VM.ComputePolicy.VmPlacementPolicy.ID, Equals, placementPolicies[1].VdcComputePolicyV2.ID) + + // Update Placement policy only + vm, err = vm.UpdateComputePolicyV2(sizingPolicies[1].VdcComputePolicyV2.ID, placementPolicies[1].VdcComputePolicyV2.ID, "") + check.Assert(err, IsNil) + check.Assert(vm.VM.ComputePolicy.VmSizingPolicy.ID, Equals, sizingPolicies[1].VdcComputePolicyV2.ID) + check.Assert(vm.VM.ComputePolicy.VmPlacementPolicy.ID, Equals, placementPolicies[1].VdcComputePolicyV2.ID) + + // Remove Placement Policy + vm, err = vm.UpdateComputePolicyV2(sizingPolicies[1].VdcComputePolicyV2.ID, "", "") + check.Assert(err, IsNil) + check.Assert(vm.VM.ComputePolicy.VmSizingPolicy.ID, Equals, sizingPolicies[1].VdcComputePolicyV2.ID) + check.Assert(vm.VM.ComputePolicy.VmPlacementPolicy, IsNil) + + // Remove Sizing Policy + vm, err = vm.UpdateComputePolicyV2("", placementPolicies[1].VdcComputePolicyV2.ID, "") + check.Assert(err, IsNil) + check.Assert(vm.VM.ComputePolicy.VmSizingPolicy, IsNil) + check.Assert(vm.VM.ComputePolicy.VmPlacementPolicy.ID, Equals, placementPolicies[1].VdcComputePolicyV2.ID) + + // Try to remove both, it should fail + _, err = vm.UpdateComputePolicyV2("", "", "") + check.Assert(err, NotNil) + check.Assert(true, Equals, strings.Contains(err.Error(), "either sizing policy ID or placement policy ID is needed")) + + // Clean VM + task, err := vapp.Undeploy() + check.Assert(err, IsNil) + check.Assert(task, Not(Equals), Task{}) + + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) + + task, err = vapp.Delete() + check.Assert(err, IsNil) + check.Assert(task, Not(Equals), Task{}) + + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) + + // Cleanup assigned compute policies + var beforeTestPolicyReferences []*types.Reference + for _, assignedPolicy := range alreadyAssignedPolicies { + beforeTestPolicyReferences = append(beforeTestPolicyReferences, &types.Reference{HREF: vdcComputePolicyHref.String() + assignedPolicy.VdcComputePolicyV2.ID}) + } + + _, err = adminVdc.SetAssignedComputePolicies(types.VdcComputePolicyReferences{VdcComputePolicyReference: beforeTestPolicyReferences}) + check.Assert(err, IsNil) +} + func (vcd *TestVCD) getNetworkConnection() *types.NetworkConnectionSection { if vcd.config.VCD.Network.Net1 == "" { @@ -1689,7 +1665,7 @@ func (vcd *TestVCD) Test_CreateStandaloneVM(check *C) { check.Assert(err, IsNil) check.Assert(vdc, NotNil) description := "created by " + check.TestName() - params := types.CreateVmParams{ + params := &types.CreateVmParams{ Name: "testStandaloneVm", PowerOn: false, Description: description, @@ -1698,11 +1674,11 @@ func (vcd *TestVCD) Test_CreateStandaloneVM(check *C) { VirtualHardwareSection: nil, NetworkConnectionSection: vcd.getNetworkConnection(), VmSpecSection: &types.VmSpecSection{ - Modified: takeBoolPointer(true), + Modified: addrOf(true), Info: "Virtual Machine specification", - OsType: "debian10Guest", - NumCpus: takeIntAddress(1), - NumCoresPerSocket: takeIntAddress(1), + OsType: "sles11_64Guest", + NumCpus: addrOf(1), + NumCoresPerSocket: addrOf(1), CpuResourceMhz: &types.CpuResourceMhz{ Configured: 0, }, @@ -1711,12 +1687,12 @@ func (vcd *TestVCD) Test_CreateStandaloneVM(check *C) { }, DiskSection: &types.DiskSection{ DiskSettings: []*types.DiskSettings{ - &types.DiskSettings{ + { SizeMb: 1024, UnitNumber: 0, BusNumber: 0, AdapterType: "5", - ThinProvisioned: takeBoolPointer(true), + ThinProvisioned: addrOf(true), OverrideVmDefault: false, }, }, @@ -1730,16 +1706,37 @@ func (vcd *TestVCD) Test_CreateStandaloneVM(check *C) { Info: "Specifies Guest OS Customization Settings", ComputerName: "standalone1", }, + BootOptions: &types.BootOptions{ + BootDelay: addrOf(0), + }, }, Xmlns: types.XMLNamespaceVCloud, } + + supportsExtendedBootOptions := vcd.client.Client.APIVCDMaxVersionIs(">=37.1") + if supportsExtendedBootOptions { + params.CreateVm.VmSpecSection.Firmware = "efi" + params.CreateVm.BootOptions.EfiSecureBootEnabled = addrOf(true) + params.CreateVm.BootOptions.BootRetryEnabled = addrOf(true) + params.CreateVm.BootOptions.BootRetryDelay = addrOf(1) + } + vappList := vdc.GetVappList() vappNum := len(vappList) - vm, err := vdc.CreateStandaloneVm(¶ms) + vm, err := vdc.CreateStandaloneVm(params) check.Assert(err, IsNil) check.Assert(vm, NotNil) + err = vm.Refresh() + check.Assert(err, IsNil) AddToCleanupList(vm.VM.ID, "standaloneVm", "", check.TestName()) + check.Assert(vm.VM.Description, Equals, description) + check.Assert(vm.VM.BootOptions.BootDelay, DeepEquals, addrOf(0)) + if supportsExtendedBootOptions { + check.Assert(vm.VM.BootOptions.EfiSecureBootEnabled, DeepEquals, addrOf(true)) + check.Assert(vm.VM.BootOptions.BootRetryEnabled, DeepEquals, addrOf(true)) + check.Assert(vm.VM.BootOptions.BootRetryDelay, DeepEquals, addrOf(1)) + } _ = vdc.Refresh() vappList = vdc.GetVappList() @@ -1790,9 +1787,11 @@ func (vcd *TestVCD) Test_CreateStandaloneVMFromTemplate(check *C) { check.Assert(vmTemplate.Type, Not(Equals), "") check.Assert(vmTemplate.Name, Not(Equals), "") + vmName := "testStandaloneTemplate" + vmDescription := "Standalone VM" params := types.InstantiateVmTemplateParams{ Xmlns: types.XMLNamespaceVCloud, - Name: "testStandaloneTemplate", + Name: vmName, PowerOn: true, AllEULAsAccepted: true, SourcedVmTemplateItem: &types.SourcedVmTemplateParams{ @@ -1803,9 +1802,14 @@ func (vcd *TestVCD) Test_CreateStandaloneVMFromTemplate(check *C) { Type: vmTemplate.Type, Name: vmTemplate.Name, }, - StorageProfile: nil, - VmCapabilities: nil, - VmGeneralParams: nil, + StorageProfile: nil, + VmCapabilities: nil, + VmGeneralParams: &types.VMGeneralParams{ + Name: vmName, + Description: vmDescription, + NeedsCustomization: false, + RegenerateBiosUuid: false, + }, VmTemplateInstantiationParams: nil, }, } @@ -1816,6 +1820,8 @@ func (vcd *TestVCD) Test_CreateStandaloneVMFromTemplate(check *C) { check.Assert(err, IsNil) check.Assert(vm, NotNil) AddToCleanupList(vm.VM.ID, "standaloneVm", "", check.TestName()) + check.Assert(vm.VM.Name, Equals, vmName) + check.Assert(vm.VM.Description, Equals, vmDescription) _ = vdc.Refresh() vappList = vdc.GetVappList() @@ -1830,3 +1836,517 @@ func (vcd *TestVCD) Test_CreateStandaloneVMFromTemplate(check *C) { vappList = vdc.GetVappList() check.Assert(len(vappList), Equals, vappNum) } + +func (vcd *TestVCD) Test_VMChangeCPU(check *C) { + if vcd.skipVappTests { + check.Skip("Skipping test because vapp was not successfully created at setup") + } + + vapp := vcd.findFirstVapp() + existingVm, vmName := vcd.findFirstVm(vapp) + if vmName == "" { + check.Skip("skipping test because no VM is found") + } + + currentCpus := existingVm.VmSpecSection.NumCpus + currentCores := existingVm.VmSpecSection.NumCoresPerSocket + + check.Assert(0, Not(Equals), currentCpus) + check.Assert(0, Not(Equals), currentCores) + + vm, err := vcd.client.Client.GetVMByHref(existingVm.HREF) + check.Assert(err, IsNil) + + cores := 2 + cpuCount := 4 + + err = vm.ChangeCPU(cpuCount, cores) + check.Assert(err, IsNil) + + check.Assert(*vm.VM.VmSpecSection.NumCpus, Equals, cpuCount) + check.Assert(*vm.VM.VmSpecSection.NumCoresPerSocket, Equals, cores) + + // return to previous value + err = vm.ChangeCPU(*currentCpus, *currentCores) + check.Assert(err, IsNil) +} + +func (vcd *TestVCD) Test_VMChangeCPUAndCoreCount(check *C) { + if vcd.skipVappTests { + check.Skip("Skipping test because vApp was not successfully created at setup") + } + + vapp := vcd.findFirstVapp() + existingVm, vmName := vcd.findFirstVm(vapp) + if vmName == "" { + check.Skip("skipping test because no VM is found") + } + + currentCpus := existingVm.VmSpecSection.NumCpus + currentCores := existingVm.VmSpecSection.NumCoresPerSocket + + check.Assert(0, Not(Equals), currentCpus) + check.Assert(0, Not(Equals), currentCores) + + vm, err := vcd.client.Client.GetVMByHref(existingVm.HREF) + check.Assert(err, IsNil) + + cores := 2 + cpuCount := 4 + + err = vm.ChangeCPUAndCoreCount(&cpuCount, &cores) + check.Assert(err, IsNil) + + check.Assert(*vm.VM.VmSpecSection.NumCpus, Equals, cpuCount) + check.Assert(*vm.VM.VmSpecSection.NumCoresPerSocket, Equals, cores) + + // Try changing only CPU count and seeing if coreCount remains the same + newCpuCount := 2 + err = vm.ChangeCPUAndCoreCount(&newCpuCount, nil) + check.Assert(err, IsNil) + + check.Assert(*vm.VM.VmSpecSection.NumCpus, Equals, newCpuCount) + check.Assert(*vm.VM.VmSpecSection.NumCoresPerSocket, Equals, cores) + + // Change only core count and check that CPU count remains as it was + newCoreCount := 1 + err = vm.ChangeCPUAndCoreCount(nil, &newCoreCount) + check.Assert(err, IsNil) + + check.Assert(*vm.VM.VmSpecSection.NumCpus, Equals, newCpuCount) + check.Assert(*vm.VM.VmSpecSection.NumCoresPerSocket, Equals, newCoreCount) + + // return to previous value + err = vm.ChangeCPUAndCoreCount(currentCpus, currentCores) + check.Assert(err, IsNil) +} + +func (vcd *TestVCD) Test_VMChangeMemory(check *C) { + if vcd.skipVappTests { + check.Skip("Skipping test because vapp was not successfully created at setup") + } + + vapp := vcd.findFirstVapp() + existingVm, vmName := vcd.findFirstVm(vapp) + if vmName == "" { + check.Skip("skipping test because no VM is found") + } + check.Assert(existingVm.VmSpecSection.MemoryResourceMb, Not(IsNil)) + + currentMemory := existingVm.VmSpecSection.MemoryResourceMb.Configured + check.Assert(0, Not(Equals), currentMemory) + + vm, err := vcd.client.Client.GetVMByHref(existingVm.HREF) + check.Assert(err, IsNil) + + err = vm.ChangeMemory(2304) + check.Assert(err, IsNil) + + check.Assert(existingVm.VmSpecSection.MemoryResourceMb, Not(IsNil)) + check.Assert(vm.VM.VmSpecSection.MemoryResourceMb.Configured, Equals, int64(2304)) + + // return to previous value + err = vm.ChangeMemory(currentMemory) + check.Assert(err, IsNil) +} + +func (vcd *TestVCD) Test_AddRawVm(check *C) { + vapp, vm := createNsxtVAppAndVm(vcd, check) + check.Assert(vapp, NotNil) + check.Assert(vm, NotNil) + + // Check that vApp did not lose its state + vappStatus, err := vapp.GetStatus() + check.Assert(err, IsNil) + check.Assert(vappStatus, Equals, "MIXED") //vApp is powered on, but the VM within is powered off + check.Assert(vapp.VApp.Name, Equals, check.TestName()) + check.Assert(vapp.VApp.Description, Equals, check.TestName()) + + // Check that VM is not powered on + vmStatus, err := vm.GetStatus() + check.Assert(err, IsNil) + check.Assert(vmStatus, Equals, "POWERED_OFF") + + // Cleanup + task, err := vapp.Undeploy() + check.Assert(err, IsNil) + check.Assert(task, Not(Equals), Task{}) + + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) + + task, err = vapp.Delete() + check.Assert(err, IsNil) + check.Assert(task, Not(Equals), Task{}) + + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) +} + +func createNsxtVAppAndVmWithEfiSupport(vcd *TestVCD, check *C) (*VApp, *VM) { + if vcd.config.VCD.Catalog.CatalogItemWithEfiSupport == "" { + check.Skip("EFI supporting OVA not provided in the config") + } + + cat, err := vcd.org.GetCatalogByName(vcd.config.VCD.Catalog.NsxtBackedCatalogName, false) + check.Assert(err, IsNil) + check.Assert(cat, NotNil) + // Populate Catalog Item + catitem, err := cat.GetCatalogItemByName(vcd.config.VCD.Catalog.CatalogItemWithEfiSupport, false) + check.Assert(err, IsNil) + check.Assert(catitem, NotNil) + // Get VAppTemplate + vapptemplate, err := catitem.GetVAppTemplate() + check.Assert(err, IsNil) + check.Assert(vapptemplate.VAppTemplate.Children.VM[0].HREF, NotNil) + + vapp, err := vcd.nsxtVdc.CreateRawVApp(check.TestName(), check.TestName()) + check.Assert(err, IsNil) + check.Assert(vapp, NotNil) + // After a successful creation, the entity is added to the cleanup list. + AddToCleanupList(vapp.VApp.Name, "vapp", vcd.nsxtVdc.Vdc.Name, check.TestName()) + + // Once the operation is successful, we won't trigger a failure + // until after the vApp deletion + check.Check(vapp.VApp.Name, Equals, check.TestName()) + check.Check(vapp.VApp.Description, Equals, check.TestName()) + + // Construct VM + vmDef := &types.ReComposeVAppParams{ + Ovf: types.XMLNamespaceOVF, + Xsi: types.XMLNamespaceXSI, + Xmlns: types.XMLNamespaceVCloud, + AllEULAsAccepted: true, + // Deploy: false, + Name: vapp.VApp.Name, + // PowerOn: false, // Not touching power state at this phase + SourcedItem: &types.SourcedCompositionItemParam{ + Source: &types.Reference{ + HREF: vapptemplate.VAppTemplate.Children.VM[0].HREF, + Name: check.TestName() + "-vm-tmpl", + }, + VMGeneralParams: &types.VMGeneralParams{ + Description: "test-vm-description", + }, + InstantiationParams: &types.InstantiationParams{ + NetworkConnectionSection: &types.NetworkConnectionSection{}, + }, + }, + } + vm, err := vapp.AddRawVM(vmDef) + check.Assert(err, IsNil) + check.Assert(vm, NotNil) + check.Assert(vm.VM.Name, Equals, vmDef.SourcedItem.Source.Name) + + // Refresh vApp to have latest state + err = vapp.Refresh() + check.Assert(err, IsNil) + + return vapp, vm +} + +func (vcd *TestVCD) Test_GetOvfEnvironment(check *C) { + version, err := vcd.client.Client.GetVcdShortVersion() + check.Assert(err, IsNil) + if version == "10.5.0" { + check.Skip("There is a known bug with the OVF environment on 10.5.0") + } + + _, vm := createNsxtVAppAndVm(vcd, check) + check.Assert(vm, NotNil) + + task, err := vm.PowerOn() + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) + + // Read ovfenv when VM is started + ovfenv, err := vm.GetEnvironment() + check.Assert(err, IsNil) + check.Assert(ovfenv, NotNil) + + // Provides information from the virtualization platform like VM moref + check.Assert(strings.Contains(ovfenv.VCenterId, "vm-"), Equals, true) + + // Check virtualization platform Vendor + check.Assert(ovfenv.PlatformSection, NotNil) + check.Assert(ovfenv.PlatformSection.Vendor, Equals, "VMware, Inc.") + + // Check guest operating system level configuration for hostname + check.Assert(ovfenv.PropertySection, NotNil) + for _, p := range ovfenv.PropertySection.Properties { + if p.Key == "vCloud_computerName" { + check.Assert(p.Value, Not(Equals), "") + } + } + check.Assert(ovfenv.EthernetAdapterSection, NotNil) + for _, p := range ovfenv.EthernetAdapterSection.Adapters { + check.Assert(p.Mac, Not(Equals), "") + } + + err = deleteNsxtVapp(vcd, check.TestName()) + check.Assert(err, IsNil) +} + +func (vcd *TestVCD) Test_QueryVMList(check *C) { + + uniqueId := "2024-01-27" + vappDefinition := map[string][]string{ + "Test_Vapp1_" + uniqueId: []string{"Test_VmA_" + uniqueId, "Test_VmB_" + uniqueId}, + "Test_Vapp2_" + uniqueId: []string{"Test_VmA_" + uniqueId, "Test_VmB_" + uniqueId}, + "Test_Vapp3_" + uniqueId: []string{"Test_VmA_" + uniqueId, "Test_VmB_" + uniqueId}, + } + listVms := func(vms []*types.QueryResultVMRecordType) { + if !testVerbose { + return + } + for i, vm := range vms { + standalone := "" + if vm.AutoNature { + standalone = " (standalone)" + } + fmt.Printf("%d (%s) %s %s\n", i, vm.VdcName, vm.Name, standalone) + } + fmt.Println() + } + _, err := makeVappGroup(check.TestName(), vcd.nsxtVdc, vappDefinition) + check.Assert(err, IsNil) + + // Retrieves all VMs with name 'Test_VmA_'+uniqueId + vmList1, err := QueryVmList(types.VmQueryFilterOnlyDeployed, &vcd.client.Client, map[string]string{"name": "Test_VmA_" + uniqueId}) + check.Assert(err, IsNil) + listVms(vmList1) + + // Retrieves all VMs with name 'Test_VmB_'+uniqueId + check.Assert(len(vmList1) == 3, Equals, true) + vmList2, err := QueryVmList(types.VmQueryFilterOnlyDeployed, &vcd.client.Client, map[string]string{"name": "Test_VmB_" + uniqueId}) + check.Assert(err, IsNil) + listVms(vmList2) + + // Retrieves all VMs + check.Assert(len(vmList2) == 3, Equals, true) + vmList3, err := QueryVmList(types.VmQueryFilterOnlyDeployed, &vcd.client.Client, nil) + check.Assert(err, IsNil) + check.Assert(len(vmList3) >= 6, Equals, true) + listVms(vmList3) +} + +// Test_VmConsolidateDisks attempts to validate vm.ConsolidateDisks by performing the following +// operations: +// * setting up a vApp and a VM +// * trying to resize VM disk and expecting to get an error (cannot be modified while the virtual machine has snapshots) +// * consolidating disks +// * resizing VM disk (growing by 1024MB) +// * verifying that new size is correct +// * attempting to consolidate once more (it is already consolidated so expecting a quick return) +// * cleanup +func (vcd *TestVCD) Test_VmConsolidateDisks(check *C) { + org := vcd.org + catalog, err := org.GetCatalogByName(vcd.config.VCD.Catalog.NsxtBackedCatalogName, false) + check.Assert(err, IsNil) + vappTemplateName := vcd.config.VCD.Catalog.CatalogItemWithMultiVms + if vappTemplateName == "" { + check.Skip(fmt.Sprintf("vApp template missing in configuration - Make sure there is such template in catalog %s -"+ + " Using test_resources/vapp_with_3_vms.ova", + vcd.config.VCD.Catalog.NsxtBackedCatalogName)) + } + vappTemplate, err := catalog.GetVAppTemplateByName(vappTemplateName) + if err != nil { + if ContainsNotFound(err) { + check.Skip(fmt.Sprintf("vApp template %s not found - Make sure there is such template in catalog %s -"+ + " Using test_resources/vapp_with_3_vms.ova", + vappTemplateName, vcd.config.VCD.Catalog.NsxtBackedCatalogName)) + } + } + check.Assert(err, IsNil) + check.Assert(vappTemplate.VAppTemplate.Children, NotNil) + check.Assert(vappTemplate.VAppTemplate.Children.VM, NotNil) + + vapp, vm := createNsxtVAppAndVmFromCustomTemplate(vcd, check, vappTemplate) + check.Assert(vapp, NotNil) + check.Assert(vm, NotNil) + + // Check that vApp did not lose its state + vappStatus, err := vapp.GetStatus() + check.Assert(err, IsNil) + check.Assert(vappStatus, Equals, "MIXED") //vApp is powered on, but the VM within is powered off + check.Assert(vapp.VApp.Name, Equals, check.TestName()) + check.Assert(vapp.VApp.Description, Equals, check.TestName()) + + // Check that VM is not powered on + vmStatus, err := vm.GetStatus() + check.Assert(err, IsNil) + check.Assert(vmStatus, Equals, "POWERED_OFF") + + // Attempt to resize before consolidating disks - it should fail + vmSpecSection := vm.VM.VmSpecSection + vmSizeBeforeGrowing := vmSpecSection.DiskSection.DiskSettings[0].SizeMb + vmSpecSection.DiskSection.DiskSettings[0].SizeMb = vmSizeBeforeGrowing + 1024 + _, err = vm.UpdateInternalDisks(vmSpecSection) + check.Assert(strings.Contains(err.Error(), "cannot be modified while the virtual machine has snapshots"), Equals, true) + + // Trigger disk consolidation + err = vm.ConsolidateDisks() + check.Assert(err, IsNil) + + // Resize disk after consolidation - it should work now + err = vm.Refresh() // reloading VM structure to avoid + check.Assert(err, IsNil) + vmSpecSection = vm.VM.VmSpecSection + vmSizeBeforeGrowing = vmSpecSection.DiskSection.DiskSettings[0].SizeMb + vmSpecSection.DiskSection.DiskSettings[0].SizeMb = vmSizeBeforeGrowing + 1024 + + _, err = vm.UpdateInternalDisks(vmSpecSection) + check.Assert(err, IsNil) + + // Refresh VM and verify size + err = vm.Refresh() + check.Assert(err, IsNil) + check.Assert(vm.VM.VmSpecSection.DiskSection.DiskSettings[0].SizeMb, Equals, vmSizeBeforeGrowing+1024) + + // Trigger async disk consolidation - it will return instantly because the disk is already + // consolidated and there is nothing to do + task, err := vm.ConsolidateDisksAsync() + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) + + // Cleanup + task, err = vapp.Undeploy() + check.Assert(err, IsNil) + check.Assert(task, Not(Equals), Task{}) + + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) + + task, err = vapp.Delete() + check.Assert(err, IsNil) + check.Assert(task, Not(Equals), Task{}) + + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) +} + +func (vcd *TestVCD) Test_VmExtraConfig(check *C) { + + fmt.Printf("Running: %s\n", check.TestName()) + if vcd.skipVappTests { + check.Skip("Skipping test because vApp wasn't properly created") + } + + vapp := vcd.findFirstVapp() + if vapp.VApp.Name == "" { + check.Skip("Disabled: No suitable vApp found in vDC") + } + vm, _ := vcd.findFirstVm(vapp) + if vm.Name == "" { + check.Skip("Disabled: No suitable VM found in vDC") + } + + poweredOffVm, err := vcd.client.Client.GetVMByHref(vm.HREF) + check.Assert(err, IsNil) + + newVapp, poweredOnVm := createNsxtVAppAndVm(vcd, check) + + testVmExtraConfig(vcd, "powered OFF VM", poweredOffVm, check, false, false) + testVmExtraConfig(vcd, "formerly powered OFF VM, now powered ON", poweredOffVm, check, true, false) + testVmExtraConfig(vcd, "powered ON VM", poweredOnVm, check, true, false) + testVmExtraConfig(vcd, "formerly powered ON VM, now powered OFF", poweredOnVm, check, false, true) + + // poweredOffVm should be brought back to its original state + task, err := poweredOffVm.PowerOff() + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) + // Removing the newly created VM and its vApp + task, err = newVapp.Delete() + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) +} + +func testVmExtraConfig(vcd *TestVCD, label string, vm *VM, check *C, wantPowerOn, wantPowerOff bool) { + + fmt.Println(label) + if wantPowerOn { + task, err := vm.PowerOn() + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) + } + if wantPowerOff && !wantPowerOn { + task, err := vm.PowerOff() + check.Assert(err, IsNil) + err = task.WaitTaskCompletion() + check.Assert(err, IsNil) + } + printVerbose("vm extra config %# v\n", pretty.Formatter(vm.VM.VirtualHardwareSection.ExtraConfig)) + + configSimilar := types.ExtraConfigMarshal{ + Key: "hpet1.present", + Value: "TRUE", + } + configWithValidKey := types.ExtraConfigMarshal{ + Key: "Norwegian.wood", + Value: "With a little help from my friends", + } + configWithInvalidKey := types.ExtraConfigMarshal{ + Key: "Eleanor Rigby", // invalid key: contains a space + Value: "The long and winding road", + } + + xtraConfig, err := vm.GetExtraConfig() + check.Assert(err, IsNil) + printVerbose("initial values %# v\n", pretty.Formatter(xtraConfig)) + + // Checks that keys containing spaces trigger an error. + invalidUpdatedCfg, err := vm.UpdateExtraConfig([]*types.ExtraConfigMarshal{&configWithInvalidKey}) + check.Assert(err, NotNil) + check.Assert(invalidUpdatedCfg, IsNil) + check.Assert(strings.Contains(err.Error(), "invalid keys"), Equals, true) + + containsKey := func(items []*types.ExtraConfigMarshal, key string) bool { + return slices.ContainsFunc(items, func(marshal *types.ExtraConfigMarshal) bool { + return marshal.Key == key + }) + } + containsKeyValue := func(items []*types.ExtraConfigMarshal, key, value string) bool { + return slices.ContainsFunc(items, func(marshal *types.ExtraConfigMarshal) bool { + return marshal.Key == key && marshal.Value == value + }) + } + + // Adds two items + updatedCfg, err := vm.UpdateExtraConfig([]*types.ExtraConfigMarshal{&configSimilar, &configWithValidKey}) + check.Assert(err, IsNil) + check.Assert(updatedCfg, NotNil) + + updatedXtraConfig, err := vm.GetExtraConfig() + check.Assert(err, IsNil) + check.Assert(updatedXtraConfig, NotNil) + printVerbose(" after update %# v\n", pretty.Formatter(updatedXtraConfig)) + + check.Assert(containsKey(updatedXtraConfig, configWithValidKey.Key), Equals, true) + check.Assert(containsKey(updatedXtraConfig, configSimilar.Key), Equals, true) + + // Change the value of an existing key + modifiedValue := "modified value" + configSimilar.Value = modifiedValue + configWithValidKey.Value = modifiedValue + modifiedExtraCfg, err := vm.UpdateExtraConfig([]*types.ExtraConfigMarshal{&configSimilar, &configWithValidKey}) + check.Assert(err, IsNil) + check.Assert(modifiedExtraCfg, NotNil) + printVerbose(" after modification %# v\n", pretty.Formatter(modifiedExtraCfg)) + check.Assert(containsKeyValue(modifiedExtraCfg, configSimilar.Key, modifiedValue), Equals, true) + check.Assert(containsKeyValue(modifiedExtraCfg, configWithValidKey.Key, modifiedValue), Equals, true) + + // Delete the recently inserted items + afterDeleteXtraConfig, err := vm.DeleteExtraConfig([]*types.ExtraConfigMarshal{&configSimilar, &configWithValidKey}) + check.Assert(err, IsNil) + check.Assert(afterDeleteXtraConfig, NotNil) + + printVerbose("after delete %# v\n", pretty.Formatter(afterDeleteXtraConfig)) + + check.Assert(containsKey(afterDeleteXtraConfig, configWithValidKey.Key), Equals, false) + check.Assert(containsKey(afterDeleteXtraConfig, configSimilar.Key), Equals, false) +} diff --git a/govcd/vm_unit_test.go b/govcd/vm_unit_test.go index 502649bf8..fff832581 100644 --- a/govcd/vm_unit_test.go +++ b/govcd/vm_unit_test.go @@ -1,4 +1,4 @@ -// +build vm unit ALL +//go:build vm || unit || ALL /* * Copyright 2019 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. diff --git a/govcd/vsphere_distributed_switch.go b/govcd/vsphere_distributed_switch.go new file mode 100644 index 000000000..85686678d --- /dev/null +++ b/govcd/vsphere_distributed_switch.go @@ -0,0 +1,39 @@ +package govcd + +import ( + "fmt" + "github.com/vmware/go-vcloud-director/v2/types/v56" + "net/url" +) + +func (vcdClient *VCDClient) GetAllVcenterDistributedSwitches(vCenterId string, queryParameters url.Values) ([]*types.VcenterDistributedSwitch, error) { + if vCenterId == "" { + return nil, fmt.Errorf("empty vCenter ID") + } + + if !isUrn(vCenterId) { + return nil, fmt.Errorf("vCenter ID is not URN (e.g. 'urn:vcloud:vimserver:09722307-aee0-4623-af95-7f8e577c9ebc)', got: %s", vCenterId) + } + + client := vcdClient.Client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVCenterDistributedSwitch + apiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + queryParams := copyOrNewUrlValues(queryParameters) + queryParams = queryParameterFilterAnd("virtualCenter.id=="+vCenterId, queryParams) + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + var typeResponses []*types.VcenterDistributedSwitch + err = client.OpenApiGetAllItems(apiVersion, urlRef, queryParams, &typeResponses, nil) + if err != nil { + return nil, err + } + + return typeResponses, nil +} diff --git a/govcd/vsphere_resource_pool.go b/govcd/vsphere_resource_pool.go new file mode 100644 index 000000000..2f8d53c17 --- /dev/null +++ b/govcd/vsphere_resource_pool.go @@ -0,0 +1,211 @@ +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "github.com/vmware/go-vcloud-director/v2/types/v56" + "github.com/vmware/go-vcloud-director/v2/util" + "net/url" +) + +type ResourcePool struct { + ResourcePool *types.ResourcePool + vcenter *VCenter + client *VCDClient +} + +// GetAllResourcePools retrieves all resource pools for a given vCenter +func (vcenter VCenter) GetAllResourcePools(queryParams url.Values) ([]*ResourcePool, error) { + client := vcenter.client.Client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointResourcePoolsBrowseAll + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, vcenter.VSphereVCenter.VcId)) + if err != nil { + return nil, err + } + + retrieved := []*types.ResourcePool{{}} + + err = client.OpenApiGetAllItems(minimumApiVersion, urlRef, queryParams, &retrieved, nil) + if err != nil { + return nil, fmt.Errorf("error getting resource pool list: %s", err) + } + + if len(retrieved) == 0 { + return nil, nil + } + var returnList []*ResourcePool + + for _, r := range retrieved { + newRp := r + returnList = append(returnList, &ResourcePool{ + ResourcePool: newRp, + vcenter: &vcenter, + client: vcenter.client, + }) + } + return returnList, nil +} + +// GetAvailableHardwareVersions finds the hardware versions of a given resource pool +// In addition to proper resource pools, this method also works for any entity that is retrieved as a resource pool, +// such as provider VDCs and Org VDCs +func (rp ResourcePool) GetAvailableHardwareVersions() (*types.OpenApiSupportedHardwareVersions, error) { + + client := rp.client.Client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointResourcePoolHardware + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, rp.vcenter.VSphereVCenter.VcId, rp.ResourcePool.Moref)) + if err != nil { + return nil, err + } + + retrieved := types.OpenApiSupportedHardwareVersions{} + err = client.OpenApiGetItem(minimumApiVersion, urlRef, nil, &retrieved, nil) + if err != nil { + return nil, fmt.Errorf("error getting resource pool hardware versions: %s", err) + } + + return &retrieved, nil +} + +// GetDefaultHardwareVersion retrieves the default hardware version for a given resource pool. +// The default version is usually the highest available, but it's not guaranteed +func (rp ResourcePool) GetDefaultHardwareVersion() (string, error) { + + versions, err := rp.GetAvailableHardwareVersions() + if err != nil { + return "", err + } + + for _, v := range versions.SupportedVersions { + if v.IsDefault { + return v.Name, nil + } + } + return "", fmt.Errorf("no default hardware version found for resource pool %s", rp.ResourcePool.Name) +} + +// GetResourcePoolById retrieves a resource pool by its ID (Moref) +func (vcenter VCenter) GetResourcePoolById(id string) (*ResourcePool, error) { + resourcePools, err := vcenter.GetAllResourcePools(nil) + if err != nil { + return nil, err + } + for _, rp := range resourcePools { + if rp.ResourcePool.Moref == id { + return rp, nil + } + } + return nil, fmt.Errorf("no resource pool found with ID '%s' :%s", id, ErrorEntityNotFound) +} + +// GetResourcePoolByName retrieves a resource pool by name. +// It may fail if there are several resource pools with the same name +func (vcenter VCenter) GetResourcePoolByName(name string) (*ResourcePool, error) { + resourcePools, err := vcenter.GetAllResourcePools(nil) + if err != nil { + return nil, err + } + var found []*ResourcePool + for _, rp := range resourcePools { + if rp.ResourcePool.Name == name { + found = append(found, rp) + } + } + if len(found) == 0 { + return nil, fmt.Errorf("no resource pool found with name '%s' :%s", name, ErrorEntityNotFound) + } + if len(found) > 1 { + var idList []string + for _, f := range found { + idList = append(idList, f.ResourcePool.Moref) + } + return nil, fmt.Errorf("more than one resource pool was found with name %s - use resource pool ID instead - %v", name, idList) + } + return found[0], nil +} + +// GetAllResourcePools retrieves all available resource pool, across all vCenters +func (vcdClient *VCDClient) GetAllResourcePools(queryParams url.Values) ([]*ResourcePool, error) { + + vcenters, err := vcdClient.GetAllVCenters(queryParams) + if err != nil { + return nil, err + } + var result []*ResourcePool + for _, vc := range vcenters { + resourcePools, err := vc.GetAllResourcePools(queryParams) + if err != nil { + return nil, err + } + result = append(result, resourcePools...) + } + return result, nil +} + +// ResourcePoolsFromIds returns a slice of resource pools from a slice of resource pool IDs +func (vcdClient *VCDClient) ResourcePoolsFromIds(resourcePoolIds []string) ([]*ResourcePool, error) { + if len(resourcePoolIds) == 0 { + return nil, nil + } + + var result []*ResourcePool + + // 1. make sure there are no duplicates in the input IDs + uniqueIds := make(map[string]bool) + var duplicates []string + for _, id := range resourcePoolIds { + _, seen := uniqueIds[id] + if seen { + duplicates = append(duplicates, id) + } + uniqueIds[id] = true + } + + if len(duplicates) > 0 { + return nil, fmt.Errorf("duplicate IDs found in input: %v", duplicates) + } + + // 2. get all resource pools + resourcePools, err := vcdClient.GetAllResourcePools(nil) + if err != nil { + return nil, err + } + + util.Logger.Printf("wantedRecords: %v\n", resourcePoolIds) + // 3. build a map of resource pools, indexed by ID, for easy search + var foundRecords = make(map[string]*ResourcePool) + + for _, rpr := range resourcePools { + foundRecords[rpr.ResourcePool.Moref] = rpr + } + + // 4. loop through the requested IDs + for wanted := range uniqueIds { + // 4.1 if the wanted ID is not found, exit with an error + foundResourcePool, ok := foundRecords[wanted] + if !ok { + return nil, fmt.Errorf("resource pool ID '%s' not found in VCD", wanted) + } + result = append(result, foundResourcePool) + } + + // 5. Check that we got as many resource pools as the requested IDs + if len(result) != len(uniqueIds) { + return result, fmt.Errorf("%d IDs were requested, but only %d found", len(uniqueIds), len(result)) + } + + return result, nil +} diff --git a/govcd/vsphere_resource_pool_test.go b/govcd/vsphere_resource_pool_test.go new file mode 100644 index 000000000..87adb4dca --- /dev/null +++ b/govcd/vsphere_resource_pool_test.go @@ -0,0 +1,51 @@ +//go:build vsphere || functional || ALL + +package govcd + +import ( + "fmt" + "github.com/kr/pretty" + . "gopkg.in/check.v1" + "strings" +) + +func (vcd *TestVCD) Test_GetResourcePools(check *C) { + + if !vcd.client.Client.IsSysAdmin { + check.Skip("this test requires system administrator privileges") + } + vcenters, err := vcd.client.GetAllVCenters(nil) + check.Assert(err, IsNil) + + check.Assert(len(vcenters) > 0, Equals, true) + + vc := vcenters[0] + + allResourcePools, err := vc.GetAllResourcePools(nil) + check.Assert(err, IsNil) + + for i, rp := range allResourcePools { + rpByID, err := vc.GetResourcePoolById(rp.ResourcePool.Moref) + check.Assert(err, IsNil) + check.Assert(rpByID.ResourcePool.Moref, Equals, rp.ResourcePool.Moref) + check.Assert(rpByID.ResourcePool.Name, Equals, rp.ResourcePool.Name) + rpByName, err := vc.GetResourcePoolByName(rp.ResourcePool.Name) + if err != nil && strings.Contains(err.Error(), "more than one") { + if testVerbose { + fmt.Printf("%s\n", err) + } + continue + } + check.Assert(err, IsNil) + check.Assert(rpByName.ResourcePool.Moref, Equals, rp.ResourcePool.Moref) + check.Assert(rpByName.ResourcePool.Name, Equals, rp.ResourcePool.Name) + if testVerbose { + fmt.Printf("%2d %# v\n", i, pretty.Formatter(rp.ResourcePool)) + } + hw, err := rp.GetAvailableHardwareVersions() + check.Assert(err, IsNil) + if testVerbose { + fmt.Printf("%s %#v\n", rp.ResourcePool.Name, hw) + } + } +} diff --git a/govcd/vsphere_storage_profile.go b/govcd/vsphere_storage_profile.go new file mode 100644 index 000000000..b54172e08 --- /dev/null +++ b/govcd/vsphere_storage_profile.go @@ -0,0 +1,103 @@ +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "github.com/vmware/go-vcloud-director/v2/types/v56" + "net/url" +) + +/* +Note: These storage profile methods refer to storage profiles before they get assigned to a provider VDC. +This file, with related tests, was created before realizing that these calls do not retrieve the `*(Any)` +storage profile. +*/ + +// StorageProfile contains a storage profile in a given context (usually, a resource pool) +type StorageProfile struct { + StorageProfile *types.OpenApiStorageProfile + vcenter *VCenter + client *VCDClient +} + +// GetAllStorageProfiles retrieves all storage profiles existing in a given storage profile context +// Note: this function finds all *named* resource pools, but not the unnamed one [*(Any)] +func (vcenter VCenter) GetAllStorageProfiles(resourcePoolId string, queryParams url.Values) ([]*StorageProfile, error) { + client := vcenter.client.Client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointStorageProfiles + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(fmt.Sprintf(endpoint, vcenter.VSphereVCenter.VcId)) + if err != nil { + return nil, err + } + + retrieved := []*types.OpenApiStorageProfile{{}} + + if queryParams == nil { + queryParams = url.Values{} + } + if resourcePoolId != "" { + queryParams.Set("filter", fmt.Sprintf("_context==%s", resourcePoolId)) + } + err = client.OpenApiGetAllItems(minimumApiVersion, urlRef, queryParams, &retrieved, nil) + if err != nil { + return nil, fmt.Errorf("error getting storage profile list: %s", err) + } + + if len(retrieved) == 0 { + return nil, nil + } + var returnList []*StorageProfile + + for _, sp := range retrieved { + newSp := sp + returnList = append(returnList, &StorageProfile{ + StorageProfile: newSp, + vcenter: &vcenter, + client: vcenter.client, + }) + } + return returnList, nil +} + +// GetStorageProfileById retrieves a storage profile in the context of a given resource pool +func (vcenter VCenter) GetStorageProfileById(resourcePoolId, id string) (*StorageProfile, error) { + storageProfiles, err := vcenter.GetAllStorageProfiles(resourcePoolId, nil) + if err != nil { + return nil, err + } + for _, sp := range storageProfiles { + if sp.StorageProfile.Moref == id { + return sp, nil + } + } + return nil, fmt.Errorf("no storage profile found with ID '%s': %s", id, err) +} + +// GetStorageProfileByName retrieves a storage profile in the context of a given resource pool +func (vcenter VCenter) GetStorageProfileByName(resourcePoolId, name string) (*StorageProfile, error) { + storageProfiles, err := vcenter.GetAllStorageProfiles(resourcePoolId, nil) + if err != nil { + return nil, err + } + var found []*StorageProfile + for _, sp := range storageProfiles { + if sp.StorageProfile.Name == name { + found = append(found, sp) + } + } + if len(found) == 0 { + return nil, fmt.Errorf("no storage profile found with name '%s': %s", name, ErrorEntityNotFound) + } + if len(found) > 1 { + return nil, fmt.Errorf("more than one storage profile found with name '%s'", name) + } + return found[0], nil +} diff --git a/govcd/vsphere_storage_profile_test.go b/govcd/vsphere_storage_profile_test.go new file mode 100644 index 000000000..3883d6929 --- /dev/null +++ b/govcd/vsphere_storage_profile_test.go @@ -0,0 +1,54 @@ +//go:build vsphere || functional || ALL + +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "github.com/kr/pretty" + . "gopkg.in/check.v1" + "strings" +) + +func (vcd *TestVCD) Test_GetStorageProfiles(check *C) { + if !vcd.client.Client.IsSysAdmin { + check.Skip("this test requires system administrator privileges") + } + vcenters, err := vcd.client.GetAllVCenters(nil) + check.Assert(err, IsNil) + + check.Assert(len(vcenters) > 0, Equals, true) + + vc := vcenters[0] + + if vcd.config.Vsphere.ResourcePoolForVcd1 == "" { + check.Skip("no resource pool found for this VCD") + } + + resourcePool, err := vc.GetResourcePoolByName(vcd.config.Vsphere.ResourcePoolForVcd1) + check.Assert(err, IsNil) + + allStorageProfiles, err := vc.GetAllStorageProfiles(resourcePool.ResourcePool.Moref, nil) + check.Assert(err, IsNil) + + for i, sp := range allStorageProfiles { + spById, err := vc.GetStorageProfileById(resourcePool.ResourcePool.Moref, sp.StorageProfile.Moref) + check.Assert(err, IsNil) + check.Assert(spById.StorageProfile.Moref, Equals, sp.StorageProfile.Moref) + check.Assert(spById.StorageProfile.Name, Equals, sp.StorageProfile.Name) + spByName, err := vc.GetStorageProfileByName(resourcePool.ResourcePool.Moref, sp.StorageProfile.Name) + if err != nil && strings.Contains(err.Error(), "more than one") { + fmt.Printf("%s\n", err) + continue + } + check.Assert(err, IsNil) + check.Assert(spByName.StorageProfile.Moref, Equals, sp.StorageProfile.Moref) + check.Assert(spByName.StorageProfile.Name, Equals, sp.StorageProfile.Name) + if testVerbose { + fmt.Printf("%2d %# v\n", i, pretty.Formatter(sp.StorageProfile)) + } + } +} diff --git a/govcd/vsphere_vcenter.go b/govcd/vsphere_vcenter.go new file mode 100644 index 000000000..37e664a40 --- /dev/null +++ b/govcd/vsphere_vcenter.go @@ -0,0 +1,93 @@ +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package govcd + +import ( + "fmt" + "github.com/vmware/go-vcloud-director/v2/types/v56" + "net/url" +) + +type VCenter struct { + VSphereVCenter *types.VSphereVirtualCenter + client *VCDClient +} + +func (vcdClient *VCDClient) GetAllVCenters(queryParams url.Values) ([]*VCenter, error) { + client := vcdClient.Client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVirtualCenters + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint) + if err != nil { + return nil, err + } + + var retrieved []*types.VSphereVirtualCenter + + err = client.OpenApiGetAllItems(minimumApiVersion, urlRef, queryParams, &retrieved, nil) + if err != nil { + return nil, fmt.Errorf("error getting vCenters list: %s", err) + } + + if len(retrieved) == 0 { + return nil, nil + } + var returnList []*VCenter + + for _, r := range retrieved { + returnList = append(returnList, &VCenter{ + VSphereVCenter: r, + client: vcdClient, + }) + } + return returnList, nil +} + +func (vcdClient *VCDClient) GetVCenterByName(name string) (*VCenter, error) { + vcenters, err := vcdClient.GetAllVCenters(nil) + if err != nil { + return nil, err + } + for _, vc := range vcenters { + if vc.VSphereVCenter.Name == name { + return vc, nil + } + } + return nil, fmt.Errorf("vcenter %s not found: %s", name, ErrorEntityNotFound) +} + +func (vcdClient *VCDClient) GetVCenterById(id string) (*VCenter, error) { + client := vcdClient.Client + endpoint := types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointVirtualCenters + minimumApiVersion, err := client.checkOpenApiEndpointCompatibility(endpoint) + if err != nil { + return nil, err + } + + urlRef, err := client.OpenApiBuildEndpoint(endpoint + "/" + id) + if err != nil { + return nil, err + } + + returnObject := &VCenter{ + VSphereVCenter: &types.VSphereVirtualCenter{}, + client: vcdClient, + } + + err = client.OpenApiGetItem(minimumApiVersion, urlRef, nil, returnObject.VSphereVCenter, nil) + if err != nil { + return nil, fmt.Errorf("error getting vCenter: %s", err) + } + + return returnObject, nil +} + +func (vcenter VCenter) GetVimServerUrl() (string, error) { + return url.JoinPath(vcenter.client.Client.VCDHREF.String(), "admin", "extension", "vimServer", extractUuid(vcenter.VSphereVCenter.VcId)) +} diff --git a/govcd/vsphere_vcenter_test.go b/govcd/vsphere_vcenter_test.go new file mode 100644 index 000000000..b0316623e --- /dev/null +++ b/govcd/vsphere_vcenter_test.go @@ -0,0 +1,28 @@ +//go:build vsphere || functional || ALL + +package govcd + +import ( + . "gopkg.in/check.v1" +) + +func (vcd *TestVCD) Test_GetVcenters(check *C) { + + if !vcd.client.Client.IsSysAdmin { + check.Skip("this test requires system administrator privileges") + } + vcenters, err := vcd.client.GetAllVCenters(nil) + check.Assert(err, IsNil) + + check.Assert(len(vcenters) > 0, Equals, true) + + for _, vc := range vcenters { + vcenterById, err := vcd.client.GetVCenterById(vc.VSphereVCenter.VcId) + check.Assert(err, IsNil) + check.Assert(vc.VSphereVCenter.VcId, Equals, vcenterById.VSphereVCenter.VcId) + vcenterByName, err := vcd.client.GetVCenterByName(vc.VSphereVCenter.Name) + check.Assert(err, IsNil) + check.Assert(vc.VSphereVCenter.VcId, Equals, vcenterByName.VSphereVCenter.VcId) + } + +} diff --git a/samples/discover/discover.go b/samples/discover/discover.go index 021fda9a1..815bcba8d 100644 --- a/samples/discover/discover.go +++ b/samples/discover/discover.go @@ -45,10 +45,8 @@ import ( "fmt" "net/url" "os" - - "io/ioutil" - - "gopkg.in/yaml.v2" + "path/filepath" + "sigs.k8s.io/yaml" "github.com/vmware/go-vcloud-director/v2/govcd" ) @@ -96,7 +94,7 @@ func check_configuration(conf Config) { // Retrieves the configuration from a Json or Yaml file func getConfig(config_file string) Config { var configuration Config - buffer, err := ioutil.ReadFile(config_file) + buffer, err := os.ReadFile(filepath.Clean(config_file)) if err != nil { fmt.Printf("Configuration file %s not found\n%s\n", config_file, err) os.Exit(1) diff --git a/samples/openapi/main.go b/samples/openapi/main.go index d147d62d5..9553b61ed 100644 --- a/samples/openapi/main.go +++ b/samples/openapi/main.go @@ -58,11 +58,6 @@ func main() { os.Exit(3) } - if vcdCli.Client.APIVCDMaxVersionIs("< 33.0") { - fmt.Println("This example requires VCD API to support at least version 33.0 (VCD 10.0) to use '1.0.0/auditTrail' endpoint") - os.Exit(4) - } - switch mode { case "1": openAPIGetRawJsonAuditTrail(vcdCli) @@ -87,7 +82,7 @@ func openAPIGetRawJsonAuditTrail(vcdClient *govcd.VCDClient) { queryParams.Add("filter", "timestamp=gt="+filterTime) allResponses := []json.RawMessage{{}} - err = vcdClient.Client.OpenApiGetAllItems("33.0", urlRef, queryParams, &allResponses) + err = vcdClient.Client.OpenApiGetAllItems("35.0", urlRef, queryParams, &allResponses, nil) if err != nil { panic(err) } @@ -145,7 +140,7 @@ func openAPIGetStructAuditTrail(vcdClient *govcd.VCDClient) { filterTime := time.Now().Add(-12 * time.Hour).Format(types.FiqlQueryTimestampFormat) queryParams.Add("filter", "timestamp=gt="+filterTime) - err = vcdClient.Client.OpenApiGetAllItems("33.0", urlRef, queryParams, &response) + err = vcdClient.Client.OpenApiGetAllItems("35.0", urlRef, queryParams, &response, nil) if err != nil { panic(err) } diff --git a/scripts/changelog-links.sh b/scripts/changelog-links.sh new file mode 100755 index 000000000..db18929f7 --- /dev/null +++ b/scripts/changelog-links.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash + +# This script rewrites [GH-nnnn]-style references in the CHANGELOG.md file to +# be Markdown links to the given github issues. +# +# This is run during releases so that the issue references in all of the +# released items are presented as clickable links, but we can just use the +# easy [GH-nnnn] shorthand for quickly adding items to the "Unrelease" section +# while merging things between releases. + +set -e + +if [[ ! -f CHANGELOG.md ]]; then + echo "ERROR: CHANGELOG.md not found in pwd." + echo "Please run this from the root of the go-vcloud-director repository" + exit 1 +fi + +if [[ `uname` == "Darwin" ]]; then + echo "Using BSD sed" + SED="sed -i.bak -E -e" +else + echo "Using GNU sed" + SED="sed -i.bak -r -e" +fi + +GOVCD_URL="https:\/\/github.com\/vmware\/go-vcloud-director\/pull" + +$SED "s/GH-([0-9]+)/\[#\1\]\($GOVCD_URL\/\1\)/g" -e 's/\[\[#(.+)([0-9])\)]$/(\[#\1\2))/g' CHANGELOG.md +if [ "$?" != "0" ] ; then exit 1 ; fi +rm CHANGELOG.md.bak diff --git a/scripts/copyright_check.sh b/scripts/copyright_check.sh index 3abd8b131..93327f207 100755 --- a/scripts/copyright_check.sh +++ b/scripts/copyright_check.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # This script will find code files that don't have a copyright notice # or the ones with an outdated copyright. # diff --git a/scripts/get_token.sh b/scripts/get_token.sh index 425ab6c78..95a8c011c 100755 --- a/scripts/get_token.sh +++ b/scripts/get_token.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # This script will connect to the vCD using username and password, # and show the headers that contain a bearer or authorization token. # diff --git a/scripts/gosec-config.sh b/scripts/gosec-config.sh new file mode 100644 index 000000000..0d219c641 --- /dev/null +++ b/scripts/gosec-config.sh @@ -0,0 +1,4 @@ +# GOSEC_URL is the address of the gosec installation script +export GOSEC_URL=https://raw.githubusercontent.com/securego/gosec/master/install.sh +# NOTE: if we want to get the latest version, we set the variable below to empty ("") +export GOSEC_VERSION=v2.17.0 diff --git a/scripts/gosec.sh b/scripts/gosec.sh new file mode 100755 index 000000000..9310074f1 --- /dev/null +++ b/scripts/gosec.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +scripts_dir=$(dirname $0) +cd $scripts_dir +scripts_dir=$PWD +cd - > /dev/null + +sc_exit_code=0 + +if [ ! -d ./govcd ] +then + echo "source directory ./govcd not found" + exit 1 +fi + +if [ ! -f ./scripts/gosec-config.sh ] +then + echo "file ./scripts/gosec-config.sh not found" + exit 1 +fi + +source ./scripts/gosec-config.sh + +function exists_in_path { + what=$1 + for dir in $(echo $PATH | tr ':' ' ') + do + wanted=$dir/$what + if [ -x $wanted ] + then + echo $wanted + return + fi + done +} + +function get_gosec { + gosec=$(exists_in_path gosec) + if [ -z "$gosec" -a -n "$GITHUB_ACTIONS" ] + then + curl=$(exists_in_path curl) + if [ -z "$curl" ] + then + echo "'curl' executable not found - Skipping gosec" + exit 0 + fi + $curl -sfL $GOSEC_URL > gosec_install.sh + exit_code=$? + if [ "$exit_code" != "0" ] + then + echo "Error downloading gosec installer" + exit $exit_code + fi + sh -x gosec_install.sh $GOSEC_VERSION > gosec_install.log 2>&1 + exit_code=$? + if [ "$exit_code" != "0" ] + then + echo "Error installing gosec" + cat gosec_install.log + exit $exit_code + fi + gosec=$PWD/bin/gosec + fi + if [ -n "$gosec" ] + then + echo "## Found $gosec" + echo -n "## " + $gosec -version + else + echo "*** gosec executable not found - Exiting" + exit 0 + fi +} + +function run_gosec { + if [ -n "$gosec" ] + then + $gosec -tests -tags ALL ./... + exit_code=$? + if [ "$exit_code" != "0" ] + then + sc_exit_code=$exit_code + fi + fi + echo "" +} + +get_gosec +echo "" + +run_gosec +echo "Exit code: $sc_exit_code" +exit $sc_exit_code + diff --git a/scripts/make-changelog.sh b/scripts/make-changelog.sh new file mode 100755 index 000000000..e67231721 --- /dev/null +++ b/scripts/make-changelog.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash + +# This script collects the single change files and generates CHANGELOG entries +# for the whole release + +# .changes is the directory where the change files are +sources=.changes + +if [ ! -d $sources ] +then + echo "Directory $sources not found" + exit 1 +fi + +function check_eol { + file_name=$1 + # Checks the last line of the file. + # Counts the number of lines (==EOL) + has_eol=$(tail -n 1 $file_name | wc -l | tr -d ' ' | tr -d '\t') + + # If there isn't an EOL, we add one on the spot + if [ "$has_eol" == "0" ] + then + echo "" + fi +} + +# We must indicate a version on the command line +version=$1 + +# If no version was provided, we use the current release version +if [ -z "$version" ] +then + echo "No version was provided" + exit 1 +fi + + +# If the provided version does not exist, there is nothing to do +if [ ! -d $sources/$version ] +then + echo "# Changes directory $sources/$version not found" + exit 1 +fi + +# The "sections" file contains the CHANGELOG headers +if [ ! -f $sources/sections ] +then + echo "File $sources/sections not found" + exit 1 +fi +sections=$(cat $sources/sections) + +cd $sources/$version + +for section in $sections +do + # Check whether we have any file for this section + num=$(ls | grep "\-${section}.md" | wc -l | tr -d ' \t') + # if there are no files for this section, we skip + if [ "$num" == "0" ] + then + continue + fi + + # Generate the header + echo "### $(echo $section | tr 'a-z' 'A-Z' | tr '-' ' ')" + + # Print the changes files, sorted by PR number + for f in $(ls *${section}.md | sort -n) + do + cat $f + check_eol $f + done + echo "" +done + diff --git a/scripts/staticcheck-config.sh b/scripts/staticcheck-config.sh index 5201aad90..1ddfab252 100644 --- a/scripts/staticcheck-config.sh +++ b/scripts/staticcheck-config.sh @@ -1,4 +1,3 @@ export STATICCHECK_URL=https://github.com/dominikh/go-tools/releases/download -export STATICCHECK_VERSION=2020.1.4 +export STATICCHECK_VERSION=2023.1.7 export STATICCHECK_FILE=staticcheck_linux_amd64.tar.gz - diff --git a/scripts/staticcheck.sh b/scripts/staticcheck.sh index f64c95523..f8b24f8cb 100755 --- a/scripts/staticcheck.sh +++ b/scripts/staticcheck.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash scripts_dir=$(dirname $0) cd $scripts_dir scripts_dir=$PWD @@ -27,7 +27,7 @@ function exists_in_path { function get_check_static { static_check=$(exists_in_path staticcheck) - if [ -z "$staticcheck" -a -n "$TRAVIS" ] + if [ -z "$staticcheck" -a -n "$GITHUB_ACTIONS" ] then # Variables found in staticcheck-config.sh # STATICCHECK_URL diff --git a/scripts/test-tags.sh b/scripts/test-tags.sh index be547f720..d50609a1a 100755 --- a/scripts/test-tags.sh +++ b/scripts/test-tags.sh @@ -17,7 +17,7 @@ then fi start=$(date +%s) -tags=$(head -n 1 api_vcd_test.go | sed -e 's/^.*build //') +tags=$(head -n 1 api_vcd_test.go | sed -e 's/^.*build //;s/|| //g') echo "=== RUN TagsTest" for tag in $tags diff --git a/test-resources/golden/TestSamlAdfsAuthenticate_RESP_api_versions.golden b/test-resources/golden/TestSamlAdfsAuthenticate_RESP_api_versions.golden index ba80fb02a..9e5a9cc4a 100644 --- a/test-resources/golden/TestSamlAdfsAuthenticate_RESP_api_versions.golden +++ b/test-resources/golden/TestSamlAdfsAuthenticate_RESP_api_versions.golden @@ -1,1049 +1,1071 @@ - - - 20.0 - https://192.168.1.109/api/sessions - - - 21.0 - https://192.168.1.109/api/sessions - - - 22.0 - https://192.168.1.109/api/sessions - - - 23.0 - https://192.168.1.109/api/sessions - - - 24.0 - https://192.168.1.109/api/sessions - - - 25.0 - https://192.168.1.109/api/sessions - - - 26.0 - https://192.168.1.109/api/sessions - - - 27.0 - https://192.168.1.109/api/sessions - - - 28.0 - https://192.168.1.109/api/sessions - - - 29.0 - https://192.168.1.109/api/sessions - - - 30.0 - https://192.168.1.109/api/sessions - - - 31.0 - https://192.168.1.109/api/sessions - - - 32.0 - https://192.168.1.109/api/sessions - - - 5.5 - https://192.168.1.109/api/sessions - - application/vnd.vmware.vcloud.error+xml - ErrorType - http://192.168.1.109/api/v1.5/schema/common.xsd - - - application/vnd.vmware.vcloud.controlAccess+xml - ControlAccessParamsType - http://192.168.1.109/api/v1.5/schema/common.xsd - - - application/vnd.vmware.vcloud.owner+xml - OwnerType - http://192.168.1.109/api/v1.5/schema/common.xsd - - - application/vnd.vmware.vcloud.query.references+xml - ReferencesType - http://192.168.1.109/api/v1.5/schema/common.xsd - - - application/vnd.vmware.admin.fileUploadParams+xml - FileUploadParamsType - http://192.168.1.109/api/v1.5/schema/common.xsd - - - application/vnd.vmware.vcloud.fileUploadSocket+xml - FileUploadSocketType - http://192.168.1.109/api/v1.5/schema/common.xsd - - - application/vnd.vmware.vcloud.apiextensibility+xml - ApiExtensibilityType - http://192.168.1.109/api/v1.5/schema/services.xsd - - - application/vnd.vmware.vcloud.service+xml - ServiceType - http://192.168.1.109/api/v1.5/schema/services.xsd - - - application/vnd.vmware.vcloud.apidefinition+xml - ApiDefinitionType - http://192.168.1.109/api/v1.5/schema/services.xsd - - - application/vnd.vmware.vcloud.filedescriptor+xml - FileDescriptorType - http://192.168.1.109/api/v1.5/schema/services.xsd - - - application/vnd.vmware.vcloud.media+xml - MediaType - http://192.168.1.109/api/v1.5/schema/media.xsd - - - application/vnd.vmware.vcloud.cloneMediaParams+xml - CloneMediaParamsType - http://192.168.1.109/api/v1.5/schema/media.xsd - - - application/vnd.vmware.vcloud.vms+xml - VmsType - http://192.168.1.109/api/v1.5/schema/vms.xsd - - - application/vnd.vmware.vcloud.supportedSystemsInfo+xml - SupportedOperatingSystemsInfoType - http://192.168.1.109/api/v1.5/schema/vms.xsd - - - application/vnd.vmware.vcloud.catalog+xml - CatalogType - http://192.168.1.109/api/v1.5/schema/catalog.xsd - - - application/vnd.vmware.admin.publishCatalogParams+xml - PublishCatalogParamsType - http://192.168.1.109/api/v1.5/schema/catalog.xsd - - - application/vnd.vmware.vcloud.task+xml - TaskType - http://192.168.1.109/api/v1.5/schema/task.xsd - - - application/vnd.vmware.admin.taskOperationList+xml - TaskOperationListType - http://192.168.1.109/api/v1.5/schema/task.xsd - - - application/vnd.vmware.vcloud.vAppTemplate+xml - VAppTemplateType - http://192.168.1.109/api/v1.5/schema/vAppTemplate.xsd - - - application/vnd.vmware.vcloud.uploadVAppTemplateParams+xml - UploadVAppTemplateParamsType - http://192.168.1.109/api/v1.5/schema/vAppTemplate.xsd - - - application/vnd.vmware.vcloud.cloneVAppTemplateParams+xml - CloneVAppTemplateParamsType - http://192.168.1.109/api/v1.5/schema/vAppTemplate.xsd - - - application/vnd.vmware.vcloud.customizationSection+xml - CustomizationSectionType - http://192.168.1.109/api/v1.5/schema/vAppTemplate.xsd - - - application/vnd.vmware.admin.vmwNetworkPool.services+xml - VendorServicesType - http://192.168.1.109/api/v1.5/schema/vendorServices.xsd - - - application/vnd.vmware.vcloud.entity+xml - EntityType - http://192.168.1.109/api/v1.5/schema/entity.xsd - - - application/vnd.vmware.vcloud.entity.reference+xml - EntityReferenceType - http://192.168.1.109/api/v1.5/schema/entity.xsd - - - application/vnd.vmware.vcloud.network+xml - NetworkType - http://192.168.1.109/api/v1.5/schema/network.xsd - - - application/vnd.vmware.vcloud.orgNetwork+xml - OrgNetworkType - http://192.168.1.109/api/v1.5/schema/network.xsd - - - application/vnd.vmware.vcloud.vAppNetwork+xml - VAppNetworkType - http://192.168.1.109/api/v1.5/schema/network.xsd - - - application/vnd.vmware.vcloud.allocatedNetworkAddress+xml - AllocatedIpAddressesType - http://192.168.1.109/api/v1.5/schema/network.xsd - - - application/vnd.vmware.vcloud.subAllocations+xml - SubAllocationsType - http://192.168.1.109/api/v1.5/schema/network.xsd - - - application/vnd.vmware.vcloud.orgVdcNetwork+xml - OrgVdcNetworkType - http://192.168.1.109/api/v1.5/schema/network.xsd - - - application/vnd.vmware.admin.edgeGateway+xml - GatewayType - http://192.168.1.109/api/v1.5/schema/network.xsd - - - application/vnd.vmware.admin.edgeGatewayServiceConfiguration+xml - GatewayFeaturesType - http://192.168.1.109/api/v1.5/schema/network.xsd - - - application/vnd.vmware.vcloud.session+xml - SessionType - http://192.168.1.109/api/v1.5/schema/session.xsd - - - application/vnd.vmware.vcloud.disk+xml - DiskType - http://192.168.1.109/api/v1.5/schema/disk.xsd - - - application/vnd.vmware.vcloud.diskCreateParams+xml - DiskCreateParamsType - http://192.168.1.109/api/v1.5/schema/disk.xsd - - - application/vnd.vmware.vcloud.diskAttachOrDetachParams+xml - DiskAttachOrDetachParamsType - http://192.168.1.109/api/v1.5/schema/disk.xsd - - - application/vnd.vmware.vcloud.vdc+xml - VdcType - http://192.168.1.109/api/v1.5/schema/vdc.xsd - - - application/vnd.vmware.vcloud.screenTicket+xml - ScreenTicketType - http://192.168.1.109/api/v1.5/schema/screenTicket.xsd - - - application/vnd.vmware.vcloud.productSections+xml - ProductSectionListType - http://192.168.1.109/api/v1.5/schema/productSectionList.xsd - - - application/vnd.vmware.vcloud.catalogItem+xml - CatalogItemType - http://192.168.1.109/api/v1.5/schema/catalogItem.xsd - - - application/vnd.vmware.vcloud.tasksList+xml - TasksListType - http://192.168.1.109/api/v1.5/schema/tasksList.xsd - - - application/vnd.vmware.vcloud.orgList+xml - OrgListType - http://192.168.1.109/api/v1.5/schema/organizationList.xsd - - - application/vnd.vmware.vcloud.org+xml - OrgType - http://192.168.1.109/api/v1.5/schema/organization.xsd - - - application/vnd.vmware.vcloud.vm+xml - VmType - http://192.168.1.109/api/v1.5/schema/vApp.xsd - - - application/vnd.vmware.vcloud.vmCapabilitiesSection+xml - VmCapabilitiesType - http://192.168.1.109/api/v1.5/schema/vApp.xsd - - - application/vnd.vmware.vcloud.vApp+xml - VAppType - http://192.168.1.109/api/v1.5/schema/vApp.xsd - - - application/vnd.vmware.vcloud.rasdItemsList+xml - RasdItemsListType - http://192.168.1.109/api/v1.5/schema/vApp.xsd - - - application/vnd.vmware.vcloud.networkConfigSection+xml - NetworkConfigSectionType - http://192.168.1.109/api/v1.5/schema/vApp.xsd - - - application/vnd.vmware.vcloud.leaseSettingsSection+xml - LeaseSettingsSectionType - http://192.168.1.109/api/v1.5/schema/vApp.xsd - - - application/vnd.vmware.vcloud.networkConnectionSection+xml - NetworkConnectionSectionType - http://192.168.1.109/api/v1.5/schema/vApp.xsd - - - application/vnd.vmware.vcloud.runtimeInfoSection+xml - RuntimeInfoSectionType - http://192.168.1.109/api/v1.5/schema/vApp.xsd - - - application/vnd.vmware.vcloud.guestCustomizationSection+xml - GuestCustomizationSectionType - http://192.168.1.109/api/v1.5/schema/vApp.xsd - - - application/vnd.vmware.vcloud.snapshot+xml - SnapshotType - http://192.168.1.109/api/v1.5/schema/vApp.xsd - - - application/vnd.vmware.vcloud.snapshotSection+xml - SnapshotSectionType - http://192.168.1.109/api/v1.5/schema/vApp.xsd - - - application/vnd.vmware.vcloud.composeVAppParams+xml - ComposeVAppParamsType - http://192.168.1.109/api/v1.5/schema/vApp.xsd - - - application/vnd.vmware.vcloud.recomposeVAppParams+xml - RecomposeVAppParamsType - http://192.168.1.109/api/v1.5/schema/vApp.xsd - - - application/vnd.vmware.vcloud.registerVAppParams+xml - RegisterVAppParamsType - http://192.168.1.109/api/v1.5/schema/vApp.xsd - - - application/vnd.vmware.vcloud.instantiateVAppTemplateParams+xml - InstantiateVAppTemplateParamsType - http://192.168.1.109/api/v1.5/schema/vApp.xsd - - - application/vnd.vmware.vcloud.instantiateOvfParams+xml - InstantiateOvfParamsType - http://192.168.1.109/api/v1.5/schema/vApp.xsd - - - application/vnd.vmware.vcloud.cloneVAppParams+xml - CloneVAppParamsType - http://192.168.1.109/api/v1.5/schema/vApp.xsd - - - application/vnd.vmware.vcloud.deployVAppParams+xml - DeployVAppParamsType - http://192.168.1.109/api/v1.5/schema/vApp.xsd - - - application/vnd.vmware.vcloud.undeployVAppParams+xml - UndeployVAppParamsType - http://192.168.1.109/api/v1.5/schema/vApp.xsd - - - application/vnd.vmware.vcloud.mediaInsertOrEjectParams+xml - MediaInsertOrEjectParamsType - http://192.168.1.109/api/v1.5/schema/vApp.xsd - - - application/vnd.vmware.vcloud.captureVAppParams+xml - CaptureVAppParamsType - http://192.168.1.109/api/v1.5/schema/vApp.xsd - - - application/vnd.vmware.vcloud.vmPendingQuestion+xml - VmPendingQuestionType - http://192.168.1.109/api/v1.5/schema/vApp.xsd - - - application/vnd.vmware.vcloud.vmPendingAnswer+xml - VmQuestionAnswerType - http://192.168.1.109/api/v1.5/schema/vApp.xsd - - - application/vnd.vmware.vcloud.relocateVmParams+xml - RelocateParamsType - http://192.168.1.109/api/v1.5/schema/vApp.xsd - - - application/vnd.vmware.vcloud.createSnapshotParams+xml - CreateSnapshotParamsType - http://192.168.1.109/api/v1.5/schema/vApp.xsd - - - application/vnd.vmware.vm.complianceResult+xml - ComplianceResultType - http://192.168.1.109/api/v1.5/schema/vApp.xsd - - - application/vnd.vmware.vcloud.vdcStorageProfile+xml - VdcStorageProfileType - http://192.168.1.109/api/v1.5/schema/vdcStorageProfile.xsd - - - application/vnd.vmware.admin.certificateUpdateParams+xml - CertificateUpdateParamsType - http://192.168.1.109/api/v1.5/schema/upload.xsd - - - application/vnd.vmware.admin.certificateUploadSocketType+xml - CertificateUploadSocketType - http://192.168.1.109/api/v1.5/schema/upload.xsd - - - application/vnd.vmware.admin.keystoreUpdateParams+xml - KeystoreUpdateParamsType - http://192.168.1.109/api/v1.5/schema/upload.xsd - - - application/vnd.vmware.admin.keystoreUploadSocketType+xml - KeystoreUploadSocketType - http://192.168.1.109/api/v1.5/schema/upload.xsd - - - application/vnd.vmware.admin.sspiKeytabUpdateParams+xml - SspiKeytabUpdateParamsType - http://192.168.1.109/api/v1.5/schema/upload.xsd - - - application/vnd.vmware.admin.sspiKeytabUploadSocketType+xml - SspiKeytabUploadSocketType - http://192.168.1.109/api/v1.5/schema/upload.xsd - - - application/vnd.vmware.admin.trustStoreUpdateParams+xml - TrustStoreUpdateParamsType - http://192.168.1.109/api/v1.5/schema/upload.xsd - - - application/vnd.vmware.admin.trustStoreUploadSocketType+xml - TrustStoreUploadSocketType - http://192.168.1.109/api/v1.5/schema/upload.xsd - - - application/vnd.vmware.admin.event+xml - EventType - http://192.168.1.109/api/v1.5/schema/event.xsd - - - application/vnd.vmware.admin.providervdc+xml - ProviderVdcType - http://192.168.1.109/api/v1.5/schema/providerVdc.xsd - - - application/vnd.vmware.admin.createVdcParams+xml - CreateVdcParamsType - http://192.168.1.109/api/v1.5/schema/providerVdc.xsd - - - application/vnd.vmware.admin.vdc+xml - AdminVdcType - http://192.168.1.109/api/v1.5/schema/providerVdc.xsd - - - application/vnd.vmware.admin.vdcReferences+xml - VdcReferencesType - http://192.168.1.109/api/v1.5/schema/providerVdc.xsd - - - application/vnd.vmware.admin.pvdcStorageProfile+xml - ProviderVdcStorageProfileType - http://192.168.1.109/api/v1.5/schema/providerVdc.xsd - - - application/vnd.vmware.vcloud.vdcStorageProfileParams+xml - VdcStorageProfileParamsType - http://192.168.1.109/api/v1.5/schema/providerVdc.xsd - - - application/vnd.vmware.admin.vdcStorageProfile+xml - AdminVdcStorageProfileType - http://192.168.1.109/api/v1.5/schema/providerVdc.xsd - - - application/vnd.vmware.admin.updateVdcStorageProfiles+xml - UpdateVdcStorageProfilesType - http://192.168.1.109/api/v1.5/schema/providerVdc.xsd - - - application/vnd.vmware.admin.user+xml - UserType - http://192.168.1.109/api/v1.5/schema/user.xsd - - - application/vnd.vmware.admin.group+xml - GroupType - http://192.168.1.109/api/v1.5/schema/user.xsd - - - application/vnd.vmware.admin.right+xml - RightType - http://192.168.1.109/api/v1.5/schema/user.xsd - - - application/vnd.vmware.admin.role+xml - RoleType - http://192.168.1.109/api/v1.5/schema/user.xsd - - - application/vnd.vmware.admin.vcloud+xml - VCloudType - http://192.168.1.109/api/v1.5/schema/vCloudEntities.xsd - - - application/vnd.vmware.admin.organization+xml - AdminOrgType - http://192.168.1.109/api/v1.5/schema/vCloudEntities.xsd - - - application/vnd.vmware.admin.vAppTemplateLeaseSettings+xml - OrgVAppTemplateLeaseSettingsType - http://192.168.1.109/api/v1.5/schema/vCloudEntities.xsd - - - application/vnd.vmware.admin.orgSettings+xml - OrgSettingsType - http://192.168.1.109/api/v1.5/schema/vCloudEntities.xsd - - - application/vnd.vmware.admin.organizationGeneralSettings+xml - OrgGeneralSettingsType - http://192.168.1.109/api/v1.5/schema/vCloudEntities.xsd - - - application/vnd.vmware.admin.vAppLeaseSettings+xml - OrgLeaseSettingsType - http://192.168.1.109/api/v1.5/schema/vCloudEntities.xsd - - - application/vnd.vmware.admin.organizationFederationSettings+xml - OrgFederationSettingsType - http://192.168.1.109/api/v1.5/schema/vCloudEntities.xsd - - - application/vnd.vmware.admin.organizationLdapSettings+xml - OrgLdapSettingsType - http://192.168.1.109/api/v1.5/schema/vCloudEntities.xsd - - - application/vnd.vmware.admin.organizationEmailSettings+xml - OrgEmailSettingsType - http://192.168.1.109/api/v1.5/schema/vCloudEntities.xsd - - - application/vnd.vmware.admin.organizationPasswordPolicySettings+xml - OrgPasswordPolicySettingsType - http://192.168.1.109/api/v1.5/schema/vCloudEntities.xsd - - - application/vnd.vmware.admin.catalog+xml - AdminCatalogType - http://192.168.1.109/api/v1.5/schema/vCloudEntities.xsd - - - application/vnd.vmware.admin.guestPersonalizationSettings+xml - OrgGuestPersonalizationSettingsType - http://192.168.1.109/api/v1.5/schema/vCloudEntities.xsd - - - application/vnd.vmware.admin.operationLimitsSettings+xml - OrgOperationLimitsSettingsType - http://192.168.1.109/api/v1.5/schema/vCloudEntities.xsd - - - application/vnd.vmware.admin.systemSettings+xml - SystemSettingsType - http://192.168.1.109/api/v1.5/schema/settings.xsd - - - application/vnd.vmware.admin.generalSettings+xml - GeneralSettingsType - http://192.168.1.109/api/v1.5/schema/settings.xsd - - - application/vnd.vmware.admin.amqpSettings+xml - AmqpSettingsType - http://192.168.1.109/api/v1.5/schema/settings.xsd - - - application/vnd.vmware.admin.amqpSettingsTest+xml - AmqpSettingsTestType - http://192.168.1.109/api/v1.5/schema/settings.xsd - - - application/vnd.vmware.admin.notificationsSettings+xml - NotificationsSettingsType - http://192.168.1.109/api/v1.5/schema/settings.xsd - - - application/vnd.vmware.admin.blockingTaskSettings+xml - BlockingTaskSettingsType - http://192.168.1.109/api/v1.5/schema/settings.xsd - - - application/vnd.vmware.admin.systemPasswordPolicySettings+xml - SystemPasswordPolicySettingsType - http://192.168.1.109/api/v1.5/schema/settings.xsd - - - application/vnd.vmware.admin.ldapSettings+xml - LdapSettingsType - http://192.168.1.109/api/v1.5/schema/settings.xsd - - - application/vnd.vmware.admin.brandingSettings+xml - BrandingSettingsType - http://192.168.1.109/api/v1.5/schema/settings.xsd - - - application/vnd.vmware.admin.licenseSettings+xml - LicenseType - http://192.168.1.109/api/v1.5/schema/settings.xsd - - - application/vnd.vmware.admin.emailSettings+xml - EmailSettingsType - http://192.168.1.109/api/v1.5/schema/settings.xsd - - - application/vnd.vmware.admin.kerberosSettings+xml - KerberosSettingsType - http://192.168.1.109/api/v1.5/schema/settings.xsd - - - application/vnd.vmware.admin.lookupServiceSettings+xml - LookupServiceSettingsType - http://192.168.1.109/api/v1.5/schema/settings.xsd - - - application/vnd.vmware.admin.lookupServiceParams+xml - LookupServiceParamsType - http://192.168.1.109/api/v1.5/schema/settings.xsd - - - application/vnd.vmware.admin.vcTrustStoreUpdateParams+xml - VcTrustStoreUpdateParamsType - http://192.168.1.109/api/v1.5/schema/settings.xsd - - - application/vnd.vmware.admin.vcTrustStoreUploadSocket+xml - VcTrustStoreUploadSocketType - http://192.168.1.109/api/v1.5/schema/settings.xsd - - - application/vnd.vmware.admin.extensionServices+xml - ExtensionServicesType - http://192.168.1.109/api/v1.5/schema/services.xsd - - - application/vnd.vmware.admin.service+xml - AdminServiceType - http://192.168.1.109/api/v1.5/schema/services.xsd - - - application/vnd.vmware.admin.apiFilter+xml - ApiFilterType - http://192.168.1.109/api/v1.5/schema/services.xsd - - - application/vnd.vmware.admin.apiFilters+xml - ApiFiltersType - http://192.168.1.109/api/v1.5/schema/services.xsd - - - application/vnd.vmware.admin.apiDefinition+xml - AdminApiDefinitionType - http://192.168.1.109/api/v1.5/schema/services.xsd - - - application/vnd.vmware.admin.apiDefinitions+xml - AdminApiDefinitionsType - http://192.168.1.109/api/v1.5/schema/services.xsd - - - application/vnd.vmware.admin.fileDescriptor+xml - AdminFileDescriptorType - http://192.168.1.109/api/v1.5/schema/services.xsd - - - application/vnd.vmware.admin.serviceLink+xml - AdminServiceLinkType - http://192.168.1.109/api/v1.5/schema/services.xsd - - - application/vnd.vmware.admin.bundleUploadParams+xml - BundleUploadParamsType - http://192.168.1.109/api/v1.5/schema/services.xsd - - - application/vnd.vmware.admin.bundleUploadSocket+xml - BundleUploadSocketType - http://192.168.1.109/api/v1.5/schema/services.xsd - - - application/vnd.vmware.admin.aclAccess+xml - AclAccessType - http://192.168.1.109/api/v1.5/schema/services.xsd - - - application/vnd.vmware.admin.aclRule+xml - AclRuleType - http://192.168.1.109/api/v1.5/schema/services.xsd - - - application/vnd.vmware.admin.resourceClassAction+xml - ResourceClassActionType - http://192.168.1.109/api/v1.5/schema/services.xsd - - - application/vnd.vmware.admin.resourceClass+xml - ResourceClassType - http://192.168.1.109/api/v1.5/schema/services.xsd - - - application/vnd.vmware.admin.serviceResource+xml - ServiceResourceType - http://192.168.1.109/api/v1.5/schema/services.xsd - - - application/vnd.vmware.admin.authorizationCheckParams+xml - AuthorizationCheckParamsType - http://192.168.1.109/api/v1.5/schema/services.xsd - - - application/vnd.vmware.admin.authorizationCheckResponse+xml - AuthorizationCheckResponseType - http://192.168.1.109/api/v1.5/schema/services.xsd - - - application/vnd.vmware.admin.vmwExtension+xml - VMWExtensionType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.prepareHostParams+xml - PrepareHostParamsType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.registerVimServerParams+xml - RegisterVimServerParamsType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.vmwvirtualcenter+xml - VimServerType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.vmwVimServerReferences+xml - VMWVimServerReferencesType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.vshieldmanager+xml - ShieldManagerType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.vmsObjectRefsList+xml - VmObjectRefsListType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.vmObjectRef+xml - VmObjectRefType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.importVmAsVAppParams+xml - ImportVmAsVAppParamsType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.importVmIntoExistingVAppParams+xml - ImportVmIntoExistingVAppParamsType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.importVmAsVAppTemplateParams+xml - ImportVmAsVAppTemplateParamsType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.importMediaParams+xml - ImportMediaParamsType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.host+xml - HostType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.vimObjectRef+xml - VimObjectRefType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.vimObjectRefs+xml - VimObjectRefsType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.vmwprovidervdc+xml - VMWProviderVdcType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.createProviderVdcParams+xml - VMWProviderVdcParamsType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.vmwProviderVdcReferences+xml - VMWProviderVdcReferencesType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.vmwPvdcStorageProfile+xml - VMWProviderVdcStorageProfileType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.vmwexternalnet+xml - VMWExternalNetworkType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.vmwExternalNetworkReferences+xml - VMWExternalNetworkReferencesType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.vmwNetworkPoolReferences+xml - VMWNetworkPoolReferencesType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.vmwNetworkPool+xml - VMWNetworkPoolType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.portGroupPool+xml - PortGroupPoolType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.vlanPool+xml - VlanPoolType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.vxlanPool+xml - VxlanPoolType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.vxlanPool+xml - VdsContextType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.vmwHostReferences+xml - VMWHostReferencesType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.resourcePoolList+xml - ResourcePoolListType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.licensingReport+xml - LicensingReportType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.licensingReportList+xml - LicensingReportListType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.datastore+xml - DatastoreType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.vmwStorageProfiles+xml - VMWStorageProfilesType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.vmwProviderVdcResourcePoolSet+xml - VMWProviderVdcResourcePoolSetType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.vmwProviderVdcResourcePool+xml - VMWProviderVdcResourcePoolType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.resourcePoolSetUpdateParams+xml - UpdateResourcePoolSetParamsType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.OrganizationVdcResourcePoolSet+xml - OrganizationResourcePoolSetType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.strandedItemVimObjects+xml - StrandedItemVimObjectType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.strandedItemVimObjects+xml - StrandedItemVimObjectsType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.strandedItem+xml - StrandedItemType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.updateProviderVdcStorageProfiles+xml - UpdateProviderVdcStorageProfilesParamsType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.providerVdcMergeParams+xml - ProviderVdcMergeParamsType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.vSphereWebClientUrl+xml - VSphereWebClientUrlType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.updateRightsParams+xml - UpdateRightsParamsType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.rights+xml - RightRefsType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.entityReferences+xml - EntityReferencesType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.userEntityRights+xml - UserEntityRightsType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.migrateVmParams+xml - MigrateParamsType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - application/vnd.vmware.admin.blockingTask+xml - BlockingTaskType - http://192.168.1.109/api/v1.5/schema/taskExtensionRequest.xsd - - - application/vnd.vmware.admin.blockingTaskOperationParams+xml - BlockingTaskOperationParamsType - http://192.168.1.109/api/v1.5/schema/taskExtensionRequest.xsd - - - application/vnd.vmware.admin.blockingTaskUpdateProgressOperationParams+xml - BlockingTaskUpdateProgressParamsType - http://192.168.1.109/api/v1.5/schema/taskExtensionRequest.xsd - - - application/vnd.vmware.vcloud.rasdItem+xml - RASD_Type - http://192.168.1.109/api/v1.5/schema/master.xsd - - - application/vnd.vmware.vcloud.startupSection+xml - StartupSection_Type - http://schemas.dmtf.org/ovf/envelope/1/dsp8023_1.1.0.xsd - - - application/vnd.vmware.vcloud.virtualHardwareSection+xml - VirtualHardwareSection_Type - http://schemas.dmtf.org/ovf/envelope/1/dsp8023_1.1.0.xsd - - - application/vnd.vmware.vcloud.operatingSystemSection+xml - OperatingSystemSection_Type - http://schemas.dmtf.org/ovf/envelope/1/dsp8023_1.1.0.xsd - - - application/vnd.vmware.vcloud.networkSection+xml - NetworkSection_Type - http://schemas.dmtf.org/ovf/envelope/1/dsp8023_1.1.0.xsd - - - application/vnd.vmware.vcloud.vAppNetwork+xml - VAppNetworkType - http://192.168.1.109/api/v1.5/schema/master.xsd - - - application/vnd.vmware.vcloud.network+xml - NetworkType - http://192.168.1.109/api/v1.5/schema/master.xsd - - - application/vnd.vmware.vcloud.orgNetwork+xml - OrgNetworkType - http://192.168.1.109/api/v1.5/schema/master.xsd - - - application/vnd.vmware.admin.vmwexternalnet+xml - VMWExternalNetworkType - http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd - - - + + + 20.0 + https://192.168.1.109/api/sessions + + + 21.0 + https://192.168.1.109/api/sessions + + + 22.0 + https://192.168.1.109/api/sessions + + + 23.0 + https://192.168.1.109/api/sessions + + + 24.0 + https://192.168.1.109/api/sessions + + + 25.0 + https://192.168.1.109/api/sessions + + + 26.0 + https://192.168.1.109/api/sessions + + + 27.0 + https://192.168.1.109/api/sessions + + + 28.0 + https://192.168.1.109/api/sessions + + + 29.0 + https://192.168.1.109/api/sessions + + + 30.0 + https://192.168.1.109/api/sessions + + + 31.0 + https://192.168.1.109/api/sessions + + + 32.0 + https://192.168.1.109/api/sessions + + + 33.0 + https://192.168.1.109/api/sessions + + + 34.0 + https://192.168.1.109/api/sessions + + + 35.0 + https://192.168.1.109/api/sessions + + + 36.0 + https://192.168.1.109/cloudapi/1.0.0/sessions + https://192.168.1.109/cloudapi/1.0.0/sessions/provider + + + 37.0 + https://192.168.1.109/cloudapi/1.0.0/sessions + https://192.168.1.109/cloudapi/1.0.0/sessions/provider + + + 5.5 + https://192.168.1.109/api/sessions + + application/vnd.vmware.vcloud.error+xml + ErrorType + http://192.168.1.109/api/v1.5/schema/common.xsd + + + application/vnd.vmware.vcloud.controlAccess+xml + ControlAccessParamsType + http://192.168.1.109/api/v1.5/schema/common.xsd + + + application/vnd.vmware.vcloud.owner+xml + OwnerType + http://192.168.1.109/api/v1.5/schema/common.xsd + + + application/vnd.vmware.vcloud.query.references+xml + ReferencesType + http://192.168.1.109/api/v1.5/schema/common.xsd + + + application/vnd.vmware.admin.fileUploadParams+xml + FileUploadParamsType + http://192.168.1.109/api/v1.5/schema/common.xsd + + + application/vnd.vmware.vcloud.fileUploadSocket+xml + FileUploadSocketType + http://192.168.1.109/api/v1.5/schema/common.xsd + + + application/vnd.vmware.vcloud.apiextensibility+xml + ApiExtensibilityType + http://192.168.1.109/api/v1.5/schema/services.xsd + + + application/vnd.vmware.vcloud.service+xml + ServiceType + http://192.168.1.109/api/v1.5/schema/services.xsd + + + application/vnd.vmware.vcloud.apidefinition+xml + ApiDefinitionType + http://192.168.1.109/api/v1.5/schema/services.xsd + + + application/vnd.vmware.vcloud.filedescriptor+xml + FileDescriptorType + http://192.168.1.109/api/v1.5/schema/services.xsd + + + application/vnd.vmware.vcloud.media+xml + MediaType + http://192.168.1.109/api/v1.5/schema/media.xsd + + + application/vnd.vmware.vcloud.cloneMediaParams+xml + CloneMediaParamsType + http://192.168.1.109/api/v1.5/schema/media.xsd + + + application/vnd.vmware.vcloud.vms+xml + VmsType + http://192.168.1.109/api/v1.5/schema/vms.xsd + + + application/vnd.vmware.vcloud.supportedSystemsInfo+xml + SupportedOperatingSystemsInfoType + http://192.168.1.109/api/v1.5/schema/vms.xsd + + + application/vnd.vmware.vcloud.catalog+xml + CatalogType + http://192.168.1.109/api/v1.5/schema/catalog.xsd + + + application/vnd.vmware.admin.publishCatalogParams+xml + PublishCatalogParamsType + http://192.168.1.109/api/v1.5/schema/catalog.xsd + + + application/vnd.vmware.vcloud.task+xml + TaskType + http://192.168.1.109/api/v1.5/schema/task.xsd + + + application/vnd.vmware.admin.taskOperationList+xml + TaskOperationListType + http://192.168.1.109/api/v1.5/schema/task.xsd + + + application/vnd.vmware.vcloud.vAppTemplate+xml + VAppTemplateType + http://192.168.1.109/api/v1.5/schema/vAppTemplate.xsd + + + application/vnd.vmware.vcloud.uploadVAppTemplateParams+xml + UploadVAppTemplateParamsType + http://192.168.1.109/api/v1.5/schema/vAppTemplate.xsd + + + application/vnd.vmware.vcloud.cloneVAppTemplateParams+xml + CloneVAppTemplateParamsType + http://192.168.1.109/api/v1.5/schema/vAppTemplate.xsd + + + application/vnd.vmware.vcloud.customizationSection+xml + CustomizationSectionType + http://192.168.1.109/api/v1.5/schema/vAppTemplate.xsd + + + application/vnd.vmware.admin.vmwNetworkPool.services+xml + VendorServicesType + http://192.168.1.109/api/v1.5/schema/vendorServices.xsd + + + application/vnd.vmware.vcloud.entity+xml + EntityType + http://192.168.1.109/api/v1.5/schema/entity.xsd + + + application/vnd.vmware.vcloud.entity.reference+xml + EntityReferenceType + http://192.168.1.109/api/v1.5/schema/entity.xsd + + + application/vnd.vmware.vcloud.network+xml + NetworkType + http://192.168.1.109/api/v1.5/schema/network.xsd + + + application/vnd.vmware.vcloud.orgNetwork+xml + OrgNetworkType + http://192.168.1.109/api/v1.5/schema/network.xsd + + + application/vnd.vmware.vcloud.vAppNetwork+xml + VAppNetworkType + http://192.168.1.109/api/v1.5/schema/network.xsd + + + application/vnd.vmware.vcloud.allocatedNetworkAddress+xml + AllocatedIpAddressesType + http://192.168.1.109/api/v1.5/schema/network.xsd + + + application/vnd.vmware.vcloud.subAllocations+xml + SubAllocationsType + http://192.168.1.109/api/v1.5/schema/network.xsd + + + application/vnd.vmware.vcloud.orgVdcNetwork+xml + OrgVdcNetworkType + http://192.168.1.109/api/v1.5/schema/network.xsd + + + application/vnd.vmware.admin.edgeGateway+xml + GatewayType + http://192.168.1.109/api/v1.5/schema/network.xsd + + + application/vnd.vmware.admin.edgeGatewayServiceConfiguration+xml + GatewayFeaturesType + http://192.168.1.109/api/v1.5/schema/network.xsd + + + application/vnd.vmware.vcloud.session+xml + SessionType + http://192.168.1.109/api/v1.5/schema/session.xsd + + + application/vnd.vmware.vcloud.disk+xml + DiskType + http://192.168.1.109/api/v1.5/schema/disk.xsd + + + application/vnd.vmware.vcloud.diskCreateParams+xml + DiskCreateParamsType + http://192.168.1.109/api/v1.5/schema/disk.xsd + + + application/vnd.vmware.vcloud.diskAttachOrDetachParams+xml + DiskAttachOrDetachParamsType + http://192.168.1.109/api/v1.5/schema/disk.xsd + + + application/vnd.vmware.vcloud.vdc+xml + VdcType + http://192.168.1.109/api/v1.5/schema/vdc.xsd + + + application/vnd.vmware.vcloud.screenTicket+xml + ScreenTicketType + http://192.168.1.109/api/v1.5/schema/screenTicket.xsd + + + application/vnd.vmware.vcloud.productSections+xml + ProductSectionListType + http://192.168.1.109/api/v1.5/schema/productSectionList.xsd + + + application/vnd.vmware.vcloud.catalogItem+xml + CatalogItemType + http://192.168.1.109/api/v1.5/schema/catalogItem.xsd + + + application/vnd.vmware.vcloud.tasksList+xml + TasksListType + http://192.168.1.109/api/v1.5/schema/tasksList.xsd + + + application/vnd.vmware.vcloud.orgList+xml + OrgListType + http://192.168.1.109/api/v1.5/schema/organizationList.xsd + + + application/vnd.vmware.vcloud.org+xml + OrgType + http://192.168.1.109/api/v1.5/schema/organization.xsd + + + application/vnd.vmware.vcloud.vm+xml + VmType + http://192.168.1.109/api/v1.5/schema/vApp.xsd + + + application/vnd.vmware.vcloud.vmCapabilitiesSection+xml + VmCapabilitiesType + http://192.168.1.109/api/v1.5/schema/vApp.xsd + + + application/vnd.vmware.vcloud.vApp+xml + VAppType + http://192.168.1.109/api/v1.5/schema/vApp.xsd + + + application/vnd.vmware.vcloud.rasdItemsList+xml + RasdItemsListType + http://192.168.1.109/api/v1.5/schema/vApp.xsd + + + application/vnd.vmware.vcloud.networkConfigSection+xml + NetworkConfigSectionType + http://192.168.1.109/api/v1.5/schema/vApp.xsd + + + application/vnd.vmware.vcloud.leaseSettingsSection+xml + LeaseSettingsSectionType + http://192.168.1.109/api/v1.5/schema/vApp.xsd + + + application/vnd.vmware.vcloud.networkConnectionSection+xml + NetworkConnectionSectionType + http://192.168.1.109/api/v1.5/schema/vApp.xsd + + + application/vnd.vmware.vcloud.runtimeInfoSection+xml + RuntimeInfoSectionType + http://192.168.1.109/api/v1.5/schema/vApp.xsd + + + application/vnd.vmware.vcloud.guestCustomizationSection+xml + GuestCustomizationSectionType + http://192.168.1.109/api/v1.5/schema/vApp.xsd + + + application/vnd.vmware.vcloud.snapshot+xml + SnapshotType + http://192.168.1.109/api/v1.5/schema/vApp.xsd + + + application/vnd.vmware.vcloud.snapshotSection+xml + SnapshotSectionType + http://192.168.1.109/api/v1.5/schema/vApp.xsd + + + application/vnd.vmware.vcloud.composeVAppParams+xml + ComposeVAppParamsType + http://192.168.1.109/api/v1.5/schema/vApp.xsd + + + application/vnd.vmware.vcloud.recomposeVAppParams+xml + RecomposeVAppParamsType + http://192.168.1.109/api/v1.5/schema/vApp.xsd + + + application/vnd.vmware.vcloud.registerVAppParams+xml + RegisterVAppParamsType + http://192.168.1.109/api/v1.5/schema/vApp.xsd + + + application/vnd.vmware.vcloud.instantiateVAppTemplateParams+xml + InstantiateVAppTemplateParamsType + http://192.168.1.109/api/v1.5/schema/vApp.xsd + + + application/vnd.vmware.vcloud.instantiateOvfParams+xml + InstantiateOvfParamsType + http://192.168.1.109/api/v1.5/schema/vApp.xsd + + + application/vnd.vmware.vcloud.cloneVAppParams+xml + CloneVAppParamsType + http://192.168.1.109/api/v1.5/schema/vApp.xsd + + + application/vnd.vmware.vcloud.deployVAppParams+xml + DeployVAppParamsType + http://192.168.1.109/api/v1.5/schema/vApp.xsd + + + application/vnd.vmware.vcloud.undeployVAppParams+xml + UndeployVAppParamsType + http://192.168.1.109/api/v1.5/schema/vApp.xsd + + + application/vnd.vmware.vcloud.mediaInsertOrEjectParams+xml + MediaInsertOrEjectParamsType + http://192.168.1.109/api/v1.5/schema/vApp.xsd + + + application/vnd.vmware.vcloud.captureVAppParams+xml + CaptureVAppParamsType + http://192.168.1.109/api/v1.5/schema/vApp.xsd + + + application/vnd.vmware.vcloud.vmPendingQuestion+xml + VmPendingQuestionType + http://192.168.1.109/api/v1.5/schema/vApp.xsd + + + application/vnd.vmware.vcloud.vmPendingAnswer+xml + VmQuestionAnswerType + http://192.168.1.109/api/v1.5/schema/vApp.xsd + + + application/vnd.vmware.vcloud.relocateVmParams+xml + RelocateParamsType + http://192.168.1.109/api/v1.5/schema/vApp.xsd + + + application/vnd.vmware.vcloud.createSnapshotParams+xml + CreateSnapshotParamsType + http://192.168.1.109/api/v1.5/schema/vApp.xsd + + + application/vnd.vmware.vm.complianceResult+xml + ComplianceResultType + http://192.168.1.109/api/v1.5/schema/vApp.xsd + + + application/vnd.vmware.vcloud.vdcStorageProfile+xml + VdcStorageProfileType + http://192.168.1.109/api/v1.5/schema/vdcStorageProfile.xsd + + + application/vnd.vmware.admin.certificateUpdateParams+xml + CertificateUpdateParamsType + http://192.168.1.109/api/v1.5/schema/upload.xsd + + + application/vnd.vmware.admin.certificateUploadSocketType+xml + CertificateUploadSocketType + http://192.168.1.109/api/v1.5/schema/upload.xsd + + + application/vnd.vmware.admin.keystoreUpdateParams+xml + KeystoreUpdateParamsType + http://192.168.1.109/api/v1.5/schema/upload.xsd + + + application/vnd.vmware.admin.keystoreUploadSocketType+xml + KeystoreUploadSocketType + http://192.168.1.109/api/v1.5/schema/upload.xsd + + + application/vnd.vmware.admin.sspiKeytabUpdateParams+xml + SspiKeytabUpdateParamsType + http://192.168.1.109/api/v1.5/schema/upload.xsd + + + application/vnd.vmware.admin.sspiKeytabUploadSocketType+xml + SspiKeytabUploadSocketType + http://192.168.1.109/api/v1.5/schema/upload.xsd + + + application/vnd.vmware.admin.trustStoreUpdateParams+xml + TrustStoreUpdateParamsType + http://192.168.1.109/api/v1.5/schema/upload.xsd + + + application/vnd.vmware.admin.trustStoreUploadSocketType+xml + TrustStoreUploadSocketType + http://192.168.1.109/api/v1.5/schema/upload.xsd + + + application/vnd.vmware.admin.event+xml + EventType + http://192.168.1.109/api/v1.5/schema/event.xsd + + + application/vnd.vmware.admin.providervdc+xml + ProviderVdcType + http://192.168.1.109/api/v1.5/schema/providerVdc.xsd + + + application/vnd.vmware.admin.createVdcParams+xml + CreateVdcParamsType + http://192.168.1.109/api/v1.5/schema/providerVdc.xsd + + + application/vnd.vmware.admin.vdc+xml + AdminVdcType + http://192.168.1.109/api/v1.5/schema/providerVdc.xsd + + + application/vnd.vmware.admin.vdcReferences+xml + VdcReferencesType + http://192.168.1.109/api/v1.5/schema/providerVdc.xsd + + + application/vnd.vmware.admin.pvdcStorageProfile+xml + ProviderVdcStorageProfileType + http://192.168.1.109/api/v1.5/schema/providerVdc.xsd + + + application/vnd.vmware.vcloud.vdcStorageProfileParams+xml + VdcStorageProfileParamsType + http://192.168.1.109/api/v1.5/schema/providerVdc.xsd + + + application/vnd.vmware.admin.vdcStorageProfile+xml + AdminVdcStorageProfileType + http://192.168.1.109/api/v1.5/schema/providerVdc.xsd + + + application/vnd.vmware.admin.updateVdcStorageProfiles+xml + UpdateVdcStorageProfilesType + http://192.168.1.109/api/v1.5/schema/providerVdc.xsd + + + application/vnd.vmware.admin.user+xml + UserType + http://192.168.1.109/api/v1.5/schema/user.xsd + + + application/vnd.vmware.admin.group+xml + GroupType + http://192.168.1.109/api/v1.5/schema/user.xsd + + + application/vnd.vmware.admin.right+xml + RightType + http://192.168.1.109/api/v1.5/schema/user.xsd + + + application/vnd.vmware.admin.role+xml + RoleType + http://192.168.1.109/api/v1.5/schema/user.xsd + + + application/vnd.vmware.admin.vcloud+xml + VCloudType + http://192.168.1.109/api/v1.5/schema/vCloudEntities.xsd + + + application/vnd.vmware.admin.organization+xml + AdminOrgType + http://192.168.1.109/api/v1.5/schema/vCloudEntities.xsd + + + application/vnd.vmware.admin.vAppTemplateLeaseSettings+xml + OrgVAppTemplateLeaseSettingsType + http://192.168.1.109/api/v1.5/schema/vCloudEntities.xsd + + + application/vnd.vmware.admin.orgSettings+xml + OrgSettingsType + http://192.168.1.109/api/v1.5/schema/vCloudEntities.xsd + + + application/vnd.vmware.admin.organizationGeneralSettings+xml + OrgGeneralSettingsType + http://192.168.1.109/api/v1.5/schema/vCloudEntities.xsd + + + application/vnd.vmware.admin.vAppLeaseSettings+xml + OrgLeaseSettingsType + http://192.168.1.109/api/v1.5/schema/vCloudEntities.xsd + + + application/vnd.vmware.admin.organizationFederationSettings+xml + OrgFederationSettingsType + http://192.168.1.109/api/v1.5/schema/vCloudEntities.xsd + + + application/vnd.vmware.admin.organizationLdapSettings+xml + OrgLdapSettingsType + http://192.168.1.109/api/v1.5/schema/vCloudEntities.xsd + + + application/vnd.vmware.admin.organizationEmailSettings+xml + OrgEmailSettingsType + http://192.168.1.109/api/v1.5/schema/vCloudEntities.xsd + + + application/vnd.vmware.admin.organizationPasswordPolicySettings+xml + OrgPasswordPolicySettingsType + http://192.168.1.109/api/v1.5/schema/vCloudEntities.xsd + + + application/vnd.vmware.admin.catalog+xml + AdminCatalogType + http://192.168.1.109/api/v1.5/schema/vCloudEntities.xsd + + + application/vnd.vmware.admin.guestPersonalizationSettings+xml + OrgGuestPersonalizationSettingsType + http://192.168.1.109/api/v1.5/schema/vCloudEntities.xsd + + + application/vnd.vmware.admin.operationLimitsSettings+xml + OrgOperationLimitsSettingsType + http://192.168.1.109/api/v1.5/schema/vCloudEntities.xsd + + + application/vnd.vmware.admin.systemSettings+xml + SystemSettingsType + http://192.168.1.109/api/v1.5/schema/settings.xsd + + + application/vnd.vmware.admin.generalSettings+xml + GeneralSettingsType + http://192.168.1.109/api/v1.5/schema/settings.xsd + + + application/vnd.vmware.admin.amqpSettings+xml + AmqpSettingsType + http://192.168.1.109/api/v1.5/schema/settings.xsd + + + application/vnd.vmware.admin.amqpSettingsTest+xml + AmqpSettingsTestType + http://192.168.1.109/api/v1.5/schema/settings.xsd + + + application/vnd.vmware.admin.notificationsSettings+xml + NotificationsSettingsType + http://192.168.1.109/api/v1.5/schema/settings.xsd + + + application/vnd.vmware.admin.blockingTaskSettings+xml + BlockingTaskSettingsType + http://192.168.1.109/api/v1.5/schema/settings.xsd + + + application/vnd.vmware.admin.systemPasswordPolicySettings+xml + SystemPasswordPolicySettingsType + http://192.168.1.109/api/v1.5/schema/settings.xsd + + + application/vnd.vmware.admin.ldapSettings+xml + LdapSettingsType + http://192.168.1.109/api/v1.5/schema/settings.xsd + + + application/vnd.vmware.admin.brandingSettings+xml + BrandingSettingsType + http://192.168.1.109/api/v1.5/schema/settings.xsd + + + application/vnd.vmware.admin.licenseSettings+xml + LicenseType + http://192.168.1.109/api/v1.5/schema/settings.xsd + + + application/vnd.vmware.admin.emailSettings+xml + EmailSettingsType + http://192.168.1.109/api/v1.5/schema/settings.xsd + + + application/vnd.vmware.admin.kerberosSettings+xml + KerberosSettingsType + http://192.168.1.109/api/v1.5/schema/settings.xsd + + + application/vnd.vmware.admin.lookupServiceSettings+xml + LookupServiceSettingsType + http://192.168.1.109/api/v1.5/schema/settings.xsd + + + application/vnd.vmware.admin.lookupServiceParams+xml + LookupServiceParamsType + http://192.168.1.109/api/v1.5/schema/settings.xsd + + + application/vnd.vmware.admin.vcTrustStoreUpdateParams+xml + VcTrustStoreUpdateParamsType + http://192.168.1.109/api/v1.5/schema/settings.xsd + + + application/vnd.vmware.admin.vcTrustStoreUploadSocket+xml + VcTrustStoreUploadSocketType + http://192.168.1.109/api/v1.5/schema/settings.xsd + + + application/vnd.vmware.admin.extensionServices+xml + ExtensionServicesType + http://192.168.1.109/api/v1.5/schema/services.xsd + + + application/vnd.vmware.admin.service+xml + AdminServiceType + http://192.168.1.109/api/v1.5/schema/services.xsd + + + application/vnd.vmware.admin.apiFilter+xml + ApiFilterType + http://192.168.1.109/api/v1.5/schema/services.xsd + + + application/vnd.vmware.admin.apiFilters+xml + ApiFiltersType + http://192.168.1.109/api/v1.5/schema/services.xsd + + + application/vnd.vmware.admin.apiDefinition+xml + AdminApiDefinitionType + http://192.168.1.109/api/v1.5/schema/services.xsd + + + application/vnd.vmware.admin.apiDefinitions+xml + AdminApiDefinitionsType + http://192.168.1.109/api/v1.5/schema/services.xsd + + + application/vnd.vmware.admin.fileDescriptor+xml + AdminFileDescriptorType + http://192.168.1.109/api/v1.5/schema/services.xsd + + + application/vnd.vmware.admin.serviceLink+xml + AdminServiceLinkType + http://192.168.1.109/api/v1.5/schema/services.xsd + + + application/vnd.vmware.admin.bundleUploadParams+xml + BundleUploadParamsType + http://192.168.1.109/api/v1.5/schema/services.xsd + + + application/vnd.vmware.admin.bundleUploadSocket+xml + BundleUploadSocketType + http://192.168.1.109/api/v1.5/schema/services.xsd + + + application/vnd.vmware.admin.aclAccess+xml + AclAccessType + http://192.168.1.109/api/v1.5/schema/services.xsd + + + application/vnd.vmware.admin.aclRule+xml + AclRuleType + http://192.168.1.109/api/v1.5/schema/services.xsd + + + application/vnd.vmware.admin.resourceClassAction+xml + ResourceClassActionType + http://192.168.1.109/api/v1.5/schema/services.xsd + + + application/vnd.vmware.admin.resourceClass+xml + ResourceClassType + http://192.168.1.109/api/v1.5/schema/services.xsd + + + application/vnd.vmware.admin.serviceResource+xml + ServiceResourceType + http://192.168.1.109/api/v1.5/schema/services.xsd + + + application/vnd.vmware.admin.authorizationCheckParams+xml + AuthorizationCheckParamsType + http://192.168.1.109/api/v1.5/schema/services.xsd + + + application/vnd.vmware.admin.authorizationCheckResponse+xml + AuthorizationCheckResponseType + http://192.168.1.109/api/v1.5/schema/services.xsd + + + application/vnd.vmware.admin.vmwExtension+xml + VMWExtensionType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.prepareHostParams+xml + PrepareHostParamsType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.registerVimServerParams+xml + RegisterVimServerParamsType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.vmwvirtualcenter+xml + VimServerType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.vmwVimServerReferences+xml + VMWVimServerReferencesType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.vshieldmanager+xml + ShieldManagerType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.vmsObjectRefsList+xml + VmObjectRefsListType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.vmObjectRef+xml + VmObjectRefType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.importVmAsVAppParams+xml + ImportVmAsVAppParamsType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.importVmIntoExistingVAppParams+xml + ImportVmIntoExistingVAppParamsType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.importVmAsVAppTemplateParams+xml + ImportVmAsVAppTemplateParamsType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.importMediaParams+xml + ImportMediaParamsType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.host+xml + HostType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.vimObjectRef+xml + VimObjectRefType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.vimObjectRefs+xml + VimObjectRefsType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.vmwprovidervdc+xml + VMWProviderVdcType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.createProviderVdcParams+xml + VMWProviderVdcParamsType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.vmwProviderVdcReferences+xml + VMWProviderVdcReferencesType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.vmwPvdcStorageProfile+xml + VMWProviderVdcStorageProfileType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.vmwexternalnet+xml + VMWExternalNetworkType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.vmwExternalNetworkReferences+xml + VMWExternalNetworkReferencesType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.vmwNetworkPoolReferences+xml + VMWNetworkPoolReferencesType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.vmwNetworkPool+xml + VMWNetworkPoolType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.portGroupPool+xml + PortGroupPoolType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.vlanPool+xml + VlanPoolType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.vxlanPool+xml + VxlanPoolType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.vxlanPool+xml + VdsContextType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.vmwHostReferences+xml + VMWHostReferencesType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.resourcePoolList+xml + ResourcePoolListType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.licensingReport+xml + LicensingReportType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.licensingReportList+xml + LicensingReportListType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.datastore+xml + DatastoreType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.vmwStorageProfiles+xml + VMWStorageProfilesType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.vmwProviderVdcResourcePoolSet+xml + VMWProviderVdcResourcePoolSetType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.vmwProviderVdcResourcePool+xml + VMWProviderVdcResourcePoolType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.resourcePoolSetUpdateParams+xml + UpdateResourcePoolSetParamsType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.OrganizationVdcResourcePoolSet+xml + OrganizationResourcePoolSetType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.strandedItemVimObjects+xml + StrandedItemVimObjectType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.strandedItemVimObjects+xml + StrandedItemVimObjectsType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.strandedItem+xml + StrandedItemType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.updateProviderVdcStorageProfiles+xml + UpdateProviderVdcStorageProfilesParamsType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.providerVdcMergeParams+xml + ProviderVdcMergeParamsType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.vSphereWebClientUrl+xml + VSphereWebClientUrlType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.updateRightsParams+xml + UpdateRightsParamsType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.rights+xml + RightRefsType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.entityReferences+xml + EntityReferencesType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.userEntityRights+xml + UserEntityRightsType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.migrateVmParams+xml + MigrateParamsType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + application/vnd.vmware.admin.blockingTask+xml + BlockingTaskType + http://192.168.1.109/api/v1.5/schema/taskExtensionRequest.xsd + + + application/vnd.vmware.admin.blockingTaskOperationParams+xml + BlockingTaskOperationParamsType + http://192.168.1.109/api/v1.5/schema/taskExtensionRequest.xsd + + + application/vnd.vmware.admin.blockingTaskUpdateProgressOperationParams+xml + BlockingTaskUpdateProgressParamsType + http://192.168.1.109/api/v1.5/schema/taskExtensionRequest.xsd + + + application/vnd.vmware.vcloud.rasdItem+xml + RASD_Type + http://192.168.1.109/api/v1.5/schema/master.xsd + + + application/vnd.vmware.vcloud.startupSection+xml + StartupSection_Type + http://schemas.dmtf.org/ovf/envelope/1/dsp8023_1.1.0.xsd + + + application/vnd.vmware.vcloud.virtualHardwareSection+xml + VirtualHardwareSection_Type + http://schemas.dmtf.org/ovf/envelope/1/dsp8023_1.1.0.xsd + + + application/vnd.vmware.vcloud.operatingSystemSection+xml + OperatingSystemSection_Type + http://schemas.dmtf.org/ovf/envelope/1/dsp8023_1.1.0.xsd + + + application/vnd.vmware.vcloud.networkSection+xml + NetworkSection_Type + http://schemas.dmtf.org/ovf/envelope/1/dsp8023_1.1.0.xsd + + + application/vnd.vmware.vcloud.vAppNetwork+xml + VAppNetworkType + http://192.168.1.109/api/v1.5/schema/master.xsd + + + application/vnd.vmware.vcloud.network+xml + NetworkType + http://192.168.1.109/api/v1.5/schema/master.xsd + + + application/vnd.vmware.vcloud.orgNetwork+xml + OrgNetworkType + http://192.168.1.109/api/v1.5/schema/master.xsd + + + application/vnd.vmware.admin.vmwexternalnet+xml + VMWExternalNetworkType + http://192.168.1.109/api/v1.5/schema/vmwextensions.xsd + + + diff --git a/test-resources/rde_type.json b/test-resources/rde_type.json new file mode 100644 index 000000000..91775659e --- /dev/null +++ b/test-resources/rde_type.json @@ -0,0 +1,41 @@ +{ + "definitions": { + "foo": { + "description": "Foo definition", + "properties": { + "key": { + "description": "Key for foo", + "type": "string" + } + }, + "type": "object" + } + }, + "properties": { + "bar": { + "description": "Bar", + "type": "string" + }, + "foo": { + "$ref": "#/definitions/foo" + }, + "prop2": { + "properties": { + "subprop1": { + "type": "string" + }, + "subprop2": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + } + }, + "required": [ + "foo" + ], + "type": "object" +} diff --git a/test-resources/test_vapp_template_ovf/descriptor.ovf b/test-resources/test_vapp_template_ovf/descriptor.ovf index 58cb3471d..cb5b46ea9 100644 --- a/test-resources/test_vapp_template_ovf/descriptor.ovf +++ b/test-resources/test_vapp_template_ovf/descriptor.ovf @@ -1,171 +1,215 @@ - - - - - - - Virtual disk information - - - - The list of logical networks - - This is a special place-holder used for disconnected network interfaces. - - - - VApp template customization section - true - - - The configuration parameters for logical networks - - This is a special place-holder used for disconnected network interfaces. - - - - false - 196.254.254.254 - 255.255.0.0 - 196.254.254.254 - - - isolated - - false - - - - A collection of virtual machines - test_vapp_template - - VApp startup section - - - - A virtual machine - testvm1 - - Specifies the operating system installed - Microsoft Windows Server 2016 (64-bit) - - - Virtual hardware requirements - - Virtual Hardware Family - 0 - testvm1 - vmx-11 - - - 0 - false - none - E1000s ethernet adapter on "none" - Network adapter 0 - 1 - E1000E - 10 - - - - 0 - SCSI Controller - SCSI Controller 0 - 2 - lsilogicsas - 6 - - - 0 - Hard disk - Hard disk 1 - ovf:/disk/vmdisk-b4652207-7d14-4d4b-bf42-c6911d947d64-2000 - 2000 - 2 - 17 - 41943040 - byte - - - - 0 - IDE Controller - IDE Controller 0 - 3 - 5 - - - 0 - false - Floppy Drive - Floppy Drive 1 - - 8000 - 14 - - - hertz * 10^6 - Number of Virtual CPUs - 1 virtual CPU(s) - 4 - 0 - 3 - 1 - 1000 - 1 - - - byte * 2^20 - Memory Size - 4 MB of memory - 5 - 0 - 4 - 4 - 40 - - - 0 - false - CD/DVD Drive - CD/DVD Drive 1 - - 3000 - 3 - 15 - - - - - - - - - - - - - - - - - - - - - - Specifies Guest OS Customization Settings - false - true - b4652207-7d14-4d4b-bf42-c6911d947d64 - false - false - true - true - false - testvm1 - - - + + + + + + + Virtual disk information + + + + The list of logical networks + + The Production_DVS - Mgmt network + + + + VApp template customization section + true + + + The configuration parameters for logical networks + + The Production_DVS - Mgmt network + + + + false + 192.168.254.1 + 255.255.255.0 + 24 + true + + + 192.168.254.100 + 192.168.254.199 + + + + + isolated + false + false + false + + false + + + + A collection of virtual machines + yVM + + A human-readable annotation + Name: yVM (a very small virtual machine) +Release date: 11th November 2015 +For more information, please visit: cloudarchitectblog.wordpress.com + + + VApp startup section + + + + A virtual machine + yVM + + A human-readable annotation + Name: yVM (a very small virtual machine) +Release date: 11th November 2015 +For more information, please visit: cloudarchitectblog.wordpress.com + + + Specifies the operating system installed + Other Linux (32-bit) + + + Virtual hardware requirements + + Virtual Hardware Family + 0 + yVM + vmx-08 + + + 0 + true + Production_DVS - Mgmt + E1000 ethernet adapter on "Production_DVS - Mgmt" + Network adapter 0 + 1 + E1000 + 10 + + + + + + 0 + IDE Controller + IDE Controller 0 + 2 + 5 + + + 0 + Hard disk + Hard disk 1 + ovf:/disk/vmdisk-6d47850d-aa68-4f3b-acdf-6b36a9bff364-3000 + 3000 + 2 + 17 + 67108864 + byte + + + + 1 + IDE Controller + IDE Controller 1 + 3 + 5 + + + hertz * 10^6 + Number of Virtual CPUs + 1 virtual CPU(s) + 4 + 0 + 3 + 1 + 1000 + 1 + + + byte * 2^20 + Memory Size + 48 MB of memory + 5 + 0 + 4 + 48 + 480 + + + 0 + false + CD/DVD Drive + CD/DVD Drive 1 + + 3002 + 3 + 15 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Specifies Guest OS Customization Settings + true + false + 6d47850d-aa68-4f3b-acdf-6b36a9bff364 + false + false + true + true + false + yVM-001 + + + diff --git a/test-resources/test_vapp_template_ovf/vm-20b41fa7-3212-4faf-9295-045d0bc49783-disk-0.vmdk b/test-resources/test_vapp_template_ovf/vm-20b41fa7-3212-4faf-9295-045d0bc49783-disk-0.vmdk deleted file mode 100644 index 8681069c2..000000000 Binary files a/test-resources/test_vapp_template_ovf/vm-20b41fa7-3212-4faf-9295-045d0bc49783-disk-0.vmdk and /dev/null differ diff --git a/test-resources/test_vapp_template_ovf/yVMFromVcd-disk1.vmdk b/test-resources/test_vapp_template_ovf/yVMFromVcd-disk1.vmdk new file mode 100644 index 000000000..9642060d8 Binary files /dev/null and b/test-resources/test_vapp_template_ovf/yVMFromVcd-disk1.vmdk differ diff --git a/test-resources/udf_test.iso b/test-resources/udf_test.iso new file mode 100644 index 000000000..9944de37c Binary files /dev/null and b/test-resources/udf_test.iso differ diff --git a/test-resources/ui_plugin.zip b/test-resources/ui_plugin.zip new file mode 100644 index 000000000..c98a8eb64 Binary files /dev/null and b/test-resources/ui_plugin.zip differ diff --git a/types/v56/constants.go b/types/v56/constants.go index d1db0ab30..710f627cc 100644 --- a/types/v56/constants.go +++ b/types/v56/constants.go @@ -1,5 +1,5 @@ /* - * Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. */ package types @@ -101,6 +101,10 @@ const ( MimeVM = "application/vnd.vmware.vcloud.vm+xml" // Mime for instantiate vApp template params MimeInstantiateVappTemplateParams = "application/vnd.vmware.vcloud.instantiateVAppTemplateParams+xml" + // Mime for capture vApp into template + MimeCaptureVappTemplateParams = "application/vnd.vmware.vcloud.captureVAppParams+xml" + // Mime for clone vApp template params + MimeCloneVapp = "application/vnd.vmware.vcloud.cloneVAppParams+xml" // Mime for product section MimeProductSection = "application/vnd.vmware.vcloud.productSections+xml" // Mime for metadata @@ -131,6 +135,38 @@ const ( MimeCreateVmParams = "application/vnd.vmware.vcloud.CreateVmParams+xml" // Mime for instantiate VM Params from template MimeInstantiateVmTemplateParams = "application/vnd.vmware.vcloud.instantiateVmTemplateParams+xml" + // Mime for adding or removing VDC storage profiles + MimeUpdateVdcStorageProfiles = "application/vnd.vmware.admin.updateVdcStorageProfiles+xml" + // Mime to modify lease settings + MimeLeaseSettingSection = "application/vnd.vmware.vcloud.leaseSettingsSection+xml" + // Mime to publish external catalog + PublishExternalCatalog = "application/vnd.vmware.admin.publishExternalCatalogParams+xml" + // Mime to publish a catalog + PublishCatalog = "application/vnd.vmware.admin.publishCatalogParams+xml" + // Mime to subscribe to an external catalog + MimeSubscribeToExternalCatalog = "application/vnd.vmware.admin.externalCatalogSubscriptionParams+json" + // Mime to identify a media item + MimeMediaItem = "application/vnd.vmware.vcloud.media+xml" + // Mime to identify a provider VDC + MimeProviderVdc = "application/vnd.vmware.admin.vmwprovidervdc+xml" + // Mime to identify SAML metadata + MimeSamlMetadata = "application/samlmetadata+xml" + // Mime to identify organization federation settings (SAML) + MimeFederationSettingsXml = "application/vnd.vmware.admin.organizationFederationSettings+xml" + MimeFederationSettingsJson = "application/vnd.vmware.admin.organizationFederationSettings+json" + // Mime to identify organization OpenID Connect (OIDC) settings + MimeOAuthSettingsXml = "application/vnd.vmware.admin.organizationoauthsettings+xml" + // Mime to identify the OpenID Provider info + MimeOpenIdProviderInfoXml = "application/vnd.vmware.vcloud.admin.openIdProviderInfo+xml" + // Mime to handle virtual hardware versions + MimeVirtualHardwareVersion = "application/vnd.vmware.vcloud.virtualHardwareVersion+xml" + // Mime to handle org associations + MimeOrgAssociation = "application/vnd.vmware.admin.organizationAssociations+xml" + // Mime to handle site associations + MimeSiteAssociation = "application/vnd.vmware.admin.siteAssociation+xml" + // Mime to instantiate VDC Templates + MimeVdcTemplateInstantiate = "application/vnd.vmware.vcloud.instantiateVdcTemplateParams+xml" + MimeVdcTemplateInstantiateType = "application/vnd.vmware.vcloud.orgVdcTemplate+xml" ) const ( @@ -236,25 +272,42 @@ const ( const ( // The Qt* (Query Type) constants are the names used with Query requests to retrieve the corresponding entities - QtVappTemplate = "vAppTemplate" // vApp template - QtAdminVappTemplate = "adminVAppTemplate" // vApp template as admin - QtEdgeGateway = "edgeGateway" // edge gateway - QtOrgVdcNetwork = "orgVdcNetwork" // Org VDC network - QtCatalog = "catalog" // catalog - QtAdminCatalog = "adminCatalog" // catalog as admin - QtCatalogItem = "catalogItem" // catalog item - QtAdminCatalogItem = "adminCatalogItem" // catalog item as admin - QtAdminMedia = "adminMedia" // media item as admin - QtMedia = "media" // media item - QtVm = "vm" // Virtual machine - QtAdminVm = "adminVM" // Virtual machine as admin - QtVapp = "vApp" // vApp - QtAdminVapp = "adminVApp" // vApp as admin + QtVappTemplate = "vAppTemplate" // vApp template + QtAdminVappTemplate = "adminVAppTemplate" // vApp template as admin + QtEdgeGateway = "edgeGateway" // edge gateway + QtOrg = "organization" // Organization + QtOrgVdcNetwork = "orgVdcNetwork" // Org VDC network + QtCatalog = "catalog" // catalog + QtAdminCatalog = "adminCatalog" // catalog as admin + QtCatalogItem = "catalogItem" // catalog item + QtAdminCatalogItem = "adminCatalogItem" // catalog item as admin + QtAdminMedia = "adminMedia" // media item as admin + QtMedia = "media" // media item + QtVm = "vm" // Virtual machine + QtAdminVm = "adminVM" // Virtual machine as admin + QtVapp = "vApp" // vApp + QtAdminVapp = "adminVApp" // vApp as admin + QtOrgVdc = "orgVdc" // Org VDC + QtAdminOrgVdc = "adminOrgVdc" // Org VDC as admin + QtOrgVdcStorageProfile = "orgVdcStorageProfile" // StorageProfile of VDC + QtAdminOrgVdcStorageProfile = "adminOrgVdcStorageProfile" // StorageProfile of VDC as admin + QtTask = "task" // Task + QtAdminTask = "adminTask" // Task as admin + QtResourcePool = "resourcePool" // Resource Pool + QtNetworkPool = "networkPool" // Network Pool + QtProviderVdcStorageProfile = "providerVdcStorageProfile" // StorageProfile of Provider VDC + QtVappNetwork = "vAppNetwork" + QtAdminVappNetwork = "adminVAppNetwork" + QtSiteAssociation = "siteAssociation" + QtOrgAssociation = "orgAssociation" + QtAdminOrgVdcTemplate = "adminOrgVdcTemplate" + QtOrgVdcTemplate = "orgVdcTemplate" ) // AdminQueryTypes returns the corresponding "admin" query type for each regular type var AdminQueryTypes = map[string]string{ QtEdgeGateway: QtEdgeGateway, // EdgeGateway query type is the same for admin and regular users + QtOrg: QtOrg, // Organisation query is admin per default QtOrgVdcNetwork: QtOrgVdcNetwork, // Org VDC Network query type is the same for admin and regular users QtVappTemplate: QtAdminVappTemplate, QtCatalog: QtAdminCatalog, @@ -262,6 +315,7 @@ var AdminQueryTypes = map[string]string{ QtMedia: QtAdminMedia, QtVm: QtAdminVm, QtVapp: QtAdminVapp, + QtOrgVdc: QtAdminOrgVdc, } const ( @@ -327,21 +381,136 @@ const ( FiqlQueryTimestampFormat = "2006-01-02T15:04:05.000Z" ) -// These constants allow to construct OpenAPI endpoint paths and avoid strings in code for easy replacement in future. +// These constants allow constructing OpenAPI endpoint paths and avoid strings in code for easy replacement in the +// future. const ( - OpenApiPathVersion1_0_0 = "1.0.0/" - OpenApiEndpointRoles = "roles/" - OpenApiEndpointAuditTrail = "auditTrail/" - OpenApiEndpointImportableTier0Routers = "nsxTResources/importableTier0Routers" - OpenApiEndpointImportableSwitches = "/network/orgvdcnetworks/importableswitches" - OpenApiEndpointEdgeClusters = "nsxTResources/edgeClusters" - OpenApiEndpointExternalNetworks = "externalNetworks/" - OpenApiEndpointVdcComputePolicies = "vdcComputePolicies/" - OpenApiEndpointVdcAssignedComputePolicies = "vdcs/%s/computePolicies" - OpenApiEndpointVdcCapabilities = "vdcs/%s/capabilities" - OpenApiEndpointEdgeGateways = "edgeGateways/" - OpenApiEndpointOrgVdcNetworks = "orgVdcNetworks/" - OpenApiEndpointOrgVdcNetworksDhcp = "orgVdcNetworks/%s/dhcp" + OpenApiPathVersion1_0_0 = "1.0.0/" + OpenApiPathVersion2_0_0 = "2.0.0/" + OpenApiEndpointRoles = "roles/" + OpenApiEndpointGlobalRoles = "globalRoles/" + OpenApiEndpointRights = "rights/" + OpenApiEndpointRightsCategories = "rightsCategories/" + OpenApiEndpointRightsBundles = "rightsBundles/" + OpenApiEndpointAuditTrail = "auditTrail/" + OpenApiEndpointImportableTier0Routers = "nsxTResources/importableTier0Routers" + OpenApiEndpointImportableSwitches = "/network/orgvdcnetworks/importableswitches" + OpenApiEndpointImportableDvpgs = "virtualCenters/resources/importableDvpgs" + OpenApiEndpointEdgeClusters = "nsxTResources/edgeClusters" + OpenApiEndpointQosProfiles = "nsxTResources/gatewayQoSProfiles" + OpenApiEndpointExternalNetworks = "externalNetworks/" + OpenApiEndpointVdcComputePolicies = "vdcComputePolicies/" + OpenApiEndpointVdcAssignedComputePolicies = "vdcs/%s/computePolicies" + OpenApiEndpointVdcCapabilities = "vdcs/%s/capabilities" + OpenApiEndpointVdcNetworkProfile = "vdcs/%s/networkProfile" + OpenApiEndpointEdgeGateways = "edgeGateways/" + OpenApiEndpointEdgeGatewayQos = "edgeGateways/%s/qos" + OpenApiEndpointEdgeGatewayDhcpForwarder = "edgeGateways/%s/dhcpForwarder" + OpenApiEndpointEdgeGatewayDns = "edgeGateways/%s/dns" + OpenApiEndpointEdgeGatewaySlaacProfile = "edgeGateways/%s/slaacProfile" + OpenApiEndpointEdgeGatewayStaticRoutes = "edgeGateways/%s/routing/staticRoutes/" + OpenApiEndpointEdgeGatewayUsedIpAddresses = "edgeGateways/%s/usedIpAddresses" + OpenApiEndpointNsxtFirewallRules = "edgeGateways/%s/firewall/rules" + OpenApiEndpointEdgeGatewayL2VpnTunnel = "edgeGateways/%s/l2vpn/tunnels/" + OpenApiEndpointEdgeGatewayL2VpnTunnelStatistics = "edgeGateways/%s/l2vpn/tunnels/%s/metrics" + OpenApiEndpointEdgeGatewayL2VpnTunnelStatus = "edgeGateways/%s/l2vpn/tunnels/%s/status" + OpenApiEndpointFirewallGroups = "firewallGroups/" + OpenApiEndpointOrgVdcNetworks = "orgVdcNetworks/" + OpenApiEndpointOrgVdcNetworkSegmentProfiles = "orgVdcNetworks/%s/segmentProfiles" + OpenApiEndpointOrgVdcNetworksDhcp = "orgVdcNetworks/%s/dhcp" + OpenApiEndpointOrgVdcNetworksDhcpBindings = "orgVdcNetworks/%s/dhcp/bindings/" + OpenApiEndpointNsxtNatRules = "edgeGateways/%s/nat/rules/" + OpenApiEndpointAppPortProfiles = "applicationPortProfiles/" + OpenApiEndpointIpSecVpnTunnel = "edgeGateways/%s/ipsec/tunnels/" + OpenApiEndpointIpSecVpnTunnelConnectionProperties = "edgeGateways/%s/ipsec/tunnels/%s/connectionProperties" + OpenApiEndpointIpSecVpnTunnelStatus = "edgeGateways/%s/ipsec/tunnels/%s/status" + OpenApiEndpointSSLCertificateLibrary = "ssl/certificateLibrary/" + OpenApiEndpointSSLCertificateLibraryOld = "ssl/cetificateLibrary/" + OpenApiEndpointSessionCurrent = "sessions/current" + OpenApiEndpointVdcGroups = "vdcGroups/" + OpenApiEndpointVdcGroupsCandidateVdcs = "vdcGroups/networkingCandidateVdcs" + OpenApiEndpointVdcGroupsDfwPolicies = "vdcGroups/%s/dfwPolicies" + OpenApiEndpointVdcGroupsDfwDefaultPolicies = "vdcGroups/%s/dfwPolicies/default" + OpenApiEndpointVdcGroupsDfwRules = "vdcGroups/%s/dfwPolicies/%s/rules" + OpenApiEndpointLogicalVmGroups = "logicalVmGroups/" + OpenApiEndpointNetworkContextProfiles = "networkContextProfiles" + OpenApiEndpointSecurityTags = "securityTags" + OpenApiEndpointNsxtRouteAdvertisement = "edgeGateways/%s/routing/advertisement" + OpenApiEndpointTestConnection = "testConnection/" + OpenApiEndpointEdgeBgpNeighbor = "edgeGateways/%s/routing/bgp/neighbors/" // '%s' is NSX-T Edge Gateway ID + OpenApiEndpointEdgeBgpConfigPrefixLists = "edgeGateways/%s/routing/bgp/prefixLists/" // '%s' is NSX-T Edge Gateway ID + OpenApiEndpointEdgeBgpConfig = "edgeGateways/%s/routing/bgp" // '%s' is NSX-T Edge Gateway ID + OpenApiEndpointRdeInterfaces = "interfaces/" + OpenApiEndpointRdeInterfaceBehaviors = "interfaces/%s/behaviors/" + OpenApiEndpointRdeEntityTypes = "entityTypes/" + OpenApiEndpointRdeTypeBehaviors = "entityTypes/%s/behaviors/" + OpenApiEndpointRdeTypeBehaviorAccessControls = "entityTypes/%s/behaviorAccessControls" + OpenApiEndpointRdeEntities = "entities/" + OpenApiEndpointRdeEntityAccessControls = "entities/%s/accessControls/" + OpenApiEndpointRdeEntitiesTypes = "entities/types/" + OpenApiEndpointRdeEntitiesResolve = "entities/%s/resolve" + OpenApiEndpointRdeEntitiesBehaviorsInvocations = "entities/%s/behaviors/%s/invocations" + OpenApiEndpointVirtualCenters = "virtualCenters" + OpenApiEndpointResourcePools = "virtualCenters/%s/resourcePools/browse" // '%s' is vCenter ID + OpenApiEndpointResourcePoolsBrowseAll = "virtualCenters/%s/resourcePools/browseAll" // '%s' is vCenter ID + OpenApiEndpointResourcePoolHardware = "virtualCenters/%s/resourcePools/%s/hwv" // first '%s' is vCenter ID. Second one is Resource Pool MoRef + OpenApiEndpointNetworkPools = "networkPools/" + OpenApiEndpointNetworkPoolSummaries = "networkPools/networkPoolSummaries" + OpenApiEndpointStorageProfiles = "virtualCenters/%s/storageProfiles" // '%s' is vCenter ID + OpenApiEndpointExtensionsUi = "extensions/ui/" + OpenApiEndpointExtensionsUiPlugin = "extensions/ui/%s/plugin" + OpenApiEndpointExtensionsUiTenants = "extensions/ui/%s/tenants" + OpenApiEndpointExtensionsUiTenantsPublishAll = "extensions/ui/%s/tenants/publishAll" + OpenApiEndpointExtensionsUiTenantsPublish = "extensions/ui/%s/tenants/publish" + OpenApiEndpointExtensionsUiTenantsUnpublishAll = "extensions/ui/%s/tenants/unpublishAll" + OpenApiEndpointExtensionsUiTenantsUnpublish = "extensions/ui/%s/tenants/unpublish" + OpenApiEndpointImportableTransportZones = "nsxTResources/importableTransportZones" + OpenApiEndpointVCenterDistributedSwitch = "virtualCenters/resources/dvSwitches" + + OpenApiEndpointNsxtSegmentProfileTemplates = "segmentProfileTemplates/" + OpenApiEndpointNsxtGlobalDefaultSegmentProfileTemplates = "segmentProfileTemplates/default" + OpenApiEndpointNsxtSegmentIpDiscoveryProfiles = "nsxTResources/segmentIpDiscoveryProfiles" + OpenApiEndpointNsxtSegmentMacDiscoveryProfiles = "nsxTResources/segmentMacDiscoveryProfiles" + OpenApiEndpointNsxtSegmentSpoofGuardProfiles = "nsxTResources/segmentSpoofGuardProfiles" + OpenApiEndpointNsxtSegmentQosProfiles = "nsxTResources/segmentQoSProfiles" + OpenApiEndpointNsxtSegmentSecurityProfiles = "nsxTResources/segmentSecurityProfiles" + + // IP Spaces + OpenApiEndpointIpSpaces = "ipSpaces/" + OpenApiEndpointIpSpaceSummaries = "ipSpaces/summaries" + OpenApiEndpointIpSpaceUplinks = "ipSpaceUplinks/" + OpenApiEndpointIpSpaceUplinksAllocate = "ipSpaces/%s/allocate" // '%s' is IP Space ID + OpenApiEndpointIpSpaceIpAllocations = "ipSpaces/%s/allocations/" // '%s' is IP Space ID + OpenApiEndpointIpSpaceOrgAssignments = "ipSpaces/orgAssignments/" // '%s' is IP Space ID + OpenApiEndpointIpSpaceFloatingIpSuggestions = "ipSpaces/floatingIpSuggestions/" + + // NSX-T ALB related endpoints + + OpenApiEndpointAlbController = "loadBalancer/controllers/" + + // OpenApiEndpointAlbImportableClouds endpoint requires a filter _context==urn:vcloud:loadBalancerController:aa23ef66-ba32-48b2-892f-7acdffe4587e + OpenApiEndpointAlbImportableClouds = "nsxAlbResources/importableClouds/" + OpenApiEndpointAlbImportableServiceEngineGroups = "nsxAlbResources/importableServiceEngineGroups" + OpenApiEndpointAlbCloud = "loadBalancer/clouds/" + OpenApiEndpointAlbServiceEngineGroups = "loadBalancer/serviceEngineGroups/" + OpenApiEndpointAlbPools = "loadBalancer/pools/" + // OpenApiEndpointAlbPoolSummaries returns a limited subset of data provided by OpenApiEndpointAlbPools + // however only the summary endpoint can list all available pools for an edge gateway + OpenApiEndpointAlbPoolSummaries = "edgeGateways/%s/loadBalancer/poolSummaries" // %s contains edge gateway + OpenApiEndpointAlbVirtualServices = "loadBalancer/virtualServices/" + OpenApiEndpointAlbVirtualServiceSummaries = "edgeGateways/%s/loadBalancer/virtualServiceSummaries" // %s contains edge gateway + OpenApiEndpointAlbServiceEngineGroupAssignments = "loadBalancer/serviceEngineGroups/assignments/" + OpenApiEndpointAlbEdgeGateway = "edgeGateways/%s/loadBalancer" + + // OpenApiEndpointServiceAccountGrant is needed for granting a Service Account + OpenApiEndpointServiceAccountGrant = "deviceLookup/grant" + OpenApiEndpointTokens = "tokens/" + OpenApiEndpointServiceAccounts = "serviceAccounts/" + + // OpenApiEndpointVgpuProfile is used to query vGPU profiles + OpenApiEndpointVgpuProfile = "vgpuProfiles" + + // OpenAPI Org + OpenApiEndpointOrgs = "orgs/" ) // Header keys to run operations in tenant context @@ -357,6 +526,8 @@ const ( ExternalNetworkBackingTypeNsxtTier0Router = "NSXT_TIER0" // ExternalNetworkBackingTypeNsxtVrfTier0Router defines backing type of NSX-T Tier-0 VRF router ExternalNetworkBackingTypeNsxtVrfTier0Router = "NSXT_VRF_TIER0" + // ExternalNetworkBackingTypeNsxtSegment defines backing type of NSX-T Segment (supported in VCD 10.3+) + ExternalNetworkBackingTypeNsxtSegment = "IMPORTED_T_LOGICAL_SWITCH" // ExternalNetworkBackingTypeNetwork defines vSwitch portgroup ExternalNetworkBackingTypeNetwork = "NETWORK" // ExternalNetworkBackingDvPortgroup refers distributed switch portgroup @@ -366,12 +537,21 @@ const ( const ( // OrgVdcNetworkTypeRouted can be used to create NSX-T or NSX-V routed Org Vdc network OrgVdcNetworkTypeRouted = "NAT_ROUTED" - // OrgVdcNetworkTypeIsolated can be used to creaate NSX-T or NSX-V isolated Org Vdc network + // 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 ( @@ -380,3 +560,208 @@ const ( // VdcCapabilityNetworkProviderNsxt is a convenience constant to match VDC capability VdcCapabilityNetworkProviderNsxt = "NSX_T" ) + +const ( + // FirewallGroupTypeSecurityGroup can be used in types.NsxtFirewallGroup for 'TypeValue' field + // to create Security Group + FirewallGroupTypeSecurityGroup = "SECURITY_GROUP" + // FirewallGroupTypeIpSet can be used in types.NsxtFirewallGroup for 'TypeValue' field to create + // IP Set + FirewallGroupTypeIpSet = "IP_SET" + + // FirewallGroupTypeVmCriteria can be used in types.NsxtFirewallGroup for 'TypeValue' field to + // create Dynamic Security Group (VCD 10.3+) + FirewallGroupTypeVmCriteria = "VM_CRITERIA" +) + +// These constants can be used to pick type of NSX-T NAT Rule +const ( + NsxtNatRuleTypeDnat = "DNAT" + NsxtNatRuleTypeNoDnat = "NO_DNAT" + NsxtNatRuleTypeSnat = "SNAT" + NsxtNatRuleTypeNoSnat = "NO_SNAT" + NsxtNatRuleTypeReflexive = "REFLEXIVE" // Only in VCD 10.3+ (API V36.0) +) + +// In VCD versions 10.2.2+ (API V35.2+) there is a FirewallMatch field in NAT rule with these +// options +const ( + // NsxtNatRuleFirewallMatchInternalAddress will match firewall rules based on NAT rules internal + // address (DEFAULT) + NsxtNatRuleFirewallMatchInternalAddress = "MATCH_INTERNAL_ADDRESS" + // NsxtNatRuleFirewallMatchExternalAddress will match firewall rules based on NAT rule external + // address + NsxtNatRuleFirewallMatchExternalAddress = "MATCH_EXTERNAL_ADDRESS" + // NsxtNatRuleFirewallMatchBypass will skip evaluating NAT rules in firewall + NsxtNatRuleFirewallMatchBypass = "BYPASS" +) + +const ( + // ApplicationPortProfileScopeSystem is a defined scope which allows user to only read (no write capability) system + // predefined Application Port Profiles + ApplicationPortProfileScopeSystem = "SYSTEM" + // ApplicationPortProfileScopeProvider allows user to read and set Application Port Profiles at provider level. In + // reality Network Provider (NSX-T Manager) must be specified while creating. + ApplicationPortProfileScopeProvider = "PROVIDER" + // ApplicationPortProfileScopeTenant allows user to read and set Application Port Profiles at Org VDC level. + ApplicationPortProfileScopeTenant = "TENANT" +) + +const ( + // VcloudUndefinedKey is the bundles key automatically added to new role related objects + VcloudUndefinedKey = "com.vmware.vcloud.undefined.key" +) + +const ( + // NsxtAlbCloudBackingTypeNsxtAlb is a backing type for NSX-T ALB used in types.NsxtAlbCloudBacking + NsxtAlbCloudBackingTypeNsxtAlb = "NSXALB_NSXT" +) + +const ( + // UrnTypeVdcGroup is the third segment of URN for VDC Group + UrnTypeVdcGroup = "vdcGroup" + // UrnTypeVdc is the third segment of URN for VDC + UrnTypeVdc = "vdc" +) + +// Metadata type constants +const ( + MetadataStringValue string = "MetadataStringValue" + MetadataNumberValue string = "MetadataNumberValue" + MetadataDateTimeValue string = "MetadataDateTimeValue" + MetadataBooleanValue string = "MetadataBooleanValue" + + MetadataReadOnlyVisibility string = "READONLY" + MetadataHiddenVisibility string = "PRIVATE" + MetadataReadWriteVisibility string = "READWRITE" + + OpenApiMetadataStringEntry string = "StringEntry" + OpenApiMetadataNumberEntry string = "NumberEntry" + OpenApiMetadataBooleanEntry string = "BoolEntry" +) + +const ( + // DistributedFirewallPolicyDefault is a constant for "default" Distributed Firewall Policy + DistributedFirewallPolicyDefault = "default" +) + +// NSX-V distributed firewall + +// Protocols +const ( + DFWProtocolTcp = "TCP" + DFWProtocolUdp = "UDP" + DFWProtocolIcmp = "ICMP" +) + +// Action types +const ( + DFWActionAllow = "allow" + DFWActionDeny = "deny" +) + +// Directions +const ( + DFWDirectionIn = "in" + DFWDirectionOut = "out" + DFWDirectionInout = "inout" +) + +// Types of packet +const ( + DFWPacketAny = "any" + DFWPacketIpv4 = "ipv4" + DFWPacketIpv6 = "ipv6" +) + +// Elements of Source, Destination, and Applies-To +const ( + DFWElementVdc = "VDC" + DFWElementVirtualMachine = "VirtualMachine" + DFWElementNetwork = "Network" + DFWElementEdge = "Edge" + DFWElementIpSet = "IPSet" + DFWElementIpv4 = "Ipv4Address" +) + +// Types of service +const ( + DFWServiceTypeApplication = "Application" + DFWServiceTypeApplicationGroup = "ApplicationGroup" +) + +var NsxvProtocolCodes = map[string]int{ + DFWProtocolTcp: 6, + DFWProtocolUdp: 17, + DFWProtocolIcmp: 1, +} + +// NSX-T DHCP Binding Type +const ( + NsxtDhcpBindingTypeIpv4 = "IPV4" + NsxtDhcpBindingTypeIpv6 = "IPV6" +) + +// NSX-T IPSec VPN authentication modes +const ( + NsxtIpSecVpnAuthenticationModePSK = "PSK" + NsxtIpSecVpnAuthenticationModeCertificate = "CERTIFICATE" +) + +// Org VDC network backing types +const ( + OpenApiOrgVdcNetworkBackingTypeNsxv = "VIRTUAL_WIRE" + OpenApiOrgVdcNetworkBackingTypeNsxt = "NSXT_FLEXIBLE_SEGMENT" +) + +// IP Space types +const ( + IpSpaceShared = "SHARED_SERVICES" + IpSpacePublic = "PUBLIC" + IpSpacePrivate = "PRIVATE" +) + +// IP Space IP Allocation Reservation Types +const ( + IpSpaceIpAllocationUsedManual = "USED_MANUAL" + IpSpaceIpAllocationUsed = "USED" + IpSpaceIpAllocationUnused = "UNUSED" +) + +// IP Space IP Allocation Types +const ( + IpSpaceIpAllocationTypeFloatingIp = "FLOATING_IP" + IpSpaceIpAllocationTypeIpPrefix = "IP_PREFIX" +) + +// Values used for SAML metadata normalization and validation +const ( + SamlNamespaceMd = "urn:oasis:names:tc:SAML:2.0:metadata" + SamlNamespaceDs = "http://www.w3.org/2000/09/xmldsig#" + SamlNamespaceHoksso = "urn:oasis:names:tc:SAML:2.0:profiles:holder-of-key:SSO:browser" +) + +// Values used to identify the type of network pool +const ( + NetworkPoolVxlanType = "VXLAN" // NSX-V backed network pool. Only used as read-only + NetworkPoolVlanType = "VLAN" + NetworkPoolGeneveType = "GENEVE" + NetworkPoolPortGroupType = "PORTGROUP_BACKED" +) + +// BackingUseConstraint is a constraint about the use of a backing in a network pool +type BackingUseConstraint string + +const ( + BackingUseExplicit BackingUseConstraint = "use-explicit-name" // use explicitly named backing + BackingUseWhenOnlyOne BackingUseConstraint = "use-when-only-one" // use automatically when only one was found + BackingUseFirstAvailable BackingUseConstraint = "use-first-available" // use the first available backing with no conditions +) + +// Values used to create a VDC Template +const ( + VdcTemplateFlexType = "VMWFlexVdcTemplateSpecificationType" + VdcTemplatePayAsYouGoType = "VMWAllocationVappVdcTemplateSpecificationType" + VdcTemplateAllocationPoolType = "VMWAllocationPoolVdcTemplateSpecificationType" + VdcTemplateReservationPoolType = "VMWReservationPoolVdcTemplateSpecificationType" +) diff --git a/types/v56/cse.go b/types/v56/cse.go new file mode 100644 index 000000000..31e82b16f --- /dev/null +++ b/types/v56/cse.go @@ -0,0 +1,225 @@ +package types + +import "time" + +// Capvcd (Cluster API Provider for VCD), is a type that represents a Kubernetes cluster inside VCD, that is created and managed +// with the Container Service Extension (CSE) +type Capvcd struct { + Kind string `json:"kind,omitempty"` + Spec struct { + VcdKe struct { + // NOTE: "Secure" struct needs to be a pointer to avoid overriding with empty values by mistake, as VCD doesn't return RDE fields + // marked with "x-vcloud-restricted: secure" + Secure *struct { + ApiToken string `json:"apiToken,omitempty"` + } `json:"secure,omitempty"` + IsVCDKECluster bool `json:"isVCDKECluster,omitempty"` + AutoRepairOnErrors bool `json:"autoRepairOnErrors,omitempty"` + DefaultStorageClassOptions struct { + Filesystem string `json:"filesystem,omitempty"` + K8SStorageClassName string `json:"k8sStorageClassName,omitempty"` + VcdStorageProfileName string `json:"vcdStorageProfileName,omitempty"` + UseDeleteReclaimPolicy bool `json:"useDeleteReclaimPolicy,omitempty"` + } `json:"defaultStorageClassOptions,omitempty"` + } `json:"vcdKe,omitempty"` + CapiYaml string `json:"capiYaml,omitempty"` + } `json:"spec,omitempty"` + Status struct { + Cpi struct { + Name string `json:"name,omitempty"` + Version string `json:"version,omitempty"` + EventSet []struct { + Name string `json:"name,omitempty"` + OccurredAt time.Time `json:"occurredAt,omitempty"` + VcdResourceId string `json:"vcdResourceId,omitempty"` + VcdResourceName string `json:"vcdResourceName,omitempty"` + AdditionalDetails struct { + DetailedEvent string `json:"Detailed Event,omitempty"` + } `json:"additionalDetails,omitempty"` + } `json:"eventSet,omitempty"` + ErrorSet []struct { + Name string `json:"name,omitempty"` + OccurredAt time.Time `json:"occurredAt,omitempty"` + VcdResourceId string `json:"vcdResourceId,omitempty"` + VcdResourceName string `json:"vcdResourceName,omitempty"` + AdditionalDetails struct { + DetailedError string `json:"Detailed Error,omitempty"` + } `json:"additionalDetails,omitempty"` + } `json:"errorSet,omitempty"` + } `json:"cpi,omitempty"` + Csi struct { + Name string `json:"name,omitempty"` + Version string `json:"version,omitempty"` + EventSet []struct { + Name string `json:"name,omitempty"` + OccurredAt time.Time `json:"occurredAt,omitempty"` + VcdResourceId string `json:"vcdResourceId,omitempty"` + VcdResourceName string `json:"vcdResourceName,omitempty"` + AdditionalDetails struct { + DetailedDescription string `json:"Detailed Description,omitempty"` + } `json:"additionalDetails,omitempty"` + } `json:"eventSet,omitempty"` + ErrorSet []struct { + Name string `json:"name,omitempty"` + OccurredAt time.Time `json:"occurredAt,omitempty"` + VcdResourceId string `json:"vcdResourceId,omitempty"` + VcdResourceName string `json:"vcdResourceName,omitempty"` + AdditionalDetails struct { + DetailedError string `json:"Detailed Error,omitempty"` + } `json:"additionalDetails,omitempty"` + } `json:"errorSet,omitempty"` + } `json:"csi,omitempty"` + VcdKe struct { + State string `json:"state,omitempty"` + EventSet []struct { + Name string `json:"name,omitempty"` + OccurredAt time.Time `json:"occurredAt,omitempty"` + VcdResourceId string `json:"vcdResourceId,omitempty"` + VcdResourceName string `json:"vcdResourceName,omitempty"` + AdditionalDetails struct { + DetailedEvent string `json:"Detailed Event,omitempty"` + } `json:"additionalDetails,omitempty"` + } `json:"eventSet,omitempty"` + ErrorSet []struct { + Name string `json:"name,omitempty"` + OccurredAt time.Time `json:"occurredAt,omitempty"` + VcdResourceId string `json:"vcdResourceId,omitempty"` + VcdResourceName string `json:"vcdResourceName,omitempty"` + AdditionalDetails struct { + DetailedError string `json:"Detailed Error,omitempty"` + } `json:"additionalDetails,omitempty"` + } `json:"errorSet,omitempty"` + WorkerId string `json:"workerId,omitempty"` + VcdKeVersion string `json:"vcdKeVersion,omitempty"` + VcdResourceSet []struct { + Id string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` + } `json:"vcdResourceSet,omitempty"` + HeartbeatString string `json:"heartbeatString,omitempty"` + VcdKeInstanceId string `json:"vcdKeInstanceId,omitempty"` + HeartbeatTimestamp string `json:"heartbeatTimestamp,omitempty"` + DefaultStorageClass struct { + FileSystem string `json:"fileSystem,omitempty"` + K8SStorageClassName string `json:"k8sStorageClassName,omitempty"` + VcdStorageProfileName string `json:"vcdStorageProfileName,omitempty"` + UseDeleteReclaimPolicy bool `json:"useDeleteReclaimPolicy,omitempty"` + } `json:"defaultStorageClass,omitempty"` + } `json:"vcdKe,omitempty"` + Capvcd struct { + Uid string `json:"uid,omitempty"` + Phase string `json:"phase,omitempty"` + // NOTE: "Private" struct needs to be a pointer to avoid overriding with empty values by mistake, as VCD doesn't return RDE fields + // marked with "x-vcloud-restricted: secure" + Private *struct { + KubeConfig string `json:"kubeConfig,omitempty"` + } `json:"private,omitempty"` + Upgrade struct { + Ready bool `json:"ready,omitempty"` + Current struct { + TkgVersion string `json:"tkgVersion,omitempty"` + KubernetesVersion string `json:"kubernetesVersion,omitempty"` + } `json:"current,omitempty"` + } `json:"upgrade,omitempty"` + EventSet []struct { + Name string `json:"name,omitempty"` + OccurredAt time.Time `json:"occurredAt,omitempty"` + VcdResourceId string `json:"vcdResourceId,omitempty"` + VcdResourceName string `json:"vcdResourceName,omitempty"` + } `json:"eventSet,omitempty"` + ErrorSet []struct { + Name string `json:"name,omitempty"` + OccurredAt time.Time `json:"occurredAt,omitempty"` + VcdResourceId string `json:"vcdResourceId,omitempty"` + VcdResourceName string `json:"vcdResourceName,omitempty"` + AdditionalDetails struct { + DetailedError string `json:"Detailed Error,omitempty"` + } `json:"additionalDetails,omitempty"` + } `json:"errorSet,omitempty"` + NodePool []struct { + Name string `json:"name,omitempty"` + DiskSizeMb int `json:"diskSizeMb,omitempty"` + SizingPolicy string `json:"sizingPolicy,omitempty"` + StorageProfile string `json:"storageProfile,omitempty"` + DesiredReplicas int `json:"desiredReplicas,omitempty"` + AvailableReplicas int `json:"availableReplicas,omitempty"` + } `json:"nodePool,omitempty"` + ParentUid string `json:"parentUid,omitempty"` + K8SNetwork struct { + Pods struct { + CidrBlocks []string `json:"cidrBlocks,omitempty"` + } `json:"pods,omitempty"` + Services struct { + CidrBlocks []string `json:"cidrBlocks,omitempty"` + } `json:"services,omitempty"` + } `json:"k8sNetwork,omitempty"` + Kubernetes string `json:"kubernetes,omitempty"` + CapvcdVersion string `json:"capvcdVersion,omitempty"` + VcdProperties struct { + Site string `json:"site,omitempty"` + OrgVdcs []struct { + Id string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + OvdcNetworkName string `json:"ovdcNetworkName,omitempty"` + } `json:"orgVdcs,omitempty"` + Organizations []struct { + Id string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + } `json:"organizations,omitempty"` + } `json:"vcdProperties,omitempty"` + CapiStatusYaml string `json:"capiStatusYaml,omitempty"` + VcdResourceSet []struct { + Id string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` + AdditionalDetails struct { + VirtualIP string `json:"virtualIP,omitempty"` + } `json:"additionalDetails,omitempty"` + } `json:"vcdResourceSet,omitempty"` + ClusterApiStatus struct { + Phase string `json:"phase,omitempty"` + ApiEndpoints []struct { + Host string `json:"host,omitempty"` + Port int `json:"port,omitempty"` + } `json:"apiEndpoints,omitempty"` + } `json:"clusterApiStatus,omitempty"` + CreatedByVersion string `json:"createdByVersion,omitempty"` + ClusterResourceSetBindings []struct { + Kind string `json:"kind,omitempty"` + Name string `json:"name,omitempty"` + Applied bool `json:"applied,omitempty"` + LastAppliedTime string `json:"lastAppliedTime,omitempty"` + ClusterResourceSetName string `json:"clusterResourceSetName,omitempty"` + } `json:"clusterResourceSetBindings,omitempty"` + } `json:"capvcd,omitempty"` + Projector struct { + Name string `json:"name,omitempty"` + Version string `json:"version,omitempty"` + EventSet []struct { + Name string `json:"name,omitempty"` + OccurredAt time.Time `json:"occurredAt,omitempty"` + VcdResourceId string `json:"vcdResourceId,omitempty"` + VcdResourceName string `json:"vcdResourceName,omitempty"` + AdditionalDetails struct { + Event string `json:"event,omitempty"` + } `json:"additionalDetails,omitempty"` + } `json:"eventSet,omitempty"` + ErrorSet []struct { + Name string `json:"name,omitempty"` + OccurredAt time.Time `json:"occurredAt,omitempty"` + VcdResourceId string `json:"vcdResourceId,omitempty"` + VcdResourceName string `json:"vcdResourceName,omitempty"` + AdditionalDetails struct { + DetailedError string `json:"Detailed Error,omitempty"` + } `json:"additionalDetails,omitempty"` + } `json:"errorSet,omitempty"` + } `json:"projector,omitempty"` + } `json:"status,omitempty"` + Metadata struct { + Name string `json:"name,omitempty"` + Site string `json:"site,omitempty"` + OrgName string `json:"orgName,omitempty"` + VirtualDataCenterName string `json:"virtualDataCenterName,omitempty"` + } `json:"metadata,omitempty"` + ApiVersion string `json:"apiVersion,omitempty"` +} diff --git a/types/v56/dse.go b/types/v56/dse.go new file mode 100644 index 000000000..d6e52ae53 --- /dev/null +++ b/types/v56/dse.go @@ -0,0 +1,116 @@ +package types + +// DataSolution represents RDE Entity for Data Solution in Data Solution Extension (DSE) +type DataSolution struct { + Kind string `json:"kind"` + Spec DseConfigSpec `json:"spec"` + Metadata DseConfigMetadata `json:"metadata"` + APIVersion string `json:"apiVersion"` +} + +type DseConfigMetadata struct { + Name string `json:"name"` +} + +type DseConfigSpec struct { + Artifacts []DseArtifactMap `json:"artifacts"` + Description string `json:"description"` + DockerConfig *DseDockerConfig `json:"dockerConfig,omitempty"` + SolutionType string `json:"solutionType"` +} + +type DseArtifactMap map[string]interface{} + +// DseDockerConfig provides registry auth configuration that is available for "VCD Data Solutions" +// Data Solution +type DseDockerConfig struct { + Auths DseDockerAuths `json:"auths"` +} + +type DseDockerAuths map[string]DseDockerAuth + +type DseDockerAuth struct { + Username string `json:"username"` + Password string `json:"password"` + Description string `json:"description"` +} + +// DataSolutionOrgConfig represents RDE Entity structure for Data Solution Org Configuration +type DataSolutionOrgConfig struct { + APIVersion string `json:"apiVersion"` + Kind string `json:"kind"` + Metadata map[string]interface{} `json:"metadata"` + Spec map[string]interface{} `json:"spec"` +} + +// DataSolutionInstanceTemplate represents a read-only structure for Data Solution Instance +// Templates in Data Storage Extension (DSE) +type DataSolutionInstanceTemplate struct { + Kind string `json:"kind"` + Spec struct { + Data struct { + CPU string `json:"cpu"` + Name string `json:"name"` + Memory string `json:"memory"` + Storage string `json:"storage"` + Namespace string `json:"namespace"` + PvcPolicy string `json:"pvcPolicy"` + HighAvailability bool `json:"highAvailability"` + } `json:"data"` + BuiltIn bool `json:"builtIn"` + Content string `json:"content"` + Version string `json:"version"` + Featured bool `json:"featured"` + DataSchema struct { + Type string `json:"type"` + Defs struct { + Quantity struct { + Type string `json:"type"` + Pattern string `json:"pattern"` + } `json:"quantity"` + LimitedQuantity struct { + Type string `json:"type"` + Pattern string `json:"pattern"` + } `json:"limitedQuantity"` + } `json:"$defs"` + Schema string `json:"$schema"` + Required []string `json:"required"` + Properties struct { + CPU DataSolutionInstanceTemplateComputeProperty `json:"cpu"` + Memory DataSolutionInstanceTemplateComputeProperty `json:"memory"` + Storage DataSolutionInstanceTemplateComputeProperty `json:"storage"` + Namespace struct { + Const string `json:"const"` + } `json:"namespace"` + PvcPolicy struct { + Enum []string `json:"enum"` + Type string `json:"type"` + Title string `json:"title"` + Default string `json:"default"` + Description string `json:"description"` + } `json:"pvcPolicy"` + HighAvailability struct { + Type string `json:"type"` + Title string `json:"title"` + Default bool `json:"default"` + Description string `json:"description"` + } `json:"highAvailability"` + } `json:"properties"` + } `json:"dataSchema"` + ContentType string `json:"contentType"` + Description string `json:"description"` + SolutionType string `json:"solutionType"` + TemplateEngine string `json:"templateEngine"` + } `json:"spec"` + Metadata struct { + Name string `json:"name"` + } `json:"metadata"` + APIVersion string `json:"apiVersion"` +} + +type DataSolutionInstanceTemplateComputeProperty struct { + Ref string `json:"$ref"` + Title string `json:"title"` + Default string `json:"default"` + Description string `json:"description"` +} diff --git a/types/v56/ip_space.go b/types/v56/ip_space.go new file mode 100644 index 000000000..81090c935 --- /dev/null +++ b/types/v56/ip_space.go @@ -0,0 +1,373 @@ +/* + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package types + +// IpSpace provides structured approach to allocating public and private IP addresses by preventing +// the use of overlapping IP addresses across organizations and organization VDCs. +// +// An IP space consists of a set of defined non-overlapping IP ranges and small CIDR blocks that are +// reserved and used during the consumption aspect of the IP space life cycle. An IP space can be +// either IPv4 or IPv6, but not both. +// +// Every IP space has an internal scope and an external scope. The internal scope of an IP space is +// a list of CIDR notations that defines the exact span of IP addresses in which all ranges and +// blocks must be contained in. The external scope defines the total span of IP addresses to which +// the IP space has access, for example the internet or a WAN. +type IpSpace struct { + ID string `json:"id,omitempty"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + + // Type is The type of the IP Space. Possible values are: + // * PUBLIC - These can be consumed by multiple organizations. These are created by System + // Administrators only, for managing public IPs. The IP addresses and IP Prefixes from this IP + // space are allocated to specific organizations for consumption. + // * PRIVATE - These can be consumed by only a single organization. All the IPs within this IP + // Space are allocated to the particular organization. + // * SHARED_SERVICES - These are for internal use only. The IP addresses and IP Prefixes from + // this IP space can be consumed by multiple organizations but those IP addresses and IP + // Prefixes will not be not visible to the individual users within the organization. These are + // created by System Administrators only, typically for a service or for management networks. + // + // Note. This project contains convenience constants for defining IP Space + // types`types.IpSpaceShared`, `types.IpSpacePublic`, `types.IpSpacePrivate` + // + // Only SHARED_SERVICES type can be changed to PUBLIC type. No other type changes are allowed. + Type string `json:"type"` + + // The organization this IP Space belongs to. This property is only applicable and is required + // for IP Spaces with type PRIVATE. + OrgRef *OpenApiReference `json:"orgRef,omitempty"` + + // Utilization summary for this IP space. + Utilization IpSpaceUtilization `json:"utilization,omitempty"` + + // List of IP Prefixes. + IPSpacePrefixes []IPSpacePrefixes `json:"ipSpacePrefixes"` + + // List of IP Ranges. These are logically treated as a single block of IPs for allocation purpose. + IPSpaceRanges IPSpaceRanges `json:"ipSpaceRanges"` + + // This defines the exact span of IP addresses in a CIDR format within which all IP Ranges and + // IP Prefixes of this IP Space must be contained. This typically defines the span of IP + // addresses used within this Data Center. + IPSpaceInternalScope []string `json:"ipSpaceInternalScope"` + + // This defines the total span of IP addresses in a CIDR format within which all IP Ranges and + // IP Prefixes of this IP Space must be contained. This is used by the system for creation of + // NAT rules and BGP prefixes. This typically defines the span of IP addresses outside the + // bounds of this Data Center. For the internet this may be 0.0.0.0/0. For a WAN, this could be + // 10.0.0.0/8. + IPSpaceExternalScope string `json:"ipSpaceExternalScope,omitempty"` + + // Whether the route advertisement is enabled for this IP Space or not. If true, the routed Org + // VDC networks which are configured from this IP Space will be advertised from the connected + // Edge Gateway to the Provider Gateway. Route advertisement must be enabled on a particular + // network for it to be advertised. Networks from the PRIVATE IP Spaces will only be advertised + // if the associated Provider Gateway is owned by the Organization. + RouteAdvertisementEnabled bool `json:"routeAdvertisementEnabled"` + + // DefaultGatewayServiceConfig specifies default gateway services configurations such as NAT and + // Firewall rules that a user can apply on either the Provider Gateway or Edge Gateway depending + // on the network topology. Note that re-applying the default services on the Provider Gateway + // or Edge Gateway may delete/update/create services that are managed/created by VCD. + // + // Requires VCD 10.5.0+ (API v38.0+) + DefaultGatewayServiceConfig *IpSpaceDefaultGatewayServiceConfig `json:"defaultGatewayServiceConfig,omitempty"` + + // Status is one of `PENDING`, `CONFIGURING`, `REALIZED`, `REALIZATION_FAILED`, `UNKNOWN` + Status string `json:"status,omitempty"` +} + +// IpSpaceDefaultGatewayServiceConfig specified the default gateway services configurations such as NAT and Firewall rules +// that a user can apply on either the Provider Gateway or Edge Gateway depending on the network +// topology. Below is an example of the ordering of NAT rule: +// * If IP Space's external scope maps to any network such as "0.0.0.0/0", the NO SNAT rules +// priority is 1001 and the default SNAT rules will have priority 1000 +// * All other default SNAT rules has priority 100 +// * All other default NO SNAT rules has priority 0 +// * User-created NAT rules has default priority 50 +// +// Requires VCD 10.5.0+ (API v38.0+) +type IpSpaceDefaultGatewayServiceConfig struct { + // If true, the user can choose to later apply the default firewall rules on either the Provider + // Gateway or Edge Gateway. These firewall rules are created only if the corresponding + // associated default No SNAT and NAT rules are configured. False means that the default + // firewall rules will not be created. + // For the associated default SNAT rule, the source is ANY and the destination is the IP Space's + // external scope. + // For the associated default No SNAT rule, the source is the IP Space's internal scopes and the + // destination is the IP Space's external scope. + EnableDefaultFirewallRuleCreation bool `json:"enableDefaultFirewallRuleCreation,omitempty"` + // If true, the user can choose to later apply the default No SNAT rules on either the Provider + // Gateway or Edge Gateway. + // False means that the default No SNAT rule will not be created. + // An example of a default No NAT rule is that the source CIDR is the IP Space's internal scope + // and the destination CIDR is the IP Space's external scope. This allows traffic to and from + // the IP Space's internal and external scope to not be affected by any NAT rule. An example of + // such traffic is that an Organization VDC Network within IP Space's internal scope will be + // able to route out to the internet. This means that this configuration can allow both + // fully-routed topology and also NAT-routed topology. + EnableDefaultNoSnatRuleCreation bool `json:"enableDefaultNoSnatRuleCreation,omitempty"` + // If true, the user can choose to later apply the default SNAT rules on either the Provider + // Gateway or Edge Gateway. + // False means that the default SNAT rule will not be created. + // An example of a default NAT rule is that the source CIDR is ANY, the destination CIDR is the + // IP Space's external scope. This allows all traffic such as from a private network to be able + // to access the external destination IPs specified by the IP Space's external scope such as the + // internet. Note that the translated external IP will be allocated from this IP Space if there + // are no free ones to be used for the SNAT rules. + EnableDefaultSnatRuleCreation bool `json:"enableDefaultSnatRuleCreation,omitempty"` +} + +type FloatingIPs struct { + // TotalCount holds the number of IP addresses or IP Prefixes defined by the IP Space. If user + // does not own this IP Space, this is the quota that the user's organization is granted. A '-1' + // value means that the user's organization has no cap on the quota (for this case, + // allocatedPercentage is unset) + TotalCount string `json:"totalCount,omitempty"` + // AllocatedCount holds the number of allocated IP addresses or IP Prefixes. + AllocatedCount string `json:"allocatedCount,omitempty"` + // UsedCount holds the number of used IP addresses or IP Prefixes. An allocated IP address or IP + // Prefix is considered used if it is being used in network services such as NAT rule or in Org + // VDC network definition. + UsedCount string `json:"usedCount,omitempty"` + // UnusedCount holds the number of unused IP addresses or IP Prefixes. An IP address or an IP + // Prefix is considered unused if it is allocated but not being used by any network service or + // any Org vDC network definition. + UnusedCount string `json:"unusedCount,omitempty"` + // AllocatedPercentage specifies the percentage of allocated IP addresses or IP Prefixes out of + // all defined IP addresses or IP Prefixes. + AllocatedPercentage float32 `json:"allocatedPercentage,omitempty"` + // UsedPercentage specifies the percentage of used IP addresses or IP Prefixes out of total + // allocated IP addresses or IP Prefixes. + UsedPercentage float32 `json:"usedPercentage,omitempty"` +} + +type PrefixLengthUtilizations struct { + PrefixLength int `json:"prefixLength"` + // TotalCount contains total number of IP Prefixes. If user does not own this IP Space, this is + // the quota that the user's organization is granted. A '-1' value means that the user's + // organization has no cap on the quota. + TotalCount int `json:"totalCount"` + // AllocatedCount contains the number of allocated IP prefixes. + AllocatedCount int `json:"allocatedCount"` +} + +type IPPrefixes struct { + // TotalCount holds the number of IP addresses or IP Prefixes defined by the IP Space. If user + // does not own this IP Space, this is the quota that the user's organization is granted. A '-1' + // value means that the user's organization has no cap on the quota; for this case, + // allocatedPercentage is unset. + TotalCount string `json:"totalCount,omitempty"` + // TAllocatedCount holds the number of allocated IP addresses or IP Prefixes. + AllocatedCount string `json:"allocatedCount,omitempty"` + // UsedCount holds the number of used IP addresses or IP Prefixes. An allocated IP address or IP + // Prefix is considered used if it is being used in network services such as NAT rule or in Org + // VDC network definition. + UsedCount string `json:"usedCount,omitempty"` + // UnusedCount holds the number of unused IP addresses or IP Prefixes. An IP address or an IP + // Prefix is considered unused if it is allocated but not being used by any network service or + // any Org vDC network definition. + UnusedCount string `json:"unusedCount,omitempty"` + // AllocatedPercentage specifies the percentage of allocated IP addresses or IP Prefixes out of + // all defined IP addresses or IP Prefixes. + AllocatedPercentage float32 `json:"allocatedPercentage,omitempty"` + // UsedPercentage specifies the percentage of used IP addresses or IP Prefixes out of total + // allocated IP addresses or IP Prefixes. + UsedPercentage float32 `json:"usedPercentage,omitempty"` + // PrefixLengthUtilizations contains utilization summary grouped by IP Prefix's prefix length. + // This information will only be returned for an individual IP Prefix. + PrefixLengthUtilizations []PrefixLengthUtilizations `json:"prefixLengthUtilizations,omitempty"` +} + +type IpSpaceUtilization struct { + // FloatingIPs holds utilization summary for floating IPs within the IP space. + FloatingIPs FloatingIPs `json:"floatingIPs,omitempty"` + // IPPrefixes holds Utilization summary for IP prefixes within the IP space. + IPPrefixes IPPrefixes `json:"ipPrefixes,omitempty"` +} + +type IPSpaceRanges struct { + IPRanges []IpSpaceRangeValues `json:"ipRanges"` + // This specifies the default number of IPs from the specified ranges which can be consumed by + // each organization using this IP Space. This is typically set for IP Spaces with type PUBLIC + // or SHARED_SERVICES. A Quota of -1 means there is no cap to the number of IP addresses that + // can be allocated. A Quota of 0 means that the IP addresses cannot be allocated. If not + // specified, all PUBLIC or SHARED_SERVICES IP Spaces have a default quota of 1 for Floating IP + // addresses and all PRIVATE IP Spaces have a default quota of -1 for Floating IP addresses. + DefaultFloatingIPQuota int `json:"defaultFloatingIpQuota"` +} + +type IpSpaceRangeValues struct { + ID string `json:"id,omitempty"` + // Starting IP address in the range. + StartIPAddress string `json:"startIpAddress"` + // endIpAddress + EndIPAddress string `json:"endIpAddress"` + + // The number of IP addresses defined by the IP range. + TotalIPCount string `json:"totalIpCount,omitempty"` + // The number of allocated IP addresses. + AllocatedIPCount string `json:"allocatedIpCount,omitempty"` + // allocatedIpPercentage + AllocatedIPPercentage float32 `json:"allocatedIpPercentage,omitempty"` +} + +type IPSpacePrefixes struct { + // IPPrefixSequence A sequence of IP prefixes with same prefix length. All the IP Prefix + // sequences with the same prefix length are treated as one logical unit for allocation purpose. + IPPrefixSequence []IPPrefixSequence `json:"ipPrefixSequence"` + + // This specifies the number of prefixes from the specified sequence which can be consumed by + // each organization using this IP Space. All the IP Prefix sequences with the same prefix + // length are treated as one logical unit for allocation purpose. This is typically set for IP + // Spaces with type PUBLIC or SHARED_SERVICES. A Quota of -1 means there is no cap to the number + // of IP Prefixes that can be allocated. A Quota of 0 means that the IP Prefixes cannot be + // allocated. If not specified, all PUBLIC or SHARED_SERVICES IP Spaces have a default quota of + // 0 for IP Prefixes and all PRIVATE IP Spaces have a default quota of -1 for IP Prefixes. + DefaultQuotaForPrefixLength int `json:"defaultQuotaForPrefixLength"` +} + +type IPPrefixSequence struct { + ID string `json:"id,omitempty"` + // Starting IP address for the IP prefix. Note that if the IP is a host IP and not the network + // definition IP for the specific prefix length, VCD will automatically modify starting IP to + // the network definition's IP for the specified host IP. An example is that for prefix length + // 30, the starting IP of 192.169.0.2 will be automatically modified to 192.169.0.0. 192.169.0.6 + // will be modified to 192.169.0.4. 192.169.0.0/30 and 192.169.0.4/30 are network definition + // CIDRs for host IPs 192.169.0.2 and 192.169.0.6, respectively. + StartingPrefixIPAddress string `json:"startingPrefixIpAddress"` + // The prefix length. + PrefixLength int `json:"prefixLength"` + // The number of prefix blocks defined by this IP prefix. + TotalPrefixCount int `json:"totalPrefixCount"` + // The number of allocated IP prefix blocks. + AllocatedPrefixCount int `json:"allocatedPrefixCount,omitempty"` + // Specifies the percentage of allocated IP prefix blocks out of total specified IP prefix blocks. + AllocatedPrefixPercentage float32 `json:"allocatedPrefixPercentage,omitempty"` +} + +// IpSpaceUplink specifies the IP Space Uplink configuration for Provider Gateway (External network +// with T0 or T0 VRF backing) +type IpSpaceUplink struct { + ID string `json:"id,omitempty"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + // ExternalNetworkRef contains information + ExternalNetworkRef *OpenApiReference `json:"externalNetworkRef"` + IPSpaceRef *OpenApiReference `json:"ipSpaceRef"` + // The type of the IP Space associated with this uplink. Possible values are: PUBLIC, PRIVATE, + // SHARED_SERVICES. This property is read-only. + IPSpaceType string `json:"ipSpaceType,omitempty"` + Status string `json:"status,omitempty"` +} + +// IpSpaceIpAllocationRequest is an IP Space IP Allocation request object. An IP Space IP allocation +// request can either request a specific IP address/IP prefix or request a specific number of any +// free IP Addresses/IP Prefixes within an IP Space. To allocate a specific IP Address or IP Prefix, +// the value field should be used and the IP Address or Prefix should be specified. Use the quantity +// field to specify the amount. The value and quantity fields should not be set simultaneously. +type IpSpaceIpAllocationRequest struct { + // The prefix length of an IP Prefix to allocate. This is required if type is IP_PREFIX. This + // field is only required if the request is for a specific quantity of IP Prefixes and not + // needed if request value is specified. + PrefixLength *int `json:"prefixLength,omitempty"` + // The number of IP addresses or IP Prefix blocks to allocate. Specifying quantity will allocate + // the given number of any free IP addresses or IP Prefixes within the IP Space. To use a + // specific IP address or IP Prefix, please use the value field to request a specific value. + Quantity *int `json:"quantity,omitempty"` + // Type The type of the IP allocation requested. Possible values are: + // * FLOATING_IP - For allocation of floating IP addresses from defined IP Space ranges. + // * IP_PREFIX - For allocation of IP prefix sequences from defined IP Space prefixes. + Type string `json:"type"` + // The specific IP address or IP Prefix to allocate. If an IP address or IP Prefix is specified, + // the quantity value should not be set. + // Note - only available in VCD 10.4.2+ + Value string `json:"value,omitempty"` +} + +// IpSpaceIpAllocationRequestResult is the result that gets returned in a +// task.Task.Result.ResultContent.Text field after submitting an IpSpaceIpAllocationRequest +type IpSpaceIpAllocationRequestResult struct { + ID string `json:"id"` + Value string `json:"value"` + SuggestedValue string `json:"suggestedValue"` +} + +// IpSpaceIpAllocation is a structure that is used for managing IP Space IP Allocation after +// submitting a request using `IpSpaceIpAllocationRequest` and processing the response in +// IpSpaceIpAllocationRequestResult +type IpSpaceIpAllocation struct { + ID string `json:"id,omitempty"` + + // Description about usage of an IP if the usageState is USED_MANUAL. + Description string `json:"description"` + // Reference to the organization where the IP is allocated. + OrgRef *OpenApiReference `json:"orgRef,omitempty"` + // Type contains type of the IP allocation. Possible values are: + // * FLOATING_IP - For allocation of floating IP addresses from defined IP Space ranges. + // * IP_PREFIX - For allocation of IP prefix sequences from defined IP Space prefixes. + Type string `json:"type"` + // UsageCategories + // The list of service categories where the IP address is being used. Typically this can be one + // of: SNAT, DNAT, LOAD_BALANCER, IPSEC_VPN, SSL_VPN or L2_VPN. This property is read-only. + UsageCategories []string `json:"usageCategories,omitempty"` + + // Specifies current usage state of an allocated IP. Possible values are: + // * UNUSED - the allocated IP is current not being used in the system. + // * USED - the allocated IP is currently in use in the system. An allocated IP address or IP Prefix is considered used if it is being used in network services such as NAT rule or in Org VDC network definition. + // * USED_MANUAL - the allocated IP is marked for manual usage. Allocation description can be referenced to get information about the manual usage. + UsageState string `json:"usageState"` + + // An individual IP Address or an IP Prefix which is allocated. + Value string `json:"value"` + + // Reference to the entity using the IP, such as an Edge Gateway Reference if the Floating IP is used for NAT or Org VDC network reference if IP Prefix is used for network definition. This property is read-only. + UsedByRef *OpenApiReference `json:"usedByRef"` + + // Date when the IP address or IP prefix is allocated. This property is read-only. + AllocationDate string `json:"allocationDate"` +} + +// IpSpaceOrgAssignment is used to override default quotas for specific Orgs +type IpSpaceOrgAssignment struct { + ID string `json:"id,omitempty"` + // IPSpaceRef is mandatory + IPSpaceRef *OpenApiReference `json:"ipSpaceRef"` + // OrgRef is mandatory + OrgRef *OpenApiReference `json:"orgRef"` + IPSpaceType string `json:"ipSpaceType,omitempty"` + // DefaultQuotas contains read-only default quotas which are controlled in IP Space itself + DefaultQuotas *IpSpaceOrgAssignmentQuotas `json:"defaultQuotas,omitempty"` + // CustomQuotas are the quotas that can be overriden for that particular Organization + CustomQuotas *IpSpaceOrgAssignmentQuotas `json:"customQuotas"` +} + +type IpSpaceOrgAssignmentQuotas struct { + // FloatingIPQuota specifies the default number of IPs from the specified ranges which can be + // consumed by each organization using this IP Space. This is typically set for IP Spaces with + // type PUBLIC or SHARED_SERVICES. A Quota of -1 means there is no cap to the number of IP + // addresses that can be allocated. A Quota of 0 means that the IP addresses cannot be + // allocated. If not specified, all PUBLIC or SHARED_SERVICES IP Spaces have a default quota of + // 1 for Floating IP addresses and all PRIVATE IP Spaces have a default quota of -1 for Floating + // IP addresses. + FloatingIPQuota *int `json:"floatingIpQuota"` + // IPPrefixQuotas contains a slice of elements that define IP Prefix Quotas + IPPrefixQuotas []IpSpaceOrgAssignmentIPPrefixQuotas `json:"ipPrefixQuotas"` +} + +// IpSpaceOrgAssignmentIPPrefixQuotas defines a single IP Prefix quota +type IpSpaceOrgAssignmentIPPrefixQuotas struct { + PrefixLength *int `json:"prefixLength"` + Quota *int `json:"quota"` +} + +// IpSpaceFloatingIpSuggestion provides a list of unused IP Addresses in an IP Space +type IpSpaceFloatingIpSuggestion struct { + IPSpaceRef OpenApiReference `json:"ipSpaceRef"` + // UnusedValues lists unused IP Addresses or IP Prefixes from the referenced IP Space + UnusedValues []string `json:"unusedValues"` +} diff --git a/types/v56/multi_site.go b/types/v56/multi_site.go new file mode 100644 index 000000000..2b3335f8c --- /dev/null +++ b/types/v56/multi_site.go @@ -0,0 +1,116 @@ +package types + +// This file defines structures used to retrieve amd modify associations between VCD entities. +// The entities could be: +// * site (the whole VCD) +// * Org (a tenant domain) + +/* + Note: every site or org can have as many associations as they want, but each association has only two members. + Thus, an organization could be associated with 3 more, but we won't see one association with 4 members; + we will see instead 3 associations of two members each +*/ + +// MultiSiteStatus defines a type used for status constants +type MultiSiteStatus string + +const ( + StatusActive MultiSiteStatus = "ACTIVE" + StatusAsymmetric MultiSiteStatus = "ASYMMETRIC" + StatusUnreachable MultiSiteStatus = "UNREACHABLE" + StatusError MultiSiteStatus = "ERROR" +) + +type SiteAssociations struct { + Href string `xml:"href,attr,omitempty"` + Type string `xml:"type,attr,omitempty"` + Link LinkList `xml:"Link,omitempty"` + SiteAssociations []*SiteAssociationMember `xml:"SiteAssociationMember"` +} + +// SiteAssociationMember describes the structure of one member of a site association +type SiteAssociationMember struct { + Xmlns string `xml:"xmlns,attr"` + Href string `xml:"href,attr,omitempty"` + Id string `xml:"id,attr,omitempty"` + Type string `xml:"type,attr,omitempty"` + Name string `xml:"name,attr"` + Description string `xml:"Description,omitempty"` // Optional Description + BaseUiEndpoint string `xml:"BaseUiEndpoint"` // The base URI of the UI end-point for the site. + PublicKey string `xml:"PublicKey,omitempty"` // PEM-encoded public key for the remote site. + RestEndpoint string `xml:"RestEndpoint"` // The URI of the REST API end-point for the site. + RestEndpointCertificate string `xml:"RestEndpointCertificate,omitempty"` // Optional PEM-encoded certificate to use when connecting to the REST API end-point. + SiteID string `xml:"SiteId"` // The URN of the remote site + SiteName string `xml:"SiteName"` // The name of the remote site + // Current status of this association. One of: + // ACTIVE (The association has been established by both members, and communication with the remote party succeeded.) + // ASYMMETRIC (The association has been established at the local site, but the remote party has not yet reciprocated.) + // UNREACHABLE (The association has been established by both members, but the remote member is currently unreachable.) + Status string `xml:"Status,omitempty"` + Link LinkList `xml:"Link,omitempty"` + Tasks *TasksInProgress `xml:"task,omitempty"` +} + +// Site is the definition of a VCD seen as an element in a collaborative pair +type Site struct { + Xmlns string `xml:"xmlns,attr,omitempty"` + Href string `xml:"href,attr,omitempty"` + Type string `xml:"type,attr,omitempty"` + Id string `xml:"id,attr,omitempty"` + Name string `xml:"name,attr"` + OperationKey string `xml:"operationKey,attr,omitempty"` // Optional unique identifier to support idempotent semantics for create and delete operations. + Description string `xml:"description,omitempty"` // Optional description + RestEndpoint string `xml:"RestEndpoint"` // The URI of the REST API end-point for the site. + BaseUiEndpoint string `xml:"BaseUiEndpoint"` // The base URI of the UI end-point for the site. + TenantUiEndpoint string `xml:"TenantUiEndpoint"` // The base URI of the UI end-point for the site. + RestEndpointCertificate string `xml:"RestEndpointCertificate,omitempty"` // Optional PEM-encoded certificate to use when connecting to the REST API end-point. + MultiSiteUrl string `xml:"MultiSiteUrl,omitempty"` // The URL that represents the entire multisite setup. + SiteAssociations SiteAssociations `xml:"SiteAssociations,omitempty"` // List of sites associated with this site. + Link LinkList `xml:"Link,omitempty"` + Tasks TasksInProgress `xml:"Tasks,omitempty"` +} + +// OrgAssociations is a collection of Org associations +type OrgAssociations struct { + OrgAssociations []*OrgAssociationMember `xml:"OrgAssociationMember"` +} + +// OrgAssociationMember describes the structure of one member of an Org association +type OrgAssociationMember struct { + Xmlns string `xml:"xmlns,attr"` + Href string `xml:"href,attr"` + Type string `xml:"type,attr"` + Link LinkList `xml:"Link,omitempty"` + OrgID string `xml:"OrgId"` + OrgName string `xml:"OrgName"` + OrgPublicKey string `xml:"OrgPublicKey"` + SiteID string `xml:"SiteId"` + Status string `xml:"Status,omitempty"` +} + +// QueryResultSiteAssociationRecord defines a structure to retrieve site associations using a query +type QueryResultSiteAssociationRecord struct { + AssociatedSiteName string `xml:"associatedSiteName,attr"` + AssociatedSiteId string `xml:"associatedSiteId,attr"` + RestEndpoint string `xml:"restEndpoint,attr"` + BaseUiEndpoint string `xml:"baseUiEndpoint,attr"` + Href string `xml:"href,attr"` + + // Current status of this association. One of: + // ACTIVE (The association has been established by both members, and communication with the remote party succeeded.) + // ASYMMETRIC (The association has been established at the local site, but the remote party has not yet reciprocated.) + // UNREACHABLE (The association has been established by both members, but the remote member is currently unreachable.) + Status string `xml:"status,attr"` + Link LinkList `xml:"Link,omitempty"` +} + +// QueryResultOrgAssociationRecord defines a structure to retrieve Org associations using a query +type QueryResultOrgAssociationRecord struct { + SiteId string `xml:"siteId,attr"` + OrgId string `xml:"orgId,attr"` + SiteName string `xml:"siteName,attr"` + OrgName string `xml:"orgName,attr"` + Href string `xml:"href,attr"` + Status string `xml:"status,attr"` + Link LinkList `xml:"Link,omitempty"` +} diff --git a/types/v56/network.go b/types/v56/network.go new file mode 100644 index 000000000..073dccb91 --- /dev/null +++ b/types/v56/network.go @@ -0,0 +1,192 @@ +package types + +import "encoding/xml" + +// NOTE: The types in this file were created using goxmlstruct +// (https://github.com/twpayne/go-xmlstruct) + +// ----------------------------------- +// type FirewallConfiguration +// ----------------------------------- + +type FirewallConfiguration struct { + ContextID string `xml:"contextId"` + Layer3Sections *Layer3Sections `xml:"layer3Sections"` + Layer2Sections *Layer2Sections `xml:"layer2Sections"` +} + +type Layer2Sections struct { + Section *FirewallSection `xml:"section"` +} + +type Layer3Sections struct { + Section *FirewallSection `xml:"section"` +} + +type FirewallSection struct { + XMLName xml.Name `xml:"section"` + GenerationNumber string `xml:"generationNumber,attr"` + ID *int `xml:"id,attr"` + Name string `xml:"name,attr"` + Stateless bool `xml:"stateless,attr"` + TcpStrict bool `xml:"tcpStrict,attr"` + Timestamp int `xml:"timestamp,attr"` + Type string `xml:"type,attr"` + UseSid bool `xml:"useSid,attr"` + Rule []NsxvDistributedFirewallRule `xml:"rule"` +} + +type NsxvDistributedFirewallRule struct { + Disabled bool `xml:"disabled,attr"` + ID *int `xml:"id,attr"` + Logged bool `xml:"logged,attr"` + Name string `xml:"name"` + Action string `xml:"action"` // allow, deny + AppliedToList *AppliedToList `xml:"appliedToList"` + SectionID *int `xml:"sectionId"` + Sources *Sources `xml:"sources"` + Destinations *Destinations `xml:"destinations"` + Services *Services `xml:"services"` + Direction string `xml:"direction"` // in, out, inout + PacketType string `xml:"packetType"` + Tag string `xml:"tag"` +} + +type AppliedToList struct { + AppliedTo []AppliedTo `xml:"appliedTo"` +} + +type AppliedTo struct { + Name string `xml:"name"` + Value string `xml:"value"` + Type string `xml:"type"` + IsValid bool `xml:"isValid"` +} + +type Sources struct { + Excluded bool `xml:"excluded,attr"` + Source []Source `xml:"source"` +} + +type Source struct { + Name string `xml:"name"` + Value string `xml:"value"` + Type string `xml:"type"` + IsValid bool `xml:"isValid"` +} + +type Destinations struct { + Excluded bool `xml:"excluded,attr"` + Destination []Destination `xml:"destination"` +} + +type Destination struct { + Name string `xml:"name"` + Value string `xml:"value"` + Type string `xml:"type"` + IsValid bool `xml:"isValid"` +} + +type Services struct { + Service []Service `xml:"service"` +} + +type Service struct { + IsValid bool `xml:"isValid"` + SourcePort *string `xml:"sourcePort"` + DestinationPort *string `xml:"destinationPort"` + Protocol *int `xml:"protocol"` + ProtocolName *string `xml:"protocolName"` + Name string `xml:"name,omitempty"` + Value string `xml:"value,omitempty"` + Type string `xml:"type,omitempty"` +} + +// ----------------------------------- +// type Service +// ----------------------------------- + +type ApplicationList struct { + Application []Application `xml:"application"` +} + +type Application struct { + ObjectID string `xml:"objectId"` + ObjectTypeName string `xml:"objectTypeName"` + VsmUuid string `xml:"vsmUuid"` + NodeID string `xml:"nodeId"` + Revision string `xml:"revision"` + Type ApplicationType `xml:"type"` + Name string `xml:"name"` + Scope Scope `xml:"scope"` + ClientHandle struct{} `xml:"clientHandle"` + ExtendedAttributes struct{} `xml:"extendedAttributes"` + IsUniversal bool `xml:"isUniversal"` + UniversalRevision string `xml:"universalRevision"` + IsTemporal bool `xml:"isTemporal"` + InheritanceAllowed bool `xml:"inheritanceAllowed"` + Element Element `xml:"element"` + Layer string `xml:"layer"` + IsReadOnly bool `xml:"isReadOnly"` + Description *string `xml:"description"` +} + +type ApplicationType struct { + TypeName string `xml:"typeName"` +} + +type Scope struct { + ID string `xml:"id"` + ObjectTypeName string `xml:"objectTypeName"` + Name string `xml:"name"` +} + +type Element struct { + ApplicationProtocol *string `xml:"applicationProtocol"` + Value *string `xml:"value"` + SourcePort *string `xml:"sourcePort"` + AppGuidName *string `xml:"appGuidName"` +} + +// ----------------------------------- +// type ServiceGroup +// ----------------------------------- + +type ApplicationGroupList struct { + ApplicationGroup []ApplicationGroup `xml:"applicationGroup"` +} + +type ApplicationGroup struct { + ObjectID string `xml:"objectId"` + ObjectTypeName string `xml:"objectTypeName"` + VsmUuid string `xml:"vsmUuid"` + NodeID string `xml:"nodeId"` + Revision string `xml:"revision"` + Type ApplicationType `xml:"type"` + Name string `xml:"name"` + Scope Scope `xml:"scope"` + ClientHandle struct{} `xml:"clientHandle"` + ExtendedAttributes struct{} `xml:"extendedAttributes"` + IsUniversal bool `xml:"isUniversal"` + UniversalRevision string `xml:"universalRevision"` + IsTemporal bool `xml:"isTemporal"` + InheritanceAllowed bool `xml:"inheritanceAllowed"` + IsReadOnly bool `xml:"isReadOnly"` + Member []Member `xml:"member"` +} + +type Member struct { + ObjectID string `xml:"objectId"` + ObjectTypeName string `xml:"objectTypeName"` + VsmUuid string `xml:"vsmUuid"` + NodeID string `xml:"nodeId"` + Revision string `xml:"revision"` + Type ApplicationType `xml:"type"` + Name string `xml:"name"` + Scope Scope `xml:"scope"` + ClientHandle struct{} `xml:"clientHandle"` + ExtendedAttributes struct{} `xml:"extendedAttributes"` + IsUniversal bool `xml:"isUniversal"` + UniversalRevision bool `xml:"universalRevision"` + IsTemporal bool `xml:"isTemporal"` +} diff --git a/types/v56/nsxt_segment_profiles.go b/types/v56/nsxt_segment_profiles.go new file mode 100644 index 000000000..f91b07ea7 --- /dev/null +++ b/types/v56/nsxt_segment_profiles.go @@ -0,0 +1,207 @@ +package types + +// NsxtSegmentProfileTemplate allows management of templates that define the segment profiles that +// will be applied during network creation. +type NsxtSegmentProfileTemplate struct { + ID string `json:"id,omitempty"` + // Name for Segment Profile template + Name string `json:"name"` + // Description for Segment Profile template + Description string `json:"description,omitempty"` + + // SourceNsxTManagerRef points to NSX-T manager providing the source segment profiles + SourceNsxTManagerRef *OpenApiReference `json:"sourceNsxTManagerRef,omitempty"` + IPDiscoveryProfile *Reference `json:"ipDiscoveryProfile,omitempty"` + MacDiscoveryProfile *Reference `json:"macDiscoveryProfile,omitempty"` + QosProfile *Reference `json:"qosProfile,omitempty"` + SegmentSecurityProfile *Reference `json:"segmentSecurityProfile,omitempty"` + SpoofGuardProfile *Reference `json:"spoofGuardProfile,omitempty"` + + LastModified string `json:"lastModified,omitempty"` +} + +// NsxtSegmentProfileCommonFields contains common fields that are used in all NSX-T Segment +// Profiles +type NsxtSegmentProfileCommonFields struct { + ID string `json:"id,omitempty"` + // Description of the segment profile. + Description string `json:"description,omitempty"` + // DisplayName represents name of the segment profile. This corresponds to the name used in + // NSX-T managers logs or GUI. + DisplayName string `json:"displayName"` + // NsxTManagerRef where this segment profile is configured. + NsxTManagerRef *OpenApiReference `json:"nsxTManagerRef"` +} + +// NsxtSegmentProfileIpDiscovery contains information about NSX-T IP Discovery Segment Profile +// It is a read-only construct in VCD +type NsxtSegmentProfileIpDiscovery struct { + NsxtSegmentProfileCommonFields + // ArpBindingLimit indicates the number of arp snooped IP addresses to be remembered per + // LogicalPort. + ArpBindingLimit int `json:"arpBindingLimit"` + // ArpNdBindingTimeout indicates ARP and ND cache timeout (in minutes). + ArpNdBindingTimeout int `json:"arpNdBindingTimeout"` + // IsArpSnoopingEnabled defines whether ARP snooping is enabled. + IsArpSnoopingEnabled bool `json:"isArpSnoopingEnabled"` + // IsDhcpSnoopingV4Enabled defines whether DHCP snooping for IPv4 is enabled. + IsDhcpSnoopingV4Enabled bool `json:"isDhcpSnoopingV4Enabled"` + // IsDhcpSnoopingV6Enabled defines whether DHCP snooping for IPv6 is enabled. + IsDhcpSnoopingV6Enabled bool `json:"isDhcpSnoopingV6Enabled"` + // IsDuplicateIPDetectionEnabled indicates whether duplicate IP detection is enabled. Duplicate + // IP detection is used to determine if there is any IP conflict with any other port on the same + // logical switch. If a conflict is detected, then the IP is marked as a duplicate on the port + // where the IP was discovered last. + IsDuplicateIPDetectionEnabled bool `json:"isDuplicateIpDetectionEnabled"` + // IsNdSnoopingEnabled indicates whether ND snooping is enabled. If true, this method will snoop + // the NS (Neighbor Solicitation) and NA (Neighbor Advertisement) messages in the ND (Neighbor + // Discovery Protocol) family of messages which are transmitted by a VM. From the NS messages, + // we will learn about the source which sent this NS message. From the NA message, we will learn + // the resolved address in the message which the VM is a recipient of. Addresses snooped by this + // method are subject to TOFU. + IsNdSnoopingEnabled bool `json:"isNdSnoopingEnabled"` + // IsTofuEnabled defines whether 'Trust on First Use(TOFU)' paradigm is enabled. + IsTofuEnabled bool `json:"isTofuEnabled"` + // IsVMToolsV4Enabled indicates whether fetching IPv4 address using vm-tools is enabled. This + // option is only supported on ESX where vm-tools is installed. + IsVMToolsV4Enabled bool `json:"isVmToolsV4Enabled"` + // IsVMToolsV6Enabled indicates whether fetching IPv6 address using vm-tools is enabled. This + // will learn the IPv6 addresses which are configured on interfaces of a VM with the help of the + // VMTools software. + IsVMToolsV6Enabled bool `json:"isVmToolsV6Enabled"` + // NdSnoopingLimit defines maximum number of ND (Neighbor Discovery Protocol) snooped IPv6 + // addresses. + NdSnoopingLimit int `json:"ndSnoopingLimit"` +} + +// NsxtSegmentProfileMacDiscovery contains information about NSX-T MAC Discovery Segment Profile +// It is a read-only construct in VCD +type NsxtSegmentProfileMacDiscovery struct { + NsxtSegmentProfileCommonFields + // IsMacChangeEnabled indcates whether source MAC address change is enabled. + IsMacChangeEnabled bool `json:"isMacChangeEnabled"` + // IsMacLearningEnabled indicates whether source MAC address learning is enabled. + IsMacLearningEnabled bool `json:"isMacLearningEnabled"` + // IsUnknownUnicastFloodingEnabled indicates whether unknown unicast flooding rule is enabled. + // This allows flooding for unlearned MAC for ingress traffic. + IsUnknownUnicastFloodingEnabled bool `json:"isUnknownUnicastFloodingEnabled"` + // MacLearningAgingTime indicates aging time in seconds for learned MAC address. Indicates how + // long learned MAC address remain. + MacLearningAgingTime int `json:"macLearningAgingTime"` + // MacLimit indicates the maximum number of MAC addresses that can be learned on this port. + MacLimit int `json:"macLimit"` + // MacPolicy defines the policy after MAC Limit is exceeded. It can be either 'ALLOW' or 'DROP'. + MacPolicy string `json:"macPolicy"` +} + +// NsxtSegmentProfileSegmentSpoofGuard contains information about NSX-T Spoof Guard Segment Profile +// It is a read-only construct in VCD +type NsxtSegmentProfileSegmentSpoofGuard struct { + NsxtSegmentProfileCommonFields + // IsAddressBindingWhitelistEnabled indicates whether Spoof Guard is enabled. If true, it only + // allows VM sending traffic with the IPs in the whitelist. + IsAddressBindingWhitelistEnabled bool `json:"isAddressBindingWhitelistEnabled"` +} + +// NsxtSegmentProfileSegmentQosProfile contains information about NSX-T QoS Segment Profile +// It is a read-only construct in VCD +type NsxtSegmentProfileSegmentQosProfile struct { + NsxtSegmentProfileCommonFields + // ClassOfService groups similar types of traffic in the network and each type of traffic is + // treated as a class with its own level of service priority. The lower priority traffic is + // slowed down or in some cases dropped to provide better throughput for higher priority + // traffic. + ClassOfService int `json:"classOfService"` + // DscpConfig contains a Differentiated Services Code Point (DSCP) Configuration for this + // Segment QoS Profile. + DscpConfig struct { + Priority int `json:"priority"` + TrustMode string `json:"trustMode"` + } `json:"dscpConfig"` + // EgressRateLimiter indicates egress rate properties in Mb/s. + EgressRateLimiter NsxtSegmentProfileSegmentQosProfileRateLimiter `json:"egressRateLimiter"` + // IngressBroadcastRateLimiter indicates broadcast rate properties in Mb/s. + IngressBroadcastRateLimiter NsxtSegmentProfileSegmentQosProfileRateLimiter `json:"ingressBroadcastRateLimiter"` + // IngressRateLimiter indicates ingress rate properties in Mb/s. + IngressRateLimiter NsxtSegmentProfileSegmentQosProfileRateLimiter `json:"ingressRateLimiter"` +} + +// NsxtSegmentProfileIpDiscovery contains information about NSX-T IP Discovery Segment Profile +// It is a read-only construct in VCD +type NsxtSegmentProfileSegmentQosProfileRateLimiter struct { + // Average bandwidth in Mb/s. + AvgBandwidth int `json:"avgBandwidth"` + // Burst size in bytes. + BurstSize int `json:"burstSize"` + // Peak bandwidth in Mb/s. + PeakBandwidth int `json:"peakBandwidth"` +} + +// NsxtSegmentProfileSegmentSecurity contains information about NSX-T Segment Security Profile +// It is a read-only construct in VCD +type NsxtSegmentProfileSegmentSecurity struct { + NsxtSegmentProfileCommonFields + // BpduFilterAllowList indicates pre-defined list of allowed MAC addresses to be excluded from + // BPDU filtering. + BpduFilterAllowList []string `json:"bpduFilterAllowList"` + // IsBpduFilterEnabled indicates whether BPDU filter is enabled. + IsBpduFilterEnabled bool `json:"isBpduFilterEnabled"` + // IsDhcpClientBlockV4Enabled indicates whether DHCP Client block IPv4 is enabled. This filters + // DHCP Client IPv4 traffic. + IsDhcpClientBlockV4Enabled bool `json:"isDhcpClientBlockV4Enabled"` + // IsDhcpClientBlockV6Enabled indicates whether DHCP Client block IPv6 is enabled. This filters + // DHCP Client IPv4 traffic. + IsDhcpClientBlockV6Enabled bool `json:"isDhcpClientBlockV6Enabled"` + // IsDhcpServerBlockV4Enabled indicates whether DHCP Server block IPv4 is enabled. This filters + // DHCP Server IPv4 traffic. + IsDhcpServerBlockV4Enabled bool `json:"isDhcpServerBlockV4Enabled"` + // IsDhcpServerBlockV6Enabled indicates whether DHCP Server block IPv6 is enabled. This filters + // DHCP Server IPv6 traffic. + IsDhcpServerBlockV6Enabled bool `json:"isDhcpServerBlockV6Enabled"` + // IsNonIPTrafficBlockEnabled indicates whether non IP traffic block is enabled. If true, it + // blocks all traffic except IP/(G)ARP/BPDU. + IsNonIPTrafficBlockEnabled bool `json:"isNonIpTrafficBlockEnabled"` + // IsRaGuardEnabled indicates whether Router Advertisement Guard is enabled. This filters DHCP + // Server IPv6 traffic. + IsRaGuardEnabled bool `json:"isRaGuardEnabled"` + // IsRateLimitingEnabled indicates whether Rate Limiting is enabled. + IsRateLimitingEnabled bool `json:"isRateLimitingEnabled"` + RateLimits struct { + // Incoming broadcast traffic limit in packets per second. + RxBroadcast int `json:"rxBroadcast"` + // Incoming multicast traffic limit in packets per second. + RxMulticast int `json:"rxMulticast"` + // Outgoing broadcast traffic limit in packets per second. + TxBroadcast int `json:"txBroadcast"` + // Outgoing multicast traffic limit in packets per second. + TxMulticast int `json:"txMulticast"` + } `json:"rateLimits"` +} + +// NsxtGlobalDefaultSegmentProfileTemplate is a structure that sets VCD global default Segment +// Profile Templates +type NsxtGlobalDefaultSegmentProfileTemplate struct { + VappNetworkSegmentProfileTemplateRef *OpenApiReference `json:"vappNetworkSegmentProfileTemplateRef,omitempty"` + VdcNetworkSegmentProfileTemplateRef *OpenApiReference `json:"vdcNetworkSegmentProfileTemplateRef,omitempty"` +} + +// OrgVdcNetworkSegmentProfiles defines Segment Profile configuration structure for Org VDC networks +// An Org VDC network may have a Segment Profile Template assigned, or individual Segment Profiles +type OrgVdcNetworkSegmentProfiles struct { + // SegmentProfileTemplate contains a read-only reference to Segment Profile Template + // To update Segment Profile Template for a particular Org VDC network, one must use + // `OpenApiOrgVdcNetwork.SegmentProfileTemplate` field and `OpenApiOrgVdcNetwork.Update()` + SegmentProfileTemplate *SegmentProfileTemplateRef `json:"segmentProfileTemplate,omitempty"` + + IPDiscoveryProfile *Reference `json:"ipDiscoveryProfile"` + MacDiscoveryProfile *Reference `json:"macDiscoveryProfile"` + QosProfile *Reference `json:"qosProfile"` + SegmentSecurityProfile *Reference `json:"segmentSecurityProfile"` + SpoofGuardProfile *Reference `json:"spoofGuardProfile"` +} + +// SegmentProfileTemplateRef contains reference to segment profile +type SegmentProfileTemplateRef struct { + Source string `json:"source"` + TemplateRef *OpenApiReference `json:"templateRef"` +} diff --git a/types/v56/nsxt_types.go b/types/v56/nsxt_types.go index d75db67c8..430853542 100644 --- a/types/v56/nsxt_types.go +++ b/types/v56/nsxt_types.go @@ -1,23 +1,37 @@ package types // OpenAPIEdgeGateway structure supports marshalling both - NSX-V and NSX-T edge gateways as returned by OpenAPI -// endpoint (cloudapi/1.0.0edgeGateways/), but the endpoint only allows to create NSX-T edge gateways. +// endpoint (cloudapi/1.0.0edgeGateways/), but the endpoint only allows users to create NSX-T edge gateways. type OpenAPIEdgeGateway struct { Status string `json:"status,omitempty"` ID string `json:"id,omitempty"` // Name of edge gateway + Name string `json:"name"` + // Description of edge gateway Description string `json:"description"` + + // OwnerRef defines Org VDC or VDC Group that this network belongs to. If the ownerRef is set to a VDC Group, this + // network will be available across all the VDCs in the vDC Group. If the VDC Group is backed by a NSX-V network + // provider, the Org VDC network is automatically connected to the distributed router associated with the VDC Group + // and the "connection" field does not need to be set. For API version 35.0 and above, this field should be set for + // network creation. + OwnerRef *OpenApiReference `json:"ownerRef,omitempty"` + // OrgVdc holds the organization vDC or vDC Group that this edge gateway belongs to. If the ownerRef is set to a VDC // Group, this gateway will be available across all the participating Organization vDCs in the VDC Group. OrgVdc *OpenApiReference `json:"orgVdc,omitempty"` + // Org holds the organization to which the gateway belongs. Org *OpenApiReference `json:"orgRef,omitempty"` + // EdgeGatewayUplink defines uplink connections for the edge gateway. EdgeGatewayUplinks []EdgeGatewayUplinks `json:"edgeGatewayUplinks"` + // DistributedRoutingEnabled is a flag indicating whether distributed routing is enabled or not. The default is false. DistributedRoutingEnabled *bool `json:"distributedRoutingEnabled,omitempty"` + // EdgeClusterConfig holds Edge Cluster Configuration for the Edge Gateway. Can be specified if a gateway needs to be // placed on a specific set of Edge Clusters. For NSX-T Edges, user should specify the ID of the NSX-T edge cluster as // the value of primaryEdgeCluster's backingId. The gateway defaults to the Edge Cluster of the connected External @@ -26,8 +40,10 @@ type OpenAPIEdgeGateway struct { // Note. The value of secondaryEdgeCluster will be set to NULL for NSX-T edge gateways. For NSX-V Edges, this is // read-only and the legacy API must be used for edge specific placement. EdgeClusterConfig *OpenAPIEdgeGatewayEdgeClusterConfig `json:"edgeClusterConfig,omitempty"` + // OrgVdcNetworkCount holds the number of Org VDC networks connected to the gateway. OrgVdcNetworkCount *int `json:"orgVdcNetworkCount,omitempty"` + // GatewayBacking must contain backing details of the edge gateway only if importing an NSX-T router. GatewayBacking *OpenAPIEdgeGatewayBacking `json:"gatewayBacking,omitempty"` @@ -36,6 +52,9 @@ type OpenAPIEdgeGateway struct { // supported for VMC. If nothing is set, the default is 192.168.255.225/27. The DHCP listener IP network is on // 192.168.255.225/30. The DNS listener IP network is on 192.168.255.228/32. This field cannot be updated. ServiceNetworkDefinition string `json:"serviceNetworkDefinition,omitempty"` + + // UsingIpSpace is a boolean flag to indicate whether the edge gateway is using IP space or not. + UsingIpSpace *bool `json:"usingIpSpace,omitempty"` } // EdgeGatewayUplink defines uplink connections for the edge gateway. @@ -47,18 +66,30 @@ type EdgeGatewayUplinks struct { // Subnets contain subnets to be used on edge gateway Subnets OpenAPIEdgeGatewaySubnets `json:"subnets,omitempty"` Connected bool `json:"connected,omitempty"` - // QuickAddAllocatedIPCount allows to allocate additional IPs during update + // QuickAddAllocatedIPCount allows users to allocate additional IPs during update QuickAddAllocatedIPCount int `json:"quickAddAllocatedIpCount,omitempty"` // Dedicated defines if the external network is dedicated. Dedicating the External Network will enable Route // Advertisement for this Edge Gateway Dedicated bool `json:"dedicated,omitempty"` + + // BackingType of uplink. Exactly one of these will be present: + // * types.ExternalNetworkBackingTypeNsxtTier0Router (NSXT_TIER0) + // * types.ExternalNetworkBackingTypeNsxtVrfTier0Router (NSXT_VRF_TIER0) + // Additional External network uplinks + // * types.ExternalNetworkBackingTypeNsxtSegment (IMPORTED_T_LOGICAL_SWITCH) - External Network uplinks (External Networks backed by NSX-T Segment) + // + // Note. VCD 10.4.1+. Not mandatory to set it - can be used as a read only value + BackingType *string `json:"backingType,omitempty"` + + // UsingIpSpace is a boolean flag showing if the uplink uses IP Space + UsingIpSpace *bool `json:"usingIpSpace,omitempty"` } -// OpenApiIPRanges is a type alias to reuse the same definitions with appropriate names -type OpenApiIPRanges = ExternalNetworkV2IPRanges +// ExternalNetworkV2IPRanges is a type alias to reuse the same definitions with appropriate names +type ExternalNetworkV2IPRanges = OpenApiIPRanges -// OpenApiIPRangeValues is a type alias to reuse the same definitions with appropriate names -type OpenApiIPRangeValues = ExternalNetworkV2IPRange +// ExternalNetworkV2IPRange is a type alias to reuse the same definitions with appropriate names +type ExternalNetworkV2IPRange = OpenApiIPRangeValues // OpenAPIEdgeGatewaySubnets lists slice of OpenAPIEdgeGatewaySubnetValue values type OpenAPIEdgeGatewaySubnets struct { @@ -80,11 +111,18 @@ type OpenAPIEdgeGatewaySubnetValue struct { // IPRanges contain IP allocations IPRanges *OpenApiIPRanges `json:"ipRanges,omitempty"` // Enabled toggles if the subnet is enabled - Enabled bool `json:"enabled"` - TotalIPCount int `json:"totalIpCount,omitempty"` - UsedIPCount int `json:"usedIpCount,omitempty"` - PrimaryIP string `json:"primaryIp,omitempty"` - AutoAllocateIPRanges bool `json:"autoAllocateIpRanges,omitempty"` + Enabled bool `json:"enabled"` + // TotalIPCount specified total allocated IP count + TotalIPCount *int `json:"totalIpCount,omitempty"` + + // UsedIPCount specifies used IP count + UsedIPCount int `json:"usedIpCount,omitempty"` + + // PrimaryIP of the Edge Gateway. Can only be one in all subnets of a single uplink + PrimaryIP string `json:"primaryIp,omitempty"` + + // AutoAllocateIPRanges provides a way to automatically allocate + AutoAllocateIPRanges bool `json:"autoAllocateIpRanges,omitempty"` } // OpenAPIEdgeGatewayBacking specifies edge gateway backing details @@ -94,10 +132,10 @@ type OpenAPIEdgeGatewayBacking struct { NetworkProvider NetworkProvider `json:"networkProvider"` } -// OpenAPIEdgeGatewayEdgeCluster allows to specify edge cluster reference +// OpenAPIEdgeGatewayEdgeCluster allows users to specify edge cluster reference type OpenAPIEdgeGatewayEdgeCluster struct { - EdgeClusterRef OpenApiReference `json:"edgeClusterRef"` - BackingID string `json:"backingId"` + EdgeClusterRef *OpenApiReference `json:"edgeClusterRef"` + BackingID string `json:"backingId"` } type OpenAPIEdgeGatewayEdgeClusterConfig struct { @@ -105,7 +143,14 @@ type OpenAPIEdgeGatewayEdgeClusterConfig struct { SecondaryEdgeCluster OpenAPIEdgeGatewayEdgeCluster `json:"secondaryEdgeCluster,omitempty"` } -// OpenApiOrgVdcNetwork allows to manage Org Vdc networks +// GatewayUsedIpAddress defines used IP address on edge gateway +type GatewayUsedIpAddress struct { + Category string `json:"category"` + IPAddress string `json:"ipAddress"` + NetworkRef OpenApiReference `json:"networkRef"` +} + +// OpenApiOrgVdcNetwork allows users to manage Org Vdc networks type OpenApiOrgVdcNetwork struct { ID string `json:"id,omitempty"` Name string `json:"name"` @@ -125,6 +170,10 @@ type OpenApiOrgVdcNetwork struct { // NetworkType describes type of Org Vdc network. ('NAT_ROUTED', 'ISOLATED') NetworkType string `json:"networkType"` + // OrgVdcIsNsxTBacked is a read only flag that indicates whether the Org VDC is backed by NSX-T or not + // Note. It returns `false` if Org VDC network is withing an NSX-T VDC Group + OrgVdcIsNsxTBacked bool `json:"orgVdcIsNsxTBacked,omitempty"` + // Connection specifies the edge gateway this network is connected to. // // Note. When NetworkType == ISOLATED, there is no uplink connection. @@ -132,8 +181,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 @@ -161,6 +212,23 @@ type OpenApiOrgVdcNetwork struct { // Shared shares network with other VDCs in the organization Shared *bool `json:"shared,omitempty"` + + // EnableDualSubnetNetwork defines whether or not this network will support two subnets (IPv4 + // and IPv6) + EnableDualSubnetNetwork *bool `json:"enableDualSubnetNetwork,omitempty"` + + // SegmentProfileTemplate reference to the Segment Profile Template that is to be used when + // creating/updating this network. Setting this will override any Org VDC Network Segment + // Profile Template defined at global level or an Org VDC level. + // + // Notes: + // * This field is only relevant during network create/update operation and will not be returned + // on GETs. To retrieve currently set Segment Profile Template one can use different endpoint + // and function `vdc.GetVdcNetworkProfile()` + // * For specific profile types where there are no corresponding profiles defined in the + // template, VCD will use the default NSX-T profile. + // * This field is only applicable for NSX-T Org VDC Networks. + SegmentProfileTemplate *OpenApiReference `json:"segmentProfileTemplateRef,omitempty"` } // OrgVdcNetworkSubnetIPRanges is a type alias to reuse the same definitions with appropriate names @@ -169,7 +237,7 @@ type OrgVdcNetworkSubnetIPRanges = ExternalNetworkV2IPRanges // OrgVdcNetworkSubnetIPRangeValues is a type alias to reuse the same definitions with appropriate names type OrgVdcNetworkSubnetIPRangeValues = ExternalNetworkV2IPRange -//OrgVdcNetworkSubnets +// OrgVdcNetworkSubnets type OrgVdcNetworkSubnets struct { Values []OrgVdcNetworkSubnetValues `json:"values"` } @@ -193,25 +261,113 @@ type Connection struct { // to back NSX-T imported Org VDC network type NsxtImportableSwitch = OpenApiReference -// OpenApiOrgVdcNetworkDhcp allows to manage DHCP configuration for Org VDC networks by using OpenAPI endpoint +// OpenApiOrgVdcNetworkDhcp allows users to manage DHCP configuration for Org VDC networks by using OpenAPI endpoint type OpenApiOrgVdcNetworkDhcp struct { - Enabled *bool `json:"enabled,omitempty"` + Enabled *bool `json:"enabled,omitempty"` + + // LeaseTime specifies the amount of time in seconds of how long a DHCP IP will be leased out + // for. The minimum is 60s while the maximum is 4,294,967,295s, which is roughly 49,710 days. LeaseTime *int `json:"leaseTime,omitempty"` DhcpPools []OpenApiOrgVdcNetworkDhcpPools `json:"dhcpPools,omitempty"` + // Mode describes how the DHCP service is configured for this network. Once a DHCP service has been created, the mode // attribute cannot be changed. The mode field will default to 'EDGE' if it is not provided. This field only applies // to networks backed by an NSX-T network provider. // - // The supported values are EDGE (default) and NETWORK. + // The supported values are EDGE, NETWORK and RELAY (VCD 10.3.1+, API 36.1+). // * If EDGE is specified, the DHCP service of the edge is used to obtain DHCP IPs. - // * If NETWORK is specified, a DHCP server is created for use by this network. (To use NETWORK + // * If NETWORK is specified, a DHCP server is created for use by this network. + // * If RELAY is specified, all the DHCP client requests will be relayed to Gateway DHCP + // Forwarder service. This mode is only supported for Routed Org vDC Networks. // - // In order to use DHCP for IPV6, NETWORK mode must be used. Routed networks which are using NETWORK DHCP services can - // be disconnected from the edge gateway and still retain their DHCP configuration, however network using EDGE DHCP - // cannot be disconnected from the gateway until DHCP has been disabled. + // In order to use DHCP for IPV6, NETWORK mode must be used. Routed networks which are using + // NETWORK DHCP services can be disconnected from the edge gateway and still retain their DHCP + // configuration, however DHCP configuration will be removed during connection change for + // networks using EDGE or RELAY DHCP mode. Mode string `json:"mode,omitempty"` + // IPAddress is only applicable when mode=NETWORK. This will specify IP address of DHCP server in network. IPAddress string `json:"ipAddress,omitempty"` + + // New fields starting with 36.1 + + // DnsServers are the IPs to be assigned by this DHCP service. The IP type must match the IP + // type of the subnet on which the DHCP config is being created. + DnsServers []string `json:"dnsServers,omitempty"` +} + +// OpenApiOrgVdcNetworkDhcpBinding defines configuration of NSX-T DHCP binding in Org VDC network +type OpenApiOrgVdcNetworkDhcpBinding struct { + // ID of DHCP binding + ID string `json:"id,omitempty"` + + // Name contains display name for the DHCP binding + Name string `json:"name"` + + // Description of the DHCP binding + Description string `json:"description,omitempty"` + + // BindingType holds the type of DHCP binding: + // * IPV4 - an IPv4 DHCP binding (`types.NsxtDhcpBindingTypeIpv4`) + // * IPV6 - an IPv6 DHCP binding (`types.NsxtDhcpBindingTypeIpv6`) + BindingType string `json:"bindingType"` + + // MacAddress for the host + MacAddress string `json:"macAddress"` + + // DhcpV4BindingConfig contains additional configuration for IPv4 DHCP binding. + // Note. This is ignored for IPV6 binding. + DhcpV4BindingConfig *DhcpV4BindingConfig `json:"dhcpV4BindingConfig,omitempty"` + + // DhcpV6BindingConfig contains additional configuration for IPv6 DHCP binding. + // Note. This is ignored for IPV4 binding. + DhcpV6BindingConfig *DhcpV6BindingConfig `json:"dhcpV6BindingConfig,omitempty"` + + // DnsServers to be set on the host. Maximum 2 DNS, order is important. + DnsServers []string `json:"dnsServers,omitempty"` + + // IpAddress assigned to host. This address must belong to the subnet of Org VDC network. For + // IPv4, this is required. For IPv6, when not specified, Stateless Address Autoconfiguration + // (SLAAC) is used to auto-assign an IPv6 address to the DHCPv6 clients. + IpAddress string `json:"ipAddress"` + + // Lease time in seconds defines how long a DHCP IP will be leased out for. The minimum is 60s + // while the maximum is 4,294,967,295s, which is roughly 49,710 days. Default is 24 hours. + LeaseTime *int `json:"leaseTime,omitempty"` + + // Version describes the current version of the entity. To prevent clients from overwriting each + // other's changes, update operations must include the version which can be obtained by issuing + // a GET operation. If the version number on an update call is missing, the operation will be + // rejected. This is only needed on update calls. + Version OpenApiOrgVdcNetworkDhcpBindingVersion `json:"version"` +} + +// DhcpV4BindingConfig describes additional configuration for IPv6 DHCP Binding of an Org VDC +// Network. +type DhcpV4BindingConfig struct { + // GatewayIPAddress contains optional Gateway IP Address. When not specified, Gateway IP of Org + // vDC network will be used. + GatewayIPAddress string `json:"gatewayIpAddress,omitempty"` + // HostName to assign to the host. + HostName string `json:"hostName,omitempty"` +} + +// DhcpV6BindingConfig describes additional configuration for IPv6 DHCP Binding of an Org VDC +// Network. +type DhcpV6BindingConfig struct { + // DomainNames to be assigned to client host. + DomainNames []string `json:"domainNames,omitempty"` + + // SntpServers contains IP addresses of SNTP servers + SntpServers []string `json:"sntpServers,omitempty"` +} + +// OpenApiOrgVdcNetworkDhcpBindingVersion describes the current version of the entity. To prevent +// clients from overwriting each other's changes, update operations must include the version which +// can be obtained by issuing a GET operation. If the version number on an update call is missing, +// the operation will be rejected. This is only needed on update calls. +type OpenApiOrgVdcNetworkDhcpBindingVersion struct { + Version int `json:"version"` } // OpenApiOrgVdcNetworkDhcpIpRange is a type alias to fit naming @@ -229,3 +385,1581 @@ type OpenApiOrgVdcNetworkDhcpPools struct { // This applies for NSX-V Isolated network DefaultLeaseTime *int `json:"defaultLeaseTime,omitempty"` } + +// NsxtFirewallGroup allows users to set either SECURITY_GROUP or IP_SET which is defined by Type field. +// SECURITY_GROUP (constant types.FirewallGroupTypeSecurityGroup) is a dynamic structure which +// allows users to add Routed Org VDC networks +// +// IP_SET (constant FirewallGroupTypeIpSet) allows users to enter static IPs and later on firewall rules +// can be created both of these objects +// +// When the type is SECURITY_GROUP 'Members' field is used to specify Org VDC networks +// When the type is IP_SET 'IpAddresses' field is used to specify IP addresses or ranges +// field is used +type NsxtFirewallGroup struct { + // ID contains Firewall Group ID (URN format) + // e.g. urn:vcloud:firewallGroup:d7f4e0b4-b83f-4a07-9f22-d242c9c0987a + ID string `json:"id,omitempty"` + // Name of Firewall Group. Name are unique per 'Type'. There cannot be two SECURITY_GROUP or two + // IP_SET objects with the same name, but there can be one object of Type SECURITY_GROUP and one + // of Type IP_SET named the same. + Name string `json:"name"` + Description string `json:"description"` + // IP Addresses included in the group. This is only applicable for IP_SET Firewall Groups. This + // can support IPv4 and IPv6 addresses in single, range, and CIDR formats. + // E.g [ + // "12.12.12.1", + // "10.10.10.0/24", + // "11.11.11.1-11.11.11.2", + // "2001:db8::/48", + // "2001:db6:0:0:0:0:0:0-2001:db6:0:ffff:ffff:ffff:ffff:ffff", + // ], + IpAddresses []string `json:"ipAddresses,omitempty"` + + // Members define list of Org VDC networks belonging to this Firewall Group (only for Security + // groups ) + Members []OpenApiReference `json:"members,omitempty"` + + // VmCriteria (VCD 10.3+) defines list of dynamic criteria that determines whether a VM belongs + // to a dynamic firewall group. A VM needs to meet at least one criteria to belong to the + // firewall group. In other words, the logical AND is used for rules within a single criteria + // and the logical OR is used in between each criteria. This is only applicable for Dynamic + // Security Groups (VM_CRITERIA Firewall Groups). + VmCriteria []NsxtFirewallGroupVmCriteria `json:"vmCriteria,omitempty"` + + // OwnerRef replaces EdgeGatewayRef in API V35.0+ and can accept both - NSX-T Edge Gateway or a + // VDC group ID + // Sample VDC Group URN - urn:vcloud:vdcGroup:89a53000-ef41-474d-80dc-82431ff8a020 + // Sample Edge Gateway URN - urn:vcloud:gateway:71df3e4b-6da9-404d-8e44-0865751c1c38 + // + // Note. Using API V34.0 Firewall Groups can be created for VDC groups, but on a GET operation + // there will be no VDC group ID returned. + OwnerRef *OpenApiReference `json:"ownerRef,omitempty"` + + // EdgeGatewayRef is a deprecated field (use OwnerRef) for setting value, but during read the + // value is only populated in this field (not OwnerRef) + EdgeGatewayRef *OpenApiReference `json:"edgeGatewayRef,omitempty"` + + // Type is deprecated starting with API 36.0 (VCD 10.3+) + Type string `json:"type,omitempty"` + + // TypeValue replaces Type starting with API 36.0 (VCD 10.3+) and can be one of: + // SECURITY_GROUP, IP_SET, VM_CRITERIA(VCD 10.3+ only) + // Constants `types.FirewallGroupTypeSecurityGroup`, `types.FirewallGroupTypeIpSet`, + // `types.FirewallGroupTypeVmCriteria` can be used to set the value. + TypeValue string `json:"typeValue,omitempty"` +} + +// NsxtFirewallGroupVmCriteria defines list of rules where criteria represents boolean OR for +// matching There can be up to 3 criteria +type NsxtFirewallGroupVmCriteria struct { + // VmCriteria is a list of rules where each rule represents boolean AND for matching VMs + VmCriteriaRule []NsxtFirewallGroupVmCriteriaRule `json:"rules,omitempty"` +} + +// NsxtFirewallGroupVmCriteriaRule defines a single rule for matching VM +// There can be up to 4 rules in a single criteria +type NsxtFirewallGroupVmCriteriaRule struct { + AttributeType string `json:"attributeType,omitempty"` + AttributeValue string `json:"attributeValue,omitempty"` + Operator string `json:"operator,omitempty"` +} + +// NsxtFirewallGroupMemberVms is a structure to read NsxtFirewallGroup associated VMs when its type +// is SECURITY_GROUP +type NsxtFirewallGroupMemberVms struct { + VmRef *OpenApiReference `json:"vmRef"` + // VappRef will be empty if it is a standalone VM (although hidden vApp exists) + VappRef *OpenApiReference `json:"vappRef"` + VdcRef *OpenApiReference `json:"vdcRef"` + OrgRef *OpenApiReference `json:"orgRef"` +} + +// NsxtFirewallRule defines single NSX-T Firewall Rule +type NsxtFirewallRule struct { + // ID contains UUID (e.g. d0bf5d51-f83a-489a-9323-1661024874b8) + ID string `json:"id,omitempty"` + // Name - API does not enforce uniqueness + Name string `json:"name"` + // Action field. Can be 'ALLOW', 'DROP' + // Deprecated in favor of ActionValue in VCD 10.2.2+ (API V35.2) + Action string `json:"action,omitempty"` + + // ActionValue replaces deprecated field Action and defines action to be applied to all the + // traffic that meets the firewall rule criteria. It determines if the rule permits or blocks + // traffic. Property is required if action is not set. Below are valid values: + // * ALLOW permits traffic to go through the firewall. + // * DROP blocks the traffic at the firewall. No response is sent back to the source. + // * REJECT blocks the traffic at the firewall. A response is sent back to the source. + ActionValue string `json:"actionValue,omitempty"` + + // Enabled allows to enable or disable the rule + Enabled bool `json:"enabled"` + // SourceFirewallGroups contains a list of references to Firewall Groups. Empty list means 'Any' + SourceFirewallGroups []OpenApiReference `json:"sourceFirewallGroups,omitempty"` + // DestinationFirewallGroups contains a list of references to Firewall Groups. Empty list means 'Any' + DestinationFirewallGroups []OpenApiReference `json:"destinationFirewallGroups,omitempty"` + // ApplicationPortProfiles contains a list of references to Application Port Profiles. Empty list means 'Any' + ApplicationPortProfiles []OpenApiReference `json:"applicationPortProfiles,omitempty"` + // IpProtocol 'IPV4', 'IPV6', 'IPV4_IPV6' + IpProtocol string `json:"ipProtocol"` + Logging bool `json:"logging"` + // Direction 'IN_OUT', 'OUT', 'IN' + Direction string `json:"direction"` + // Version of firewall rule. Must not be set when creating. + Version *struct { + // Version is incremented after each update + Version *int `json:"version,omitempty"` + } `json:"version,omitempty"` +} + +// NsxtFirewallRuleContainer wraps NsxtFirewallRule for user-defined and default and system Firewall Rules suitable for +// API. Only UserDefinedRules are writeable. Others are read-only. +type NsxtFirewallRuleContainer struct { + // SystemRules contain ordered list of system defined edge firewall rules. System rules are applied before user + // defined rules in the order in which they are returned. + SystemRules []*NsxtFirewallRule `json:"systemRules"` + // DefaultRules contain ordered list of user defined edge firewall rules. Users are allowed to add/modify/delete rules + // only to this list. + DefaultRules []*NsxtFirewallRule `json:"defaultRules"` + // UserDefinedRules ordered list of default edge firewall rules. Default rules are applied after the user defined + // rules in the order in which they are returned. + UserDefinedRules []*NsxtFirewallRule `json:"userDefinedRules"` +} + +// NsxtAppPortProfile allows user to set custom application port definitions so that these can later be used +// in NSX-T Firewall rules in combination with IP Sets and Security Groups. +type NsxtAppPortProfile struct { + ID string `json:"id,omitempty"` + // Name must be unique per Scope + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + // ApplicationPorts contains one or more protocol and port definitions + ApplicationPorts []NsxtAppPortProfilePort `json:"applicationPorts,omitempty"` + // OrgRef must contain at least Org ID when SCOPE==TENANT + OrgRef *OpenApiReference `json:"orgRef,omitempty"` + // ContextEntityId must contain: + // * NSX-T Manager URN (when scope==PROVIDER) + // * VDC or VDC Group ID (when scope==TENANT) + ContextEntityId string `json:"contextEntityId,omitempty"` + // Scope can be one of the following: + // * SYSTEM - Read-only (The ones that are provided by SYSTEM). Constant `types.ApplicationPortProfileScopeSystem` + // * PROVIDER - Created by Provider on a particular network provider (NSX-T manager). Constant `types.ApplicationPortProfileScopeProvider` + // * TENANT (Created by Tenant at Org VDC level). Constant `types.ApplicationPortProfileScopeTenant` + // + // When scope==PROVIDER: + // OrgRef is not required + // ContextEntityId must have NSX-T Managers URN + // When scope==TENANT + // OrgRef ID must be specified + // ContextEntityId must be set to VDC or VDC group URN + Scope string `json:"scope,omitempty"` +} + +// NsxtAppPortProfilePort allows user to set protocol and one or more ports +type NsxtAppPortProfilePort struct { + // Protocol can be one of the following: + // * "ICMPv4" + // * "ICMPv6" + // * "TCP" + // * "UDP" + Protocol string `json:"protocol"` + // DestinationPorts is optional, but can define list of ports ("1000", "1500") or port ranges ("1200-1400") + DestinationPorts []string `json:"destinationPorts,omitempty"` +} + +// NsxtNatRule describes a single NAT rule of 4 diferent RuleTypes - DNAT`, `NO_DNAT`, `SNAT`, `NO_SNAT`. +// +// A SNAT or a DNAT rule on an Edge Gateway in the VMware Cloud Director environment, you always configure the rule +// from the perspective of your organization VDC. +// DNAT and NO_DNAT - outside traffic going inside +// SNAT and NO_SNAT - inside traffic going outside +// More docs in https://docs.vmware.com/en/VMware-Cloud-Director/10.2/VMware-Cloud-Director-Tenant-Portal-Guide/GUID-9E43E3DC-C028-47B3-B7CA-59F0ED40E0A6.html +type NsxtNatRule struct { + ID string `json:"id,omitempty"` + // Name holds a meaningful name for the rule. (API does not enforce uniqueness) + Name string `json:"name"` + // Description holds optional description for the rule + Description string `json:"description"` + // Enabled defines if the rule is active + Enabled bool `json:"enabled"` + + // RuleType - one of the following: `DNAT`, `NO_DNAT`, `SNAT`, `NO_SNAT` + // * An SNAT rule translates an internal IP to an external IP and is used for outbound traffic + // * A NO SNAT rule prevents the translation of the internal IP address of packets sent from an organization VDC out + // to an external network or to another organization VDC network. + // * A DNAT rule translates the external IP to an internal IP and is used for inbound traffic. + // * A NO DNAT rule prevents the translation of the external IP address of packets received by an organization VDC + // from an external network or from another organization VDC network. + // Deprecated in API V36.0 + RuleType string `json:"ruleType,omitempty"` + // Type replaces RuleType in V36.0 and adds a new Rule - REFLEXIVE + Type string `json:"type,omitempty"` + + // ExternalAddresses + // * SNAT - enter the public IP address of the edge gateway for which you are configuring the SNAT rule. + // * NO_SNAT - leave empty (but field cannot be skipped at all, therefore it does not have 'omitempty' tag) + // + // * DNAT - public IP address of the edge gateway for which you are configuring the DNAT rule. The IP + // addresses that you enter must belong to the suballocated IP range of the edge gateway. + // * NO_DNAT - leave empty + ExternalAddresses string `json:"externalAddresses"` + + // InternalAddresses + // * SNAT - the IP address or a range of IP addresses of the virtual machines for which you are configuring SNAT, so + // that they can send traffic to the external network. + // + // * DNAT - enter the IP address or a range of IP addresses of the virtual machines for which you are configuring + // DNAT, so that they can receive traffic from the external network. + // * NO_DNAT - leave empty + InternalAddresses string `json:"internalAddresses"` + ApplicationPortProfile *OpenApiReference `json:"applicationPortProfile,omitempty"` + + // InternalPort specifies port number or port range for incoming network traffic. If Any Traffic is selected for the + // Application Port Profile, the default internal port is "ANY". + // Deprecated since API V35.0 and is replaced by DnatExternalPort + InternalPort string `json:"internalPort,omitempty"` + + // DnatExternalPort can set a port into which the DNAT rule is translating for the packets inbound to the virtual + // machines. + DnatExternalPort string `json:"dnatExternalPort,omitempty"` + + // SnatDestinationAddresses applies only for RuleTypes `SNAT`, `NO_SNAT` + // If you want the rule to apply only for traffic to a specific domain, enter an IP address for this domain or an IP + // address range in CIDR format. If you leave this text box blank, the SNAT rule applies to all destinations outside + // of the local subnet. + SnatDestinationAddresses string `json:"snatDestinationAddresses,omitempty"` + + // Logging enabled or disabled logging of that rule + Logging bool `json:"logging"` + + // Below two fields are only supported in VCD 10.2.2+ (API v35.2) + + // FirewallMatch determines how the firewall matches the address during NATing if firewall stage is not skipped. + // * MATCH_INTERNAL_ADDRESS indicates the firewall will be applied to internal address of a NAT rule. For SNAT, the + // internal address is the original source address before NAT is done. For DNAT, the internal address is the translated + // destination address after NAT is done. For REFLEXIVE, to egress traffic, the internal address is the original + // source address before NAT is done; to ingress traffic, the internal address is the translated destination address + // after NAT is done. + // * MATCH_EXTERNAL_ADDRESS indicates the firewall will be applied to external address of a NAT rule. For SNAT, the + // external address is the translated source address after NAT is done. For DNAT, the external address is the original + // destination address before NAT is done. For REFLEXIVE, to egress traffic, the external address is the translated + // internal address after NAT is done; to ingress traffic, the external address is the original destination address + // before NAT is done. + // * BYPASS firewall stage will be skipped. + FirewallMatch string `json:"firewallMatch,omitempty"` + // Priority helps to select rule with highest priority if an address has multiple NAT rules. A lower value means a + // higher precedence for this rule. Maximum value 2147481599 + Priority *int `json:"priority,omitempty"` + + // Version of NAT rule. Must not be set when creating. + Version *struct { + // Version is incremented after each update + Version *int `json:"version,omitempty"` + } `json:"version,omitempty"` +} + +// NsxtIpSecVpnTunnel defines the IPsec VPN Tunnel configuration +// Some of the fields like AuthenticationMode and ConnectorInitiationMode are meant for future, because they have only +// one default value at the moment. +type NsxtIpSecVpnTunnel struct { + // ID unique for IPsec VPN tunnel. On updates, the ID is required for the tunnel, while for create a new ID will be + // generated. + ID string `json:"id,omitempty"` + // Name for the IPsec VPN Tunnel + Name string `json:"name"` + // Description for the IPsec VPN Tunnel + Description string `json:"description,omitempty"` + // Enabled describes whether the IPsec VPN Tunnel is enabled or not. The default is true. + Enabled bool `json:"enabled"` + // LocalEndpoint which corresponds to the Edge Gateway the IPsec VPN Tunnel is being configured on. Local Endpoint + // requires an IP. That IP must be sub-allocated to the edge gateway + LocalEndpoint NsxtIpSecVpnTunnelLocalEndpoint `json:"localEndpoint"` + // RemoteEndpoint corresponds to the device on the remote site terminating the VPN tunnel + RemoteEndpoint NsxtIpSecVpnTunnelRemoteEndpoint `json:"remoteEndpoint"` + // PreSharedKey is key used for authentication. It must be the same on the other end of IPsec VPN Tunnel + PreSharedKey string `json:"preSharedKey"` + // SecurityType is the security type used for the IPsec VPN Tunnel. If nothing is specified, this will be set to + // DEFAULT in which the default settings in NSX will be used. For custom settings, one should use the + // NsxtIpSecVpnTunnelSecurityProfile and UpdateTunnelConnectionProperties(), GetTunnelConnectionProperties() endpoint to + // specify custom settings. The security type will then appropriately reflect itself as CUSTOM. + // To revert back to system default, this field must be set to "DEFAULT" + SecurityType string `json:"securityType,omitempty"` + // Logging sets whether logging for the tunnel is enabled or not. The default is false. + Logging bool `json:"logging"` + + // AuthenticationMode is authentication mode this IPsec tunnel will use to authenticate with the peer endpoint. The + // default is a pre-shared key (PSK). + // * PSK - A known key is shared between each site before the tunnel is established. + // * CERTIFICATE - Incoming connections are required to present an identifying digital certificate, which VCD verifies + // has been signed by a trusted certificate authority. + // + // Note. Up to version 10.3 VCD only supports PSK + AuthenticationMode string `json:"authenticationMode,omitempty"` + + // ConnectorInitiationMode is the mode used by the local endpoint to establish an IKE Connection with the remote site. + // The default is INITIATOR. + // Possible values are: INITIATOR , RESPOND_ONLY , ON_DEMAND + // + // Note. Up to version 10.3 VCD only supports INITIATOR + ConnectorInitiationMode string `json:"connectorInitiationMode,omitempty"` + + // CertificateRef points server certificate which will be used to secure the tunnel's local + // endpoint. The certificate must be the end-entity certificate (leaf) for the local endpoint. + CertificateRef *OpenApiReference `json:"certificateRef,omitempty"` + + // CaCertificateRef points to certificate authority used to verify the remote endpoint's + // certificate. The selected CA must be a root or intermediate CA. The selected CA should be + // able to directly verify the remote endpoint's certificate. + CaCertificateRef *OpenApiReference `json:"caCertificateRef,omitempty"` + + // Version of IPsec VPN Tunnel configuration. Must not be set when creating, but required for updates + Version *struct { + // Version is incremented after each update + Version *int `json:"version,omitempty"` + } `json:"version,omitempty"` +} + +// NsxtIpSecVpnTunnelLocalEndpoint which corresponds to the Edge Gateway the IPsec VPN Tunnel is being configured on. +// Local Endpoint requires an IP. That IP must be sub-allocated to the edge gateway +type NsxtIpSecVpnTunnelLocalEndpoint struct { + // LocalId is the optional local identifier for the endpoint. It is usually the same as LocalAddress + LocalId string `json:"localId,omitempty"` + // LocalAddress is the IPv4 Address for the endpoint. This has to be a sub-allocated IP on the Edge Gateway. This is + // required + LocalAddress string `json:"localAddress"` + // LocalNetworks is the list of local networks. These must be specified in normal Network CIDR format. At least one is + // required + LocalNetworks []string `json:"localNetworks,omitempty"` +} + +// NsxtIpSecVpnTunnelRemoteEndpoint corresponds to the device on the remote site terminating the VPN tunnel +type NsxtIpSecVpnTunnelRemoteEndpoint struct { + // This Remote ID is needed to uniquely identify the peer site. If the remote ID is not set, it + // will default to the remote IP address. The requirement for remote id depends on the + // authentication mode for the tunnel: + // * PSK - The Remote ID is the public IP Address of the remote device terminating the VPN + // Tunnel. When NAT is configured on the Remote ID, enter the private IP Address of the Remote + // Site. + // * CERTIFICATE - The remote ID needs to match the certificate SAN (Subject Alternative Name) + // if available. If the remote certificate does not contain a SAN, the remote ID must match the + // the distinguished name of the certificate used to secure the remote endpoint (for example, + // C=US,ST=Massachusetts,O=VMware,OU=VCD,CN=Edge1). + RemoteId string `json:"remoteId,omitempty"` + // RemoteAddress is IPv4 Address of the remote endpoint on the remote site. This is the Public IPv4 Address of the + // remote device terminating the IPsec VPN Tunnel connection. This is required + RemoteAddress string `json:"remoteAddress"` + // RemoteNetworks is the list of remote networks. These must be specified in normal Network CIDR format. + // Specifying no value is interpreted as 0.0.0.0/0 + RemoteNetworks []string `json:"remoteNetworks,omitempty"` +} + +// NsxtIpSecVpnTunnelStatus helps to read IPsec VPN Tunnel Status +type NsxtIpSecVpnTunnelStatus struct { + // TunnelStatus gives the overall IPsec VPN Tunnel Status. If IKE is properly set and the tunnel is up, the tunnel + // status will be UP + TunnelStatus string `json:"tunnelStatus"` + IkeStatus struct { + // IkeServiceStatus status for the actual IKE Session for the given tunnel. + IkeServiceStatus string `json:"ikeServiceStatus"` + // FailReason contains more details of failure if the IKE service is not UP + FailReason string `json:"failReason"` + } `json:"ikeStatus"` +} + +// NsxtIpSecVpnTunnelSecurityProfile specifies the given security profile/connection properties of a given IP Sec VPN +// Tunnel, such as Dead Probe Interval and IKE settings. If a security type is set to 'CUSTOM', then ike, tunnel, and/or +// dpd configurations can be specified. Otherwise, those fields are read only and are set to the values based on the +// specific security type. +type NsxtIpSecVpnTunnelSecurityProfile struct { + // SecurityType is the security type used for the IPSec Tunnel. If nothing is specified, this will be set to DEFAULT + // in which the default settings in NSX will be used. If CUSTOM is specified, then IKE, Tunnel, and DPD + // configurations can be set. + // To "RESET" configuration to DEFAULT, the NsxtIpSecVpnTunnel.SecurityType field should be changed instead of this + SecurityType string `json:"securityType,omitempty"` + // IkeConfiguration is the IKE Configuration to be used for the tunnel. If nothing is explicitly set, the system + // defaults will be used. + IkeConfiguration NsxtIpSecVpnTunnelProfileIkeConfiguration `json:"ikeConfiguration,omitempty"` + // TunnelConfiguration contains parameters such as encryption algorithm to be used. If nothing is explicitly set, + // the system defaults will be used. + TunnelConfiguration NsxtIpSecVpnTunnelProfileTunnelConfiguration `json:"tunnelConfiguration,omitempty"` + // DpdConfiguration contains Dead Peer Detection configuration. If nothing is explicitly set, the system defaults + // will be used. + DpdConfiguration NsxtIpSecVpnTunnelProfileDpdConfiguration `json:"dpdConfiguration,omitempty"` +} + +// NsxtIpSecVpnTunnelProfileIkeConfiguration is the Internet Key Exchange (IKE) profiles provide information about the +// algorithms that are used to authenticate, encrypt, and establish a shared secret between network sites when you +// establish an IKE tunnel. +// +// Note. While quite a few fields accepts a []string it actually supports single values only. +type NsxtIpSecVpnTunnelProfileIkeConfiguration struct { + // IkeVersion IKE Protocol Version to use. + // The default is IKE_V2. + // + // Possible values are: IKE_V1 , IKE_V2 , IKE_FLEX + IkeVersion string `json:"ikeVersion"` + // EncryptionAlgorithms contains list of Encryption algorithms for IKE. This is used during IKE negotiation. + // Default is AES_128. + // + // Possible values are: AES_128 , AES_256 , AES_GCM_128 , AES_GCM_192 , AES_GCM_256 + EncryptionAlgorithms []string `json:"encryptionAlgorithms"` + // DigestAlgorithms contains list of Digest algorithms - secure hashing algorithms to use during the IKE negotiation. + // + // Default is SHA2_256. + // + // Possible values are: SHA1 , SHA2_256 , SHA2_384 , SHA2_512 + DigestAlgorithms []string `json:"digestAlgorithms"` + // DhGroups contains list of Diffie-Hellman groups to be used if Perfect Forward Secrecy is enabled. These are + // cryptography schemes that allows the peer site and the edge gateway to establish a shared secret over an insecure + // communications channel + // + // Default is GROUP14. + // + // Possible values are: GROUP2, GROUP5, GROUP14, GROUP15, GROUP16, GROUP19, GROUP20, GROUP21 + DhGroups []string `json:"dhGroups"` + // SaLifeTime is the Security Association life time in seconds. It is number of seconds before the IPsec tunnel needs + // to reestablish + // + // Default is 86400 seconds (1 day). + SaLifeTime *int `json:"saLifeTime"` +} + +// NsxtIpSecVpnTunnelProfileTunnelConfiguration adjusts IPsec VPN Tunnel settings +// +// Note. While quite a few fields accepts a []string it actually supports single values only. +type NsxtIpSecVpnTunnelProfileTunnelConfiguration struct { + // PerfectForwardSecrecyEnabled enabled or disabled. PFS (Perfect Forward Secrecy) ensures the same key will not be + // generated and used again, and because of this, the VPN peers negotiate a new Diffie-Hellman key exchange. This + // would ensure if a hacker\criminal was to compromise the private key, they would only be able to access data in + // transit protected by that key. Any future data will not be compromised, as future data would not be associated + // with that compromised key. Both sides of the VPN must be able to support PFS in order for PFS to work. + // + // The default value is true. + PerfectForwardSecrecyEnabled bool `json:"perfectForwardSecrecyEnabled"` + // DfPolicy Policy for handling defragmentation bit. The default is COPY. + // + // Possible values are: COPY, CLEAR + // * COPY Copies the defragmentation bit from the inner IP packet to the outer packet. + // * CLEAR Ignores the defragmentation bit present in the inner packet. + DfPolicy string `json:"dfPolicy"` + + // EncryptionAlgorithms contains list of Encryption algorithms to use in IPSec tunnel establishment. + // Default is AES_GCM_128. + // * NO_ENCRYPTION_AUTH_AES_GMAC_XX (XX is 128, 192, 256) enables authentication on input data without encryption. + // If one of these options is used, digest algorithm should be empty. + // + // Possible values are: AES_128, AES_256, AES_GCM_128, AES_GCM_192, AES_GCM_256, NO_ENCRYPTION_AUTH_AES_GMAC_128, + // NO_ENCRYPTION_AUTH_AES_GMAC_192, NO_ENCRYPTION_AUTH_AES_GMAC_256, NO_ENCRYPTION + EncryptionAlgorithms []string `json:"encryptionAlgorithms"` + + // DigestAlgorithms contains list of Digest algorithms to be used for message digest. The default digest algorithm is + // implicitly covered by default encryption algorithm AES_GCM_128. + // + // Possible values are: SHA1 , SHA2_256 , SHA2_384 , SHA2_512 + // Note. Only one value can be set inside the slice + DigestAlgorithms []string `json:"digestAlgorithms"` + + // DhGroups contains list of Diffie-Hellman groups to be used is PFS is enabled. Default is GROUP14. + // + // Possible values are: GROUP2, GROUP5, GROUP14, GROUP15, GROUP16, GROUP19, GROUP20, GROUP21 + // Note. Only one value can be set inside the slice + DhGroups []string `json:"dhGroups"` + + // SaLifeTime is the Security Association life time in seconds. + // + // Default is 3600 seconds. + SaLifeTime *int `json:"saLifeTime"` +} + +// NsxtIpSecVpnTunnelProfileDpdConfiguration specifies the Dead Peer Detection Profile. This configurations determines +// the number of seconds to wait in time between probes to detect if an IPSec peer is alive or not. The default value +// for the DPD probe interval is 60 seconds. +type NsxtIpSecVpnTunnelProfileDpdConfiguration struct { + // ProbeInternal is value of the probe interval in seconds. This defines a periodic interval for DPD probes. The + // minimum is 3 seconds and the maximum is 60 seconds. + ProbeInterval int `json:"probeInterval"` +} + +// NsxtL2VpnTunnel defines the L2 VPN Tunnel configuration +type NsxtL2VpnTunnel struct { + // ID of the tunnel + ID string `json:"id"` + // Name and description of the tunnel + Name string `json:"name"` + Description string `json:"description"` + + // The current session mode, one of either SERVER or CLIENT. + // * SERVER - In which the edge gateway acts as the server side of the L2 VPN tunnel and generates peer codes to distribute to client sessions. + // * CLIENT - In which the edge gateway receives peer codes from the server side of the L2 VPN tunnel to establish a connection. + SessionMode string `json:"sessionMode"` + + // State of the tunnel. Default is true + Enabled bool `json:"enabled"` + + // The IP address of the local endpoint, which corresponds to the Edge Gateway the tunnel is being configured on. + LocalEndpointIp string `json:"localEndpointIP"` + + // The IP address of the remote endpoint, which corresponds to the device on the remote site terminating the VPN tunnel. + RemoteEndpointIp string `json:"remoteEndpointIP"` + + // The network CIDR block over which the session interfaces. Relevant only + // for SERVER session modes. If provided, the underlying IPSec tunnel will + // use the specified tunnel interface. If not provided, Cloud Director will + // attempt to automatically allocate a tunnel interface. + TunnelInterface string `json:"tunnelInterface,omitempty"` + + // This is the mode used by the local endpoint to establish an IKE Connection with the remote site. The default is INITIATOR. + // * INITIATOR - Local endpoint initiates tunnel setup and will also respond to incoming tunnel setup requests from the peer gateway. + // * RESPOND_ONLY - Local endpoint shall only respond to incoming tunnel setup requests, it shall not initiate the tunnel setup. + // * ON_DEMAND - In this mode local endpoint will initiate tunnel creation once first packet matching the policy rule is received, and will also respond to incoming initiation requests. + ConnectorInitiationMode string `json:"connectorInitiationMode"` + + // This property is a base64 encoded string of the full configuration for the tunnel, + // generated by the server-side L2 VPN session. An L2 VPN client session must receive + // and validate this string in order to successfully establish a tunnel, but be careful + // sharing or storing this code since it does contain the encoded PSK. + // Leave this property blank if this call is being used to establish a server-side session. + PeerCode string `json:"peerCode"` + + // This is the Pre-shared key used for authentication, no specific format is required. Relevant only for SERVER session modes. + PreSharedKey string `json:"preSharedKey"` + + // The list of OrgVDC Network entity references which are currently attached to this L2VPN tunnel. + StretchedNetworks []EdgeL2VpnStretchedNetwork `json:"stretchedNetworks,omitempty"` + + // Whether logging for the tunnel is enabled or not. + Logging bool `json:"logging"` + + // Version of the entity, needs to be provided on tunnel update calls, can be retrieved by getting the tunnel. + Version VersionField `json:"version"` +} + +// EdgeL2VpnStretchedNetwork specifies the Org vDC network that is stretched through the given L2 VPN Tunnel. +type EdgeL2VpnStretchedNetwork struct { + NetworkRef OpenApiReference `json:"networkRef"` + TunnelID int `json:"tunnelId"` +} + +// EdgeL2VpnTunnelStatistics specifies the statistics for the given L2 VPN Tunnel. +type EdgeL2VpnTunnelStatistics struct { + BumBytesIn int `json:"bumBytesIn"` + BumBytesOut int `json:"bumBytesOut"` + BumPacketsIn int `json:"bumPacketsIn"` + BumPacketsOut int `json:"bumPacketsOut"` + BytesIn int `json:"bytesIn"` + BytesOut int `json:"bytesOut"` + PacketsIn int `json:"packetsIn"` + PacketsOut int `json:"packetsOut"` + PacketsReceiveError int `json:"packetsReceiveError"` + PacketsSentError int `json:"packetsSentError"` + SegmentPath string `json:"segmentPath"` +} + +// EdgeL2VpnTunnelStatus includes the L2 VPN tunnel status such as whether +// the tunnel is up or down and the IKE Session status +type EdgeL2VpnTunnelStatus struct { + FailureReason string `json:"failureReason"` + RuntimeStatus string `json:"runtimeStatus"` +} + +// NsxtAlbController helps to integrate VMware Cloud Director with NSX-T Advanced Load Balancer deployment. +// Controller instances are registered with VMware Cloud Director instance. Controller instances serve as a central +// control plane for the load-balancing services provided by NSX-T Advanced Load Balancer. +// To configure an NSX-T ALB one needs to supply AVI Controller endpoint, credentials and license to be used. +type NsxtAlbController struct { + // ID holds URN for load balancer controller (e.g. urn:vcloud:loadBalancerController:aa23ef66-ba32-48b2-892f-7acdffe4587e) + ID string `json:"id,omitempty"` + // Name as shown in VCD + Name string `json:"name"` + // Description as shown in VCD + Description string `json:"description,omitempty"` + // Url of ALB controller + Url string `json:"url"` + // Username of user + Username string `json:"username"` + // Password (will not be returned on read) + Password string `json:"password,omitempty"` + // LicenseType By enabling this feature, the provider acknowledges that they have independently licensed the + // enterprise version of the NSX AVI LB. + // Possible options: 'BASIC', 'ENTERPRISE' + // This field was removed since VCD 10.4.0 (v37.0) in favor of NsxtAlbServiceEngineGroup.SupportedFeatureSet + LicenseType string `json:"licenseType,omitempty"` + // Version of ALB (e.g. 20.1.3). Read-only + Version string `json:"version,omitempty"` +} + +// NsxtAlbImportableCloud allows user to list importable NSX-T ALB Clouds. Each importable cloud can only be imported +// once. It has a flag AlreadyImported which hints if it is already consumed or not. +type NsxtAlbImportableCloud struct { + // ID (e.g. 'cloud-43726181-f73e-41f2-bf1d-8a9609502586') + ID string `json:"id"` + + DisplayName string `json:"displayName"` + // AlreadyImported shows if this ALB Cloud is already imported + AlreadyImported bool `json:"alreadyImported"` + + // NetworkPoolRef contains a reference to NSX-T network pool + NetworkPoolRef OpenApiReference `json:"networkPoolRef"` + + // TransportZoneName contains transport zone name + TransportZoneName string `json:"transportZoneName"` +} + +// NsxtAlbCloud helps to use the virtual infrastructure provided by NSX Advanced Load Balancer, register NSX-T Cloud +// instances with VMware Cloud Director by consuming NsxtAlbImportableCloud. +type NsxtAlbCloud struct { + // ID (e.g. 'urn:vcloud:loadBalancerCloud:947ea2ba-e448-4249-91f7-1432b3d2fcbf') + ID string `json:"id,omitempty"` + Status string `json:"status,omitempty"` + // Name of NSX-T ALB Cloud + Name string `json:"name"` + // Description of NSX-T ALB Cloud + Description string `json:"description,omitempty"` + // LoadBalancerCloudBacking uniquely identifies a Load Balancer Cloud configured within a Load Balancer Controller. At + // the present, VCD only supports NSX-T Clouds configured within an NSX-ALB Controller deployment. + LoadBalancerCloudBacking NsxtAlbCloudBacking `json:"loadBalancerCloudBacking"` + // NetworkPoolRef for the Network Pool associated with this Cloud + NetworkPoolRef *OpenApiReference `json:"networkPoolRef"` + // HealthStatus contains status of the Load Balancer Cloud. Possible values are: + // UP - The cloud is healthy and ready to enable Load Balancer for an Edge Gateway. + // DOWN - The cloud is in a failure state. Enabling Load balancer on an Edge Gateway may not be possible. + // RUNNING - The cloud is currently processing. An example is if it's enabling a Load Balancer for an Edge Gateway. + // UNAVAILABLE - The cloud is unavailable. + // UNKNOWN - The cloud state is unknown. + HealthStatus string `json:"healthStatus,omitempty"` + // DetailedHealthMessage contains detailed message on the health of the Cloud. + DetailedHealthMessage string `json:"detailedHealthMessage,omitempty"` +} + +// NsxtAlbCloudBacking is embedded into NsxtAlbCloud +type NsxtAlbCloudBacking struct { + // BackingId is the ID of NsxtAlbImportableCloud + BackingId string `json:"backingId"` + // BackingType contains type of ALB (The only supported now is 'NSXALB_NSXT') + BackingType string `json:"backingType,omitempty"` + // LoadBalancerControllerRef contains reference to NSX-T ALB Controller + LoadBalancerControllerRef OpenApiReference `json:"loadBalancerControllerRef"` +} + +// NsxtAlbServiceEngineGroup provides virtual service management capabilities for tenants. This entity can be created +// by referencing a backing importable service engine group - NsxtAlbImportableServiceEngineGroups. +// +// A service engine group is an isolation domain that also defines shared service engine properties, such as size, +// network access, and failover. Resources in a service engine group can be used for different virtual services, +// depending on your tenant needs. These resources cannot be shared between different service engine groups. +type NsxtAlbServiceEngineGroup struct { + // ID of the Service Engine Group + ID string `json:"id,omitempty"` + // Name of the Service Engine Group + Name string `json:"name"` + // Description of the Service Engine Group + Description string `json:"description"` + // ServiceEngineGroupBacking holds backing details that uniquely identifies a Load Balancer Service Engine Group + // configured within a load balancer cloud. + ServiceEngineGroupBacking ServiceEngineGroupBacking `json:"serviceEngineGroupBacking"` + // HaMode defines High Availability Mode for Service Engine Group + // * ELASTIC_N_PLUS_M_BUFFER - Service Engines will scale out to N active nodes with M nodes as buffer. + // * ELASTIC_ACTIVE_ACTIVE - Active-Active with scale out. + // * LEGACY_ACTIVE_STANDBY - Traditional single Active-Standby configuration + HaMode string `json:"haMode,omitempty"` + // ReservationType can be `DEDICATED` or `SHARED` + // * DEDICATED - Dedicated to a single Edge Gateway and can only be assigned to a single Edge Gateway + // * SHARED - Shared between multiple Edge Gateways. Can be assigned to multiple Edge Gateways + ReservationType string `json:"reservationType"` + // MaxVirtualServices holds maximum number of virtual services supported on the Load Balancer Service Engine Group + MaxVirtualServices *int `json:"maxVirtualServices,omitempty"` + // NumDeployedVirtualServices shows number of virtual services currently deployed on the Load Balancer Service Engine + // Group + NumDeployedVirtualServices *int `json:"numDeployedVirtualServices,omitempty"` + // ReservedVirtualServices holds number of virtual services already reserved on the Load Balancer Service Engine Group. + // This value is the sum of the guaranteed virtual services given to Edge Gateways assigned to the Load Balancer + // Service Engine Group. + ReservedVirtualServices *int `json:"reservedVirtualServices,omitempty"` + // OverAllocated indicates whether the maximum number of virtual services supported on the Load Balancer Service + // Engine Group has been surpassed by the current number of reserved virtual services. + OverAllocated *bool `json:"overAllocated,omitempty"` + // SupportedFeatureSet was added in VCD 10.4.0 (v37.0) as substitute of NsxtAlbController.LicenseType. + // Possible values are: "STANDARD", "PREMIUM". + SupportedFeatureSet string `json:"supportedFeatureSet,omitempty"` +} + +type ServiceEngineGroupBacking struct { + BackingId string `json:"backingId"` + BackingType string `json:"backingType,omitempty"` + LoadBalancerCloudRef *OpenApiReference `json:"loadBalancerCloudRef"` +} + +// NsxtAlbImportableServiceEngineGroups provides capability to list all Importable Service Engine Groups available in +// ALB Controller so that they can be consumed by NsxtAlbServiceEngineGroup +// +// Note. The API does not return Importable Service Engine Group once it is consumed. +type NsxtAlbImportableServiceEngineGroups struct { + // ID (e.g. 'serviceenginegroup-b633f16f-2733-4bf5-b552-3a6c4949caa4') + ID string `json:"id"` + // DisplayName is the name of + DisplayName string `json:"displayName"` + // HaMode (e.g. 'ELASTIC_N_PLUS_M_BUFFER') + HaMode string `json:"haMode"` +} + +// NsxtAlbConfig describes Load Balancer Service configuration on an NSX-T Edge Gateway +type NsxtAlbConfig struct { + // Enabled is a mandatory flag indicating whether Load Balancer Service is enabled or not + Enabled bool `json:"enabled"` + // LicenseType of the backing Load Balancer Cloud. + // * BASIC - Basic edition of the NSX Advanced Load Balancer. + // * ENTERPRISE - Full featured edition of the NSX Advanced Load Balancer. + // This field was removed since VCD 10.4.0 (v37.0) in favor of NsxtAlbConfig.SupportedFeatureSet + LicenseType string `json:"licenseType,omitempty"` + // SupportedFeatureSet was added in VCD 10.4.0 (v37.0) as substitute of NsxtAlbConfig.LicenseType. + // Possible values are: "STANDARD", "PREMIUM". + SupportedFeatureSet string `json:"supportedFeatureSet,omitempty"` + // LoadBalancerCloudRef + LoadBalancerCloudRef *OpenApiReference `json:"loadBalancerCloudRef,omitempty"` + // ServiceNetworkDefinition in Gateway CIDR format which will be used by Load Balancer service. All the load balancer + // service engines associated with the Service Engine Group will be attached to this network. The subnet prefix length + // must be 25. If nothing is set, the default is 192.168.255.1/25. Default CIDR can be configured. This field cannot + // be updated. + ServiceNetworkDefinition string `json:"serviceNetworkDefinition,omitempty"` + + // The IPv6 network definition in Gateway CIDR format which will be used by Load Balancer + // service on Edge. All the load balancer service engines associated with the Service Engine + // Group will be attached to this network. + // Once set, this field cannot be updated. The default + // IPv4 service network will be used if both the serviceNetworkDefinition and + // ipv6ServiceNetworkDefinition properties are unset. If both are set, it will still be one + // service network with a dual IPv4 and IPv6 stack. + // This field is only available for VCD 10.4.0+ (v37.0+) + Ipv6ServiceNetworkDefinition string `json:"ipv6ServiceNetworkDefinition,omitempty"` + + // TransparentModeEnabled allows to configure Preserve Client IP on a Virtual Service + // This field is only available for VCD 10.4.1+ (v37.1+) + TransparentModeEnabled *bool `json:"transparentModeEnabled,omitempty"` +} + +// NsxtAlbServiceEngineGroupAssignment configures Service Engine Group assignments to Edge Gateway. The only mandatory +// fields are `GatewayRef` and `ServiceEngineGroupRef`. `MinVirtualServices` and `MaxVirtualServices` are only available +// for SHARED Service Engine Groups. +type NsxtAlbServiceEngineGroupAssignment struct { + ID string `json:"id,omitempty"` + // GatewayRef contains reference to Edge Gateway + GatewayRef *OpenApiReference `json:"gatewayRef"` + // ServiceEngineGroupRef contains a reference to Service Engine Group + ServiceEngineGroupRef *OpenApiReference `json:"serviceEngineGroupRef"` + // GatewayOrgRef optional Org reference for gateway + GatewayOrgRef *OpenApiReference `json:"gatewayOrgRef,omitempty"` + // GatewayOwnerRef can be a VDC or VDC group + GatewayOwnerRef *OpenApiReference `json:"gatewayOwnerRef,omitempty"` + MaxVirtualServices *int `json:"maxVirtualServices,omitempty"` + MinVirtualServices *int `json:"minVirtualServices,omitempty"` + // NumDeployedVirtualServices is a read only value + NumDeployedVirtualServices int `json:"numDeployedVirtualServices,omitempty"` +} + +// NsxtAlbPool defines configuration of a single NSX-T ALB Pool. Pools maintain the list of servers assigned to them and +// perform health monitoring, load balancing, persistence. A pool may only be used or referenced by only one virtual +// service at a time. +type NsxtAlbPool struct { + ID string `json:"id,omitempty"` + // Name is mandatory + Name string `json:"name"` + // Description is optional + Description string `json:"description,omitempty"` + + // GatewayRef is mandatory and associates NSX-T Edge Gateway with this Load Balancer Pool. + GatewayRef OpenApiReference `json:"gatewayRef"` + + // Enabled defines if the Pool is enabled + Enabled *bool `json:"enabled,omitempty"` + + // Algorithm for choosing a member within the pools list of available members for each new connection. + // Default value is LEAST_CONNECTIONS + // Supported algorithms are: + // * LEAST_CONNECTIONS + // * ROUND_ROBIN + // * CONSISTENT_HASH (uses Source IP Address hash) + // * FASTEST_RESPONSE + // * LEAST_LOAD + // * FEWEST_SERVERS + // * RANDOM + // * FEWEST_TASKS + // * CORE_AFFINITY + Algorithm string `json:"algorithm,omitempty"` + + // DefaultPort defines destination server port used by the traffic sent to the member. + DefaultPort *int `json:"defaultPort,omitempty"` + + // GracefulTimeoutPeriod sets maximum time (in minutes) to gracefully disable a member. Virtual service waits for the + // specified time before terminating the existing connections to the pool members that are disabled. + // + // Special values: 0 represents Immediate, -1 represents Infinite. + GracefulTimeoutPeriod *int `json:"gracefulTimeoutPeriod,omitempty"` + + // PassiveMonitoringEnabled sets if client traffic should be used to check if pool member is up or down. + PassiveMonitoringEnabled *bool `json:"passiveMonitoringEnabled,omitempty"` + + // HealthMonitors check member servers health. It can be monitored by using one or more health monitors. Active + // monitors generate synthetic traffic and mark a server up or down based on the response. + HealthMonitors []NsxtAlbPoolHealthMonitor `json:"healthMonitors,omitempty"` + + // Members field defines list of destination servers which are used by the Load Balancer Pool to direct load balanced + // traffic. + // + // Note. Only one of Members or MemberGroupRef can be specified + Members []NsxtAlbPoolMember `json:"members,omitempty"` + + // MemberGroupRef contains reference to the Edge Firewall Group (`types.NsxtFirewallGroup`) + // representing destination servers which are used by the Load Balancer Pool to direct load + // balanced traffic. + // + // This field is only available in VCD 10.4.1+ (v37.1+) + // Note. Only one of Members or MemberGroupRef can be specified + MemberGroupRef *OpenApiReference `json:"memberGroupRef,omitempty"` + + // CaCertificateRefs point to root certificates to use when validating certificates presented by the pool members. + CaCertificateRefs []OpenApiReference `json:"caCertificateRefs,omitempty"` + + // CommonNameCheckEnabled specifies whether to check the common name of the certificate presented by the pool member. + // This cannot be enabled if no caCertificateRefs are specified. + CommonNameCheckEnabled *bool `json:"commonNameCheckEnabled,omitempty"` + + // DomainNames holds a list of domain names which will be used to verify the common names or subject alternative + // names presented by the pool member certificates. It is performed only when common name check + // (CommonNameCheckEnabled) is enabled. If common name check is enabled, but domain names are not specified then the + // incoming host header will be used to check the certificate. + DomainNames []string `json:"domainNames,omitempty"` + + // PersistenceProfile of a Load Balancer Pool. Persistence profile will ensure that the same user sticks to the same + // server for a desired duration of time. If the persistence profile is unmanaged by Cloud Director, updates that + // leave the values unchanged will continue to use the same unmanaged profile. Any changes made to the persistence + // profile will cause Cloud Director to switch the pool to a profile managed by Cloud Director. + PersistenceProfile *NsxtAlbPoolPersistenceProfile `json:"persistenceProfile,omitempty"` + + // MemberCount is a read only value that reports number of members added + MemberCount int `json:"memberCount,omitempty"` + + // EnabledMemberCount is a read only value that reports number of enabled members + EnabledMemberCount int `json:"enabledMemberCount,omitempty"` + + // UpMemberCount is a read only value that reports number of members that are serving traffic + UpMemberCount int `json:"upMemberCount,omitempty"` + + // HealthMessage shows a pool health status (e.g. "The pool is unassigned.") + HealthMessage string `json:"healthMessage,omitempty"` + + // VirtualServiceRefs holds list of Load Balancer Virtual Services associated with this Load balancer Pool. + VirtualServiceRefs []OpenApiReference `json:"virtualServiceRefs,omitempty"` + + // SslEnabled is required when CA Certificates are used starting with API V37.0 + SslEnabled *bool `json:"sslEnabled,omitempty"` +} + +// NsxtAlbPoolHealthMonitor checks member servers health. Active monitor generates synthetic traffic and mark a server +// up or down based on the response. +type NsxtAlbPoolHealthMonitor struct { + Name string `json:"name,omitempty"` + // SystemDefined is a boolean value + SystemDefined bool `json:"systemDefined,omitempty"` + // Type + // * HTTP - HTTP request/response is used to validate health. + // * HTTPS - Used against HTTPS encrypted web servers to validate health. + // * TCP - TCP connection is used to validate health. + // * UDP - A UDP datagram is used to validate health. + // * PING - An ICMP ping is used to validate health. + Type string `json:"type"` +} + +// NsxtAlbPoolMember defines a single destination server which is used by the Load Balancer Pool to direct load balanced +// traffic. +type NsxtAlbPoolMember struct { + // Enabled defines if member is enabled (will receive incoming requests) or not + Enabled bool `json:"enabled"` + // IpAddress of the Load Balancer Pool member. + IpAddress string `json:"ipAddress"` + + // Port number of the Load Balancer Pool member. If unset, the port that the client used to connect will be used. + Port int `json:"port,omitempty"` + + // Ratio of selecting eligible servers in the pool. + Ratio *int `json:"ratio,omitempty"` + + // MarkedDownBy gives the names of the health monitors that marked the member as down when it is DOWN. If a monitor + // cannot be determined, the value will be UNKNOWN. + MarkedDownBy []string `json:"markedDownBy,omitempty"` + + // HealthStatus of the pool member. Possible values are: + // * UP - The member is operational + // * DOWN - The member is down + // * DISABLED - The member is disabled + // * UNKNOWN - The state is unknown + HealthStatus string `json:"healthStatus,omitempty"` + + // DetailedHealthMessage contains non-localized detailed message on the health of the pool member. + DetailedHealthMessage string `json:"detailedHealthMessage,omitempty"` +} + +// NsxtAlbPoolPersistenceProfile holds Persistence Profile of a Load Balancer Pool. Persistence profile will ensure that +// the same user sticks to the same server for a desired duration of time. If the persistence profile is unmanaged by +// Cloud Director, updates that leave the values unchanged will continue to use the same unmanaged profile. Any changes +// made to the persistence profile will cause Cloud Director to switch the pool to a profile managed by Cloud Director. +type NsxtAlbPoolPersistenceProfile struct { + // Name field is tricky. It remains empty in some case, but if it is sent it can become computed. + // (e.g. setting 'CUSTOM_HTTP_HEADER' results in value being + // 'VCD-LoadBalancer-3510eae9-53bb-49f1-b7aa-7aedf5ce3a77-CUSTOM_HTTP_HEADER') + Name string `json:"name,omitempty"` + + // Type of persistence strategy to use. Supported values are: + // * CLIENT_IP - The clients IP is used as the identifier and mapped to the server + // * HTTP_COOKIE - Load Balancer inserts a cookie into HTTP responses. Cookie name must be provided as value + // * CUSTOM_HTTP_HEADER - Custom, static mappings of header values to specific servers are used. Header name must be + // provided as value + // * APP_COOKIE - Load Balancer reads existing server cookies or URI embedded data such as JSessionID. Cookie name + // must be provided as value + // * TLS - Information is embedded in the client's SSL/TLS ticket ID. This will use default system profile + // System-Persistence-TLS + Type string `json:"type,omitempty"` + + // Value of attribute based on selected persistence type. + // This is required for HTTP_COOKIE, CUSTOM_HTTP_HEADER and APP_COOKIE persistence types. + // + // HTTP_COOKIE, APP_COOKIE must have cookie name set as the value and CUSTOM_HTTP_HEADER must have header name set as + // the value. + Value string `json:"value,omitempty"` +} + +// NsxtAlbVirtualService combines Load Balancer Pools with Service Engine Groups and exposes a virtual service on +// defined VIP (virtual IP address) while optionally allowing to use encrypted traffic +type NsxtAlbVirtualService struct { + ID string `json:"id,omitempty"` + + // Name contains meaningful name + Name string `json:"name,omitempty"` + + // Description is optional + Description string `json:"description,omitempty"` + + // Enabled defines if the virtual service is enabled to accept traffic + Enabled *bool `json:"enabled"` + + // ApplicationProfile sets protocol for load balancing by using NsxtAlbVirtualServiceApplicationProfile + ApplicationProfile NsxtAlbVirtualServiceApplicationProfile `json:"applicationProfile"` + + // GatewayRef contains NSX-T Edge Gateway reference + GatewayRef OpenApiReference `json:"gatewayRef"` + //LoadBalancerPoolRef contains Pool reference + LoadBalancerPoolRef OpenApiReference `json:"loadBalancerPoolRef"` + // ServiceEngineGroupRef points to service engine group (which must be assigned to NSX-T Edge Gateway) + ServiceEngineGroupRef OpenApiReference `json:"serviceEngineGroupRef"` + + // CertificateRef contains certificate reference if serving encrypted traffic + CertificateRef *OpenApiReference `json:"certificateRef,omitempty"` + + // ServicePorts define one or more ports (or port ranges) of the virtual service + ServicePorts []NsxtAlbVirtualServicePort `json:"servicePorts"` + + // VirtualIpAddress to be used for exposing this virtual service + VirtualIpAddress string `json:"virtualIpAddress"` + + // IPv6VirtualIpAddress defined IPv6 address to be used for this virtual service + // This field is only available in VCD 10.4.0 (v37.0+) + IPv6VirtualIpAddress string `json:"ipv6VirtualIpAddress,omitempty"` + + // TransparentModeEnabled allows to configure Preserve Client IP on a Virtual Service + // This field is only available for VCD 10.4.1+ (v37.1+) + // Note. `types.NsxtAlbConfig.TransparentModeEnabled` must be set to `true` for this field to be + // available. + TransparentModeEnabled *bool `json:"transparentModeEnabled,omitempty"` + + // HealthStatus contains status of the Load Balancer Cloud. Possible values are: + // UP - The cloud is healthy and ready to enable Load Balancer for an Edge Gateway. + // DOWN - The cloud is in a failure state. Enabling Load balancer on an Edge Gateway may not be possible. + // RUNNING - The cloud is currently processing. An example is if it's enabling a Load Balancer for an Edge Gateway. + // UNAVAILABLE - The cloud is unavailable. + // UNKNOWN - The cloud state is unknown. + HealthStatus string `json:"healthStatus,omitempty"` + + // HealthMessage shows a pool health status (e.g. "The pool is unassigned.") + HealthMessage string `json:"healthMessage,omitempty"` + + // DetailedHealthMessage containes a more in depth health message + DetailedHealthMessage string `json:"detailedHealthMessage,omitempty"` +} + +// NsxtAlbVirtualServicePort port (or port ranges) of the virtual service +type NsxtAlbVirtualServicePort struct { + // PortStart is always required + PortStart *int `json:"portStart"` + // PortEnd is only required if a port range is specified. For single port cases PortStart is sufficient + PortEnd *int `json:"portEnd,omitempty"` + // SslEnabled defines if traffic is served as secure. CertificateRef must be specified in NsxtAlbVirtualService when + // true + SslEnabled *bool `json:"sslEnabled,omitempty"` + // TcpUdpProfile defines + TcpUdpProfile *NsxtAlbVirtualServicePortTcpUdpProfile `json:"tcpUdpProfile,omitempty"` +} + +// NsxtAlbVirtualServicePortTcpUdpProfile profile determines the type and settings of the network protocol that a +// subscribing virtual service will use. It sets a number of parameters, such as whether the virtual service is a TCP +// proxy versus a pass-through via fast path. A virtual service can have both TCP and UDP enabled, which is useful for +// protocols such as DNS or syslog. +type NsxtAlbVirtualServicePortTcpUdpProfile struct { + SystemDefined bool `json:"systemDefined"` + // Type defines L4 or L4_TLS profiles: + // * TCP_PROXY (the only possible type when L4_TLS is used). Enabling TCP Proxy causes ALB to terminate an inbound + // connection from a client. Any application data from the client that is destined for a server is forwarded to that + // server over a new TCP connection. Separating (or proxying) the client-to-server connections enables ALB to provide + // enhanced security, such as TCP protocol sanitization or DoS mitigation. It also provides better client and server + // performance, such as maximizing client and server TCP MSS or window sizes independently and buffering server + // responses. One must use a TCP/UDP profile with the type set to Proxy for application profiles such as HTTP. + // + // * TCP_FAST_PATH profile does not proxy TCP connections - rather, it directly connects clients to the + // destination server and translates the client's destination virtual service address with the chosen destination + // server's IP address. The client's source IP address is still translated to the Service Engine address to ensure + // that server response traffic returns symmetrically. + // + // * UDP_FAST_PATH profile enables a virtual service to support UDP. Avi Vantage translates the client's destination + // virtual service address to the destination server and rewrites the client's source IP address to the Service + // Engine's address when forwarding the packet to the server. This ensures that server response traffic traverses + // symmetrically through the original SE. + Type string `json:"type"` +} + +// NsxtAlbVirtualServiceApplicationProfile sets protocol for load balancing. Type field defines possible options. +type NsxtAlbVirtualServiceApplicationProfile struct { + SystemDefined bool `json:"systemDefined,omitempty"` + // Type defines Traffic + // * HTTP + // * HTTPS (certificate reference is mandatory) + // * L4 + // * L4 TLS (certificate reference is mandatory) + Type string `json:"type"` +} + +// DistributedFirewallRule represents a single Distributed Firewall rule +type DistributedFirewallRule struct { + ID string `json:"id,omitempty"` + Name string `json:"name"` + + // Action field. Deprecated in favor of ActionValue in VCD 10.2.2+ (API V35.2) + Action string `json:"action,omitempty"` + + // Description field is not shown in UI. 'Comments' field was introduced in 10.3.2 and is shown + // in UI. + Description string `json:"description,omitempty"` + + // ApplicationPortProfiles contains a list of references to Application Port Profiles. Empty + // list means 'Any' + ApplicationPortProfiles []OpenApiReference `json:"applicationPortProfiles,omitempty"` + + // SourceFirewallGroups contains a list of references to Firewall Groups. Empty list means 'Any' + SourceFirewallGroups []OpenApiReference `json:"sourceFirewallGroups,omitempty"` + // DestinationFirewallGroups contains a list of references to Firewall Groups. Empty list means + // 'Any' + DestinationFirewallGroups []OpenApiReference `json:"destinationFirewallGroups,omitempty"` + + // Direction 'IN_OUT', 'OUT', 'IN' + Direction string `json:"direction"` + Enabled bool `json:"enabled"` + + // IpProtocol 'IPV4', 'IPV6', 'IPV4_IPV6' + IpProtocol string `json:"ipProtocol"` + + Logging bool `json:"logging"` + + // NetworkContextProfiles sets list of layer 7 network context profiles where this firewall + // rule is applicable. Null value or an empty list will be treated as 'ANY' which means rule + // applies to all applications and domains. + NetworkContextProfiles []OpenApiReference `json:"networkContextProfiles,omitempty"` + + // Version describes the current version of the entity. To prevent clients from overwriting each + // other's changes, update operations must include the version which can be obtained by issuing + // a GET operation. If the version number on an update call is missing, the operation will be + // rejected. This is only needed on update calls. + Version *DistributedFirewallRuleVersion `json:"version,omitempty"` + + // New fields starting with 35.2 + + // ActionValue replaces deprecated field Action and defines action to be applied to all the + // traffic that meets the firewall rule criteria. It determines if the rule permits or blocks + // traffic. Property is required if action is not set. Below are valid values: + // * ALLOW permits traffic to go through the firewall. + // * DROP blocks the traffic at the firewall. No response is sent back to the source. + // * REJECT blocks the traffic at the firewall. A response is sent back to the source. + ActionValue string `json:"actionValue,omitempty"` + + // New fields starting with 36.2 + + // Comments permits setting text for user entered comments on the firewall rule. Length cannot + // exceed 2048 characters. Comments are shown in UI for 10.3.2+. + Comments string `json:"comments,omitempty"` + + // SourceGroupsExcluded reverses the list specified in SourceFirewallGroups and the rule gets + // applied on all the groups that are NOT part of the SourceFirewallGroups. If false, the rule + // applies to the all the groups including the source groups. + SourceGroupsExcluded *bool `json:"sourceGroupsExcluded,omitempty"` + + // DestinationGroupsExcluded reverses the list specified in DestinationFirewallGroups and the + // rule gets applied on all the groups that are NOT part of the DestinationFirewallGroups. If + // false, the rule applies to the all the groups in DestinationFirewallGroups. + DestinationGroupsExcluded *bool `json:"destinationGroupsExcluded,omitempty"` +} + +type DistributedFirewallRules struct { + Values []*DistributedFirewallRule `json:"values"` +} + +type DistributedFirewallRuleVersion struct { + Version int `json:"version"` +} + +type NsxtNetworkContextProfile struct { + OrgRef *OpenApiReference `json:"orgRef"` + ContextEntityID interface{} `json:"contextEntityId"` + NetworkProviderScope interface{} `json:"networkProviderScope"` + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + + // Scope of NSX-T Network Context Profile + // SYSTEM profiles are available to all tenants. They are default profiles from the backing networking provider. + // PROVIDER profiles are available to all tenants. They are defined by the provider at a system level. + // TENANT profiles are available only to the specific tenant organization. They are defined by the tenant or by a provider on behalf of a tenant. + Scope string `json:"scope"` + Attributes []NsxtNetworkContextProfileAttributes `json:"attributes"` +} +type NsxtNetworkContextProfileAttributes struct { + Type string `json:"type"` + Values []string `json:"values"` + SubAttributes interface{} `json:"subAttributes"` +} + +// SecurityTag represents An individual security tag +type SecurityTag struct { + // Entities are the list of entities to tag in urn format. + Entities []string `json:"entities"` + // Tag is the tag name to use. + Tag string `json:"tag"` +} + +// SecurityTaggedEntity is an entity that has a tag. +type SecurityTaggedEntity struct { + // EntityType is the type of entity. Currently, only 'vm' is supported. + EntityType string `json:"entityType"` + // ID is the unique identifier of the entity in URN format. + ID string `json:"id"` + // Name of the entity. + Name string `json:"name"` + // OwnerRef is the owner of the specified entity such as vDC or vDC Group. If not applicable, field is not set. + OwnerRef *OpenApiReference `json:"ownerRef"` + // ParentRef is the parent of the entity such as vApp if the entity is a VM. If not applicable, field is not set. + ParentRef *OpenApiReference `json:"parentRef"` +} + +// SecurityTagValue describes the most basic tag structure: its value. +type SecurityTagValue struct { + // Tag is the value of the tag. The value is case-agnostic and will be converted to lower-case. + Tag string `json:"tag"` +} + +// EntitySecurityTags is a list of a tags assigned to a specific entity +type EntitySecurityTags struct { + // Tags is the list of tags. The value is case-agnostic and will be converted to lower-case. + Tags []string `json:"tags"` +} + +// RouteAdvertisement lists the subnets that will be advertised so that the Edge Gateway can route out to the +// connected external network. +type RouteAdvertisement struct { + // Enable if true, means that the subnets will be advertised. + Enable bool `json:"enable"` + // Subnets is the list of subnets that will be advertised so that the Edge Gateway can route out to the connected + // external network. + Subnets []string `json:"subnets"` +} + +// EdgeBgpNeighbor represents a BGP neighbor on the NSX-T Edge Gateway +type EdgeBgpNeighbor struct { + ID string `json:"id,omitempty"` + + // NeighborAddress holds IP address of the BGP neighbor. Both IPv4 and IPv6 formats are supported. + // + // Note. Uniqueness is enforced by NeighborAddress + NeighborAddress string `json:"neighborAddress"` + + // RemoteASNumber specified Autonomous System (AS) number of a BGP neighbor in ASPLAIN format. + RemoteASNumber string `json:"remoteASNumber"` + + // KeepAliveTimer specifies the time interval (in seconds) between keep alive messages sent to + // peer. + KeepAliveTimer int `json:"keepAliveTimer,omitempty"` + + // HoldDownTimer specifies the time interval (in seconds) before declaring a peer dead. + HoldDownTimer int `json:"holdDownTimer,omitempty"` + + // NeighborPassword for BGP neighbor authentication. Empty string ("") clears existing password. + // Not specifying a value will be treated as "no password". + NeighborPassword string `json:"neighborPassword"` + + // AllowASIn is a flag indicating whether BGP neighbors can receive routes with same AS. + AllowASIn bool `json:"allowASIn,omitempty"` + + // GracefulRestartMode Describes Graceful Restart configuration Modes for BGP configuration on + // an Edge Gateway. + // + // Possible values are: DISABLE , HELPER_ONLY , GRACEFUL_AND_HELPER + // * DISABLE - Both graceful restart and helper modes are disabled. + // * HELPER_ONLY - Only helper mode is enabled. (ability for a BGP speaker to indicate its ability to preserve + // forwarding state during BGP restart + // * GRACEFUL_AND_HELPER - Both graceful restart and helper modes are enabled. Ability of a BGP + // speaker to advertise its restart to its peers. + GracefulRestartMode string `json:"gracefulRestartMode,omitempty"` + + // IpAddressTypeFiltering specifies IP address type based filtering in each direction. Setting + // the value to "DISABLED" will disable address family based filtering. + // + // Possible values are: IPV4 , IPV6 , DISABLED + IpAddressTypeFiltering string `json:"ipAddressTypeFiltering,omitempty"` + + // InRoutesFilterRef specifies route filtering configuration for the BGP neighbor in 'IN' + // direction. It is the reference to the prefix list, indicating which routes to filter for IN + // direction. Not specifying a value will be treated as "no IN route filters". + InRoutesFilterRef *OpenApiReference `json:"inRoutesFilterRef,omitempty"` + + // OutRoutesFilterRef specifies route filtering configuration for the BGP neighbor in 'OUT' + // direction. It is the reference to the prefix list, indicating which routes to filter for OUT + // direction. Not specifying a value will be treated as "no OUT route filters". + OutRoutesFilterRef *OpenApiReference `json:"outRoutesFilterRef,omitempty"` + + // Specifies the BFD (Bidirectional Forwarding Detection) configuration for failure detection. Not specifying a value + // results in default behavior. + Bfd *EdgeBgpNeighborBfd `json:"bfd,omitempty"` +} + +// EdgeBgpNeighborBfd describes BFD (Bidirectional Forwarding Detection) configuration for failure detection. +type EdgeBgpNeighborBfd struct { + // A flag indicating whether BFD configuration is enabled or not. + Enabled bool `json:"enabled"` + + // BfdInterval specifies the time interval (in milliseconds) between heartbeat packets. + BfdInterval int `json:"bfdInterval,omitempty"` + + // DeclareDeadMultiple specifies number of times heartbeat packet is missed before BFD declares + // that the neighbor is down. + DeclareDeadMultiple int `json:"declareDeadMultiple,omitempty"` + // EdgeBgpIpPrefixList holds BGP IP Prefix List configuration for NSX-T Edge Gateways + +} + +type EdgeBgpIpPrefixList struct { + // ID is the unique identifier of the entity in URN format. + ID string `json:"id,omitempty"` + + // Name of the entity + Name string `json:"name"` + + // Description of the entity + Description string `json:"description,omitempty"` + + // Prefixes is the list of prefixes that will be advertised so that the Edge Gateway can route out to the + // connected external network. + Prefixes []EdgeBgpConfigPrefixListPrefixes `json:"prefixes,omitempty"` +} + +// EdgeBgpConfigPrefixListPrefixes is a list of prefixes that will be advertised so that the Edge Gateway can route out to the +// connected external network. +type EdgeBgpConfigPrefixListPrefixes struct { + // Network is the network address of the prefix + Network string `json:"network,omitempty"` + + // Action is the action to be taken on the prefix. Can be 'PERMIT' or 'DENY' + Action string `json:"action,omitempty"` + + // GreateerThan is the the value which the prefix length must be greater than or equal to. Must + // be less than or equal to 'LessThanEqualTo' + GreaterThanEqualTo int `json:"greaterThanEqualTo,omitempty"` + + // The value which the prefix length must be less than or equal to. Must be greater than or + // equal to 'GreaterThanEqualTo' + LessThanEqualTo int `json:"lessThanEqualTo,omitempty"` +} + +// EdgeBgpConfig defines BGP configuration on NSX-T Edge Gateways (Tier1 NSX-T Gateways) +type EdgeBgpConfig struct { + // A flag indicating whether BGP configuration is enabled or not. + Enabled bool `json:"enabled"` + + // Ecmp A flag indicating whether ECMP is enabled or not. + Ecmp bool `json:"ecmp"` + + // BGP AS (Autonomous system) number to advertise to BGP peers. BGP AS number can be specified + // in either ASPLAIN or ASDOT formats, like ASPLAIN format :- '65546', ASDOT format :- '1.10'. + // + // Read only if using a VRF-Lite backed external network. + LocalASNumber string `json:"localASNumber,omitempty"` + + // BGP Graceful Restart configuration. Not specifying a value results in default bahavior. + // + // Read only if using a VRF-Lite backed external network. + GracefulRestart *EdgeBgpGracefulRestartConfig `json:"gracefulRestart,omitempty"` + + // This property describes the current version of the entity. To prevent clients from + // overwriting each other's changes, update operations must include the version which can be + // obtained by issuing a GET operation. If the version number on an update call is missing, the + // operation will be rejected. This is only needed on update calls. + Version EdgeBgpConfigVersion `json:"version"` +} + +// EdgeBgpGracefulRestartConfig describes current graceful restart configuration mode and timer for +// BGP configuration on an edge gateway. +type EdgeBgpGracefulRestartConfig struct { + // Mode describes Graceful Restart configuration Modes for BGP configuration on an edge gateway. + // HELPER_ONLY mode is the ability for a BGP speaker to indicate its ability to preserve + // forwarding state during BGP restart. GRACEFUL_RESTART mode is the ability of a BGP speaker to + // advertise its restart to its peers. + // + // DISABLE - Both graceful restart and helper modes are disabled. + // HELPER_ONLY - Only helper mode is enabled. + // GRACEFUL_AND_HELPER - Both graceful restart and helper modes are enabled. + // + // Possible values are: DISABLE , HELPER_ONLY , GRACEFUL_AND_HELPER + Mode string `json:"mode"` + + // RestartTimer specifies maximum time taken (in seconds) for a BGP session to be established + // after a restart. If the session is not re-established within this timer, the receiving + // speaker will delete all the stale routes from that peer. + RestartTimer int `json:"restartTimer"` + + // StaleRouteTimer defines maximum time (in seconds) before stale routes are removed when BGP + // restarts. + StaleRouteTimer int `json:"staleRouteTimer"` +} + +// EdgeBgpConfigVersion is part of EdgeBgpConfig type and describes current version of the entity +// being modified +type EdgeBgpConfigVersion struct { + Version int `json:"version"` +} + +// VdcNetworkProfile defines a VDC Network Profile. +// +// All fields are optional, but omiting them will reset value. The general approach while updating +// VdcNetworkProfile should be to retrieve existing configuration and mutate it. +type VdcNetworkProfile struct { + // PrimaryEdgeCluster defines NSX-V Edge Cluster where the primary appliance for an NSX-V Edge + // Gateway will be deployed. (NSX-V only) + PrimaryEdgeCluster *OpenApiReference `json:"primaryEdgeCluster,omitempty"` + + // SecondaryEdgeCluster defines NSX-V Edge Cluster where the secondary appliance for an NSX-V + // Edge Gateway will be deployed if HA is enabled on the Edge. (NSX-V only) + SecondaryEdgeCluster *OpenApiReference `json:"secondaryEdgeCluster,omitempty"` + + // ServicesEdgeCluster contains NSX-T Edge Cluster where the DHCP server profile will be stored + // for NSX-T networks using NETWORK mode DHCP. (NSX-T only) + ServicesEdgeCluster *VdcNetworkProfileServicesEdgeCluster `json:"servicesEdgeCluster,omitempty"` + + // VappNetworkSegmentProfileTemplateRef defines vApp Network Segment Profile Template that is to + // be used when any new vApp Network is created under this VDC. Setting this will override any + // global level vApp Network Segment Profile Template. This field is only applicable for (NSX-T + // only) + // VCD 10.3.2+ (API 36.2+) + VappNetworkSegmentProfileTemplateRef *OpenApiReference `json:"vappNetworkSegmentProfileTemplateRef,omitempty"` + + // VdcNetworkSegmentProfileTemplateRef defines Org vDC Network Segment Profile Template that is + // to be used when any new Org vDC Network is created under this VDC. Setting this will override + // any global level Org vDC Network Segment Profile Template. (NSX-T only) + // VCD 10.3.2+ (API 36.2+) + VdcNetworkSegmentProfileTemplateRef *OpenApiReference `json:"vdcNetworkSegmentProfileTemplateRef,omitempty"` +} + +// VdcNetworkProfileServicesEdgeCluster contains reference to NSX-T Edge Cluster used in +// VdcNetworkProfile +type VdcNetworkProfileServicesEdgeCluster struct { + BackingID string `json:"backingId"` + EdgeClusterRef *OpenApiReference `json:"edgeClusterRef,omitempty"` +} + +// NsxtEdgeGatewayQosProfiles defines a Gateway QoS Profile Object (structure comes from NSX-T) +// This is a read-only entity in VCD +type NsxtEdgeGatewayQosProfile struct { + // ID of the gateway QoS profile. + ID string `json:"id"` + + DisplayName string `json:"displayName"` + Description string `json:"description"` + + //BurstSize defines burst size in bytes. + BurstSize int `json:"burstSize"` + + // CommittedBandwidth defines committed bandwidth in both directions specificd in Mb/s. + // Bandwidth is limited to line rate when the value configured is greater than line rate. + CommittedBandwidth int `json:"committedBandwidth"` + + // ExcessAction defines action on traffic exceeding bandwidth. + ExcessAction string `json:"excessAction"` + + // NsxTManagerRef contains reference to the originating NSX-T manager + NsxTManagerRef *OpenApiReference `json:"nsxTManagerRef"` +} + +// NsxtEdgeGatewayQos provides Rate Limiting (QoS) configuration on an Edge Gateway by defining QoS +// profiles in ingress and egress directions. +// +// Note. Sending `null` for either ingressProfile or egressProfile will reset the value to default +// (unlimited) +type NsxtEdgeGatewayQos struct { + EgressProfile *OpenApiReference `json:"egressProfile"` + IngressProfile *OpenApiReference `json:"ingressProfile"` +} + +// NsxtEdgeGatewayDhcpForwarder provides DHCP forwarding configuration on an Edge Gateway by defining +// DHCP servers +type NsxtEdgeGatewayDhcpForwarder struct { + Enabled bool `json:"enabled"` + DhcpServers []string `json:"dhcpServers"` + Version VersionField `json:"version,omitempty"` +} + +// 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"` +} + +// NsxtEdgeGatewaySlaacProfile provides configuration for NSX-T Edge Gateway IPv6 configuration +type NsxtEdgeGatewaySlaacProfile struct { + Enabled bool `json:"enabled"` + // Mode is 'SLAAC' ,'DHCPv6', 'DISABLED' + Mode string `json:"mode"` + // DNSConfig provides additional configuration when Mode is set to 'SLAAC' + DNSConfig NsxtEdgeGatewaySlaacProfileDNSConfig `json:"dnsConfig,omitempty"` +} + +// NsxtEdgeGatewaySlaacProfileDNSConfig contains additional NSX-T Edge Gateway IPv6 configuration +// when it is configured for 'SLAAC' mode +type NsxtEdgeGatewaySlaacProfileDNSConfig struct { + DNSServerIpv6Addresses []string `json:"dnsServerIpv6Addresses,omitempty"` + DomainNames []string `json:"domainNames,omitempty"` +} + +// NsxtEdgeGatewayStaticRoute provides configuration structure for NSX-T Edge Gateway static route +// configuration +type NsxtEdgeGatewayStaticRoute struct { + // ID of this static route. On updates, the ID is required for the object, while for create a + // new ID will be generated. This ID is not a VCD URN + ID string `json:"id,omitempty"` + Name string `json:"name"` + // Description + Description string `json:"description,omitempty"` + // NetworkCidr contains network prefix in CIDR format. Both IPv4 and IPv6 formats are supported + NetworkCidr string `json:"networkCidr"` + // NextHops contains the list of next hops to use within the static route. List must contain at + // least one valid next hop + NextHops []NsxtEdgeGatewayStaticRouteNextHops `json:"nextHops"` + + // SystemOwned contains a read-only flag whether this static route is managed by the system + SystemOwned *bool `json:"systemOwned,omitempty"` + // Version property describes the current version of the entity. To prevent clients from + // overwriting each other's changes, update operations must include the version which can be + // obtained by issuing a GET operation. If the version number on an update call is missing, the + // operation will be rejected. This is only needed on update calls. + Version string `json:"version,omitempty"` +} + +// NsxtEdgeGatewayStaticRouteNextHops sets one next hop entry for the list +type NsxtEdgeGatewayStaticRouteNextHops struct { + // AdminDistance for the next hop + AdminDistance int `json:"adminDistance"` + // IPAddress for next hop gateway IP Address for the static route. + IPAddress string `json:"ipAddress"` + // Scope holds a reference to an entity where the next hop of a static route is reachable. In + // general, the reference should be an org vDC network or segment backed external network, but + // scope could also reference a SYSTEM_OWNED entity if the next hop is configured outside of + // VCD. + Scope *NsxtEdgeGatewayStaticRouteNextHopScope `json:"scope,omitempty"` +} + +// NsxtEdgeGatewayStaticRouteNextHopScope for a single NsxtEdgeGatewayStaticRouteNextHops entry +type NsxtEdgeGatewayStaticRouteNextHopScope struct { + // ID of this scoped entity. + ID string `json:"id"` + // Name of the scoped entity. + Name string `json:"name"` + // ScopeType of this entity. This can be an network or a system-owned entity if the static + // route is SYSTEM_OWNED. Supported types are: + // * NETWORK + // * SYSTEM_OWNED + ScopeType string `json:"scopeType"` +} + +// NsxtManager structure for reading NSX-T Manager +type NsxtManager struct { + OtherAttributes struct { + } `json:"otherAttributes"` + Link LinkList `json:"link"` + Href string `json:"href"` + Type string `json:"type"` + ID string `json:"id"` + OperationKey interface{} `json:"operationKey"` + Description string `json:"description"` + Tasks interface{} `json:"tasks"` + Name string `json:"name"` + Username string `json:"username"` + Password interface{} `json:"password"` + URL string `json:"url"` + DeploymentType string `json:"deploymentType"` + VirtualCenterReference interface{} `json:"virtualCenterReference"` + ServiceAppReference interface{} `json:"serviceAppReference"` + NetworkProviderScope interface{} `json:"networkProviderScope"` + Version string `json:"version"` + ProxyConfigurationReference interface{} `json:"proxyConfigurationReference"` + GlobalManager bool `json:"globalManager"` + LocalNsxTManagerRef []interface{} `json:"localNsxTManagerRef"` + LocalManagerLocationName interface{} `json:"localManagerLocationName"` + VCloudExtension []interface{} `json:"vCloudExtension"` +} + +// NsxtEdgeGatewayDns is used for configuring the DNS forwarder for a specific Edge Gateway +type NsxtEdgeGatewayDns struct { + // Status of the DNS forwarder + Enabled bool `json:"enabled"` + // The IP on which the DNS forwarder listens. If the Edge Gateway has a dedicated + // external network, this can be changed. + ListenerIp string `json:"listenerIp,omitempty"` + // Whether an SNAT rule exists for the DNS forwarder or not. In NAT + // routed environments, an SNAT rule is required for the Edge DNS forwarder + // to send traffic to an upstream server. In fully routed environments, + // this is not needed if the listener IP is on an advertised subnet. + SnatRuleEnabled bool `json:"snatRuleEnabled,omitempty"` + // The external IP address of the SNAT rule. This property only applies if the + // Edge Gateway is connected to a Provider Gateway using IP Spaces. + SnatRuleExternalIpAddress string `json:"snatRuleExternalIpAddress,omitempty"` + // The default forwarder zone to use if there’s no matching domain in the conditional forwarder zone. + DefaultForwarderZone *NsxtDnsForwarderZoneConfig `json:"defaultForwarderZone,omitempty"` + // The list of forwarder zones with its matching DNS domains. + ConditionalForwarderZones []*NsxtDnsForwarderZoneConfig `json:"conditionalForwarderZones,omitempty"` + Version *VersionField `json:"version,omitempty"` +} + +type NsxtDnsForwarderZoneConfig struct { + // The unique id of the DNS forwarder zone. If value is set, + // the zone is updated; if not, a new zone is created. + ID string `json:"id,omitempty"` + // User friendly name for the zone + DisplayName string `json:"displayName,omitempty"` + // DNS servers to which the DNS request needs to be forwarded. + UpstreamServers []string `json:"upstreamServers,omitempty"` + // List of domain names on which conditional forwarding is based. This + // field is required if the DNS Zone is being used for a conditional forwarder. + // This field will also be used for conditional reverse lookup. This field should + // not be set if the zone is used as default forwarder zone. + DnsDomainNames []string `json:"dnsDomainNames,omitempty"` +} diff --git a/types/v56/nsxv_types.go b/types/v56/nsxv_types.go index 0c0750429..b4a065886 100644 --- a/types/v56/nsxv_types.go +++ b/types/v56/nsxv_types.go @@ -7,12 +7,12 @@ package types import "encoding/xml" // FirewallConfigWithXml allows to enable/disable firewall on a specific edge gateway -// Reference: vCloud Director API for NSX Programming Guide +// Reference: VMware Cloud Director API for NSX Programming Guide // https://code.vmware.com/docs/6900/vcloud-director-api-for-nsx-programming-guide // // Warning. It nests all firewall rules because Edge Gateway API is done so that if this data is not // sent while enabling it would wipe all firewall rules. InnerXML type field is used with struct tag -//`innerxml` to prevent any manipulation of configuration and sending it verbatim +// `innerxml` to prevent any manipulation of configuration and sending it verbatim type FirewallConfigWithXml struct { XMLName xml.Name `xml:"firewall"` Enabled bool `xml:"enabled"` @@ -34,7 +34,7 @@ type FirewallDefaultPolicy struct { } // LbGeneralParamsWithXml allows to enable/disable load balancing capabilities on specific edge gateway -// Reference: vCloud Director API for NSX Programming Guide +// Reference: VMware Cloud Director API for NSX Programming Guide // https://code.vmware.com/docs/6900/vcloud-director-api-for-nsx-programming-guide // // Warning. It nests all components (LbMonitor, LbPool, LbAppProfile, LbAppRule, LbVirtualServer) @@ -74,7 +74,7 @@ type InnerXML struct { } // LbMonitor defines health check parameters for a particular type of network traffic -// Reference: vCloud Director API for NSX Programming Guide +// Reference: VMware Cloud Director API for NSX Programming Guide // https://code.vmware.com/docs/6900/vcloud-director-api-for-nsx-programming-guide type LbMonitor struct { XMLName xml.Name `xml:"monitor"` @@ -94,7 +94,7 @@ type LbMonitor struct { type LbMonitors []LbMonitor -// LbPool represents a load balancer server pool as per "vCloud Director API for NSX Programming Guide" +// LbPool represents a load balancer server pool as per "VMware Cloud Director API for NSX Programming Guide" // Type: LBPoolHealthCheckType // https://code.vmware.com/docs/6900/vcloud-director-api-for-nsx-programming-guide type LbPool struct { @@ -126,7 +126,7 @@ type LbPoolMember struct { type LbPoolMembers []LbPoolMember -// LbAppProfile represents a load balancer application profile as per "vCloud Director API for NSX +// LbAppProfile represents a load balancer application profile as per "VMware Cloud Director API for NSX // Programming Guide" // https://code.vmware.com/docs/6900/vcloud-director-api-for-nsx-programming-guide type LbAppProfile struct { @@ -158,7 +158,7 @@ type LbAppProfileHttpRedirect struct { To string `xml:"to,omitempty"` } -// LbAppRule represents a load balancer application rule as per "vCloud Director API for NSX +// LbAppRule represents a load balancer application rule as per "VMware Cloud Director API for NSX // Programming Guide" // https://code.vmware.com/docs/6900/vcloud-director-api-for-nsx-programming-guide type LbAppRule struct { @@ -170,7 +170,7 @@ type LbAppRule struct { type LbAppRules []LbAppRule -// LbVirtualServer represents a load balancer virtual server as per "vCloud Director API for NSX +// LbVirtualServer represents a load balancer virtual server as per "VMware Cloud Director API for NSX // Programming Guide" // https://code.vmware.com/docs/6900/vcloud-director-api-for-nsx-programming-guide type LbVirtualServer struct { diff --git a/types/v56/oidc.go b/types/v56/oidc.go new file mode 100644 index 000000000..833682492 --- /dev/null +++ b/types/v56/oidc.go @@ -0,0 +1,99 @@ +/* + * Copyright 2024 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + */ + +package types + +// OrgOAuthSettings contains OAuth identity provider settings for an Organization. +type OrgOAuthSettings struct { + Xmlns string `xml:"xmlns,attr"` + Href string `xml:"href,attr,omitempty"` + Type string `xml:"type,attr,omitempty"` + + Link *LinkList `xml:"Link,omitempty"` // A reference to an entity or operation associated with this object + OrgRedirectUri string `xml:"OrgRedirectUri,omitempty"` // OAuth redirect URI for this org. This value is read only + IssuerId string `xml:"IssuerId,omitempty"` // Issuer Id for the OAuth Identity Provider + OAuthKeyConfigurations *OAuthKeyConfigurationsList `xml:"OAuthKeyConfigurations,omitempty"` // A list of OAuth Key configurations + Enabled bool `xml:"Enabled"` // True if the OAuth Identity Provider for this organization is enabled. Unset or empty defaults to true + ClientId string `xml:"ClientId,omitempty"` // Client ID for VCD to use when talking to the Identity Provider + ClientSecret string `xml:"ClientSecret,omitempty"` // Client Secret for vCD to use when talking to the Identity Provider + UserAuthorizationEndpoint string `xml:"UserAuthorizationEndpoint,omitempty"` // Identity Provider's OpenID Connect user authorization endpoint + AccessTokenEndpoint string `xml:"AccessTokenEndpoint,omitempty"` // Identity Provider's OpenId Connect access token endpoint + UserInfoEndpoint string `xml:"UserInfoEndpoint,omitempty"` // Identity Provider's OpenID Connect user info endpoint + ScimEndpoint string `xml:"ScimEndpoint,omitempty"` // Identity Provider's SCIM user information endpoint + Scope []string `xml:"Scope,omitempty"` // Scope that VCD needs access to for authenticating the user + OIDCAttributeMapping *OIDCAttributeMapping `xml:"OIDCAttributeMapping,omitempty"` // Custom claim keys for the /userinfo endpoint + MaxClockSkew int `xml:"MaxClockSkew,omitempty"` // Allowed difference between token expiration and vCD system time in seconds + JwksUri string `xml:"JwksUri,omitempty"` // Endpoint to fetch the keys from + AutoRefreshKey bool `xml:"AutoRefreshKey"` // Flag indicating whether VCD should auto-refresh the keys + + // Strategy to use when updated list of keys does not include keys known to VCD. + // The values must be one of the below: ADD: Will add new keys to set of keys that VCD will use. + // REPLACE: The retrieved list of keys will replace the existing list of keys and will become the definitive list of keys used by VCD going forward. + // EXPIRE_AFTER: Keys known to VCD that are no longer returned by the OIDC server will be marked as expired, 'KeyExpireDurationInHours' specified hours after the key refresh is performed. After that later time, VCD will no longer use the keys. + KeyRefreshStrategy string `xml:"KeyRefreshStrategy,omitempty"` + + KeyRefreshFrequencyInHours int `xml:"KeyRefreshFrequencyInHours,omitempty"` // Time interval, in hours, between subsequent key refresh attempts + KeyExpireDurationInHours int `xml:"KeyExpireDurationInHours,omitempty"` // Duration in which the keys are set to expire + WellKnownEndpoint string `xml:"WellKnownEndpoint,omitempty"` // Endpoint from the Identity Provider that serves OpenID Connect configuration value + LastKeyRefreshAttempt string `xml:"LastKeyRefreshAttempt,omitempty"` // Last time refresh of the keys was attempted + LastKeySuccessfulRefresh string `xml:"LastKeySuccessfulRefresh,omitempty"` // Last time refresh of the keys was successful + + // Added in v37.1 + EnableIdTokenClaims *bool `xml:"EnableIdTokenClaims"` // Flag indicating whether Id-Token Claims should be used when establishing user details + // Added in v38.0 + UsePKCE *bool `xml:"UsePKCE"` // Flag indicating whether client must use PKCE (Proof Key for Code Exchange), which provides additional verification against potential authorization code interception. Default is false + SendClientCredentialsAsAuthorizationHeader *bool `xml:"SendClientCredentialsAsAuthorizationHeader"` // Flag indicating whether client credentials should be sent as an Authorization header when fetching the token. Default is false, which means client credentials will be sent within the body of the request + // Added in v38.1 + CustomUiButtonLabel *string `xml:"CustomUiButtonLabel,omitempty"` // Custom label to use when displaying this OpenID Connect configuration on the VCD login pane. If null, a default label will be used +} + +// OAuthKeyConfigurationsList contains a list of OAuth Key configurations +type OAuthKeyConfigurationsList struct { + Xmlns string `xml:"xmlns,attr"` + + Link *LinkList `xml:"Link,omitempty"` // A reference to an entity or operation associated with this object + OAuthKeyConfiguration []OAuthKeyConfiguration `xml:"OAuthKeyConfiguration,omitempty"` // OAuth key configuration +} + +// OAuthKeyConfiguration describes the OAuth key configuration +type OAuthKeyConfiguration struct { + Xmlns string `xml:"xmlns,attr"` + + Link *LinkList `xml:"Link,omitempty"` // A reference to an entity or operation associated with this object + KeyId string `xml:"KeyId,omitempty"` // Identifier for the key used by the Identity Provider. This key id is expected to be present in the header portion of OAuth tokens issued by the Identity provider + Algorithm string `xml:"Algorithm,omitempty"` // Identifies the cryptographic algorithm family of the key. Supported values are RSA and EC for asymmetric keys + Key string `xml:"Key,omitempty"` // PEM formatted key body. Key is used during validation of OAuth tokens for this Org + ExpirationDate string `xml:"ExpirationDate,omitempty"` // Expiration date for this key. If specified, tokens signed with this key should be considered invalid after this time +} + +// OIDCAttributeMapping contains custom claim keys for the /userinfo endpoint +type OIDCAttributeMapping struct { + Xmlns string `xml:"xmlns,attr"` + + Link *LinkList `xml:"Link,omitempty"` // A reference to an entity or operation associated with this object + SubjectAttributeName string `xml:"SubjectAttributeName,omitempty"` // The name of the OIDC attribute used to get the username from the IDP's userInfo + EmailAttributeName string `xml:"EmailAttributeName,omitempty"` // The name of the OIDC attribute used to get the email from the IDP's userInfo + FullNameAttributeName string `xml:"FullNameAttributeName,omitempty"` // The name of the OIDC attribute used to get the full name from the IDP's userInfo. The full name attribute overrides the use of the firstName and lastName attributes + FirstNameAttributeName string `xml:"FirstNameAttributeName,omitempty"` // The name of the OIDC attribute used to get the first name from the IDP's userInfo. This is only used if the Full Name key is not specified + LastNameAttributeName string `xml:"LastNameAttributeName,omitempty"` // The name of the OIDC attribute used to get the last name from the IDP's userInfo. This is only used if the Full Name key is not specified + GroupsAttributeName string `xml:"GroupsAttributeName,omitempty"` // The name of the OIDC attribute used to get the full name from the IDP's userInfo. The full name attribute overrides the use of the firstName and lastName attributes + RolesAttributeName string `xml:"RolesAttributeName,omitempty"` // The name of the OIDC attribute used to get the user's roles from the IDP's userInfo +} + +// OpenIdProviderInfo contains the information about the OpenID Connect provider for creating initial org oauth settings +type OpenIdProviderInfo struct { + Xmlns string `xml:"xmlns,attr"` + + OpenIdProviderConfigurationEndpoint string `xml:"OpenIdProviderConfigurationEndpoint,omitempty"` // URL for the OAuth IDP well known openId connect configuration endpoint + Link *LinkList `xml:"Link,omitempty"` // A reference to an entity or operation associated with this object +} + +// OpenIdProviderConfiguration is result from reading the IDP OpenID Provider config endpoint +type OpenIdProviderConfiguration struct { + Xmlns string `xml:"xmlns,attr"` + + Link *LinkList `xml:"Link,omitempty"` // A reference to an entity or operation associated with this object + OrgOAuthSettings OrgOAuthSettings `xml:"OrgOAuthSettings,omitempty"` // OrgOauthSettings object configured using information from the IDP + ProviderConfigResponse string `xml:"ProviderConfigResponse,omitempty"` // Raw response from the IDP config endpoint +} diff --git a/types/v56/openapi.go b/types/v56/openapi.go index 9b5c82c66..42b3ef91e 100644 --- a/types/v56/openapi.go +++ b/types/v56/openapi.go @@ -45,10 +45,10 @@ func (openApiError OpenApiError) ErrorWithStack() string { // Role defines access roles in VCD type Role struct { ID string `json:"id,omitempty"` - Name string `json:"name"` - Description string `json:"description"` - BundleKey string `json:"bundleKey"` - ReadOnly bool `json:"readOnly"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + BundleKey string `json:"bundleKey,omitempty"` + ReadOnly bool `json:"readOnly,omitempty"` } // NsxtTier0Router defines NSX-T Tier 0 router @@ -82,14 +82,85 @@ type ExternalNetworkV2 struct { // Description of the network Description string `json:"description"` // Subnets define one or more subnets and IP allocation pools in edge gateway - Subnets ExternalNetworkV2Subnets `json:"subnets"` + Subnets ExternalNetworkV2Subnets `json:"subnets,omitempty"` // NetworkBackings for this external network. Describes if this external network is backed by // port groups, vCenter standard switch or an NSX-T Tier-0 router. NetworkBackings ExternalNetworkV2Backings `json:"networkBackings"` + + // UsingIpSpace indicates whether the external network is using IP Spaces or not. This field is + // applicable only to the external networks backed by NSX-T Tier-0 router. + // This field is only available in VCD 10.4.1+ + UsingIpSpace *bool `json:"usingIpSpace,omitempty"` + + // DedicatedEdgeGateway contains reference to the Edge Gateway that this external network is + // dedicated to. This is null if this is not a dedicated external network. This field is unset + // if external network is using IP Spaces. + DedicatedEdgeGateway *OpenApiReference `json:"dedicatedEdgeGateway,omitempty"` + + // DedicatedOrg specifies the Organization that this external network belongs to. This is unset + // for the external networks which are available to more than one organization. + // + // If this external network is dedicated to an Edge Gateway, this field is read-only and will be + // set to the Organization of the Edge Gateway. + // + // If this external network is using IP Spaces, this field can + // be used to dedicate this external network to the specified Organization. + DedicatedOrg *OpenApiReference `json:"dedicatedOrg,omitempty"` + + // NatAndFirewallServiceIntention defines different types of intentions to configure NAT and + // firewall rules: + // * PROVIDER_GATEWAY - Allow management of NAT and firewall rules only on Provider Gateways. + // + // * EDGE_GATEWAY - Allow management of NAT and firewall rules only on Edge Gateways. + // + // * PROVIDER_AND_EDGE_GATEWAY - Allow management of NAT and firewall rules on both the Provider + // and Edge gateways. + // + // This only applies to external networks backed by NSX-T Tier-0 router (i.e. Provider Gateway) + // and is unset otherwise. Public Provider Gateway supports only EDGE_GATEWAY_ONLY. All other + // values are ignored. Private Provider Gateway can support all the intentions and if unset, the + // default is EDGE_GATEWAY. + // + // This field requires VCD 10.5.1+ (API 38.1+) + NatAndFirewallServiceIntention string `json:"natAndFirewallServiceIntention,omitempty"` + + // NetworkRouteAdvertisementIntention configures different types of route advertisement + // intentions for routed Org VDC network connected to Edge Gateway that is connected to this + // Provider Gateway. Possible values are: + // + // * IP_SPACE_UPLINKS_ADVERTISED_STRICT - All networks within IP Space associated with IP Space + // Uplink will be advertised by default. This can be changed on an individual network level + // later, if necessary. All other networks outside of IP Spaces associated with IP Space Uplinks + // cannot be configured to be advertised. + // + // * IP_SPACE_UPLINKS_ADVERTISED_FLEXIBLE - All networks within IP Space associated with IP + // Space Uplink will be advertised by default. This can be changed on an individual network + // level later, if necessary. All other networks outside of IP Spaces associated with IP Space + // Uplinks are not advertised by default but can be configured to be advertised after creation. + // + // * ALL_NETWORKS_ADVERTISED - All networks, regardless on whether they fall inside of any IP + // Spaces associated with IP Space Uplinks, will be advertised by default. This can be changed + // on an individual network level later, if necessary. + // + // This only applies to external networks backed by NSX-T Tier-0 router (i.e. Provider Gateway) + // and is unset otherwise. Public Provider Gateway supports only + // IP_SPACE_UPLINKS_ADVERTISED_STRICT. All other values are ignored. Private Provider Gateway + // can support all the intentions and if unset, the default is also + // IP_SPACE_UPLINKS_ADVERTISED_STRICT. + // + // This field requires VCD 10.5.1+ (API 38.1+) + NetworkRouteAdvertisementIntention string `json:"networkRouteAdvertisementIntention,omitempty"` + + // TotalIpCount contains the number of IP addresses defined by the static ip pools. If the + // network contains any IPv6 subnets, the total ip count will be null. + TotalIpCount *int `json:"totalIpCount,omitempty"` + + // UsedIpCount holds the number of IP address used from the static ip pools. + UsedIpCount *int `json:"usedIpCount,omitempty"` } -// ExternalNetworkV2IPRange defines allocated IP pools for a subnet in external network -type ExternalNetworkV2IPRange struct { +// OpenApiIPRangeValues defines allocated IP pools for a subnet in external network +type OpenApiIPRangeValues struct { // StartAddress holds starting IP address in the range StartAddress string `json:"startAddress"` // EndAddress holds ending IP address in the range @@ -97,8 +168,8 @@ type ExternalNetworkV2IPRange struct { } // ExternalNetworkV2IPRanges contains slice of ExternalNetworkV2IPRange -type ExternalNetworkV2IPRanges struct { - Values []ExternalNetworkV2IPRange `json:"values"` +type OpenApiIPRanges struct { + Values []OpenApiIPRangeValues `json:"values"` } // ExternalNetworkV2Subnets contains slice of ExternalNetworkV2Subnet @@ -139,7 +210,12 @@ type ExternalNetworkV2Backing struct { Name string `json:"name,omitempty"` // BackingType can be either ExternalNetworkBackingTypeNsxtTier0Router in case of NSX-T or one // of ExternalNetworkBackingTypeNetwork or ExternalNetworkBackingDvPortgroup in case of NSX-V - BackingType string `json:"backingType"` + // Deprecated in favor of BackingTypeValue in API V35.0 + BackingType string `json:"backingType,omitempty"` + + // BackingTypeValue replaces BackingType in API V35.0 and adds support for additional network backing type + // ExternalNetworkBackingTypeNsxtSegment + BackingTypeValue string `json:"backingTypeValue,omitempty"` // NetworkProvider defines backing network manager NetworkProvider NetworkProvider `json:"networkProvider"` } @@ -150,10 +226,11 @@ type NetworkProvider struct { ID string `json:"id"` } -// VdcComputePolicy is represented as VM sizing policy in UI +// VdcComputePolicy contains VDC specific configuration for workloads. (version 1.0.0) +// Deprecated: Use VdcComputePolicyV2 instead (version 2.0.0) type VdcComputePolicy struct { ID string `json:"id,omitempty"` - Description string `json:"description,omitempty"` + Description *string `json:"description"` // It's a not-omitempty pointer to be able to send "null" values for empty descriptions. Name string `json:"name"` CPUSpeed *int `json:"cpuSpeed,omitempty"` Memory *int `json:"memory,omitempty"` @@ -170,26 +247,45 @@ type VdcComputePolicy struct { AdditionalProp2 string `json:"additionalProp2,omitempty"` AdditionalProp3 string `json:"additionalProp3,omitempty"` } `json:"extraConfigs,omitempty"` - PvdcComputePolicyRef *struct { - Name string `json:"name,omitempty"` - ID string `json:"id,omitempty"` - } `json:"pvdcComputePolicyRef,omitempty"` - PvdcComputePolicy *struct { - Name string `json:"name,omitempty"` - ID string `json:"id,omitempty"` - } `json:"pvdcComputePolicy,omitempty"` - CompatibleVdcTypes []string `json:"compatibleVdcTypes,omitempty"` - IsSizingOnly bool `json:"isSizingOnly,omitempty"` - PvdcID string `json:"pvdcId,omitempty"` - NamedVMGroups [][]struct { - Name string `json:"name,omitempty"` - ID string `json:"id,omitempty"` - } `json:"namedVmGroups,omitempty"` - LogicalVMGroupReferences []struct { - Name string `json:"name,omitempty"` - ID string `json:"id,omitempty"` - } `json:"logicalVmGroupReferences,omitempty"` - IsAutoGenerated bool `json:"isAutoGenerated,omitempty"` + PvdcComputePolicyRef *OpenApiReference `json:"pvdcComputePolicyRef,omitempty"` + PvdcComputePolicy *OpenApiReference `json:"pvdcComputePolicy,omitempty"` + CompatibleVdcTypes []string `json:"compatibleVdcTypes,omitempty"` + IsSizingOnly bool `json:"isSizingOnly,omitempty"` + PvdcID string `json:"pvdcId,omitempty"` + NamedVMGroups []OpenApiReferences `json:"namedVmGroups,omitempty"` + LogicalVMGroupReferences OpenApiReferences `json:"logicalVmGroupReferences,omitempty"` + IsAutoGenerated bool `json:"isAutoGenerated,omitempty"` +} + +// VdcComputePolicyV2 contains VDC specific configuration for workloads (version 2.0.0) +// https://developer.vmware.com/apis/vmware-cloud-director/latest/data-structures/VdcComputePolicy2/ +type VdcComputePolicyV2 struct { + VdcComputePolicy + PolicyType string `json:"policyType"` // Required. Can be "VdcVmPolicy" or "VdcKubernetesPolicy" + IsVgpuPolicy bool `json:"isVgpuPolicy,omitempty"` + PvdcNamedVmGroupsMap []PvdcNamedVmGroupsMap `json:"pvdcNamedVmGroupsMap,omitempty"` + PvdcLogicalVmGroupsMap []PvdcLogicalVmGroupsMap `json:"pvdcLogicalVmGroupsMap,omitempty"` + PvdcVgpuClustersMap []PvdcVgpuClustersMap `json:"pvdcVgpuClustersMap,omitempty"` + VgpuProfiles []VgpuProfile `json:"vgpuProfiles,omitempty"` +} + +// PvdcNamedVmGroupsMap is a combination of a reference to a Provider VDC and a list of references to Named VM Groups. +// This is used for VM Placement Policies (see VdcComputePolicyV2) +type PvdcNamedVmGroupsMap struct { + NamedVmGroups []OpenApiReferences `json:"namedVmGroups,omitempty"` + Pvdc OpenApiReference `json:"pvdc,omitempty"` +} + +// PvdcLogicalVmGroupsMap is a combination of a reference to a Provider VDC and a list of references to Logical VM Groups. +// This is used for VM Placement Policies (see VdcComputePolicyV2) +type PvdcLogicalVmGroupsMap struct { + LogicalVmGroups OpenApiReferences `json:"logicalVmGroups,omitempty"` + Pvdc OpenApiReference `json:"pvdc,omitempty"` +} + +type PvdcVgpuClustersMap struct { + Clusters []string `json:"clusters,omitempty"` + Pvdc OpenApiReference `json:"pvdc,omitempty"` } // OpenApiReference is a generic reference type commonly used throughout OpenAPI endpoints @@ -216,3 +312,451 @@ type VdcCapability struct { // Category of capability (e.g. "Security", "EdgeGateway", "OrgVdcNetwork") Category string `json:"category"` } + +// A Right is a component of a role, a global role, or a rights bundle. +// In this view, roles, global roles, and rights bundles are collections of rights. +// Note that the rights are not stored in the above collection structures, but retrieved separately +type Right struct { + Name string `json:"name"` + ID string `json:"id"` + Description string `json:"description,omitempty"` + BundleKey string `json:"bundleKey,omitempty"` // key used for internationalization + Category string `json:"category,omitempty"` // Category ID + ServiceNamespace string `json:"serviceNamespace,omitempty"` // Not used + RightType string `json:"rightType,omitempty"` // VIEW or MODIFY + ImpliedRights []OpenApiReference `json:"impliedRights,omitempty"` +} + +// RightsCategory defines the category to which the Right belongs +type RightsCategory struct { + Name string `json:"name"` + Id string `json:"id"` + BundleKey string `json:"bundleKey"` // key used for internationalization + Parent string `json:"parent"` + RightsCount struct { + View int `json:"view"` + Modify int `json:"modify"` + } `json:"rightsCount"` + SubCategories []string `json:"subCategories"` +} + +// RightsBundle is a collection of Rights to be assigned to a tenant(= organization). +// Changing a rights bundle and publishing it for a given tenant will limit +// the rights that the global roles implement in such tenant. +type RightsBundle struct { + Name string `json:"name"` + Id string `json:"id"` + Description string `json:"description,omitempty"` + BundleKey string `json:"bundleKey,omitempty"` // key used for internationalization + ReadOnly bool `json:"readOnly"` + PublishAll *bool `json:"publishAll"` +} + +// GlobalRole is a Role definition implemented in the provider that is passed on to tenants (=organizations) +// Modifying an existing global role has immediate effect on the corresponding roles in the tenants (no need +// to re-publish) while creating a new GlobalRole is only passed to the tenants if it is published. +type GlobalRole struct { + Name string `json:"name"` + Id string `json:"id"` + Description string `json:"description,omitempty"` + BundleKey string `json:"bundleKey,omitempty"` // key used for internationalization + ReadOnly bool `json:"readOnly"` + PublishAll *bool `json:"publishAll"` +} + +// OpenApiItems defines the input when multiple items need to be passed to a POST or PUT operation +// All the fields are optional, except Values +// This structure is the same as OpenApiPages, except for the type of Values, which is explicitly +// defined as a collection of name+ID structures +type OpenApiItems struct { + ResultTotal int `json:"resultTotal,omitempty"` + PageCount int `json:"pageCount,omitempty"` + Page int `json:"page,omitempty"` + PageSize int `json:"pageSize,omitempty"` + Associations interface{} `json:"associations,omitempty"` + Values []OpenApiReference `json:"values"` // a collection of items defined by an ID + a name +} + +// CertificateLibraryItem is a Certificate Library definition of stored Certificate details +type CertificateLibraryItem struct { + Alias string `json:"alias"` + Id string `json:"id,omitempty"` + Certificate string `json:"certificate"` // PEM encoded certificate + Description string `json:"description,omitempty"` + PrivateKey string `json:"privateKey,omitempty"` // PEM encoded private key. Required if providing a certificate chain + PrivateKeyPassphrase string `json:"privateKeyPassphrase,omitempty"` // passphrase for the private key. Required if the private key is encrypted +} + +// CurrentSessionInfo gives information about the current session +type CurrentSessionInfo struct { + ID string `json:"id"` // Session ID + User OpenApiReference `json:"user"` // Name of the user associated with this session + Org OpenApiReference `json:"org"` // Organization for this connection + Location string `json:"location"` // Location ID: unknown usage + Roles []string `json:"roles"` // Roles associated with the session user + RoleRefs OpenApiReferences `json:"roleRefs"` // Roles references for the session user + SessionIdleTimeoutMinutes int `json:"sessionIdleTimeoutMinutes"` // session idle timeout +} + +// VdcGroup is a VDC group definition +type VdcGroup struct { + Description string `json:"description,omitempty"` // The description of this group. + DfwEnabled bool `json:"dfwEnabled,omitempty"` // Whether Distributed Firewall is enabled for this vDC Group. Only applicable for NSX_T vDC Groups. + ErrorMessage string `json:"errorMessage,omitempty"` // If the group has an error status, a more detailed error message is set here. + Id string `json:"id,omitempty"` // The unique ID for the vDC Group (read-only). + LocalEgress bool `json:"localEgress,omitempty"` // Determines whether local egress is enabled for a universal router belonging to a universal vDC group. This value is used on create if universalNetworkingEnabled is set to true. This cannot be updated. This value is always false for local vDC groups. + Name string `json:"name"` // The name of this group. The name must be unique. + NetworkPoolId string `json:"networkPoolId,omitempty"` // ID of network pool to use if creating a local vDC group router. Must be set if creating a local group. Ignored if creating a universal group. + NetworkPoolUniversalId string `json:"networkPoolUniversalId,omitempty"` // The network provider’s universal id that is backing the universal network pool. This field is read-only and is derived from the list of participating vDCs if a universal vDC group is created. For universal vDC groups, each participating vDC should have a universal network pool that is backed by this same id. + NetworkProviderType string `json:"networkProviderType,omitempty"` // The values currently supported are NSX_V and NSX_T. Defines the networking provider backing the vDC Group. This is used on create. If not specified, NSX_V value will be used. NSX_V is used for existing vDC Groups and vDC Groups where Cross-VC NSX is used for the underlying technology. NSX_T is used when the networking provider type for the Organization vDCs in the group is NSX-T. NSX_T only supports groups of type LOCAL (single site). + OrgId string `json:"orgId"` // The organization that this group belongs to. + ParticipatingOrgVdcs []ParticipatingOrgVdcs `json:"participatingOrgVdcs"` // The list of organization vDCs that are participating in this group. + Status string `json:"status,omitempty"` // The status that the group can be in. Possible values are: SAVING, SAVED, CONFIGURING, REALIZED, REALIZATION_FAILED, DELETING, DELETE_FAILED, OBJECT_NOT_FOUND, UNCONFIGURED + Type string `json:"type,omitempty"` // Defines the group as LOCAL or UNIVERSAL. This cannot be changed. Local vDC Groups can have networks stretched across multiple vDCs in a single Cloud Director instance. Local vDC Groups share the same broadcast domain/transport zone and network provider scope. Universal vDC groups can have networks stretched across multiple vDCs in a single or multiple Cloud Director instance(s). Universal vDC groups are backed by a broadcast domain/transport zone that strectches across a single or multiple Cloud Director instance(s). Local vDC groups are supported for both NSX-V and NSX-T Network Provider Types. Universal vDC Groups are supported for only NSX_V Network Provider Type. Possible values are: LOCAL , UNIVERSAL + UniversalNetworkingEnabled bool `json:"universalNetworkingEnabled,omitempty"` // True means that a vDC group router has been created. If set to true for vdc group creation, a universal router will also be created. +} + +// ParticipatingOrgVdcs is a participating Org VDCs definition +type ParticipatingOrgVdcs struct { + FaultDomainTag string `json:"faultDomainTag,omitempty"` // Represents the fault domain of a given organization vDC. For NSX_V backed organization vDCs, this is the network provider scope. For NSX_T backed organization vDCs, this can vary (for example name of the provider vDC or compute provider scope). + NetworkProviderScope string `json:"networkProviderScope,omitempty"` // Read-only field that specifies the network provider scope of the vDC. + OrgRef OpenApiReference `json:"orgRef,omitempty"` // Read-only field that specifies what organization this vDC is in. + RemoteOrg bool `json:"remoteOrg,omitempty"` // Read-only field that specifies whether the vDC is local to this VCD site. + SiteRef OpenApiReference `json:"siteRef,omitempty"` // The site ID that this vDC belongs to. Required for universal vDC groups. + Status string `json:"status,omitempty"` // The status that the vDC can be in. An example is if the vDC has been deleted from the system but is still part of the group. Possible values are: SAVING, SAVED, CONFIGURING, REALIZED, REALIZATION_FAILED, DELETING, DELETE_FAILED, OBJECT_NOT_FOUND, UNCONFIGURED + VdcRef OpenApiReference `json:"vdcRef"` // The reference to the vDC that is part of this a vDC group. +} + +// CandidateVdc defines possible candidate VDCs for VDC group +type CandidateVdc struct { + FaultDomainTag string `json:"faultDomainTag"` + Id string `json:"id"` + Name string `json:"name"` + NetworkProviderScope string `json:"networkProviderScope"` + OrgRef OpenApiReference `json:"orgRef"` + SiteRef OpenApiReference `json:"siteRef"` +} + +// DfwPolicies defines Distributed firewall policies +type DfwPolicies struct { + Enabled bool `json:"enabled"` + DefaultPolicy *DefaultPolicy `json:"defaultPolicy,omitempty"` +} + +// DefaultPolicy defines Default policy for Distributed firewall +type DefaultPolicy struct { + Description string `json:"description,omitempty"` // Description for the security policy. + Enabled *bool `json:"enabled,omitempty"` // Whether this security policy is enabled. + Id string `json:"id,omitempty"` // The unique id of this security policy. On updates, the id is required for the policy, while for create a new id will be generated. This id is not a VCD URN. + Name string `json:"name"` // Name for the security policy. + Version *VersionField `json:"version,omitempty"` // This property describes the current version of the entity. To prevent clients from overwriting each other’s changes, update operations must include the version which can be obtained by issuing a GET operation. If the version number on an update call is missing, the operation will be rejected. This is only needed on update calls. +} + +// VersionField defines Version +type VersionField struct { + Version int `json:"version"` +} + +// TestConnection defines the parameters used when testing a connection, including SSL handshake and hostname verification. +type TestConnection struct { + Host string `json:"host"` // The host (or IP address) to connect to. + Port int `json:"port"` // The port to use when connecting. + Secure *bool `json:"secure,omitempty"` // If the connection should use https. + Timeout int `json:"timeout,omitempty"` // Maximum time (in seconds) any step in the test should wait for a response. + HostnameVerificationAlgorithm string `json:"hostnameVerificationAlgorithm,omitempty"` // Endpoint/Hostname verification algorithm to be used during SSL/TLS/DTLS handshake. + AdditionalCAIssuers []string `json:"additionalCAIssuers,omitempty"` // A list of URLs being authorized by the user to retrieve additional CA certificates from, if necessary, to complete the certificate chain to its trust anchor. + ProxyConnection *ProxyTestConnection `json:"proxyConnection,omitempty"` // Proxy connection to use for test. Only one of proxyConnection and preConfiguredProxy can be specified. + PreConfiguredProxy string `json:"preConfiguredProxy,omitempty"` // The URN of a ProxyConfiguration to use for the test. Only one of proxyConnection or preConfiguredProxy can be specified. If neither is specified then no proxy is used to test the connection. +} + +// ProxyTestConnection defines the proxy connection to use for TestConnection (if any). +type ProxyTestConnection struct { + ProxyHost string `json:"proxyHost"` // The host (or IP address) of the proxy. + ProxyPort int `json:"proxyPort"` // The port to use when connecting to the proxy. + ProxyUsername string `json:"proxyUsername,omitempty"` // Username to authenticate to the proxy. + ProxyPassword string `json:"proxyPassword,omitempty"` // Password to authenticate to the proxy. + ProxySecure *bool `json:"proxySecure,omitempty"` // If the connection to the proxy should use https. +} + +// TestConnectionResult is the result of a connection test. +type TestConnectionResult struct { + TargetProbe *ProbeResult `json:"targetProbe,omitempty"` // Results of a connection test to a specific endpoint. + ProxyProbe *ProbeResult `json:"proxyProbe,omitempty"` // Results of a connection test to a specific endpoint. +} + +// ProbeResult results of a connection test to a specific endpoint. +type ProbeResult struct { + Result string `json:"result,omitempty"` // Localized message describing the connection result stating success or an error message with a brief summary. + ResolvedIp string `json:"resolvedIp,omitempty"` // The IP address the host was resolved to, if not going through a proxy. + CanConnect bool `json:"canConnect,omitempty"` // If vCD can establish a connection on the specified port. + SSLHandshake bool `json:"sslHandshake,omitempty"` // If an SSL Handshake succeeded (secure requests only). + ConnectionResult string `json:"connectionResult,omitempty"` // A code describing the result of establishing a connection. It can be either SUCCESS, ERROR_CANNOT_RESOLVE_IP or ERROR_CANNOT_CONNECT. + SSLResult string `json:"sslResult,omitempty"` // A code describing the result of the SSL handshake. It can be either SUCCESS, ERROR_SSL_ERROR, ERROR_UNTRUSTED_CERTIFICATE, ERROR_CANNOT_VERIFY_HOSTNAME or null. + CertificateChain string `json:"certificateChain,omitempty"` // The SSL certificate chain presented by the server if a secure connection was made. + AdditionalCAIssuers []string `json:"additionalCAIssuers,omitempty"` // URLs supplied by Certificate Authorities to retrieve signing certificates, when those certificates are not included in the chain. +} + +// LogicalVmGroup is used to create VM Placement Policies in VCD. +type LogicalVmGroup struct { + Name string `json:"name,omitempty"` // Display name + Description string `json:"description,omitempty"` + ID string `json:"id,omitempty"` // UUID for LogicalVmGroup. This is immutable + NamedVmGroupReferences OpenApiReferences `json:"namedVmGroupReferences,omitempty"` // List of named VM Groups associated with LogicalVmGroup. + PvdcID string `json:"pvdcId,omitempty"` // URN for Provider VDC +} + +// DefinedInterface defines a interface for a defined entity. The combination of nss+version+vendor should be unique +type DefinedInterface struct { + ID string `json:"id,omitempty"` // The id of the defined interface type in URN format + Name string `json:"name,omitempty"` // The name of the defined interface + Nss string `json:"nss,omitempty"` // A unique namespace associated with the interface + Version string `json:"version,omitempty"` // The interface's version. The version should follow semantic versioning rules + Vendor string `json:"vendor,omitempty"` // The vendor name + IsReadOnly bool `json:"readonly,omitempty"` // True if the entity type cannot be modified +} + +// Behavior defines a concept similar to a "procedure" that lives inside Defined Interfaces or Defined Entity Types as overrides. +type Behavior struct { + ID string `json:"id,omitempty"` // The Behavior ID is generated and is an output-only property + Description string `json:"description,omitempty"` // A description specifying the contract of the Behavior + Execution map[string]interface{} `json:"execution,omitempty"` // The Behavior execution mechanism. Can be defined both in an Interface and in a Defined Entity Type as an override + Ref string `json:"ref,omitempty"` // The Behavior invocation reference to be used for polymorphic behavior invocations. It is generated and is an output-only property + Name string `json:"name,omitempty"` +} + +// BehaviorAccess defines the access control configuration of a Behavior. +type BehaviorAccess struct { + AccessLevelId string `json:"accessLevelId,omitempty"` // The ID of an AccessLevel + BehaviorId string `json:"behaviorId,omitempty"` // The ID of the Behavior. It can be both a behavior-interface or an overridden behavior-type ID +} + +// BehaviorInvocation is an invocation of a Behavior on a Defined Entity instance. Currently, the Behavior interfaces are key-value maps specified in the Behavior description. +type BehaviorInvocation struct { + Arguments interface{} `json:"arguments,omitempty"` + Metadata interface{} `json:"metadata,omitempty"` +} + +// DefinedEntityType describes what a Defined Entity Type should look like. +type DefinedEntityType struct { + ID string `json:"id,omitempty"` // The id of the defined entity type in URN format + Name string `json:"name,omitempty"` // The name of the defined entity type + Nss string `json:"nss,omitempty"` // A unique namespace specific string. The combination of nss and version must be unique + Version string `json:"version,omitempty"` // The version of the defined entity. The combination of nss and version must be unique. The version string must follow semantic versioning rules + Description string `json:"description,omitempty"` // Description of the defined entity + ExternalId string `json:"externalId,omitempty"` // An external entity’s id that this definition may apply to + Hooks map[string]string `json:"hooks,omitempty"` // A mapping defining which behaviors should be invoked upon specific lifecycle events, like PostCreate, PostUpdate, PreDelete. For example: "hooks": { "PostCreate": "urn:vcloud:behavior-interface:postCreateHook:vendorA:containerCluster:1.0.0" }. Added in 36.0 + InheritedVersion string `json:"inheritedVersion,omitempty"` // To be used when creating a new version of a defined entity type. Specifies the version of the type that will be the template for the authorization configuration of the new version. The Type ACLs and the access requirements of the Type Behaviors of the new version will be copied from those of the inherited version. If the value of this property is ‘0’, then the new type version will not inherit another version and will have the default authorization settings, just like the first version of a new type. Added in 36.0 + Interfaces []string `json:"interfaces,omitempty"` // List of interface IDs that this defined entity type is referenced by + MaxImplicitRight string `json:"maxImplicitRight,omitempty"` // The maximum Type Right level that will be implied from the user’s Type ACLs if this field is defined. For example, “maxImplicitRight”: “urn:vcloud:accessLevel:ReadWrite” would mean that a user with RO , RW, and FC ACLs to the Type would implicitly get the “Read: ” and “Write: ” rights, but not the “Full Control: ” right. The valid values are “urn:vcloud:accessLevel:ReadOnly”, “urn:vcloud:accessLevel:ReadWrite”, “urn:vcloud:accessLevel:FullControl” + IsReadOnly bool `json:"readonly,omitempty"` // `true` if the entity type cannot be modified + Schema map[string]interface{} `json:"schema,omitempty"` // The JSON-Schema valid definition of the defined entity type. If no JSON Schema version is specified, version 4 will be assumed + Vendor string `json:"vendor,omitempty"` // The vendor name +} + +// DefinedEntity describes an instance of a defined entity type. +type DefinedEntity struct { + ID string `json:"id,omitempty"` // The id of the defined entity in URN format + EntityType string `json:"entityType,omitempty"` // The URN ID of the defined entity type that the entity is an instance of. This is a read-only field + Name string `json:"name,omitempty"` // The name of the defined entity + ExternalId string `json:"externalId,omitempty"` // An external entity's id that this entity may have a relation to. + Entity map[string]interface{} `json:"entity,omitempty"` // A JSON value representation. The JSON will be validated against the schema of the DefinedEntityType that the entity is an instance of + State *string `json:"state,omitempty"` // Every entity is created in the "PRE_CREATED" state. Once an entity is ready to be validated against its schema, it will transition in another state - RESOLVED, if the entity is valid according to the schema, or RESOLUTION_ERROR otherwise. If an entity in an "RESOLUTION_ERROR" state is updated, it will transition to the inital "PRE_CREATED" state without performing any validation. If its in the "RESOLVED" state, then it will be validated against the entity type schema and throw an exception if its invalid + Owner *OpenApiReference `json:"owner,omitempty"` // The owner of the defined entity + Org *OpenApiReference `json:"org,omitempty"` // The organization of the defined entity. + Message string `json:"message,omitempty"` // A message field that might be populated in case entity Resolution fails +} + +// DefinedEntityAccess describes Access Control structure for an RDE +type DefinedEntityAccess struct { + Id string `json:"id,omitempty"` + Tenant OpenApiReference `json:"tenant"` + GrantType string `json:"grantType"` + ObjectId string `json:"objectId,omitempty"` + AccessLevelID string `json:"accessLevelId"` + MemberID string `json:"memberId"` +} + +type VSphereVirtualCenter struct { + VcId string `json:"vcId"` + Name string `json:"name"` + Description string `json:"description"` + Username string `json:"username"` + Password string `json:"password"` + Url string `json:"url"` + IsEnabled bool `json:"isEnabled"` + VsphereWebClientServerUrl string `json:"vsphereWebClientServerUrl"` + HasProxy bool `json:"hasProxy"` + RootFolder string `json:"rootFolder"` + VcNoneNetwork string `json:"vcNoneNetwork"` + TenantVisibleName string `json:"tenantVisibleName"` + IsConnected bool `json:"isConnected"` + Mode string `json:"mode"` + ListenerState string `json:"listenerState"` + ClusterHealthStatus string `json:"clusterHealthStatus"` + VcVersion string `json:"vcVersion"` + BuildNumber string `json:"buildNumber"` + Uuid string `json:"uuid"` + NsxVManager struct { + Username string `json:"username"` + Password string `json:"password"` + Url string `json:"url"` + SoftwareVersion string `json:"softwareVersion"` + } `json:"nsxVManager"` + ProxyConfigurationUrn string `json:"proxyConfigurationUrn"` +} + +type ResourcePoolSummary struct { + Associations []struct { + EntityId string `json:"entityId"` + AssociationId string `json:"associationId"` + } `json:"associations"` + Values []ResourcePool `json:"values"` +} + +// ResourcePool defines a vSphere Resource Pool +type ResourcePool struct { + Moref string `json:"moref"` + ClusterMoref string `json:"clusterMoref"` + Name string `json:"name"` + VcId string `json:"vcId"` + Eligible bool `json:"eligible"` + KubernetesEnabled bool `json:"kubernetesEnabled"` + VgpuEnabled bool `json:"vgpuEnabled"` +} + +// OpenApiSupportedHardwareVersions is the list of versions supported by a given resource +type OpenApiSupportedHardwareVersions struct { + Versions []string `json:"versions"` + SupportedVersions []struct { + IsDefault bool `json:"isDefault"` + Name string `json:"name"` + } `json:"supportedVersions"` +} + +// NetworkPool is the full data retrieved for a provider network pool +type NetworkPool struct { + Status string `json:"status,omitempty"` + Id string `json:"id,omitempty"` + Name string `json:"name"` + Description string `json:"description"` + PoolType string `json:"poolType"` + PromiscuousMode bool `json:"promiscuousMode,omitempty"` + TotalBackingsCount int `json:"totalBackingsCount,omitempty"` + UsedBackingsCount int `json:"usedBackingsCount,omitempty"` + ManagingOwnerRef OpenApiReference `json:"managingOwnerRef"` + Backing NetworkPoolBacking `json:"backing"` +} + +// NetworkPoolBacking is the definition of the objects supporting the network pool +type NetworkPoolBacking struct { + VlanIdRanges VlanIdRanges `json:"vlanIdRanges,omitempty"` + VdsRefs []OpenApiReference `json:"vdsRefs,omitempty"` + PortGroupRefs []OpenApiReference `json:"portGroupRefs,omitempty"` + TransportZoneRef OpenApiReference `json:"transportZoneRef,omitempty"` + ProviderRef OpenApiReference `json:"providerRef"` +} + +type VlanIdRanges struct { + Values []VlanIdRange `json:"values"` +} + +// VlanIdRange is a component of a network pool of type VLAN +type VlanIdRange struct { + StartId int `json:"startId"` + EndId int `json:"endId"` +} + +// OpenApiStorageProfile defines a storage profile before it is assigned to a provider VDC +type OpenApiStorageProfile struct { + Moref string `json:"moref"` + Name string `json:"name"` +} + +// UIPluginMetadata gives meta information about a UI Plugin +type UIPluginMetadata struct { + ID string `json:"id,omitempty"` + Vendor string `json:"vendor,omitempty"` + License string `json:"license,omitempty"` + Link string `json:"link,omitempty"` + PluginName string `json:"pluginName,omitempty"` + Version string `json:"version,omitempty"` + Description string `json:"description,omitempty"` + ProviderScoped bool `json:"provider_scoped,omitempty"` + TenantScoped bool `json:"tenant_scoped,omitempty"` + Enabled bool `json:"enabled,omitempty"` + PluginStatus string `json:"plugin_status,omitempty"` +} + +// UploadSpec gives information about an upload +type UploadSpec struct { + FileName string `json:"fileName,omitempty"` + Size int64 `json:"size,omitempty"` + Checksum string `json:"checksum,omitempty"` + ChecksumAlgo string `json:"checksumAlgo,omitempty"` +} + +type TransportZones struct { + Values []*TransportZone `json:"values"` +} + +// TransportZone is a backing component of a network pool of type 'GENEVE' (NSX-T backed) +type TransportZone struct { + Id string `json:"id"` + Name string `json:"name"` + Type string `json:"type,omitempty"` + AlreadyImported bool `json:"alreadyImported"` +} + +// VcenterDistributedSwitch is a backing component of a network pool of type VLAN +type VcenterDistributedSwitch struct { + BackingRef OpenApiReference `json:"backingRef"` + VirtualCenter OpenApiReference `json:"virtualCenter"` +} + +// OpenApiMetadataEntry represents a metadata entry in VCD. +type OpenApiMetadataEntry struct { + ID string `json:"id,omitempty"` // UUID for OpenApiMetadataEntry. This is immutable + IsPersistent bool `json:"persistent,omitempty"` // Persistent entries can be copied over on some entity operation, for example: Creating a copy of an Org VDC, capturing a vApp to a template, instantiating a catalog item as a VM, etc. + IsReadOnly bool `json:"readOnly,omitempty"` // The kind of level of access organizations of the entry’s domain have + KeyValue OpenApiMetadataKeyValue `json:"keyValue,omitempty"` // Contains core metadata entry data +} + +// OpenApiMetadataKeyValue contains core metadata entry data. +type OpenApiMetadataKeyValue struct { + Domain string `json:"domain,omitempty"` // Only meaningful for providers. Allows them to share entries with their tenants. Currently, accepted values are: `TENANT`, `PROVIDER`, where that is the ascending sort order of the enumeration. + Key string `json:"key,omitempty"` // Key of the metadata entry + Value OpenApiMetadataTypedValue `json:"value,omitempty"` // Value of the metadata entry + Namespace string `json:"namespace,omitempty"` // Namespace of the metadata entry +} + +// OpenApiMetadataTypedValue the type and value of the metadata entry. +type OpenApiMetadataTypedValue struct { + Value interface{} `json:"value,omitempty"` // The Value is anything because it depends on the Type field. + Type string `json:"type,omitempty"` +} + +// VgpuProfile uniquely represents a type of vGPU +// vGPU Profiles are fetched from your NVIDIA GRID GPU enabled Clusters in vCenter. +type VgpuProfile struct { + Id string `json:"id"` + Name string `json:"name"` + TenantFacingName string `json:"tenantFacingName"` + Instructions string `json:"instructions,omitempty"` + AllowMultiplePerVm bool `json:"allowMultiplePerVm"` + Count int `json:"count,omitempty"` +} + +type OpenApiOrg struct { + Id string `json:"id"` + Name string `json:"name"` + DisplayName string `json:"displayName"` + Description string `json:"description"` + IsEnabled bool `json:"isEnabled"` + OrgVdcCount int `json:"orgVdcCount"` + CatalogCount int `json:"catalogCount"` + VappCount int `json:"vappCount"` + RunningVMCount int `json:"runningVMCount"` + UserCount int `json:"userCount"` + DiskCount int `json:"diskCount"` + CanPublish bool `json:"canPublish"` +} diff --git a/types/v56/saml.go b/types/v56/saml.go index 6fe5fc39b..f8ade72e2 100644 --- a/types/v56/saml.go +++ b/types/v56/saml.go @@ -1,5 +1,5 @@ /* - * Copyright 2020 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. */ package types @@ -11,14 +11,50 @@ import ( // VcdSamlMetadata helps to marshal vCD SAML Metadata endpoint response // https://1.1.1.1/cloud/org/my-org/saml/metadata/alias/vcd -// -// Note. This structure is not complete and has many more fields. type VcdSamlMetadata struct { XMLName xml.Name `xml:"EntityDescriptor"` + Xmlns string `xml:"xmlns,attr,omitempty"` Text string `xml:",chardata"` ID string `xml:"ID,attr"` + Md string `xml:"xmlns:md,attr,omitempty"` + // EntityID is the configured vCD Entity ID which is used in ADFS authentication request + // Note: once this field is set, it is not possible to change it back to empty, + // but only to replace it with a different value EntityID string `xml:"entityID,attr"` + // SPSSODescriptor is the main body of the SAML metadata file, which defines what the SAML identity provider can do + SPSSODescriptor SPSSODescriptor `xml:"SPSSODescriptor,omitempty"` +} + +// SPSSODescriptor is the main body of the SAML metadata file, which defines what the SAML identity provider can do +type SPSSODescriptor struct { + Ds string `xml:"xmlns:ds,attr,omitempty"` + AuthnRequestsSigned bool `xml:"AuthnRequestsSigned,attr"` + ProtocolSupportEnumeration string `xml:"protocolSupportEnumeration,attr"` + WantAssertionsSigned bool `xml:"WantAssertionsSigned,attr"` + KeyDescriptor []struct { + Use string `xml:"use,attr"` + KeyInfo struct { + //Ds string `xml:"xmlns:ds,attr"` + X509Data struct { + X509Certificate string `xml:"X509Certificate"` + } `xml:"X509Data"` + } `xml:"KeyInfo"` + } `xml:"KeyDescriptor"` + + SingleLogoutService []struct { + Binding string `xml:"Binding,attr"` + Location string `xml:"Location,attr"` + } `xml:"SingleLogoutService"` + NameIDFormat []string `xml:"NameIDFormat"` + AssertionConsumerService []struct { + Binding string `xml:"Binding,attr"` + Hoksso string `xml:"xmlns:hoksso,attr"` + Index int `xml:"index,attr"` + IsDefault bool `xml:"isDefault,attr,omitempty"` + Location string `xml:"Location,attr"` + ProtocolBinding string `xml:"ProtocolBinding,attr"` + } `xml:"AssertionConsumerService"` } // AdfsAuthErrorEnvelope helps to parse ADFS authentication error with help of Error() method @@ -53,12 +89,12 @@ type AdfsAuthErrorEnvelope struct { } // Error satisfies Go's default `error` interface for AdfsAuthErrorEnvelope and formats -// error for humand readable output +// error for human readable output func (samlErr AdfsAuthErrorEnvelope) Error() string { return fmt.Sprintf("SAML request got error: %s", samlErr.Body.Fault.Reason.Text) } -// AdfsAuthResponseEnvelope helps to marshal ADFS reponse to authentication request. +// AdfsAuthResponseEnvelope helps to marshal ADFS response to authentication request. // // Note. This structure is not complete and has many more fields. type AdfsAuthResponseEnvelope struct { @@ -72,3 +108,30 @@ type AdfsAuthResponseEnvelope struct { } `xml:"RequestSecurityTokenResponseCollection"` } `xml:"Body"` } + +// OrgFederationSettings is the structure used to set SAML identity service for an organization +type OrgFederationSettings struct { + Href string `xml:"href,attr,omitempty" json:"href,omitempty"` + Type string `xml:"type,attr,omitempty" json:"type,omitempty"` + Link LinkList `xml:"Link,omitempty" json:"link,omitempty"` + SAMLMetadata string `xml:"SAMLMetadata" json:"samlMetadata"` + Enabled bool `xml:"Enabled" json:"enabled"` + CertificateExpiration string `xml:"CertificateExpiration" json:"certificateExpiration"` + SigningCertificateExpiration string `xml:"SigningCertificateExpiration" json:"signingCertificateExpiration"` + EncryptionCertificateExpiration string `xml:"EncryptionCertificateExpiration" json:"encryptionCertificateExpiration"` + SamlSPEntityID string `xml:"SamlSPEntityId" json:"samlSPEntityId"` + SamlAttributeMapping struct { // The names of SAML attributes used to populate user profiles. + Href string `xml:"href,attr,omitempty" json:"href,omitempty"` + Type string `xml:"type,attr,omitempty" json:"type,omitempty"` + Link LinkList `xml:"Link,omitempty" json:"link,omitempty"` + EmailAttributeName string `xml:"EmailAttributeName,omitempty" json:"emailAttributeName,omitempty"` + UserNameAttributeName string `xml:"UserNameAttributeName,omitempty" json:"userNameAttributeName,omitempty"` + FirstNameAttributeName string `xml:"FirstNameAttributeName,omitempty" json:"firstNameAttributeName,omitempty"` + SurnameAttributeName string `xml:"SurnameAttributeName,omitempty" json:"surnameAttributeName,omitempty"` + FullNameAttributeName string `xml:"FullNameAttributeName,omitempty" json:"fullNameAttributeName,omitempty"` + GroupAttributeName string `xml:"GroupAttributeName,omitempty" json:"groupAttributeName,omitempty"` + RoleAttributeName string `xml:"RoleAttributeName,omitempty" json:"roleAttributeName,omitempty"` + } `xml:"SamlAttributeMapping,omitempty" json:"samlAttributeMapping,omitempty"` + SigningCertLibraryItemID string `xml:"SigningCertLibraryItemId" json:"signingCertLibraryItemId"` + EncryptionCertLibraryItemID string `xml:"EncryptionCertLibraryItemId" json:"encryptionCertLibraryItemID"` +} diff --git a/types/v56/slz.go b/types/v56/slz.go new file mode 100644 index 000000000..bb9e935c3 --- /dev/null +++ b/types/v56/slz.go @@ -0,0 +1,111 @@ +package types + +import "time" + +// SolutionLandingZoneType defines the configuration of Solution Landing Zone. +// It uses RDE so this body must be inserted into `types.DefinedEntity.State` field +type SolutionLandingZoneType struct { + // ID is the Org ID that the Solution Landing Zone is configured for + ID string `json:"id"` + // Name is the Org name that the Solution Landing Zone is configured for + Name string `json:"name,omitempty"` + // Catalogs + Catalogs []SolutionLandingZoneCatalog `json:"catalogs"` + Vdcs []SolutionLandingZoneVdc `json:"vdcs"` +} + +type SolutionLandingZoneCatalog struct { + ID string `json:"id"` + Name string `json:"name,omitempty"` + Capabilities []string `json:"capabilities"` +} + +type SolutionLandingZoneVdc struct { + ID string `json:"id"` + Name string `json:"name,omitempty"` + Capabilities []string `json:"capabilities"` + IsDefault bool `json:"isDefault"` + Networks []SolutionLandingZoneVdcChild `json:"networks"` + StoragePolicies []SolutionLandingZoneVdcChild `json:"storagePolicies"` + ComputePolicies []SolutionLandingZoneVdcChild `json:"computePolicies"` +} + +type SolutionLandingZoneVdcChild struct { + ID string `json:"id"` + Name string `json:"name,omitempty"` + IsDefault bool `json:"isDefault"` + Capabilities []string `json:"capabilities"` +} + +// SolutionAddOn defines structure of Solution Add-On that is deployed in the Solution Landing Zone +type SolutionAddOn struct { + Eula string `json:"eula"` + Icon string `json:"icon"` + Manifest map[string]any `json:"manifest"` + Origin SolutionAddOnOrigin `json:"origin"` + Status string `json:"status"` +} + +type SolutionAddOnOrigin struct { + Type string `json:"type"` + AcceptedBy string `json:"acceptedBy"` + AcceptedOn string `json:"acceptedOn"` + CatalogItemId string `json:"catalogItemId"` +} + +// SolutionAddOnInstance represents the RDE Entity structure for Solution Add-On Instances +type SolutionAddOnInstance struct { + Name string `json:"name"` + Scope SolutionAddOnInstanceScope `json:"scope"` + Status string `json:"status"` + Runtime SolutionAddOnInstanceRuntime `json:"runtime"` + Elements []any `json:"elements"` + Requests []SolutionAddOnInstanceRequests `json:"requests"` + Prototype string `json:"prototype"` + Resources []any `json:"resources"` + Properties map[string]any `json:"properties"` + EncryptionKey string `json:"encryptionKey"` + AddonInstanceSolutionName string `json:"addonInstanceSolutionName"` + AddonInstanceSolutionVendor string `json:"addonInstanceSolutionVendor"` + AddonInstanceSolutionVersion string `json:"addonInstanceSolutionVersion"` +} +type SolutionAddOnInstanceScope struct { + AllTenants bool `json:"allTenants"` + TenantScoped bool `json:"tenantScoped"` + Tenants []string `json:"tenants"` + ProviderScoped bool `json:"providerScoped"` +} +type SolutionAddOnInstanceRuntime struct { + GoVersion string `json:"goVersion"` + SdkVersion string `json:"sdkVersion"` + VcdVersion string `json:"vcdVersion"` + Environment string `json:"environment"` +} +type SolutionAddOnInstanceRequests struct { + Error string `json:"error"` + Status string `json:"status"` + Operation string `json:"operation"` + StartedBy string `json:"startedBy"` + StartedOn time.Time `json:"startedOn"` + InvocationID string `json:"invocationId"` +} + +// SolutionAddOnInputField represents the schema that is defined for each field that is specified in +// a particular Solution Add-On +type SolutionAddOnInputField struct { + Name string `json:"name"` + Type string `json:"type"` + Title string `json:"title"` + Values map[string]string `json:"values,omitempty"` + Default any `json:"default,omitempty"` + Required bool `json:"required,omitempty"` + Description string `json:"description"` + Secure bool `json:"secure,omitempty"` + Validation string `json:"validation,omitempty"` + View string `json:"view,omitempty"` + Delete bool `json:"delete,omitempty"` +} + +type SolutionAddOnInput struct { + Inputs []SolutionAddOnInputField `json:"inputs"` +} diff --git a/types/v56/types.go b/types/v56/types.go index 83c9c0c2f..6a963b5c7 100644 --- a/types/v56/types.go +++ b/types/v56/types.go @@ -1,5 +1,5 @@ /* - * Copyright 2021 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. + * Copyright 2023 VMware, Inc. All rights reserved. Licensed under the Apache v2 License. */ // Package types/v56 provider all types which are used by govcd package in order to perform API @@ -35,6 +35,8 @@ var VAppStatuses = map[int]string{ 17: "TRANSFER_TIMEOUT", 18: "VAPP_UNDEPLOYED", 19: "VAPP_PARTIALLY_DEPLOYED", + 20: "PARTIALLY_POWERED_OFF", // VCD 10.3+ + 21: "PARTIALLY_SUSPENDED", } // Maps status Attribute Values for VDC Objects @@ -88,14 +90,30 @@ type LeaseSettingsSection struct { StorageLeaseInSeconds int `xml:"StorageLeaseInSeconds,omitempty"` } +// UpdateLeaseSettingsSection is an extended version of LeaseSettingsSection +// with additional fields for update +type UpdateLeaseSettingsSection struct { + XMLName xml.Name `xml:"LeaseSettingsSection"` + XmlnsOvf string `xml:"xmlns:ovf,attr,omitempty"` + Xmlns string `xml:"xmlns,attr,omitempty"` + OVFInfo string `xml:"ovf:Info"` + HREF string `xml:"href,attr,omitempty"` + Type string `xml:"type,attr,omitempty"` + DeploymentLeaseExpiration string `xml:"DeploymentLeaseExpiration,omitempty"` + DeploymentLeaseInSeconds *int `xml:"DeploymentLeaseInSeconds,omitempty"` + Link *Link `xml:"Link,omitempty"` + StorageLeaseExpiration string `xml:"StorageLeaseExpiration,omitempty"` + StorageLeaseInSeconds *int `xml:"StorageLeaseInSeconds,omitempty"` +} + // IPRange represents a range of IP addresses, start and end inclusive. // Type: IpRangeType // Namespace: http://www.vmware.com/vcloud/v1.5 // Description: Represents a range of IP addresses, start and end inclusive. // Since: 0.9 type IPRange struct { - StartAddress string `xml:"StartAddress"` // Start address of the IP range. - EndAddress string `xml:"EndAddress"` // End address of the IP range. + StartAddress string `xml:"StartAddress" json:"startAddress,omitempty"` // Start address of the IP range. + EndAddress string `xml:"EndAddress" json:"endAddress,omitempty"` // End address of the IP range. } // DhcpService represents a DHCP network service. @@ -135,7 +153,7 @@ type NetworkFeatures struct { // Description: A list of IP addresses. // Since: 0.9 type IPAddresses struct { - IPAddress []string `xml:"IpAddress,omitempty"` // A list of IP addresses. + IPAddress []string `xml:"IpAddress,omitempty" json:"ipAddress,omitempty"` // A list of IP addresses. } // IPRanges represents a list of IP ranges. @@ -144,7 +162,7 @@ type IPAddresses struct { // Description: Represents a list of IP ranges. // Since: 0.9 type IPRanges struct { - IPRange []*IPRange `xml:"IpRange,omitempty"` // IP range. + IPRange []*IPRange `xml:"IpRange,omitempty" json:"ipRange,omitempty"` // IP range. } // IPScope specifies network settings like gateway, network mask, DNS servers IP ranges etc @@ -153,16 +171,18 @@ type IPRanges struct { // Description: Specify network settings like gateway, network mask, DNS servers, IP ranges, etc. // Since: 0.9 type IPScope struct { - IsInherited bool `xml:"IsInherited"` // True if the IP scope is inherit from parent network. - Gateway string `xml:"Gateway,omitempty"` // Gateway of the network. - Netmask string `xml:"Netmask,omitempty"` // Network mask. - DNS1 string `xml:"Dns1,omitempty"` // Primary DNS server. - DNS2 string `xml:"Dns2,omitempty"` // Secondary DNS server. - DNSSuffix string `xml:"DnsSuffix,omitempty"` // DNS suffix. - IsEnabled bool `xml:"IsEnabled,omitempty"` // Indicates if subnet is enabled or not. Default value is True. - IPRanges *IPRanges `xml:"IpRanges,omitempty"` // IP ranges used for static pool allocation in the network. - AllocatedIPAddresses *IPAddresses `xml:"AllocatedIpAddresses,omitempty"` // Read-only list of allocated IP addresses in the network. - SubAllocations *SubAllocations `xml:"SubAllocations,omitempty"` // Read-only list of IP addresses that are sub allocated to edge gateways. + IsInherited bool `xml:"IsInherited" json:"isInherited,omitempty"` // True if the IP scope is inherit from parent network. + Gateway string `xml:"Gateway,omitempty" json:"gateway,omitempty"` // Gateway of the network. + Netmask string `xml:"Netmask,omitempty" json:"netmask,omitempty"` // Network mask. + SubnetPrefixLength string `xml:"SubnetPrefixLength,omitempty"` // Prefix length (as an string, used everywhere). + SubnetPrefixLengthInt *int `json:"subnetPrefixLength,omitempty"` // Prefix length (as an int, used in VDC Templates). + DNS1 string `xml:"Dns1,omitempty" json:"dns1,omitempty"` // Primary DNS server. + DNS2 string `xml:"Dns2,omitempty" json:"dns2,omitempty"` // Secondary DNS server. + DNSSuffix string `xml:"DnsSuffix,omitempty" json:"dnsSuffix,omitempty"` // DNS suffix. + IsEnabled bool `xml:"IsEnabled,omitempty" json:"isEnabled,omitempty"` // Indicates if subnet is enabled or not. Default value is True. + IPRanges *IPRanges `xml:"IpRanges,omitempty" json:"ipRanges,omitempty"` // IP ranges used for static pool allocation in the network. + AllocatedIPAddresses *IPAddresses `xml:"AllocatedIpAddresses,omitempty" json:"allocatedIPAddresses,omitempty"` // Read-only list of allocated IP addresses in the network. + SubAllocations *SubAllocations `xml:"SubAllocations,omitempty" json:"subAllocations,omitempty"` // Read-only list of IP addresses that are sub allocated to edge gateways. } // SubAllocations a list of IP addresses that are sub allocated to edge gateways. @@ -172,11 +192,11 @@ type IPScope struct { // Since: 5.1 type SubAllocations struct { // Attributes - HREF string `xml:"href,attr,omitempty"` // The URI of the entity. - Type string `xml:"type,attr,omitempty"` // The MIME type of the entity. + HREF string `xml:"href,attr,omitempty" json:"href,omitempty"` // The URI of the entity. + Type string `xml:"type,attr,omitempty" json:"type,omitempty"` // The MIME type of the entity. // Elements - Link LinkList `xml:"Link,omitempty"` // A reference to an entity or operation associated with this object. - SubAllocation *SubAllocation `xml:"SubAllocation,omitempty"` // IP Range sub allocated to a edge gateway. + Link LinkList `xml:"Link,omitempty" json:"link,omitempty"` // A reference to an entity or operation associated with this object. + SubAllocation *SubAllocation `xml:"SubAllocation,omitempty" json:"subAllocation,omitempty"` // IP Range sub allocated to a edge gateway. } // SubAllocation IP range sub allocated to an edge gateway. @@ -185,8 +205,8 @@ type SubAllocations struct { // Description: IP range sub allocated to an edge gateway. // Since: 5.1 type SubAllocation struct { - EdgeGateway *Reference `xml:"EdgeGateway,omitempty"` // Edge gateway that uses this sub allocation. - IPRanges *IPRanges `xml:"IpRanges,omitempty"` // IP range sub allocated to the edge gateway. + EdgeGateway *Reference `xml:"EdgeGateway,omitempty" json:"edgeGateway,omitempty"` // Edge gateway that uses this sub allocation. + IPRanges *IPRanges `xml:"IpRanges,omitempty" json:"ipRanges,omitempty"` // IP range sub allocated to the edge gateway. } // IPScopes represents a list of IP scopes. @@ -195,7 +215,7 @@ type SubAllocation struct { // Description: Represents a list of IP scopes. // Since: 5.1 type IPScopes struct { - IPScope []*IPScope `xml:"IpScope"` // IP scope. + IPScope []*IPScope `xml:"IpScope" json:"ipScope,omitempty"` // IP scope. } // NetworkConfiguration is the configuration applied to a network. This is an abstract base type. @@ -206,25 +226,30 @@ type IPScopes struct { // Since: 0.9 type NetworkConfiguration struct { Xmlns string `xml:"xmlns,attr,omitempty"` - BackwardCompatibilityMode bool `xml:"BackwardCompatibilityMode"` - IPScopes *IPScopes `xml:"IpScopes,omitempty"` - ParentNetwork *Reference `xml:"ParentNetwork,omitempty"` - FenceMode string `xml:"FenceMode"` - RetainNetInfoAcrossDeployments *bool `xml:"RetainNetInfoAcrossDeployments,omitempty"` - Features *NetworkFeatures `xml:"Features,omitempty"` + BackwardCompatibilityMode bool `xml:"BackwardCompatibilityMode" json:"backwardCompatibilityMode,omitempty"` + IPScopes *IPScopes `xml:"IpScopes,omitempty" json:"ipScopes,omitempty"` + ParentNetwork *Reference `xml:"ParentNetwork,omitempty" json:"parentNetwork,omitempty"` + FenceMode string `xml:"FenceMode" json:"fenceMode,omitempty"` + RetainNetInfoAcrossDeployments *bool `xml:"RetainNetInfoAcrossDeployments,omitempty" json:"retainNetInfoAcrossDeployments,omitempty"` + Features *NetworkFeatures `xml:"Features,omitempty" json:"features,omitempty"` // SubInterface and DistributedInterface are mutually exclusive // When they are both nil, it means the "internal" interface (the default) will be used. // When one of them is set, the corresponding interface will be used. // They cannot be both set (we'll get an API error if we do). - SubInterface *bool `xml:"SubInterface,omitempty"` - DistributedInterface *bool `xml:"DistributedInterface,omitempty"` - GuestVlanAllowed *bool `xml:"GuestVlanAllowed,omitempty"` + SubInterface *bool `xml:"SubInterface,omitempty"` + DistributedInterface *bool `xml:"DistributedInterface,omitempty"` + GuestVlanAllowed *bool `xml:"GuestVlanAllowed,omitempty"` + RouterInfo RouterInfo `xml:"RouterInfo,omitempty"` // TODO: Not Implemented - // RouterInfo RouterInfo `xml:"RouterInfo,omitempty"` // SyslogServerSettings SyslogServerSettings `xml:"SyslogServerSettings,omitempty"` } +// RouterInfo represents the router information for a network. +type RouterInfo struct { + ExternalIP string `xml:"ExternalIp,omitempty"` +} + // VAppNetworkConfiguration represents a vApp network configuration // Used in vApp network configuration actions as part of vApp type, // VApp.NetworkConfigSection.NetworkConfig or directly as NetworkConfigSection.NetworkConfig for various API calls. @@ -356,20 +381,20 @@ type InstantiationParams struct { type OrgVDCNetwork struct { XMLName xml.Name `xml:"OrgVdcNetwork"` Xmlns string `xml:"xmlns,attr,omitempty"` - HREF string `xml:"href,attr,omitempty"` - Type string `xml:"type,attr,omitempty"` - ID string `xml:"id,attr,omitempty"` - OperationKey string `xml:"operationKey,attr,omitempty"` - Name string `xml:"name,attr"` - Status string `xml:"status,attr,omitempty"` - Link []Link `xml:"Link,omitempty"` - Description string `xml:"Description,omitempty"` - Configuration *NetworkConfiguration `xml:"Configuration,omitempty"` - EdgeGateway *Reference `xml:"EdgeGateway,omitempty"` - ServiceConfig *GatewayFeatures `xml:"ServiceConfig,omitempty"` // Specifies the service configuration for an isolated Org VDC networks - IsShared bool `xml:"IsShared"` - VimPortGroupRef []*VimObjectRef `xml:"VimPortGroupRef,omitempty"` // Needed to set up DHCP inside ServiceConfig - Tasks *TasksInProgress `xml:"Tasks,omitempty"` + HREF string `xml:"href,attr,omitempty" json:"href,omitempty"` + Type string `xml:"type,attr,omitempty" json:"type,omitempty"` + ID string `xml:"id,attr,omitempty" json:"id,omitempty"` + OperationKey string `xml:"operationKey,attr,omitempty" json:"operationKey,omitempty"` + Name string `xml:"name,attr" json:"name"` + Status string `xml:"status,attr,omitempty" json:"status,omitempty"` + Link []Link `xml:"Link,omitempty" json:"link,omitempty"` + Description string `xml:"Description,omitempty" json:"description"` + Configuration *NetworkConfiguration `xml:"Configuration,omitempty" json:"configuration,omitempty"` + EdgeGateway *Reference `xml:"EdgeGateway,omitempty" json:"edgeGateway,omitempty"` + ServiceConfig *GatewayFeatures `xml:"ServiceConfig,omitempty" json:"serviceConfig,omitempty"` // Specifies the service configuration for an isolated Org VDC networks + IsShared bool `xml:"IsShared" json:"isShared,omitempty"` + VimPortGroupRef []*VimObjectRef `xml:"VimPortGroupRef,omitempty" json:"vimPortGroupRef,omitempty"` // Needed to set up DHCP inside ServiceConfig + Tasks *TasksInProgress `xml:"Tasks,omitempty" json:"tasks,omitempty"` } // SupportedHardwareVersions contains a list of VMware virtual hardware versions supported in this vDC. @@ -378,7 +403,7 @@ type OrgVDCNetwork struct { // Description: Contains a list of VMware virtual hardware versions supported in this vDC. // Since: 1.5 type SupportedHardwareVersions struct { - SupportedHardwareVersion []string `xml:"SupportedHardwareVersion,omitempty"` // A virtual hardware version supported in this vDC. + SupportedHardwareVersion []Reference `xml:"SupportedHardwareVersion,omitempty"` // A virtual hardware version supported in this vDC. } // Capabilities collection of supported hardware capabilities. @@ -387,7 +412,7 @@ type SupportedHardwareVersions struct { // Description: Collection of supported hardware capabilities. // Since: 1.5 type Capabilities struct { - SupportedHardwareVersions *SupportedHardwareVersions `xml:"SupportedHardwareVersions,omitempty"` // Read-only list of virtual hardware versions supported by this vDC. + SupportedHardwareVersions *SupportedHardwareVersions `xml:"SupportedHardwareVersions,omitempty" json:"supportedHardwareVersions,omitempty"` // Read-only list of virtual hardware versions supported by this vDC. } // Vdc represents the user view of an organization VDC. @@ -429,14 +454,17 @@ type AdminVdc struct { Xmlns string `xml:"xmlns,attr"` Vdc - VCpuInMhz2 *int64 `xml:"VCpuInMhz2,omitempty"` - ResourceGuaranteedMemory *float64 `xml:"ResourceGuaranteedMemory,omitempty"` - ResourceGuaranteedCpu *float64 `xml:"ResourceGuaranteedCpu,omitempty"` - VCpuInMhz *int64 `xml:"VCpuInMhz,omitempty"` - IsThinProvision *bool `xml:"IsThinProvision,omitempty"` - NetworkPoolReference *Reference `xml:"NetworkPoolReference,omitempty"` - ProviderVdcReference *Reference `xml:"ProviderVdcReference"` - ResourcePoolRefs *VimObjectRefs `xml:"vmext:ResourcePoolRefs,omitempty"` + VCpuInMhz2 *int64 `xml:"VCpuInMhz2,omitempty"` + ResourceGuaranteedMemory *float64 `xml:"ResourceGuaranteedMemory,omitempty"` + ResourceGuaranteedCpu *float64 `xml:"ResourceGuaranteedCpu,omitempty"` + VCpuInMhz *int64 `xml:"VCpuInMhz,omitempty"` + IsThinProvision *bool `xml:"IsThinProvision,omitempty"` + NetworkPoolReference *Reference `xml:"NetworkPoolReference,omitempty"` + ProviderVdcReference *Reference `xml:"ProviderVdcReference"` + + // ResourcePoolRefs is a read-only field and should be avoided in XML structure for write + // operations because it breaks on Go marshalling bug https://github.com/golang/go/issues/9519 + ResourcePoolRefs *VimObjectRefs `xml:"ResourcePoolRefs,omitempty"` UsesFastProvisioning *bool `xml:"UsesFastProvisioning,omitempty"` OverCommitAllowed bool `xml:"OverCommitAllowed,omitempty"` VmDiscoveryEnabled bool `xml:"VmDiscoveryEnabled,omitempty"` @@ -445,6 +473,102 @@ type AdminVdc struct { UniversalNetworkPoolReference *Reference `xml:"UniversalNetworkPoolReference,omitempty"` // Reference to a universal network pool } +// ProviderVdc represents a Provider VDC. +// Type: ProviderVdcType +// Namespace: http://www.vmware.com/vcloud/v1.5 +// Description: Represents a Provider VDC. +// Since: 0.9 +type ProviderVdc struct { + HREF string `xml:"href,attr,omitempty" json:"href,omitempty"` + Type string `xml:"type,attr,omitempty" json:"type,omitempty"` + ID string `xml:"id,attr,omitempty" json:"id,omitempty"` + OperationKey string `xml:"operationKey,attr,omitempty" json:"operationKey,omitempty"` + Name string `xml:"name,attr" json:"name"` + Status int `xml:"status,attr,omitempty" json:"status,omitempty"` // -1 (creation failed), 0 (not ready), 1 (ready), 2 (unknown), 3 (unrecognized) + + AvailableNetworks *AvailableNetworks `xml:"AvailableNetworks,omitempty" json:"availableNetworks,omitempty"` // Read-only list of available networks. + Capabilities *Capabilities `xml:"Capabilities,omitempty" json:"capabilities,omitempty"` // Read-only list of virtual hardware versions supported by this Provider VDC. + ComputeCapacity *RootComputeCapacity `xml:"ComputeCapacity,omitempty" json:"computeCapacity,omitempty"` // Read-only indicator of CPU and memory capacity. + Description string `xml:"Description,omitempty" json:"description,omitempty"` // Optional description. + IsEnabled *bool `xml:"IsEnabled,omitempty" json:"isEnabled,omitempty"` // True if this Provider VDC is enabled and can provide resources to organization VDCs. A Provider VDC is always enabled on creation. + Link *LinkList `xml:"Link,omitempty" json:"link,omitempty"` // A reference to an entity or operation associated with this object. + NetworkPoolReferences *NetworkPoolReferences `xml:"NetworkPoolReferences,omitempty" json:"networkPoolReferences,omitempty"` // Read-only list of network pools used by this Provider VDC. + StorageProfiles *ProviderStorageProfiles `xml:"StorageProfiles,omitempty" json:"storageProfiles,omitempty"` // Container for references to vSphere storage profiles available to this Provider VDC. + Tasks *TasksInProgress `xml:"Tasks,omitempty" json:"tasks,omitempty"` // A list of queued, running, or recently completed tasks associated with this entity. +} + +// VMWProviderVdc represents an extension of ProviderVdc. +// Type: VMWProviderVdcType +// Namespace: http://www.vmware.com/vcloud/v1.5 +// Description: Represents an extension of ProviderVdc. +// Since: 1.0 +type VMWProviderVdc struct { + ProviderVdc + + AvailableUniversalNetworkPool *Reference `xml:"AvailableUniversalNetworkPool,omitempty" json:"availableUniversalNetworkPool,omitempty"` // Selectable universal network reference. + ComputeProviderScope string `xml:"ComputeProviderScope,omitempty" json:"computeProviderScope,omitempty"` // The compute provider scope represents the compute fault domain for this provider VDC. This value is a tenant-facing tag that is shown to tenants when viewing fault domains of the child Organization VDCs (for ex. a VDC Group). + DataStoreRefs VimObjectRefs `xml:"DataStoreRefs" json:"dataStoreRefs"` // vSphere datastores backing this provider VDC. + HighestSupportedHardwareVersion string `xml:"HighestSupportedHardwareVersion,omitempty" json:"highestSupportedHardwareVersion,omitempty"` // The highest virtual hardware version supported by this Provider VDC. If empty or omitted on creation, the system sets it to the highest virtual hardware version supported by all hosts in the primary resource pool. You can modify it when you add more resource pools. + HostReferences *VMWHostReferences `xml:"HostReferences,omitempty" json:"hostReferences,omitempty"` // Shows all hosts which are connected to VC server. + NsxTManagerReference *Reference `xml:"NsxTManagerReference,omitempty" json:"nsxTManagerReference,omitempty"` // An optional reference to a registered NSX-T Manager to back networking operations for this provider VDC. + ResourcePoolRefs *VimObjectRefs `xml:"ResourcePoolRefs,omitempty" json:"resourcePoolRefs,omitempty"` // Resource pools backing this provider VDC. On create, you must specify a resource pool that is not used by (and is not the child of a resource pool used by) any other provider VDC. On modify, this element is required for schema validation, but its contents cannot be changed. + VimServer []*Reference `xml:"VimServer,omitempty" json:"vimServer,omitempty"` // The vCenter server that provides the resource pools and datastores. A valid reference is required on create. On modify, this element is required for schema validation, but its contents cannot be changed. +} + +// VMWHostReferences represents a list of available hosts. +// Type: VMWHostReferencesType +// Namespace: http://www.vmware.com/vcloud/v1.5 +// Description: Represents a list of available hosts. +// Since: 1.0 +type VMWHostReferences struct { + HostReference []*Reference `xml:"HostReference,omitempty" json:"hostReference,omitempty"` + Link *Link `xml:"Link,omitempty" json:"link,omitempty"` +} + +// RootComputeCapacity represents compute capacity with units. +// Type: RootComputeCapacityType +// Namespace: http://www.vmware.com/vcloud/v1.5 +// Description: Represents compute capacity with units. +// Since: 0.9 +type RootComputeCapacity struct { + Cpu *ProviderVdcCapacity `xml:"Cpu" json:"cpu"` + IsElastic bool `xml:"IsElastic,omitempty" json:"isElastic,omitempty"` + IsHA bool `xml:"IsHA,omitempty" json:"isHA,omitempty"` + Memory *ProviderVdcCapacity `xml:"Memory" json:"memory"` +} + +// NetworkPoolReferences is a container for references to network pools in this vDC. +// Type: NetworkPoolReferencesType +// Namespace: http://www.vmware.com/vcloud/v1.5 +// Description: Container for references to network pools in this vDC. +// Since: 0.9 +type NetworkPoolReferences struct { + NetworkPoolReference []*Reference `xml:"NetworkPoolReference" json:"networkPoolReference"` +} + +// ProviderStorageProfiles is a container for references to storage profiles associated with a Provider vDC. +// Type: ProviderVdcStorageProfilesType +// Namespace: http://www.vmware.com/vcloud/v1.5 +// Description: Container for references to storage profiles associated with a Provider vDC. +// Since: 0.9 +type ProviderStorageProfiles struct { + ProviderVdcStorageProfile []*Reference `xml:"ProviderVdcStorageProfile" json:"providerVdcStorageProfile,omitempty"` +} + +// ProviderVdcCapacity represents resource capacity in a Provider vDC. +// Type: ProviderVdcCapacityType +// Namespace: http://www.vmware.com/vcloud/v1.5 +// Description: Represents resource capacity in a Provider vDC. +// Since: 0.9 +type ProviderVdcCapacity struct { + Allocation int64 `xml:"Allocation,omitempty"` + Overhead int64 `xml:"Overhead,omitempty"` + Reserved int64 `xml:"Reserved,omitempty"` + Total int64 `xml:"Total,omitempty"` + Units string `xml:"Units"` + Used int64 `xml:"Used,omitempty"` +} + // VdcStorageProfileConfiguration represents the parameters to assign a storage profile in creation of organization vDC. // Type: VdcStorageProfileParamsType // Namespace: http://www.vmware.com/vcloud/v1.5 @@ -452,7 +576,7 @@ type AdminVdc struct { // Since: 5.1 // https://code.vmware.com/apis/220/vcloud#/doc/doc/types/VdcStorageProfileParamsType.html type VdcStorageProfileConfiguration struct { - Enabled bool `xml:"Enabled,omitempty"` + Enabled *bool `xml:"Enabled,omitempty"` Units string `xml:"Units"` Limit int64 `xml:"Limit"` Default bool `xml:"Default"` @@ -465,16 +589,17 @@ type VdcStorageProfileConfiguration struct { // https://vdc-repo.vmware.com/vmwb-repository/dcr-public/7a028e78-bd37-4a6a-8298-9c26c7eeb9aa/09142237-dd46-4dee-8326-e07212fb63a8/doc/doc/types/VdcStorageProfileType.html // https://vdc-repo.vmware.com/vmwb-repository/dcr-public/71e12563-bc11-4d64-821d-92d30f8fcfa1/7424bf8e-aec2-44ad-be7d-b98feda7bae0/doc/doc/types/AdminVdcStorageProfileType.html type VdcStorageProfile struct { - Xmlns string `xml:"xmlns,attr"` - Name string `xml:"name,attr"` - Enabled bool `xml:"Enabled,omitempty"` - Units string `xml:"Units"` - Limit int64 `xml:"Limit"` - Default bool `xml:"Default"` - IopsSettings *VdcStorageProfileIopsSettings `xml:"IopsSettingsint64"` - StorageUsedMB int64 `xml:"StorageUsedMB"` - IopsAllocated int64 `xml:"IopsAllocated"` - ProviderVdcStorageProfile *Reference `xml:"ProviderVdcStorageProfile"` + ID string `xml:"id,attr,omitempty" json:"id,omitempty"` + Xmlns string `xml:"xmlns,attr,omitempty"` + Name string `xml:"name,attr" json:"name,omitempty"` + Enabled *bool `xml:"Enabled,omitempty" json:"enabled,omitempty"` + Units string `xml:"Units" json:"units,omitempty"` + Limit int64 `xml:"Limit" json:"limit,omitempty"` + Default bool `xml:"Default" json:"default,omitempty"` + IopsSettings *VdcStorageProfileIopsSettings `xml:"IopsSettings" json:"iopsSettings,omitempty"` + StorageUsedMB int64 `xml:"StorageUsedMB" json:"storageUsedMB,omitempty"` + IopsAllocated int64 `xml:"IopsAllocated" json:"iopsAllocated,omitempty"` + ProviderVdcStorageProfile *Reference `xml:"ProviderVdcStorageProfile" json:"providerVdcStorageProfile,omitempty"` } // AdminVdcStorageProfile represents the parameters for fetched storage profile @@ -488,7 +613,7 @@ type AdminVdcStorageProfile struct { Units string `xml:"Units"` Limit int64 `xml:"Limit"` Default bool `xml:"Default"` - IopsSettings *VdcStorageProfileIopsSettings `xml:"IopsSettingsint64"` + IopsSettings *VdcStorageProfileIopsSettings `xml:"IopsSettings"` StorageUsedMB int64 `xml:"StorageUsedMB"` IopsAllocated int64 `xml:"IopsAllocated"` ProviderVdcStorageProfile *Reference `xml:"ProviderVdcStorageProfile"` @@ -499,11 +624,11 @@ type AdminVdcStorageProfile struct { // https://vdc-repo.vmware.com/vmwb-repository/dcr-public/71e12563-bc11-4d64-821d-92d30f8fcfa1/7424bf8e-aec2-44ad-be7d-b98feda7bae0/doc/doc/types/VdcStorageProfileIopsSettingsType.html type VdcStorageProfileIopsSettings struct { Xmlns string `xml:"xmlns,attr"` - Enabled bool `xml:"enabled"` - DiskIopsMax int64 `xml:"diskIopsMax,"` - DiskIopsDefault int64 `xml:"diskIopsDefault"` - StorageProfileIopsLimit int64 `xml:"storageProfileIopsLimit,omitempty"` - DiskIopsPerGbMax int64 `xml:"diskIopsPerGbMax"` + Enabled bool `xml:"Enabled" json:"enabled,omitempty"` + DiskIopsMax int64 `xml:"DiskIopsMax" json:"diskIopsMax,omitempty"` + DiskIopsDefault int64 `xml:"DiskIopsDefault" json:"diskIopsDefault,omitempty"` + StorageProfileIopsLimit int64 `xml:"StorageProfileIopsLimit,omitempty" json:"storageProfileIopsLimit,omitempty"` + DiskIopsPerGbMax int64 `xml:"DiskIopsPerGbMax" json:"diskIopsPerGbMax,omitempty"` } // VdcConfiguration models the payload for creating a VDC. @@ -538,42 +663,220 @@ type VdcConfiguration struct { IncludeMemoryOverhead *bool `xml:"IncludeMemoryOverhead,omitempty"` // Supported from 32.0 for the Flex model } -// Task represents an asynchronous operation in vCloud Director. +// VMWVdcTemplate references a VDC Template. +// Type: VMWVdcTemplateType +// Namespace: http://www.vmware.com/vcloud/extension/v1.5 +// Description: A reference to a VDC template. +// Since: 5.7 +type VMWVdcTemplate struct { + HREF string `json:"href,omitempty"` + Type string `json:"type,omitempty"` + ID string `json:"id,omitempty"` + OperationKey string `json:"operationKey,omitempty"` + Name string `json:"name,omitempty"` + + Link LinkList `json:"link,omitempty"` + Description string `json:"description,omitempty"` + Tasks *TasksInProgress `json:"tasks,omitempty"` + TenantName string `json:"tenantName,omitempty"` + TenantDescription string `json:"tenantDescription,omitempty"` + NetworkBackingType string `json:"networkBackingType,omitempty"` // "NSX_V" or "NSX_T" + ProviderVdcReference []*VMWVdcTemplateProviderVdcSpecification `json:"providerVdcReference,omitempty"` + VdcTemplateSpecification *VMWVdcTemplateSpecification `json:"vdcTemplateSpecification,omitempty"` +} + +// VMWVdcTemplateProviderVdcSpecification references a Provider VDC for a VDC Template. +// Type: VMWVdcTemplateProviderVdcSpecificationType +// Namespace: http://www.vmware.com/vcloud/extension/v1.5 +// Since: 5.7 +type VMWVdcTemplateProviderVdcSpecification struct { + HREF string `json:"href,omitempty"` + Type string `json:"type,omitempty"` + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + + Binding []*VMWVdcTemplateBinding `json:"binding,omitempty"` +} + +// VMWVdcTemplateBinding specifies a binding for a VDC Template +// Type: VMWVdcTemplateBindingType +// Namespace: http://www.vmware.com/vcloud/extension/v1.5 +// Description: A Binding pairs a Name element that contains a user-specified identifier in URN format with a Value element +// that contains a reference to an object. The Name can then be used anywhere in the request where a reference +// to that type of object is allowed. For example, when specifying multiple Provider VDCs in a VMWVdcTemplate, +// create a Binding where the Value is a reference to an external network in a candidate Provider VDC, then use +// the Name from that binding in place of the href attribute required by the Network element in the GatewayConfiguration +// of the VdcTemplateSpecification. When the template is instantiated, the Name is replaced by the network reference +// in the Value part of the Binding associated with the Provider VDC that the system selects during instantiation. +// Supported binding values are references to External networks and Edge clusters. +// +// Since: 5.10 +type VMWVdcTemplateBinding struct { + Name string `json:"name,omitempty"` // URI format + Value *Reference `json:"value,omitempty"` +} + +// VMWVdcTemplateSpecification references a VDC for a VDC Template. +// Type: VMWVdcTemplateSpecificationType +// Namespace: http://www.vmware.com/vcloud/extension/v1.5 +// Description: A reference to a Provider VDC. +// Since: 5.7 +type VMWVdcTemplateSpecification struct { + Type string `json:"_type,omitempty"` + + // Indicates that the Provider VDC's automatically-created VXLAN network pool should be used. + // NetworkPoolReference must be empty if this element appears in the request. + AutomaticNetworkPoolReference *AutomaticNetworkPoolReference `json:"automaticNetworkPoolReference,omitempty"` + + // Maximum number of virtual NICs allowed in this VDC. Defaults to 0, which specifies an unlimited number. + NicQuota int `json:"nicQuota"` + + // The quota of VMs that can be created in this VDC. Includes VMs in both vApps and vApp templates, deployed, or otherwise. + // Defaults to 0, which specifies an unlimited number. + VmQuota int `json:"vmQuota"` + + // Maximum number of network objects that can be deployed in this VDC. Defaults to 0, which means no networks can be deployed. + ProvisionedNetworkQuota int `json:"provisionedNetworkQuota"` + + // Defines a gateway and NAT Routed organization VDC network to be created. + GatewayConfiguration *VdcTemplateSpecificationGatewayConfiguration `json:"gatewayConfiguration,omitempty"` + + // A set of name of Storage Profiles, with corresponding limit value, that all Provider VDCs must have, and that are selected at the time of VDC Template instantiation. + StorageProfile []*VdcStorageProfile `json:"storageProfile,omitempty"` + + // Set to true to indicate if the FLEX VDC is to be elastic. This field can only be set on input for FLEX VDC templates + // and Allocation VApp VDC templates. However, this field will be returned properly when read. + IsElastic *bool `json:"isElastic,omitempty"` + + // Set to true to indicate if the FLEX VDC is to include memory overhead into its accounting for admission control. + // This field can only be set on input for FLEX VDC templates and Allocation VApp VDC templates. + // However, this field will be returned properly when read. + IncludeMemoryOverhead *bool `json:"includeMemoryOverhead,omitempty"` + + // Boolean to request thin provisioning. Request will be honored only if the underlying datastore supports it. + // Thin provisioning saves storage space by committing it on demand. This allows over-allocation of storage. + ThinProvision bool `json:"thinProvision,omitempty"` + + // Boolean to request fast provisioning. Request will be honored only if the underlying datastore supports it. + // Fast provisioning can reduce the time it takes to create virtual machines by using vSphere linked clones. + // If you disable fast provisioning, all provisioning operations will result in full clones. + FastProvisioningEnabled bool `json:"fastProvisioningEnabled,omitempty"` + + // Reference to a network pool in the Provider VDC. Must be empty if you specify AutomaticNetworkPoolReference. + NetworkPoolReference *Reference `json:"networkPoolReference,omitempty"` + NetworkProfileConfiguration *VdcTemplateNetworkProfile `json:"networkProfileConfiguration,omitempty"` + + // Only in Flex VDCs + CpuAllocationMhz int `json:"cpuAllocationMhz"` + CpuLimitMhzPerVcpu int `json:"cpuLimitMhzPerVcpu"` + VCpuInMhz int `json:"vCpuInMhz"` + CpuLimitMhz int `json:"cpuLimitMhz"` + MemoryAllocationMB int `json:"memoryAllocationMB"` + MemoryLimitMb int `json:"memoryLimitMb"` + CpuGuaranteedPercentage int `json:"cpuGuaranteedPercentage"` + MemoryGuaranteedPercentage int `json:"memoryGuaranteedPercentage"` +} + +// AutomaticNetworkPoolReference is an empty struct that states that the Network pool of the Edge Gateway in a VDC Template must +// be chosen automatically. +// Type: AutomaticNetworkPoolReferenceType +// Namespace: http://www.vmware.com/vcloud/extension/v1.5 +type AutomaticNetworkPoolReference struct { +} + +// VdcTemplateSpecificationGatewayConfiguration specifies the Edge Gateway configuration for a VDC Template. +// Type: VdcTemplateSpecificationGatewayConfigurationType +// Namespace: http://www.vmware.com/vcloud/extension/v1.5 +// Description: Defines a gateway and NAT Routed organization VDC network to be created. +// Since: 5.7 +type VdcTemplateSpecificationGatewayConfiguration struct { + // EdgeGateway configuration for the VDC created by this template. The following restrictions apply: + // * You may not specify a BackwardCompatibilityMode or an EdgeGatewayServiceConfiguration. + // * The GatewayInterfaces element must contain exactly one GatewayInterface. That GatewayInterface must have an InterfaceType + // of uplink and must not contain a SubnetParticipation element. + Gateway *EdgeGateway `json:"gateway,omitempty"` + + // Org VDC network configuration created by this template. The following restrictions apply: + // * You may not specify a BackwardCompatibilityMode, EdgeGatewayServiceConfiguration, or NetworkFeatures. + // * The NetworkConfiguration must specify a FenceMode of natRouted. + Network *OrgVDCNetwork `json:"network,omitempty"` +} + +// VdcTemplateNetworkProfile specifies the network profile for a VDC Template. +// Type: VdcTemplateNetworkProfileType +// Namespace: http://www.vmware.com/vcloud/extension/v1.5 +// Description: Network Profile configuration that is applied to VDC instantiated from a template. +// In NSX_V VDCs Primary and Secondary Edge Clusters can be configured and used for Edge Gateway deployments. +// In NSX_T VDC only Services Edge Cluster can be configured and used for deploying DHCP/VApp services. Binding name from +// the binding names needs to specified as ReferenceType to PrimaryEdgeCluster SecondaryEdgeCluster and ServicesEdgeCluster +// properties. When VDC is instantiated, based on PVDC and binding name appropriate binding value is selected to configure network profiles. +// Since: 35.2 +type VdcTemplateNetworkProfile struct { + PrimaryEdgeCluster *Reference `json:"primaryEdgeCluster,omitempty"` + SecondaryEdgeCluster *Reference `json:"secondaryEdgeCluster,omitempty"` + ServicesEdgeCluster *Reference `json:"servicesEdgeCluster,omitempty"` +} + +// InstantiateVdcTemplateParams specifies the network profile for a VDC Template. +// Type: InstantiateVdcTemplateParamsType +// Namespace: http://www.vmware.com/vcloud/v1.5 +// Description: A basic type used to pass arguments to the instantiate VDC template operation, this provides a name and +// optional description for a VDC instantiated from a template. +// Since: 5.7 +type InstantiateVdcTemplateParams struct { + XMLName xml.Name `xml:"InstantiateVdcTemplateParams"` + Xmlns string `xml:"xmlns,attr"` + Name string `xml:"name,attr"` + Source *Reference `xml:"Source,omitempty"` + Description string `xml:"Description,omitempty"` +} + +// Task represents an asynchronous operation in VMware Cloud Director. // Type: TaskType // Namespace: http://www.vmware.com/vcloud/v1.5 -// Description: Represents an asynchronous operation in vCloud Director. +// Description: Represents an asynchronous operation in VMware Cloud Director.u // Since: 0.9 // Comments added from https://code.vmware.com/apis/912/vmware-cloud-director/doc/doc/types/TaskType.html type Task struct { - HREF string `xml:"href,attr,omitempty"` // The URI of the entity. - Type string `xml:"type,attr,omitempty"` // The MIME type of the entity. - ID string `xml:"id,attr,omitempty"` // The entity identifier, expressed in URN format. The value of this attribute uniquely identifies the entity, persists for the life of the entity, and is never reused. - OperationKey string `xml:"operationKey,attr,omitempty"` // Optional unique identifier to support idempotent semantics for create and delete operations. - Name string `xml:"name,attr"` // The name of the entity. - Status string `xml:"status,attr"` // The execution status of the task. One of queued, preRunning, running, success, error, aborted - Operation string `xml:"operation,attr,omitempty"` // A message describing the operation that is tracked by this task. - OperationName string `xml:"operationName,attr,omitempty"` // The short name of the operation that is tracked by this task. - ServiceNamespace string `xml:"serviceNamespace,attr,omitempty"` // Identifier of the service that created the task. It must not start with com.vmware.vcloud and the length must be between 1 and 128 symbols. - StartTime string `xml:"startTime,attr,omitempty"` // The date and time the system started executing the task. May not be present if the task has not been executed yet. - EndTime string `xml:"endTime,attr,omitempty"` // The date and time that processing of the task was completed. May not be present if the task is still being executed. - ExpiryTime string `xml:"expiryTime,attr,omitempty"` // The date and time at which the task resource will be destroyed and no longer available for retrieval. May not be present if the task has not been executed or is still being executed. - CancelRequested bool `xml:"cancelRequested,attr,omitempty"` // Whether user has requested this processing to be canceled. - Description string `xml:"Description,omitempty"` // Optional description. - Details string `xml:"Details,omitempty"` // Detailed message about the task. Also contained by the Owner entity when task status is preRunning. - Error *Error `xml:"Error,omitempty"` // Represents error information from a failed task. - Link *Link `xml:"Link,omitempty"` // A reference to an entity or operation associated with this object. - Organization *Reference `xml:"Organization,omitempty"` // The organization to which the User belongs. - Owner *Reference `xml:"Owner,omitempty"` // Reference to the owner of the task. This is typically the object that the task is creating or updating. - Progress int `xml:"Progress,omitempty"` // Read-only indicator of task progress as an approximate percentage between 0 and 100. Not available for all tasks. - Tasks *TasksInProgress `xml:"Tasks,omitempty"` // A list of queued, running, or recently completed tasks associated with this entity. - User *Reference `xml:"User,omitempty"` // The user who started the task. + HREF string `xml:"href,attr,omitempty" json:"HREF,omitempty"` // The URI of the entity. + Type string `xml:"type,attr,omitempty" json:"type,omitempty"` // The MIME type of the entity. + ID string `xml:"id,attr,omitempty" json:"ID,omitempty"` // The entity identifier, expressed in URN format. The value of this attribute uniquely identifies the entity, persists for the life of the entity, and is never reused. + OperationKey string `xml:"operationKey,attr,omitempty" json:"operationKey,omitempty"` // Optional unique identifier to support idempotent semantics for create and delete operations. + Name string `xml:"name,attr" json:"name,omitempty"` // The name of the entity. + Status string `xml:"status,attr" json:"status,omitempty"` // The execution status of the task. One of queued, preRunning, running, success, error, aborted + Operation string `xml:"operation,attr,omitempty" json:"operation,omitempty"` // A message describing the operation that is tracked by this task. + OperationName string `xml:"operationName,attr,omitempty" json:"operationName,omitempty"` // The short name of the operation that is tracked by this task. + ServiceNamespace string `xml:"serviceNamespace,attr,omitempty" json:"serviceNamespace,omitempty"` // Identifier of the service that created the task. It must not start with com.vmware.vcloud and the length must be between 1 and 128 symbols. + StartTime string `xml:"startTime,attr,omitempty" json:"startTime,omitempty"` // The date and time the system started executing the task. May not be present if the task has not been executed yet. + EndTime string `xml:"endTime,attr,omitempty" json:"endTime,omitempty"` // The date and time that processing of the task was completed. May not be present if the task is still being executed. + ExpiryTime string `xml:"expiryTime,attr,omitempty" json:"expiryTime,omitempty"` // The date and time at which the task resource will be destroyed and no longer available for retrieval. May not be present if the task has not been executed or is still being executed. + CancelRequested bool `xml:"cancelRequested,attr,omitempty" json:"cancelRequested,omitempty"` // Whether user has requested this processing to be canceled. + Link *LinkList `xml:"Link,omitempty" json:"link,omitempty"` // A reference to an entity or operation associated with this object. + Description string `xml:"Description,omitempty" json:"description,omitempty"` // Optional description. + Tasks *TasksInProgress `xml:"Tasks,omitempty" json:"tasks,omitempty"` // A list of queued, running, or recently completed tasks associated with this entity. + Owner *Reference `xml:"Owner,omitempty" json:"owner,omitempty"` // Reference to the owner of the task. This is typically the object that the task is creating or updating. + Error *Error `xml:"Error,omitempty" json:"error,omitempty"` // Represents error information from a failed task. + User *Reference `xml:"User,omitempty" json:"user,omitempty"` // The user who started the task. + Organization *Reference `xml:"Organization,omitempty" json:"organization,omitempty"` // The organization to which the User belongs. + Progress int `xml:"Progress,omitempty" json:"progress,omitempty"` // Read-only indicator of task progress as an approximate percentage between 0 and 100. Not available for all tasks. + Details string `xml:"Details,omitempty" json:"details,omitempty"` // Detailed message about the task. Also contained by the Owner entity when task status is preRunning. + Result *TaskResult `xml:"Result,omitempty" json:"result,omitempty"` // Result contains additional details that the task may expose // // TODO: add the following fields // Params anyType The parameters with which this task was started. - // Result ResultType An optional element that can be used to hold the result of a task. // VcTaskList VcTaskListType List of Virtual Center tasks related to this vCD task. } +// TaskResult contains additional details that the task may expose after finishing +type TaskResult struct { + ResultContent struct { + Text string `xml:",chardata"` + Xsi string `xml:"xsi,attr"` + Ns11 string `xml:"ns11,attr"` + Type string `xml:"type,attr"` + } `xml:"ResultContent"` +} + // CapacityWithUsage represents a capacity and usage of a given resource. // Type: CapacityWithUsageType // Namespace: http://www.vmware.com/vcloud/v1.5 @@ -603,10 +906,10 @@ type ComputeCapacity struct { // Description: A reference to a resource. Contains an href attribute and optional name and type attributes. // Since: 0.9 type Reference struct { - HREF string `xml:"href,attr,omitempty"` - ID string `xml:"id,attr,omitempty"` - Type string `xml:"type,attr,omitempty"` - Name string `xml:"name,attr,omitempty"` + HREF string `xml:"href,attr,omitempty" json:"href,omitempty"` + ID string `xml:"id,attr,omitempty" json:"id,omitempty"` + Type string `xml:"type,attr,omitempty" json:"type,omitempty"` + Name string `xml:"name,attr,omitempty" json:"name,omitempty"` } // ResourceReference represents a reference to a resource. Contains an href attribute, a resource status attribute, and optional name and type attributes. @@ -647,7 +950,7 @@ type ResourceEntities struct { // Description: Container for references to available organization vDC networks. // Since: 0.9 type AvailableNetworks struct { - Network []*Reference `xml:"Network,omitempty"` + Network []*Reference `xml:"Network,omitempty" json:"network,omitempty"` } // Link extends reference type by adding relation attribute. Defines a hyper-link with a relationship, hyper-link reference, and an optional MIME type. @@ -656,27 +959,27 @@ type AvailableNetworks struct { // Description: Extends reference type by adding relation attribute. Defines a hyper-link with a relationship, hyper-link reference, and an optional MIME type. // Since: 0.9 type Link struct { - HREF string `xml:"href,attr"` - ID string `xml:"id,attr,omitempty"` - Type string `xml:"type,attr,omitempty"` - Name string `xml:"name,attr,omitempty"` - Rel string `xml:"rel,attr"` + HREF string `xml:"href,attr" json:"href,omitempty"` + ID string `xml:"id,attr,omitempty" json:"id,omitempty"` + Type string `xml:"type,attr,omitempty" json:"type,omitempty"` + Name string `xml:"name,attr,omitempty" json:"name,omitempty"` + Rel string `xml:"rel,attr" json:"rel,omitempty"` } // OrgList represents a lists of Organizations // Type: OrgType // Namespace: http://www.vmware.com/vcloud/v1.5 -// Description: Represents a list of vCloud Director organizations. +// Description: Represents a list of VMware Cloud Director organizations. // Since: 0.9 type OrgList struct { Link LinkList `xml:"Link,omitempty"` Org []*Org `xml:"Org,omitempty"` } -// Org represents the user view of a vCloud Director organization. +// Org represents the user view of a VMware Cloud Director organization. // Type: OrgType // Namespace: http://www.vmware.com/vcloud/v1.5 -// Description: Represents the user view of a vCloud Director organization. +// Description: Represents the user view of a VMware Cloud Director organization. // Since: 0.9 type Org struct { HREF string `xml:"href,attr,omitempty"` @@ -711,10 +1014,10 @@ type RightsType struct { RightReference []*Reference `xml:"RightReference,omitempty"` } -// AdminOrg represents the admin view of a vCloud Director organization. +// AdminOrg represents the admin view of a VMware Cloud Director organization. // Type: AdminOrgType // Namespace: http://www.vmware.com/vcloud/v1.5 -// Description: Represents the admin view of a vCloud Director organization. +// Description: Represents the admin view of a VMware Cloud Director organization. // Since: 0.9 type AdminOrg struct { XMLName xml.Name `xml:"AdminOrg"` @@ -739,10 +1042,10 @@ type AdminOrg struct { RoleReferences *OrgRoleType `xml:"RoleReferences,omitempty"` } -// OrgSettingsType represents the settings for a vCloud Director organization. +// OrgSettingsType represents the settings for a VMware Cloud Director organization. // Type: OrgSettingsType // Namespace: http://www.vmware.com/vcloud/v1.5 -// Description: Represents the settings of a vCloud Director organization. +// Description: Represents the settings of a VMware Cloud Director organization. // Since: 0.9 type OrgSettings struct { //attributes @@ -757,10 +1060,10 @@ type OrgSettings struct { } -// OrgGeneralSettingsType represents the general settings for a vCloud Director organization. +// OrgGeneralSettingsType represents the general settings for a VMware Cloud Director organization. // Type: OrgGeneralSettingsType // Namespace: http://www.vmware.com/vcloud/v1.5 -// Description: Represents the user view of a vCloud Director organization. +// Description: Represents the user view of a VMware Cloud Director organization. // Since: 0.9 type OrgGeneralSettings struct { HREF string `xml:"href,attr,omitempty"` // The URI of the entity. @@ -768,16 +1071,18 @@ type OrgGeneralSettings struct { Link LinkList `xml:"Link,omitempty"` // A reference to an entity or operation associated with this object. CanPublishCatalogs bool `xml:"CanPublishCatalogs,omitempty"` + CanPublishExternally bool `xml:"CanPublishExternally,omitempty"` + CanSubscribe bool `xml:"CanSubscribe,omitempty"` DeployedVMQuota int `xml:"DeployedVMQuota,omitempty"` StoredVMQuota int `xml:"StoredVmQuota,omitempty"` UseServerBootSequence bool `xml:"UseServerBootSequence,omitempty"` DelayAfterPowerOnSeconds int `xml:"DelayAfterPowerOnSeconds,omitempty"` } -// VAppTemplateLeaseSettings represents the vapp template lease settings for a vCloud Director organization. +// VAppTemplateLeaseSettings represents the vapp template lease settings for a VMware Cloud Director organization. // Type: VAppTemplateLeaseSettingsType // Namespace: http://www.vmware.com/vcloud/v1.5 -// Description: Represents the vapp template lease settings of a vCloud Director organization. +// Description: Represents the vapp template lease settings of a VMware Cloud Director organization. // Since: 0.9 type VAppTemplateLeaseSettings struct { HREF string `xml:"href,attr,omitempty"` // The URI of the entity. @@ -799,18 +1104,10 @@ type VAppLeaseSettings struct { PowerOffOnRuntimeLeaseExpiration *bool `xml:"PowerOffOnRuntimeLeaseExpiration,omitempty"` } -type OrgFederationSettings struct { - HREF string `xml:"href,attr,omitempty"` // The URI of the entity. - Type string `xml:"type,attr,omitempty"` // The MIME type of the entity. - Link LinkList `xml:"Link,omitempty"` // A reference to an entity or operation associated with this object. - - Enabled bool `xml:"Enabled,omitempty"` -} - -// OrgLdapSettingsType represents the ldap settings for a vCloud Director organization. +// OrgLdapSettingsType represents the ldap settings for a VMware Cloud Director organization. // Type: OrgLdapSettingsType // Namespace: http://www.vmware.com/vcloud/v1.5 -// Description: Represents the ldap settings of a vCloud Director organization. +// Description: Represents the ldap settings of a VMware Cloud Director organization. // Since: 0.9 type OrgLdapSettingsType struct { XMLName xml.Name `xml:"OrgLdapSettings"` @@ -819,15 +1116,15 @@ type OrgLdapSettingsType struct { Type string `xml:"type,attr,omitempty"` // The MIME type of the entity. Link LinkList `xml:"Link,omitempty"` // A reference to an entity or operation associated with this object. - CustomUsersOu string `xml:"CustomUsersOu,omitempty"` // If OrgLdapMode is SYSTEM, specifies an LDAP attribute=value pair to use for OU (organizational unit). OrgLdapMode string `xml:"OrgLdapMode,omitempty"` // LDAP mode you want + CustomUsersOu string `xml:"CustomUsersOu,omitempty"` // If OrgLdapMode is SYSTEM, specifies an LDAP attribute=value pair to use for OU (organizational unit). CustomOrgLdapSettings *CustomOrgLdapSettings `xml:"CustomOrgLdapSettings,omitempty"` // Needs to be set if user chooses custom mode } -// CustomOrgLdapSettings represents the custom ldap settings for a vCloud Director organization. +// CustomOrgLdapSettings represents the custom ldap settings for a VMware Cloud Director organization. // Type: CustomOrgLdapSettingsType // Namespace: http://www.vmware.com/vcloud/v1.5 -// Description: Represents the custom ldap settings of a vCloud Director organization. +// Description: Represents the custom ldap settings of a VMware Cloud Director organization. // Since: 0.9 // Note. Order of these fields matter and API will error if it is changed type CustomOrgLdapSettings struct { @@ -853,10 +1150,10 @@ type CustomOrgLdapSettings struct { Realm string `xml:"Realm,omitempty"` } -// OrgLdapGroupAttributes represents the ldap group attribute settings for a vCloud Director organization. +// OrgLdapGroupAttributes represents the ldap group attribute settings for a VMware Cloud Director organization. // Type: OrgLdapGroupAttributesType // Namespace: http://www.vmware.com/vcloud/v1.5 -// Description: Represents the ldap group attribute settings of a vCloud Director organization. +// Description: Represents the ldap group attribute settings of a VMware Cloud Director organization. // Since: 0.9 // Note. Order of these fields matter and API will error if it is changed type OrgLdapGroupAttributes struct { @@ -864,14 +1161,14 @@ type OrgLdapGroupAttributes struct { ObjectIdentifier string `xml:"ObjectIdentifier"` GroupName string `xml:"GroupName"` Membership string `xml:"Membership"` - BackLinkIdentifier string `xml:"BackLinkIdentifier,omitempty"` MembershipIdentifier string `xml:"MembershipIdentifier"` + BackLinkIdentifier string `xml:"BackLinkIdentifier,omitempty"` } -// OrgLdapUserAttributesType represents the ldap user attribute settings for a vCloud Director organization. +// OrgLdapUserAttributesType represents the ldap user attribute settings for a VMware Cloud Director organization. // Type: OrgLdapUserAttributesType // Namespace: http://www.vmware.com/vcloud/v1.5 -// Description: Represents the ldap user attribute settings of a vCloud Director organization. +// Description: Represents the ldap user attribute settings of a VMware Cloud Director organization. // Since: 0.9 // Note. Order of these fields matter and API will error if it is changed. type OrgLdapUserAttributes struct { @@ -893,7 +1190,7 @@ type OrgLdapUserAttributes struct { // Description: Represents a list of organization vDCs. // Since: 0.9 type VDCList struct { - Vdcs []*Reference `xml:"Vdc,omitempty"` + Vdcs []*Reference `xml:"Vdc,omitempty" json:"vdcs,omitempty"` } // NetworksListType contains a list of references to Org Networks @@ -966,19 +1263,20 @@ type CatalogItems struct { // https://code.vmware.com/apis/287/vcloud#/doc/doc/types/CatalogType.html // Since: 0.9 type Catalog struct { - HREF string `xml:"href,attr,omitempty"` - Type string `xml:"type,attr,omitempty"` - ID string `xml:"id,attr,omitempty"` - OperationKey string `xml:"operationKey,attr,omitempty"` - Name string `xml:"name,attr"` - CatalogItems []*CatalogItems `xml:"CatalogItems,omitempty"` - DateCreated string `xml:"DateCreated,omitempty"` - Description string `xml:"Description,omitempty"` - IsPublished bool `xml:"IsPublished,omitempty"` - Link LinkList `xml:"Link,omitempty"` - Owner *Owner `xml:"Owner,omitempty"` - Tasks *TasksInProgress `xml:"Tasks,omitempty"` - VersionNumber int64 `xml:"VersionNumber,omitempty"` + HREF string `xml:"href,attr,omitempty"` + Type string `xml:"type,attr,omitempty"` + ID string `xml:"id,attr,omitempty"` + OperationKey string `xml:"operationKey,attr,omitempty"` + Name string `xml:"name,attr"` + CatalogItems []*CatalogItems `xml:"CatalogItems,omitempty"` + DateCreated string `xml:"DateCreated,omitempty"` + Description string `xml:"Description,omitempty"` + IsPublished bool `xml:"IsPublished,omitempty"` + Link LinkList `xml:"Link,omitempty"` + Owner *Owner `xml:"Owner,omitempty"` + Tasks *TasksInProgress `xml:"Tasks,omitempty"` + VersionNumber int64 `xml:"VersionNumber,omitempty"` + PublishExternalCatalogParams *PublishExternalCatalogParams `xml:"PublishExternalCatalogParams,omitempty"` } // AdminCatalog represents the Admin view of a Catalog object. @@ -1003,11 +1301,20 @@ type AdminCatalog struct { // Description: Represents the configuration parameters of a catalog published externally. // Since: 5.5 type PublishExternalCatalogParams struct { - IsCachedEnabled bool `xml:"IsCacheEnabled,omitempty"` - IsPublishedExternally bool `xml:"IsPublishedExternally,omitempty"` - Password string `xml:"Password,omitempty"` - PreserveIdentityInfoFlag bool `xml:"PreserveIdentityInfoFlag,omitempty"` - CatalogPublishedUrl string `xml:"catalogPublishedUrl,omitempty"` + Xmlns string `xml:"xmlns,attr,omitempty"` + IsPublishedExternally *bool `xml:"IsPublishedExternally,omitempty"` // True enables external publication as configured by these parameters. + CatalogPublishedUrl string `xml:"catalogPublishedUrl,omitempty"` // Read-only endpoint URL supplied by the server. External subscribers can connect to the catalog at this URL. + Password string `xml:"Password,omitempty"` // Password required when connecting to the endpoint. + IsCachedEnabled *bool `xml:"IsCacheEnabled,omitempty"` // True enables content caching for this catalog. All items in the catalog are created and stored in transfer storage. If false, items are not placed in transfer storage until they are requested by a subscriber. Note that access to this attribute is reserved to users with role that includes the right 'Catalog: VCSP Publish Subscribe Caching'. + PreserveIdentityInfoFlag *bool `xml:"PreserveIdentityInfoFlag,omitempty"` // True includes BIOS UUIDs and MAC addresses in the downloaded OVF package. If false, those information will be excluded. +} + +// PublishCatalogParams represents the configuration parameters of a catalog published to other orgs. +// It is used in conjunction with the "IsPublished" state of the catalog itself +type PublishCatalogParams struct { + XMLName xml.Name `xml:"PublishCatalogParams"` + Xmlns string `xml:"xmlns,attr,omitempty"` + IsPublished *bool `xml:"IsPublished,omitempty"` // True enables publication (read-only access) } // ExternalCatalogSubscription represents the configuration parameters for a catalog that has an external subscription @@ -1016,11 +1323,13 @@ type PublishExternalCatalogParams struct { // Description: Represents the configuration parameters for a catalog that has an external subscription. // Since: 5.5 type ExternalCatalogSubscription struct { - ExpectedSslThumbprint bool `xml:"ExpectedSslThumbprint,omitempty"` - LocalCopy bool `xml:"LocalCopy,omitempty"` - Password string `xml:"Password,omitempty"` - SubscribeToExternalFeeds bool `xml:"SubscribeToExternalFeeds,omitempty"` - Location string `xml:"Location,omitempty"` + XMLName xml.Name `xml:"ExternalCatalogSubscriptionParams"` + Xmlns string `xml:"xmlns,attr,omitempty"` + ExpectedSslThumbprint string `xml:"ExpectedSslThumbprint,omitempty"` + SubscribeToExternalFeeds bool `xml:"SubscribeToExternalFeeds,omitempty"` + Location string `xml:"Location,omitempty"` + Password string `xml:"Password,omitempty"` + LocalCopy bool `xml:"LocalCopy,omitempty"` } // CatalogStorageProfiles represents a container for storage profiles used by this catalog @@ -1050,11 +1359,11 @@ type Owner struct { // Description: The standard error message type used in the vCloud REST API. // Since: 0.9 type Error struct { - Message string `xml:"message,attr"` - MajorErrorCode int `xml:"majorErrorCode,attr"` - MinorErrorCode string `xml:"minorErrorCode,attr"` - VendorSpecificErrorCode string `xml:"vendorSpecificErrorCode,attr,omitempty"` - StackTrace string `xml:"stackTrace,attr,omitempty"` + Message string `xml:"message,attr" json:"message,omitempty"` + MajorErrorCode int `xml:"majorErrorCode,attr" json:"majorErrorCode,omitempty"` + MinorErrorCode string `xml:"minorErrorCode,attr" json:"minorErrorCode,omitempty"` + VendorSpecificErrorCode string `xml:"vendorSpecificErrorCode,attr,omitempty" json:"vendorSpecificErrorCode,omitempty"` + StackTrace string `xml:"stackTrace,attr,omitempty" json:"stackTrace,omitempty"` } func (err Error) Error() string { @@ -1186,6 +1495,18 @@ type ReComposeVAppParams struct { DeleteItem *DeleteItem `xml:"DeleteItem,omitempty"` } +// SmallRecomposeVappParams is used to update name and description of a vApp +// Using the full definition (ReComposeVAppParams), the description can be changed but not removed +type SmallRecomposeVappParams struct { + XMLName xml.Name `xml:"RecomposeVAppParams"` + Ovf string `xml:"xmlns:ovf,attr"` + Xsi string `xml:"xmlns:xsi,attr"` + Xmlns string `xml:"xmlns,attr"` + Name string `xml:"name,attr"` + Deploy bool `xml:"deploy,attr"` + Description string `xml:"Description"` +} + type DeleteItem struct { HREF string `xml:"href,attr,omitempty"` } @@ -1260,6 +1581,7 @@ type VApp struct { OvfDescriptorUploaded bool `xml:"ovfDescriptorUploaded,attr,omitempty"` // Read-only indicator that the OVF descriptor for this vApp has been uploaded. // Elements Link LinkList `xml:"Link,omitempty"` // A reference to an entity or operation associated with this object. + LeaseSettingsSection *LeaseSettingsSection `xml:"LeaseSettingsSection,omitempty"` // A reference to the lease section of the vApp NetworkConfigSection *NetworkConfigSection `xml:"NetworkConfigSection,omitempty"` // Represents vAPP network configuration Description string `xml:"Description,omitempty"` // Optional description. Tasks *TasksInProgress `xml:"Tasks,omitempty"` // A list of queued, running, or recently completed tasks associated with this entity. @@ -1309,43 +1631,67 @@ type Value struct { Value string `xml:"http://schemas.dmtf.org/ovf/envelope/1 value,attr,omitempty"` } +// MetadataValue is the type returned when querying a unique entry of metadata. +// Type: MetadataValueType +// Namespace: http://www.vmware.com/vcloud/v1.5 type MetadataValue struct { - XMLName xml.Name `xml:"MetadataValue"` - Xsi string `xml:"xmlns:xsi,attr"` - Xmlns string `xml:"xmlns,attr"` - TypedValue *TypedValue `xml:"TypedValue"` + XMLName xml.Name `xml:"MetadataValue"` + Xsi string `xml:"xmlns:xsi,attr"` + Xmlns string `xml:"xmlns,attr"` + Domain *MetadataDomainTag `xml:"Domain,omitempty"` + TypedValue *MetadataTypedValue `xml:"TypedValue"` } -type TypedValue struct { - XsiType string `xml:"xsi:type,attr"` +// MetadataTypedValue is the content of a metadata entry. +// Type: MetadataTypedValue +// Namespace: http://www.vmware.com/vcloud/v1.5 +// Description: One of: MetadataStringValue, MetadataNumberValue, MetadataBooleanValue, MetadataDateTimeValue +// Since: 5.1 +type MetadataTypedValue struct { + XsiType string `xml:"http://www.w3.org/2001/XMLSchema-instance type,attr"` Value string `xml:"Value"` } +// Deprecated: Use MetadataTypedValue instead +type TypedValue = MetadataTypedValue + +// Metadata is the user-defined metadata associated with an object. // Type: MetadataType // Namespace: http://www.vmware.com/vcloud/v1.5 -// Description: User-defined metadata associated with with an object. +// Description: User-defined metadata associated with an object. // Since: 1.5 type Metadata struct { XMLName xml.Name `xml:"Metadata"` Xmlns string `xml:"xmlns,attr"` HREF string `xml:"href,attr"` - Type string `xml:"type,attr,omitempty"` + Type string `xml:"type,attr,omitempty"` // The MIME type of the entity. Xsi string `xml:"xmlns:xsi,attr"` Link []*Link `xml:"Link,omitempty"` MetadataEntry []*MetadataEntry `xml:"MetadataEntry,omitempty"` } +// MetadataEntry is a single metadata entry. // Type: MetadataEntryType // Namespace: http://www.vmware.com/vcloud/v1.5 type MetadataEntry struct { - Xmlns string `xml:"xmlns,attr"` - HREF string `xml:"href,attr"` - Type string `xml:"type,attr,omitempty"` - Xsi string `xml:"xmlns:xsi,attr"` - Domain string `xml:"Domain,omitempty"` // A value of SYSTEM places this MetadataEntry in the SYSTEM domain. Omit or leave empty to place this MetadataEntry in the GENERAL domain. - Key string `xml:"Key"` // An arbitrary key name. Length cannot exceed 256 UTF-8 characters. - Link []*Link `xml:"Link,omitempty"` //A reference to an entity or operation associated with this object. - TypedValue *TypedValue `xml:"TypedValue"` + Xmlns string `xml:"xmlns,attr"` + HREF string `xml:"href,attr"` + Type string `xml:"type,attr,omitempty"` // The MIME type of the entity + Xsi string `xml:"xmlns:xsi,attr"` + Domain *MetadataDomainTag `xml:"Domain,omitempty"` + Key string `xml:"Key"` // An arbitrary key name. Length cannot exceed 256 UTF-8 characters. + Link []*Link `xml:"Link,omitempty"` // A reference to an entity or operation associated with this object. + TypedValue *MetadataTypedValue `xml:"TypedValue"` +} + +// MetadataDomainTag contains both the visibility and the domain of the metadata. +// Type: MetadataDomainTagType +// Namespace: http://www.vmware.com/vcloud/v1.5 +// Description: A value of SYSTEM places this MetadataEntry in the SYSTEM domain. Omit or leave empty to place this MetadataEntry in the GENERAL domain. +// Since: 5.1 +type MetadataDomainTag struct { + Visibility string `xml:"visibility,attr"` // One of: PRIVATE (hidden), READONLY, READWRITE (read/write) + Domain string `xml:",chardata"` } // VAppChildren is a container for virtual machines included in this vApp. @@ -1364,7 +1710,7 @@ type VAppChildren struct { // Since: 0.9 type TasksInProgress struct { // Elements - Task []*Task `xml:"Task"` // A task. + Task []*Task `xml:"Task" json:"task"` // A task. } // VAppTemplateChildren is a container for virtual machines included in this vApp template. @@ -1410,10 +1756,70 @@ type VAppTemplate struct { NetworkConnectionSection *NetworkConnectionSection `xml:"NetworkConnectionSection,omitempty"` LeaseSettingsSection *LeaseSettingsSection `xml:"LeaseSettingsSection,omitempty"` CustomizationSection *CustomizationSection `xml:"CustomizationSection,omitempty"` + ProductSection *ProductSection `xml:"ProductSection,omitempty"` // OVF Section needs to be added // Section Section `xml:"Section,omitempty"` } +// VAppTemplateForUpdate represents a vApp template. +// It is shrunken version of VAppTemplateType used for update calls. +// Full VAppTemplateType isn't accepted by API +// Type: VAppTemplateType +// Namespace: http://www.vmware.com/vcloud/v1.5 +// Description: Represents a vApp template. +type VAppTemplateForUpdate struct { + XMLName xml.Name `xml:"VAppTemplate"` + // Attributes + Xmlns string `xml:"xmlns,attr,omitempty"` + HREF string `xml:"href,attr,omitempty"` // The URI of the entity. + ID string `xml:"id,attr,omitempty"` // The entity identifier, expressed in URN format. The value of this attribute uniquely identifies the entity, persists for the life of the entity, and is never reused. + Name string `xml:"name,attr"` // The name of the entity. + GoldMaster bool `xml:"goldMaster,attr,omitempty"` // True if this template is a gold master. + // Elements + Link LinkList `xml:"Link,omitempty"` // A reference to an entity or operation associated with this object. + Description string `xml:"Description,omitempty"` // Optional description. +} + +// CaptureVAppParams is a configuration that can be supplied for capturing a vApp template from +// existing vApp +type CaptureVAppParams struct { + XMLName xml.Name `xml:"CaptureVAppParams"` + + Xmlns string `xml:"xmlns,attr"` + XmlnsNs0 string `xml:"xmlns:ns0,attr,omitempty"` + + // Name of vApp template + Name string `xml:"name,attr"` + // Description of vApp template + Description string `xml:"Description,omitempty"` + + // Source vApp reference. At least HREF field must be set + Source *Reference `xml:"Source"` + + // CustomizationSection section + CustomizationSection CaptureVAppParamsCustomizationSection `xml:"CustomizationSection"` + + // TargetCatalogItem can be used to overwrite existing item. To overwrite an existing vApp + // template with the one created by this capture, place a reference to the existing template + // here. Otherwise, the operation creates a new vApp template. + TargetCatalogItem *Reference `xml:"TargetCatalogItem,omitempty"` + + // CopyTpmOnInstantiate defines if TPM device is copied (`true`) to instantiated vApp from this + // template or `false` if a new TPM device is created for instantiated vApp. + // Note. Supported on VCD 10.4.2+ + CopyTpmOnInstantiate *bool `xml:"CopyTpmOnInstantiate"` +} + +// CaptureVAppParamsCustomizationSection settings for CaptureVAppParams type +type CaptureVAppParamsCustomizationSection struct { + // This field must contain value "CustomizeOnInstantiate Settings" so that API does not reject + // the request + Info string `xml:"ns0:Info,omitempty"` + // CustomizeOnInstantiate marks if instantiating this template applies customization settings + // (`true`). `false` creates an identical copy. + CustomizeOnInstantiate bool `xml:"CustomizeOnInstantiate"` +} + // VMDiskChange represents a virtual machine only with Disk setting update part type VMDiskChange struct { XMLName xml.Name `xml:"Vm"` @@ -1421,10 +1827,11 @@ type VMDiskChange struct { Xsi string `xml:"xmlns:xsi,attr,omitempty"` Xmlns string `xml:"xmlns,attr,omitempty"` - HREF string `xml:"href,attr,omitempty"` // The URI of the VM entity. - Type string `xml:"type,attr,omitempty"` // The MIME type of the entity - application/vnd.vmware.vcloud.vm+xml - Name string `xml:"name,attr"` // VM name - ID string `xml:"id,attr,omitempty"` // VM ID. The entity identifier, expressed in URN format. The value of this attribute uniquely identifies the entity, persists for the life of the entity, and is never reused. + HREF string `xml:"href,attr,omitempty"` // The URI of the VM entity. + Type string `xml:"type,attr,omitempty"` // The MIME type of the entity - application/vnd.vmware.vcloud.vm+xml + Name string `xml:"name,attr"` // VM name + Description string `xml:"Description,omitempty"` // Optional description. + ID string `xml:"id,attr,omitempty"` // VM ID. The entity identifier, expressed in URN format. The value of this attribute uniquely identifies the entity, persists for the life of the entity, and is never reused. VmSpecSection *VmSpecSection `xml:"VmSpecSection,omitempty"` // Container for the specification of this virtual machine. This is an alternative to using ovf:VirtualHardwareSection + ovf:OperatingSystemSection } @@ -1436,18 +1843,25 @@ type DiskSection struct { // DiskSettings from Vm/VmSpecSection/DiskSection struct type DiskSettings struct { - DiskId string `xml:"DiskId,omitempty"` // Specifies a unique identifier for this disk in the scope of the corresponding VM. This element is optional when creating a VM, but if it is provided it should be unique. This element is mandatory when updating an existing disk. - SizeMb int64 `xml:"SizeMb"` // The size of the disk in MB. - UnitNumber int `xml:"UnitNumber"` // The device number on the SCSI or IDE controller of the disk. - BusNumber int `xml:"BusNumber"` // The number of the SCSI or IDE controller itself. - AdapterType string `xml:"AdapterType"` // The type of disk controller, e.g. IDE vs SCSI and if SCSI bus-logic vs LSI logic. - ThinProvisioned *bool `xml:"ThinProvisioned,omitempty"` // Specifies whether the disk storage is pre-allocated or allocated on demand. - Disk *Reference `xml:"Disk,omitempty"` // Specifies reference to a named disk. - StorageProfile *Reference `xml:"StorageProfile,omitempty"` // Specifies reference to a storage profile to be associated with the disk. - OverrideVmDefault bool `xml:"overrideVmDefault"` // Specifies that the disk storage profile overrides the VM's default storage profile. - Iops *int64 `xml:"iops,omitempty"` // Specifies the IOPS for the disk. - VirtualQuantity *int64 `xml:"VirtualQuantity,omitempty"` // The actual size of the disk. - VirtualQuantityUnit string `xml:"VirtualQuantityUnit,omitempty"` // The units in which VirtualQuantity is measured. + DiskId string `xml:"DiskId,omitempty"` // Specifies a unique identifier for this disk in the scope of the corresponding VM. This element is optional when creating a VM, but if it is provided it should be unique. This element is mandatory when updating an existing disk. + SizeMb int64 `xml:"SizeMb"` // The size of the disk in MB. + UnitNumber int `xml:"UnitNumber"` // The device number on the SCSI or IDE controller of the disk. + BusNumber int `xml:"BusNumber"` // The number of the SCSI or IDE controller itself. + AdapterType string `xml:"AdapterType"` // The type of disk controller, e.g. IDE vs SCSI and if SCSI bus-logic vs LSI logic. + ThinProvisioned *bool `xml:"ThinProvisioned,omitempty"` // Specifies whether the disk storage is pre-allocated or allocated on demand. + Disk *Reference `xml:"Disk,omitempty"` // Specifies reference to a named disk. + StorageProfile *Reference `xml:"StorageProfile,omitempty"` // Specifies reference to a storage profile to be associated with the disk. + OverrideVmDefault bool `xml:"overrideVmDefault"` // Specifies that the disk storage profile overrides the VM's default storage profile. + IopsAllocation *IopsResource `xml:"IopsAllocation"` // IOPS definition for the disk - added in 10.4 in replacement of 'iops' + VirtualQuantity *int64 `xml:"VirtualQuantity,omitempty"` // The actual size of the disk. + VirtualQuantityUnit string `xml:"VirtualQuantityUnit,omitempty"` // The units in which VirtualQuantity is measured. +} + +type IopsResource struct { + Reservation int64 `xml:"Reservation"` // The amount of reservation of IOPS on the underlying virtualization infrastructure. This is a read-only. + Limit int64 `xml:"Limit"` // The limit for how much of IOPS can be consumed on the underlying virtualization infrastructure. This is only valid when the resource allocation is not unlimited. + SharesLevel string `xml:"SharesLevel"` // LOW - NORMAL - HIGH - CUSTOM + Shares int64 `xml:"Shares"` // Custom priority for IOPS. This is a read-only. } // MediaSection from Vm/VmSpecSection struct @@ -1501,6 +1915,15 @@ type VirtualHardwareSection struct { HREF string `xml:"href,attr,omitempty"` Type string `xml:"type,attr,omitempty"` Item []*VirtualHardwareItem `xml:"Item,omitempty"` + + ExtraConfig []*VmVirtualHardwareSectionExtraConfig `xml:"ExtraConfig,omitempty"` + Link []*Link `xml:"Link,omitempty"` +} + +type VmVirtualHardwareSectionExtraConfig struct { + Key string `xml:"key,attr"` + Value string `xml:"value,attr"` + Required bool `xml:"required,attr"` } // Each ovf:Item parsed from the ovf:VirtualHardwareSection @@ -1582,9 +2005,10 @@ type OVFItem struct { Reservation int `xml:"rasd:Reservation"` ResourceType int `xml:"rasd:ResourceType"` VirtualQuantity int64 `xml:"rasd:VirtualQuantity"` - Weight int `xml:"rasd:Weight"` - CoresPerSocket *int `xml:"vmw:CoresPerSocket,omitempty"` - Link *Link `xml:"vcloud:Link"` + // Weight corresponds to Shares when used for CPU and/or memory settings + Weight int `xml:"rasd:Weight,omitempty"` + CoresPerSocket *int `xml:"vmw:CoresPerSocket,omitempty"` + Link *Link `xml:"vcloud:Link"` } // DeployVAppParams are the parameters to a deploy vApp request @@ -1672,6 +2096,25 @@ type InstantiateVAppTemplateParams struct { AllEULAsAccepted bool `xml:"AllEULAsAccepted,omitempty"` // True confirms acceptance of all EULAs in a vApp template. Instantiation fails if this element is missing, empty, or set to false and one or more EulaSection elements are present. } +// CloneVAppParams is used to copy one vApp into another +type CloneVAppParams struct { + XMLName xml.Name `xml:"CloneVAppParams"` + Ovf string `xml:"xmlns:ovf,attr"` + Xsi string `xml:"xmlns:xsi,attr,omitempty"` + Xmlns string `xml:"xmlns,attr"` + // Attributes + Name string `xml:"name,attr,omitempty"` // Typically used to name or identify the subject of the request. For example, the name of the object being created or modified. + Deploy bool `xml:"deploy,attr"` // True if the vApp should be deployed at instantiation. Defaults to true. + PowerOn bool `xml:"powerOn,attr"` // True if the vApp should be powered-on at instantiation. Defaults to true. + LinkedClone bool `xml:"linkedClone,attr,omitempty"` // Reserved. Unimplemented. + // Elements + Description string `xml:"Description,omitempty"` // Optional description. + InstantiationParams *InstantiationParams `xml:"InstantiationParams,omitempty"` // Instantiation parameters for the composed vApp. + Source *Reference `xml:"Source"` // A reference to a source object such as a vApp or vApp template. + IsSourceDelete *bool `xml:"IsSourceDelete"` // Set to true to delete the source object after the operation completes. + SourcedItem *SourcedCompositionItemParam `xml:"SourcedItem,omitempty"` // Composition item. One of: vApp vAppTemplate VM. +} + // EdgeGateway represents a gateway. // Element: EdgeGateway // Type: GatewayType @@ -1681,17 +2124,17 @@ type InstantiateVAppTemplateParams struct { type EdgeGateway struct { // Attributes Xmlns string `xml:"xmlns,attr,omitempty"` - HREF string `xml:"href,attr,omitempty"` // The URI of the entity. - Type string `xml:"type,attr,omitempty"` // The MIME type of the entity. - ID string `xml:"id,attr,omitempty"` // The entity identifier, expressed in URN format. The value of this attribute uniquely identifies the entity, persists for the life of the entity, and is never reused - OperationKey string `xml:"operationKey,attr,omitempty"` // Optional unique identifier to support idempotent semantics for create and delete operations. - Name string `xml:"name,attr"` // The name of the entity. - Status int `xml:"status,attr,omitempty"` // Creation status of the gateway. One of: 0 (The gateway is still being created) 1 (The gateway is ready) -1 (There was an error while creating the gateway). + HREF string `xml:"href,attr,omitempty" json:"href,omitempty"` // The URI of the entity. + Type string `xml:"type,attr,omitempty" json:"type,omitempty"` // The MIME type of the entity. + ID string `xml:"id,attr,omitempty" json:"id,omitempty"` // The entity identifier, expressed in URN format. The value of this attribute uniquely identifies the entity, persists for the life of the entity, and is never reused + OperationKey string `xml:"operationKey,attr,omitempty" json:"operationKey,omitempty"` // Optional unique identifier to support idempotent semantics for create and delete operations. + Name string `xml:"name,attr" json:"name"` // The name of the entity. + Status int `xml:"status,attr,omitempty" json:"status,omitempty"` // Creation status of the gateway. One of: 0 (The gateway is still being created) 1 (The gateway is ready) -1 (There was an error while creating the gateway). // Elements - Link LinkList `xml:"Link,omitempty"` // A link to an operation on this section. - Description string `xml:"Description,omitempty"` // Optional description. - Tasks *TasksInProgress `xml:"Tasks,omitempty"` // A list of queued, running, or recently completed tasks associated with this entity. - Configuration *GatewayConfiguration `xml:"Configuration"` // Gateway configuration. + Link LinkList `xml:"Link,omitempty" json:"link,omitempty"` // A link to an operation on this section. + Description string `xml:"Description" json:"description"` // Description. + Tasks *TasksInProgress `xml:"Tasks,omitempty" json:"tasks,omitempty"` // A list of queued, running, or recently completed tasks associated with this entity. + Configuration *GatewayConfiguration `xml:"Configuration" json:"configuration,omitempty"` // Gateway configuration. } // GatewayConfiguration is the gateway configuration @@ -1705,31 +2148,37 @@ type GatewayConfiguration struct { // rules in the old 1.5 format. The new format does not require to use direction in firewall // rules. Also, for firewall rules to allow NAT traffic the filter is applied on the original IP // addresses. Once set to true cannot be reverted back to false. - BackwardCompatibilityMode bool `xml:"BackwardCompatibilityMode,omitempty"` + BackwardCompatibilityMode bool `xml:"BackwardCompatibilityMode,omitempty" json:"backwardCompatibilityMode,omitempty"` // GatewayBackingConfig defines configuration of the vShield edge VM for this gateway. One of: // compact, full. - GatewayBackingConfig string `xml:"GatewayBackingConfig"` + GatewayBackingConfig string `xml:"GatewayBackingConfig" json:"gatewayBackingConfig,omitempty"` // GatewayInterfaces holds configuration for edge gateway interfaces, ip allocations, traffic // rate limits and ip sub-allocations - GatewayInterfaces *GatewayInterfaces `xml:"GatewayInterfaces"` + GatewayInterfaces *GatewayInterfaces `xml:"GatewayInterfaces" json:"gatewayInterfaces,omitempty"` // EdgeGatewayServiceConfiguration represents Gateway Features. - EdgeGatewayServiceConfiguration *GatewayFeatures `xml:"EdgeGatewayServiceConfiguration,omitempty"` + EdgeGatewayServiceConfiguration *GatewayFeatures `xml:"EdgeGatewayServiceConfiguration,omitempty" json:"edgeGatewayServiceConfiguration,omitempty"` // True if this gateway is highly available. (Requires two vShield edge VMs.) - HaEnabled *bool `xml:"HaEnabled,omitempty"` + HaEnabled *bool `xml:"HaEnabled,omitempty" json:"haEnabled,omitempty"` // UseDefaultRouteForDNSRelay defines if the default gateway on the external network selected // for default route should be used as the DNS relay. - UseDefaultRouteForDNSRelay *bool `xml:"UseDefaultRouteForDnsRelay,omitempty"` + UseDefaultRouteForDNSRelay *bool `xml:"UseDefaultRouteForDnsRelay,omitempty" json:"useDefaultRouteForDNSRelay,omitempty"` // AdvancedNetworkingEnabled allows to use NSX capabilities such dynamic routing (BGP, OSPF), // zero trust networking (DLR), enchanced VPN support (IPsec VPN, SSL VPN-Plus). - AdvancedNetworkingEnabled *bool `xml:"AdvancedNetworkingEnabled,omitempty"` + AdvancedNetworkingEnabled *bool `xml:"AdvancedNetworkingEnabled,omitempty" json:"advancedNetworkingEnabled,omitempty"` // DistributedRoutingEnabled enables distributed routing on the gateway to allow creation of // many more organization VDC networks. Traffic in those networks is optimized for VM-to-VM // communication. - DistributedRoutingEnabled *bool `xml:"DistributedRoutingEnabled,omitempty"` + DistributedRoutingEnabled *bool `xml:"DistributedRoutingEnabled,omitempty" json:"distributedRoutingEnabled,omitempty"` // FipsModeEnabled allows any secure communication to or from the NSX Edge uses cryptographic // algorithms or protocols that are allowed by United States Federal Information Processing // Standards (FIPS). FIPS mode turns on the cipher suites that comply with FIPS. - FipsModeEnabled *bool `xml:"FipsModeEnabled,omitempty"` + FipsModeEnabled *bool `xml:"FipsModeEnabled,omitempty" json:"fipsModeEnabled,omitempty"` + // EdgeClusterConfiguration represents the Edge Cluster Configuration for a given Edge Gateway. + // Can be changed if a gateway needs to be placed on a specific set of Edge Clusters. + // For NSX-V Edges, if nothing is specified on create or update, the Org VDC Default will be used. + // For NSX-T Edges, Open API must be used and this field is read only. + // If there is no value, the gateway uses the Edge Cluster of the connected External Network's backing Tier-0 router. + EdgeClusterConfiguration *EdgeClusterConfiguration `xml:"EdgeClusterConfiguration,omitempty" json:"edgeClusterConfiguration,omitempty"` } // GatewayInterfaces is a list of Gateway Interfaces. @@ -1738,7 +2187,7 @@ type GatewayConfiguration struct { // Description: A list of Gateway Interfaces. // Since: 5.1 type GatewayInterfaces struct { - GatewayInterface []*GatewayInterface `xml:"GatewayInterface"` // Gateway Interface. + GatewayInterface []*GatewayInterface `xml:"GatewayInterface" json:"gatewayInterface,omitempty"` // Gateway Interface. } // GatewayInterface is a gateway interface configuration. @@ -1747,15 +2196,17 @@ type GatewayInterfaces struct { // Description: Gateway Interface configuration. // Since: 5.1 type GatewayInterface struct { - Name string `xml:"Name,omitempty"` // Internally generated name for the Gateway Interface. - DisplayName string `xml:"DisplayName,omitempty"` // Gateway Interface display name. - Network *Reference `xml:"Network"` // A reference to the network connected to the gateway interface. - InterfaceType string `xml:"InterfaceType"` // The type of interface: One of: Uplink, Internal - SubnetParticipation []*SubnetParticipation `xml:"SubnetParticipation,omitempty"` // Slice of subnets for IP allocations. - ApplyRateLimit bool `xml:"ApplyRateLimit,omitempty"` // True if rate limiting is applied on this interface. - InRateLimit float64 `xml:"InRateLimit,omitempty"` // Incoming rate limit expressed as Gbps. - OutRateLimit float64 `xml:"OutRateLimit,omitempty"` // Outgoing rate limit expressed as Gbps. - UseForDefaultRoute bool `xml:"UseForDefaultRoute,omitempty"` // True if this network is default route for the gateway. + Name string `xml:"Name,omitempty" json:"name,omitempty"` // Internally generated name for the Gateway Interface. + DisplayName string `xml:"DisplayName,omitempty" json:"displayName,omitempty"` // Gateway Interface display name. + Network *Reference `xml:"Network" json:"network,omitempty"` // A reference to the network connected to the gateway interface. + InterfaceType string `xml:"InterfaceType" json:"interfaceType,omitempty"` // The type of interface: One of: Uplink, Internal + SubnetParticipation []*SubnetParticipation `xml:"SubnetParticipation,omitempty" json:"subnetParticipation,omitempty"` // Slice of subnets for IP allocations. + ApplyRateLimit bool `xml:"ApplyRateLimit,omitempty" json:"applyRateLimit,omitempty"` // True if rate limiting is applied on this interface. + InRateLimit float64 `xml:"InRateLimit,omitempty" json:"inRateLimit,omitempty"` // Incoming rate limit expressed as Gbps. + OutRateLimit float64 `xml:"OutRateLimit,omitempty" json:"outRateLimit,omitempty"` // Outgoing rate limit expressed as Gbps. + UseForDefaultRoute bool `xml:"UseForDefaultRoute,omitempty" json:"useForDefaultRoute,omitempty"` // True if this network is default route for the gateway. + Connected bool `xml:"Connected,omitempty" json:"connected,omitempty"` // True if interface is marked as connected in NSX + QuickAddAllocatedIpCount int `xml:"QuickAddAllocatedIpCount,omitempty" json:"quickAddAllocatedIpCount,omitempty"` // If set on create or update api calls, the specified number of IP addresses will be additionally allocated for this uplink. IPs will be allocated from multiple subnets if needed } // SortBySubnetParticipationGateway allows to sort SubnetParticipation property slice by gateway @@ -1775,11 +2226,11 @@ func (g *GatewayInterface) SortBySubnetParticipationGateway() { // Note. Field order is important and should not be changed as API returns errors if IPRanges come // before Gateway and Netmask type SubnetParticipation struct { - Gateway string `xml:"Gateway"` // Gateway for subnet - Netmask string `xml:"Netmask"` // Netmask for the subnet. - IPAddress string `xml:"IpAddress,omitempty"` // Ip Address to be assigned. Keep empty or omit element for auto assignment - IPRanges *IPRanges `xml:"IpRanges,omitempty"` // Range of IP addresses available for external interfaces. - UseForDefaultRoute bool `xml:"UseForDefaultRoute,omitempty"` // True if this network is default route for the gateway. + Gateway string `xml:"Gateway" json:"gateway,omitempty"` // Gateway for subnet + Netmask string `xml:"Netmask" json:"netmask,omitempty"` // Netmask for the subnet. + IPAddress string `xml:"IpAddress,omitempty" json:"ipAddress,omitempty"` // Ip Address to be assigned. Keep empty or omit element for auto assignment + IPRanges *IPRanges `xml:"IpRanges,omitempty" json:"ipRanges,omitempty"` // Range of IP addresses available for external interfaces. + UseForDefaultRoute bool `xml:"UseForDefaultRoute,omitempty" json:"useForDefaultRoute,omitempty"` // True if this network is default route for the gateway. } type EdgeGatewayServiceConfiguration struct { @@ -1847,24 +2298,24 @@ type VendorTemplate struct { // Since: 5.1 type GatewayIpsecVpnService struct { IsEnabled bool `xml:"IsEnabled"` // Enable or disable the service using this flag - Endpoint *GatewayIpsecVpnEndpoint `xml:"Endpoint,omitempty"` // List of IPSec VPN Service Endpoints. - Tunnel []*GatewayIpsecVpnTunnel `xml:"Tunnel"` // List of IPSec VPN tunnels. + Endpoint *GatewayIpsecVpnEndpoint `xml:"Endpoint,omitempty"` // List of IPsec VPN Service Endpoints. + Tunnel []*GatewayIpsecVpnTunnel `xml:"Tunnel"` // List of IPsec VPN tunnels. } -// GatewayIpsecVpnEndpoint represents an IPSec VPN endpoint. +// GatewayIpsecVpnEndpoint represents an IPsec VPN endpoint. // Type: GatewayIpsecVpnEndpointType // Namespace: http://www.vmware.com/vcloud/v1.5 -// Description: Represents an IPSec VPN endpoint. +// Description: Represents an IPsec VPN endpoint. // Since: 5.1 type GatewayIpsecVpnEndpoint struct { Network *Reference `xml:"Network"` // External network reference. - PublicIP string `xml:"PublicIp,omitempty"` // Public IP for IPSec endpoint. + PublicIP string `xml:"PublicIp,omitempty"` // Public IP for IPsec endpoint. } -// GatewayIpsecVpnTunnel represents an IPSec VPN tunnel. +// GatewayIpsecVpnTunnel represents an IPsec VPN tunnel. // Type: GatewayIpsecVpnTunnelType // Namespace: http://www.vmware.com/vcloud/v1.5 -// Description: Represents an IPSec VPN tunnel. +// Description: Represents an IPsec VPN tunnel. // Since: 5.1 type GatewayIpsecVpnTunnel struct { Name string `xml:"Name"` // The name of the tunnel. @@ -1919,6 +2370,18 @@ type GatewayDhcpService struct { Pool []*DhcpPoolService `xml:"Pool,omitempty"` // A DHCP pool. } +// EdgeClusterConfiguration configures Edge clusters in an Edge Gateway. +// Type: EdgeClusterConfigurationType +// Namespace: http://www.vmware.com/vcloud/v1.5 +// Description: Used for specifying specific Edge Cluster(s) for a given Edge Gateway. Specification is only applicable +// for NSX-V Edges, and if specified this takes precedence over the Edge Cluster configuration on an Org vDC. For NSX-T Edges, +// this is only read-only and edge management must be done via Cloud API. +// Since: 5.1 +type EdgeClusterConfiguration struct { + PrimaryEdgeCluster *Reference `xml:"PrimaryEdgeCluster,omitempty" json:"primaryEdgeCluster,omitempty"` + SecondaryEdgeCluster *Reference `xml:"SecondaryEdgeCluster,omitempty" json:"secondaryEdgeCluster,omitempty"` +} + // DhcpPoolService represents DHCP pool service. // Type: DhcpPoolServiceType // Namespace: http://www.vmware.com/vcloud/v1.5 @@ -2135,6 +2598,7 @@ type QueryResultRecordsType struct { VAppRecord []*QueryResultVAppRecordType `xml:"VAppRecord"` // A record representing a VApp result. AdminVAppRecord []*QueryResultVAppRecordType `xml:"AdminVAppRecord"` // A record representing a VApp result as admin. OrgVdcStorageProfileRecord []*QueryResultOrgVdcStorageProfileRecordType `xml:"OrgVdcStorageProfileRecord"` // A record representing storage profiles + AdminOrgVdcStorageProfileRecord []*QueryResultAdminOrgVdcStorageProfileRecordType `xml:"AdminOrgVdcStorageProfileRecord"` // A record representing storage profiles as admin MediaRecord []*MediaRecordType `xml:"MediaRecord"` // A record representing media AdminMediaRecord []*MediaRecordType `xml:"AdminMediaRecord"` // A record representing Admin media VMWProviderVdcRecord []*QueryResultVMWProviderVdcRecordType `xml:"VMWProviderVdcRecord"` // A record representing a Provider VDC result. @@ -2152,6 +2616,147 @@ type QueryResultRecordsType struct { VappTemplateRecord []*QueryResultVappTemplateType `xml:"VAppTemplateRecord"` // A record representing a vApp template AdminVappTemplateRecord []*QueryResultVappTemplateType `xml:"AdminVAppTemplateRecord"` // A record representing an admin vApp template NsxtManagerRecord []*QueryResultNsxtManagerRecordType `xml:"NsxTManagerRecord"` // A record representing NSX-T manager + OrgVdcRecord []*QueryResultOrgVdcRecordType `xml:"OrgVdcRecord"` // A record representing Org VDC + OrgVdcAdminRecord []*QueryResultOrgVdcRecordType `xml:"AdminVdcRecord"` // A record representing Org VDC + ResourcePoolRecord []*QueryResultResourcePoolRecordType `xml:"ResourcePoolRecord"` // A record representing a Resource Pool + VmGroupsRecord []*QueryResultVmGroupsRecordType `xml:"VmGroupsRecord"` // A record representing a VM Group + TaskRecord []*QueryResultTaskRecordType `xml:"TaskRecord"` // A record representing a Task + AdminTaskRecord []*QueryResultTaskRecordType `xml:"AdminTaskRecord"` // A record representing an Admin Task + VappNetworkRecord []*QueryResultVappNetworkRecordType `xml:"VAppNetworkRecord"` // A record representing a vApp network + AdminVappNetworkRecord []*QueryResultVappNetworkRecordType `xml:"AdminVAppNetworkRecord"` // A record representing an admin vApp network + SiteAssociationRecord []*QueryResultSiteAssociationRecord `xml:"SiteAssociationRecord"` // A record representing a site association + OrgAssociationRecord []*QueryResultOrgAssociationRecord `xml:"OrgAssociationRecord"` // A record representing an Org association + OrgRecord []*QueryResultOrgRecordType `xml:"OrgRecord"` // A record representing an Organisation + AdminOrgVdcTemplateRecord []*QueryResultAdminOrgVdcTemplateRecordType `xml:"AdminOrgVdcTemplateRecord"` // A record representing an admin VDC Template + OrgVdcTemplateRecord []*QueryResultOrgVdcTemplateRecordType `xml:"OrgVdcTemplateRecord"` // A record representing an VDC Template +} + +// QueryResultVmGroupsRecordType represent a VM Groups record +type QueryResultVmGroupsRecordType struct { + HREF string `xml:"href,attr,omitempty"` + ID string `xml:"vmGroupId,attr,omitempty"` + Name string `xml:"vmGroupName,attr,omitempty"` + ClusterMoref string `xml:"clusterMoref,attr,omitempty"` + ClusterName string `xml:"clusterName,attr,omitempty"` + VcenterId string `xml:"vcId,attr,omitempty"` + NamedVmGroupId string `xml:"namedVmGroupId,attr,omitempty"` +} + +// QueryResultAdminOrgVdcTemplateRecordType represents an admin VDC Template +type QueryResultAdminOrgVdcTemplateRecordType struct { + HREF string `xml:"href,attr,omitempty"` + ID string `xml:"id,attr,omitempty"` + Name string `xml:"name,attr,omitempty"` + Description string `xml:"description,attr,omitempty"` + TenantVisibleName string `xml:"tenantVisibleName,attr,omitempty"` + TenantVisibleDescription string `xml:"tenantVisibleDescription,attr,omitempty"` + NetworkBackingType string `xml:"networkBackingType,attr,omitempty"` +} + +// QueryResultOrgVdcTemplateRecordType represents an admin VDC Template +type QueryResultOrgVdcTemplateRecordType struct { + HREF string `xml:"href,attr,omitempty"` + ID string `xml:"id,attr,omitempty"` + Name string `xml:"name,attr,omitempty"` + Description string `xml:"description,attr,omitempty"` + OrgHref string `xml:"org,attr,omitempty"` +} + +type QueryResultVappNetworkRecordType struct { + HREF string `xml:"href,attr,omitempty"` + ID string `xml:"id,attr,omitempty"` + Name string `xml:"name,attr,omitempty"` + Type string `xml:"linkType,attr,omitempty"` + IpScopeId string `xml:"ipScopeId,attr,omitempty"` + IpScopeInherited bool `xml:"ipScopeInherited,attr,omitempty"` + Gateway string `xml:"gateway,attr,omitempty"` + Netmask string `xml:"netmask,attr,omitempty"` + SubnetPrefixLength int `xml:"subnetPrefixLength,attr,omitempty"` + Dns1 string `xml:"dns1,attr,omitempty"` + Dns2 string `xml:"dns2,attr,omitempty"` + DnsSuffix string `xml:"dnsSuffix,attr,omitempty"` + Vapp string `xml:"vApp,attr,omitempty"` // the HREF of the parent vApp + VappName string `xml:"vAppName,attr,omitempty"` // the name of the parent vApp + LinkNetworkName string `xml:"linkNetworkName,attr,omitempty"` // this field is filled when called in tenant context + RealNetworkName string `xml:"realNetworkName,attr,omitempty"` + RealNetworkPortgroupId string `xml:"realNetworkPortgroupId,attr,omitempty"` + VCenterName string `xml:"vcName,attr,omitempty"` + VCenter string `xml:"vc,attr,omitempty"` + IsBusy bool `xml:"isBusy,attr,omitempty"` + IsLinked bool `xml:"isLinked,attr,omitempty"` + RetainNicResources bool `xml:"retainNicResources,attr,omitempty"` + Metadata *Metadata `xml:"Metadata,omitempty"` +} + +// QueryResultResourcePoolRecordType represent a Resource Pool record +type QueryResultResourcePoolRecordType struct { + HREF string `xml:"href,attr,omitempty"` + Name string `xml:"name,attr,omitempty"` + Moref string `xml:"moref,attr,omitempty"` + IsDeleted bool `xml:"isDeleted,attr,omitempty"` + VcenterHREF string `xml:"vc,attr,omitempty"` + VcenterName string `xml:"vcName,attr,omitempty"` + ProviderVdcHREF string `xml:"providerVdc,attr,omitempty"` + ProviderName string `xml:"providerName,attr,omitempty"` + IsEnabled bool `xml:"isEnabled,attr,omitempty"` + IsPrimary bool `xml:"isPrimary,attr,omitempty"` + ClusterMoref string `xml:"clusterMoref,attr,omitempty"` + IsKubernetesEnabled bool `xml:"isKubernetesEnabled,attr,omitempty"` +} + +// QueryResultOrgVdcRecordType represents an Org VDC record +type QueryResultOrgVdcRecordType struct { + HREF string `xml:"href,attr,omitempty"` + Name string `xml:"name,attr,omitempty"` + ComputeProviderScope string `xml:"computeProviderScope,attr,omitempty"` + NetworkProviderScope string `xml:"networkProviderScope,attr,omitempty"` + IsEnabled string `xml:"isEnabled,attr,omitempty"` + CpuAllocationMhz *int `xml:"cpuAllocationMhz,attr,omitempty"` + CpuLimitMhz *int `xml:"cpuLimitMhz,attr,omitempty"` + CpuUsedMhz *int `xml:"cpuUsedMhz,attr,omitempty"` + MemoryAllocationMB *int `xml:"memoryAllocationMB,attr,omitempty"` + MemoryLimitMB *int `xml:"memoryLimitMB,attr,omitempty"` + MemoryUsedMB *int `xml:"memoryUsedMB,attr,omitempty"` + StorageLimitMB *int `xml:"storageLimitMB,attr,omitempty"` + StorageUsedMB *int `xml:"storageUsedMB,attr,omitempty"` + StorageOverheadMB *int `xml:"storageOverheadMB,attr,omitempty"` + MemoryOverheadMB *int `xml:"memoryOverheadMB,attr,omitempty"` + NumberOfVApps *int `xml:"numberOfVApps,attr,omitempty"` + NumberOfUnmanagedVApps *int `xml:"numberOfUnmanagedVApps,attr,omitempty"` + NumberOfMedia *int `xml:"numberOfMedia,attr,omitempty"` + NumberOfDisks *int `xml:"numberOfDisks,attr,omitempty"` + NumberOfVAppTemplates *int `xml:"numberOfVAppTemplates,attr,omitempty"` + NumberOfStorageProfiles *int `xml:"numberOfStorageProfiles,attr,omitempty"` + NumberOfVMs *int `xml:"numberOfVMs,attr,omitempty"` + NumberOfRunningVMs *int `xml:"numberOfRunningVMs,attr,omitempty"` + NumberOfDeployedVApps *int `xml:"numberOfDeployedVApps,attr,omitempty"` + NumberOfDeployedUnmanagedVApps *int `xml:"numberOfDeployedUnmanagedVApps,attr,omitempty"` + CpuOverheadMhz *int `xml:"cpuOverheadMhz,attr,omitempty"` + OrgName string `xml:"orgName,attr,omitempty"` + AllocationModel string `xml:"allocationModel,attr,omitempty"` + VcName string `xml:"vcName,attr,omitempty"` + IsBusy string `xml:"isBusy,attr,omitempty"` + Status string `xml:"status,attr,omitempty"` + TaskStatusName string `xml:"taskStatusName,attr,omitempty"` + Task string `xml:"task,attr,omitempty"` + TaskStatus string `xml:"taskStatus,attr,omitempty"` + TaskDetails string `xml:"taskDetails,attr,omitempty"` + Metadata *Metadata `xml:"Metadata,omitempty"` + + // Admin Org VDC fields + ProviderVdcName string `xml:"providerVdcName,attr,omitempty"` + ProviderVdc string `xml:"providerVdc,attr,omitempty"` + Org string `xml:"org,attr,omitempty"` + NetworkPool string `xml:"networkPool,attr,omitempty"` + NumberOfResourcePools *int `xml:"numberOfResourcePools,attr,omitempty"` + UsedNetworksInVdc string `xml:"usedNetworksInVdc,attr,omitempty"` + IsThinProvisioned string `xml:"isThinProvisioned,attr,omitempty"` + IsFastProvisioned string `xml:"isFastProvisioned,attr,omitempty"` + NetworkProviderType string `xml:"networkProviderType,attr,omitempty"` + IsVCEnabled string `xml:"isVCEnabled,attr,omitempty"` + MemoryReservedMB *int `xml:"memoryReservedMB,attr,omitempty"` + CpuReservedMhz *int `xml:"cpuReservedMhz,attr,omitempty"` + Vc string `xml:"vc,attr,omitempty"` } // QueryResultCatalogItemType represents a catalog item as query result @@ -2161,7 +2766,7 @@ type QueryResultCatalogItemType struct { Type string `xml:"type,attr,omitempty"` // The MIME type of the entity. Entity string `xml:"entity,attr,omitempty"` // Entity reference or ID EntityName string `xml:"entityName,attr,omitempty"` // Entity name - EntityType string `xml:"entityType,attr,omitempty"` // Entity name + EntityType string `xml:"entityType,attr,omitempty"` // Entity type Catalog string `xml:"catalog,attr,omitempty"` // Catalog reference or ID CatalogName string `xml:"catalogName,attr,omitempty"` // Catalog name OwnerName string `xml:"ownerName,attr,omitempty"` // Owner name @@ -2180,29 +2785,53 @@ type QueryResultCatalogItemType struct { // QueryResultVappTemplateType represents a vApp template as query result type QueryResultVappTemplateType struct { - HREF string `xml:"href,attr,omitempty"` // The URI of the entity. - ID string `xml:"id,attr,omitempty"` // vApp template ID. - Type string `xml:"type,attr,omitempty"` // The MIME type of the entity. - OwnerName string `xml:"ownerName,attr,omitempty"` // Owner name - CatalogName string `xml:"catalogName,attr,omitempty"` // Catalog name - IsPublished bool `xml:"isPublished,attr,omitempty"` // True if this entity is in a published catalog - Name string `xml:"name,attr,omitempty"` // vApp template name. - Description string `xml:"description,attr,omitempty"` // vApp template description. - Vdc string `xml:"vdc,attr,omitempty"` // VDC reference or ID - VdcName string `xml:"vdcName,attr,omitempty"` // VDC name - Org string `xml:"org,attr,omitempty"` // Organization reference or ID - CreationDate string `xml:"creationDate,attr,omitempty"` // Creation date - IsBusy bool `xml:"isBusy,attr,omitempty"` // True if the vApp template is busy - IsGoldMaster bool `xml:"isGoldMaster,attr,omitempty"` // True if the vApp template is a gold master - IsEnabled bool `xml:"isEnabled,attr,omitempty"` // True if the vApp template is enabled - Status string `xml:"status,attr,omitempty"` // Status - IsDeployed bool `xml:"isDeployed,attr,omitempty"` // True if this entity is deployed - IsExpired bool `xml:"isExpired,attr,omitempty"` // True if this entity is expired - StorageProfileName string `xml:"storageProfileName,attr,omitempty"` // Storage profile name - Version string `xml:"version,attr,omitempty"` // Storage profile name - LastSuccessfulSync string `xml:"lastSuccessfulSync,attr,omitempty"` // Date of last successful sync - Link *Link `xml:"Link,omitempty"` - Metadata *Metadata `xml:"Metadata,omitempty"` + HREF string `xml:"href,attr,omitempty"` // The URI of the entity. + ID string `xml:"id,attr,omitempty"` // vApp template ID. + Type string `xml:"type,attr,omitempty"` // The MIME type of the entity. + OwnerName string `xml:"ownerName,attr,omitempty"` // Owner name + Owner string `xml:"owner,attr,omitempty"` // Owner reference or ID + CatalogName string `xml:"catalogName,attr,omitempty"` // Catalog name + Catalog string `xml:"catalog,attr,omitempty"` // Catalog reference or ID + CatalogItem string `xml:"catalogItem,attr,omitempty"` // CatalogItem reference or ID + IsPublished bool `xml:"isPublished,attr,omitempty"` // True if this entity is in a published catalog + PublishSubscriptionType string `xml:"publishSubscriptionType,attr,omitempty"` // PUBLISHED if parent catalog published externally, SUBSCRIBED if parent catalog subscribed to an external catalog, UNPUBLISHED otherwise. + Name string `xml:"name,attr,omitempty"` // vApp template name. + Description string `xml:"description,attr,omitempty"` // vApp template description. + Vdc string `xml:"vdc,attr,omitempty"` // VDC reference or ID + VdcName string `xml:"vdcName,attr,omitempty"` // VDC name + IsVdcEnabled bool `xml:"isVdcEnabled,attr,omitempty"` // true if the containing VDC is enabled + Org string `xml:"org,attr,omitempty"` // Organization reference or ID + CreationDate string `xml:"creationDate,attr,omitempty"` // Creation date + IsBusy bool `xml:"isBusy,attr,omitempty"` // True if the vApp template is busy + IsGoldMaster bool `xml:"isGoldMaster,attr,omitempty"` // True if the vApp template is a gold master + IsEnabled bool `xml:"isEnabled,attr,omitempty"` // True if the vApp template is enabled + Status string `xml:"status,attr,omitempty"` // Status + IsDeployed bool `xml:"isDeployed,attr,omitempty"` // True if this entity is deployed + IsExpired bool `xml:"isExpired,attr,omitempty"` // True if this entity is expired + StorageProfileName string `xml:"storageProfileName,attr,omitempty"` // Storage profile name + Version string `xml:"version,attr,omitempty"` // Storage profile name + LastSuccessfulSync string `xml:"lastSuccessfulSync,attr,omitempty"` // Date of last successful sync + Link *Link `xml:"Link,omitempty"` + Metadata *Metadata `xml:"Metadata,omitempty"` + + // Undocumented fields + // Provisionally used in some catalog synchronisation tasks. To be removed and replaced by using a different algorithm + // https://developer.vmware.com/apis/1260/vmware-cloud-director/doc/doc/types/QueryResultAdminVAppTemplateRecordType.html + // https://developer.vmware.com/apis/1260/vmware-cloud-director/doc/doc/types/QueryResultVAppTemplateRecordType.html + IsInCatalog bool `xml:"isInCatalog,attr,omitempty"` // True if this vApp template is in a catalog + HonorBootOrder bool `xml:"honorBootOrder,attr,omitempty"` // ? + NumberOfShadowVms int `xml:"numberOfShadowVMs,attr,omitempty"` // number of shadow VMs + NumberOfVms int `xml:"numberOfVMs,attr,omitempty"` // number of VMs + TaskStatusName string `xml:"taskStatusName,attr,omitempty"` // name of the associated task + TaskStatus string `xml:"taskStatus,attr,omitempty"` // status of the associated task + Task string `xml:"task,attr,omitempty"` // ID or reference of the associated task + TaskDetails string `xml:"taskDetails,attr,omitempty"` // details of the associated task + IsDeleteUndeployNotified bool `xml:"isDeleteUndeployNotified,attr,omitempty"` // ? + IsAutoUndeployNotified bool `xml:"isAutoUndeployNotified,attr,omitempty"` // ? + CpuAllocationInMhz int `xml:"cpuAllocationInMhz,attr,omitempty"` // CPU allocation + NumberOfCpus int `xml:"numberOfCpus,attr,omitempty"` // Number of CPUs + MemoryAllocationMb int `xml:"memoryAllocationMB,attr,omitempty"` // Memory allocation in MB + StorageKb int `xml:"storageKB,attr,omitempty"` // Storage allocation in Kb } // QueryResultEdgeGatewayRecordType represents an edge gateway record as query result. @@ -2225,6 +2854,7 @@ type QueryResultVMRecordType struct { // Attributes HREF string `xml:"href,attr,omitempty"` // The URI of the entity. ID string `xml:"id,attr,omitempty"` + Moref string `xml:"moref,attr,omitempty"` // VM moref id. Name string `xml:"name,attr,omitempty"` // VM name. Type string `xml:"type,attr,omitempty"` // Contains the type of the resource. ContainerName string `xml:"containerName,attr,omitempty"` // The name of the vApp or vApp template that contains this VM. @@ -2232,9 +2862,11 @@ type QueryResultVMRecordType struct { OwnerName string `xml:"ownerName,attr,omitempty"` Owner string `xml:"owner,attr,omitempty"` VdcHREF string `xml:"vdc,attr,omitempty"` + VdcName string `xml:"vdcName,attr,omitempty"` VAppTemplate bool `xml:"isVAppTemplate,attr,omitempty"` Deleted bool `xml:"isDeleted,attr,omitempty"` GuestOS string `xml:"guestOs,attr,omitempty"` + DetectedGuestOS string `xml:"detectedGuestOs,attr,omitempty"` Cpus int `xml:"numberOfCpus,attr,omitempty"` MemoryMB int `xml:"memoryMB,attr,omitempty"` Status string `xml:"status,attr,omitempty"` @@ -2262,6 +2894,7 @@ type QueryResultVMRecordType struct { DateCreated string `xml:"dateCreated,attr,omitempty"` TotalStorageAllocatedMb string `xml:"totalStorageAllocatedMb,attr,omitempty"` IsExpired bool `xml:"isExpired,attr,omitempty"` + HostName string `xml:"hostName,attr,omitempty"` // HostName=Hypervisor of virtual machine Link []*Link `xml:"Link,omitempty"` MetaData *Metadata `xml:"Metadata,omitempty"` } @@ -2273,6 +2906,7 @@ type QueryResultVAppRecordType struct { Name string `xml:"name,attr"` // The name of the entity. CreationDate string `xml:"creationDate,attr,omitempty"` // Creation date/time of the vApp. Busy bool `xml:"isBusy,attr,omitempty"` + Description string `xml:"description,attr,omitempty"` Deployed bool `xml:"isDeployed,attr,omitempty"` // True if the vApp is deployed. Enabled bool `xml:"isEnabled,attr,omitempty"` Expired bool `xml:"isExpired,attr,omitempty"` @@ -2303,18 +2937,52 @@ type QueryResultVAppRecordType struct { // QueryResultOrgVdcStorageProfileRecordType represents a storage // profile as query result. +// https://code.vmware.com/apis/722/vmware-cloud-director/doc/doc/types/QueryResultOrgVdcStorageProfileRecordType.html type QueryResultOrgVdcStorageProfileRecordType struct { // Attributes - HREF string `xml:"href,attr,omitempty"` // The URI of the entity. - Name string `xml:"name,attr,omitempty"` // Storage Profile name. - VdcHREF string `xml:"vdc,attr,omitempty"` - VdcName string `xml:"vdcName,attr,omitempty"` - IsDefaultStorageProfile bool `xml:"isDefaultStorageProfile,attr,omitempty"` - IsEnabled bool `xml:"isEnabled,attr,omitempty"` - IsVdcBusy bool `xml:"isVdcBusy,attr,omitempty"` - NumberOfConditions int `xml:"numberOfConditions,attr,omitempty"` - StorageUsedMB int `xml:"storageUsedMB,attr,omitempty"` - StorageLimitMB int `xml:"storageLimitMB,attr,omitempty"` + HREF string `xml:"href,attr,omitempty"` // The URI of the entity. + ID string `xml:"id,attr,omitempty"` // The ID of the entity. + Type string `xml:"type,attr,omitempty"` // Contains the type of the resource. + Name string `xml:"name,attr,omitempty"` // Name of the storage profile. + IsEnabled bool `xml:"isEnabled,attr,omitempty"` // True if this entity is enabled. + IsDefaultStorageProfile bool `xml:"isDefaultStorageProfile,attr,omitempty"` // True if this is the default storage profile for a VDC. + StorageUsedMB uint64 `xml:"storageUsedMB,attr,omitempty"` // Storage used in MB. + StorageLimitMB uint64 `xml:"storageLimitMB,attr,omitempty"` // Storage limit in MB. + IopsAllocated uint64 `xml:"iopsAllocated,attr,omitempty"` // Total currently allocated IOPS on the storage profile. + IopsLimit uint64 `xml:"iopsLimit,attr,omitempty"` // IOPS limit for the storage profile. + NumberOfConditions int `xml:"numberOfConditions,attr,omitempty"` // Number of conditions on the storage profile. + Vdc string `xml:"vdc,attr,omitempty"` // VDC reference or id. + VdcName string `xml:"vdcName,attr,omitempty"` // VDC name. + IsVdcBusy bool `xml:"isVdcBusy,attr,omitempty"` // True if the associated VDC is busy. + // Elements + Link []*Link `xml:"Link,omitempty"` + MetadataEntry []*MetadataEntry `xml:"MetadataEntry,omitempty"` +} + +// QueryResultAdminOrgVdcStorageProfileRecordType represents a storage +// profile as query result. +// https://code.vmware.com/apis/722/vmware-cloud-director/doc/doc/types/QueryResultAdminOrgVdcStorageProfileRecordType.html +type QueryResultAdminOrgVdcStorageProfileRecordType struct { + // Attributes + HREF string `xml:"href,attr,omitempty"` // The URI of the entity. + ID string `xml:"id,attr,omitempty"` // The ID of the entity. + Type string `xml:"type,attr,omitempty"` // Contains the type of the resource. + Name string `xml:"name,attr,omitempty"` // Name of the storage profile. + IsEnabled bool `xml:"isEnabled,attr,omitempty"` // True if this entity is enabled. + IsDefaultStorageProfile bool `xml:"isDefaultStorageProfile,attr,omitempty"` // True if this is the default storage profile for a VDC. + StorageUsedMB uint64 `xml:"storageUsedMB,attr,omitempty"` // Storage used in MB. + StorageLimitMB uint64 `xml:"storageLimitMB,attr,omitempty"` // Storage limit in MB. + IopsAllocated uint64 `xml:"iopsAllocated,attr,omitempty"` // Total currently allocated IOPS on the storage profile. + IopsLimit uint64 `xml:"iopsLimit,attr,omitempty"` // IOPS limit for the storage profile. + NumberOfConditions int `xml:"numberOfConditions,attr,omitempty"` // Number of conditions on the storage profile. + Vdc string `xml:"vdc,attr,omitempty"` // VDC reference or id. + VdcName string `xml:"vdcName,attr,omitempty"` // VDC name. + Org string `xml:"org,attr,omitempty"` // Organization reference or id. + VC string `xml:"vc,attr,omitempty"` // Virtual center reference or id. + StorageProfileMoref string `xml:"storageProfileMoref,omitempty"` + // Elements + Link []*Link `xml:"Link,omitempty"` + MetadataEntry []*MetadataEntry `xml:"MetadataEntry,omitempty"` } // QueryResultVMWProviderVdcRecordType represents a Provider VDC as query result. @@ -2418,9 +3086,9 @@ type ExternalNetworkReference struct { // Description: Represents the Managed Object Reference (MoRef) and the type of a vSphere object. // Since: 0.9 type VimObjectRef struct { - VimServerRef *Reference `xml:"VimServerRef"` - MoRef string `xml:"MoRef"` - VimObjectType string `xml:"VimObjectType"` + VimServerRef *Reference `xml:"VimServerRef" json:"vimServerRef"` + MoRef string `xml:"MoRef" json:"moRef"` + VimObjectType string `xml:"VimObjectType" json:"vimObjectType"` } // Type: VimObjectRefsType @@ -2429,7 +3097,7 @@ type VimObjectRef struct { // Description: List of VimObjectRef elements. // Since: 0.9 type VimObjectRefs struct { - VimObjectRef []*VimObjectRef `xml:"VimObjectRef"` + VimObjectRef []*VimObjectRef `xml:"VimObjectRef" json:"vimObjectRef"` } // Type: VMWExternalNetworkType @@ -2528,23 +3196,30 @@ type DiskCreateParams struct { // Reference: vCloud API 30.0 - DiskType // https://code.vmware.com/apis/287/vcloud?h=Director#/doc/doc/types/DiskType.html type Disk struct { - XMLName xml.Name `xml:"Disk"` - Xmlns string `xml:"xmlns,attr,omitempty"` - HREF string `xml:"href,attr,omitempty"` - Type string `xml:"type,attr,omitempty"` - Id string `xml:"id,attr,omitempty"` - OperationKey string `xml:"operationKey,attr,omitempty"` - Name string `xml:"name,attr"` - Status int `xml:"status,attr,omitempty"` - Size int64 `xml:"size,attr"` + XMLName xml.Name `xml:"Disk"` + Xmlns string `xml:"xmlns,attr,omitempty"` + HREF string `xml:"href,attr,omitempty"` + Type string `xml:"type,attr,omitempty"` + Id string `xml:"id,attr,omitempty"` + OperationKey string `xml:"operationKey,attr,omitempty"` + Name string `xml:"name,attr"` + Status int `xml:"status,attr,omitempty"` + // Size of the disk in bytes. No longer supported in API V33.0+. + // Size int64 `xml:"size,attr"` + // SizeMb is the size of disk in MB. It has replaced Size (in bytes) field as of API V33.0 + SizeMb int64 `xml:"sizeMb,attr,omitempty"` Iops *int `xml:"iops,attr,omitempty"` BusType string `xml:"busType,attr,omitempty"` BusSubType string `xml:"busSubType,attr,omitempty"` + Encrypted bool `xml:"encrypted,attr,omitempty"` + Shareable bool `xml:"shareable,attr,omitempty"` + SharingType string `xml:"sharingType,attr,omitempty"` + UUID string `xml:"uuid,attr,omitempty"` Description string `xml:"Description,omitempty"` Files *FilesList `xml:"Files,omitempty"` Link []*Link `xml:"Link,omitempty"` - Owner *Owner `xml:"Owner,omitempty"` StorageProfile *Reference `xml:"StorageProfile,omitempty"` + Owner *Owner `xml:"Owner,omitempty"` Tasks *TasksInProgress `xml:"Tasks,omitempty"` VCloudExtension *VCloudExtension `xml:"VCloudExtension,omitempty"` } @@ -2573,11 +3248,11 @@ type DiskAttachOrDetachParams struct { // Reference: vCloud API 30.0 - VmsType // https://code.vmware.com/apis/287/vcloud?h=Director#/doc/doc/types/FilesListType.html type Vms struct { - XMLName xml.Name `xml:"Vms"` - Xmlns string `xml:"xmlns,attr,omitempty"` - Type string `xml:"type,attr"` - HREF string `xml:"href,attr"` - VmReference *Reference `xml:"VmReference,omitempty"` + XMLName xml.Name `xml:"Vms"` + Xmlns string `xml:"xmlns,attr,omitempty"` + Type string `xml:"type,attr"` + HREF string `xml:"href,attr"` + VmReference []*Reference `xml:"VmReference,omitempty"` } // Parameters for inserting and ejecting virtual media for VM as CD/DVD @@ -2626,27 +3301,34 @@ type VmQuestionAnswer struct { // Reference: vCloud API 27.0 - DiskType // https://code.vmware.com/apis/287/vcloud#/doc/doc/types/QueryResultDiskRecordType.html type DiskRecordType struct { - Xmlns string `xml:"xmlns,attr,omitempty"` - HREF string `xml:"href,attr,omitempty"` - Id string `xml:"id,attr,omitempty"` - Type string `xml:"type,attr,omitempty"` - Name string `xml:"name,attr,omitempty"` - Vdc string `xml:"vdc,attr,omitempty"` - SizeB int64 `xml:"sizeB,attr,omitempty"` - DataStore string `xml:"dataStore,attr,omitempty"` - DataStoreName string `xml:"datastoreName,attr,omitempty"` - OwnerName string `xml:"ownerName,attr,omitempty"` - VdcName string `xml:"vdcName,attr,omitempty"` - Task string `xml:"task,attr,omitempty"` - StorageProfile string `xml:"storageProfile,attr,omitempty"` - StorageProfileName string `xml:"storageProfileName,attr,omitempty"` - Status string `xml:"status,attr,omitempty"` - BusType string `xml:"busType,attr,omitempty"` - BusSubType string `xml:"busSubType,attr,omitempty"` - BusTypeDesc string `xml:"busTypeDesc,attr,omitempty"` - IsAttached bool `xml:"isAttached,attr,omitempty"` - Description string `xml:"description,attr,omitempty"` - Link []*Link `xml:"Link,omitempty"` + Xmlns string `xml:"xmlns,attr,omitempty"` + HREF string `xml:"href,attr,omitempty"` + Id string `xml:"id,attr,omitempty"` + Type string `xml:"type,attr,omitempty"` + Name string `xml:"name,attr,omitempty"` + Vdc string `xml:"vdc,attr,omitempty"` + SizeMb int64 `xml:"sizeMb,attr,omitempty"` + Iops int64 `xml:"iops,attr,omitempty"` + Encrypted bool `xml:"encrypted,attr,omitempty"` + UUID string `xml:"uuid,attr,omitempty"` + DataStore string `xml:"dataStore,attr,omitempty"` + DataStoreName string `xml:"datastoreName,attr,omitempty"` + OwnerName string `xml:"ownerName,attr,omitempty"` + VdcName string `xml:"vdcName,attr,omitempty"` + Task string `xml:"task,attr,omitempty"` + StorageProfile string `xml:"storageProfile,attr,omitempty"` + StorageProfileName string `xml:"storageProfileName,attr,omitempty"` + Status string `xml:"status,attr,omitempty"` + BusType string `xml:"busType,attr,omitempty"` + BusSubType string `xml:"busSubType,attr,omitempty"` + BusTypeDesc string `xml:"busTypeDesc,attr,omitempty"` + AttachedVmCount int32 `xml:"attachedVmCount,attr,omitempty"` + SharingType string `xml:"sharingType,attr,omitempty"` + IsAttached bool `xml:"isAttached,attr,omitempty"` + IsShareable bool `xml:"isShareable,attr,omitempty"` + Description string `xml:"description,attr,omitempty"` + Link []*Link `xml:"Link,omitempty"` + Metadata *Metadata `xml:"Metadata,omitempty"` } // Represents port group @@ -2747,8 +3429,8 @@ type User struct { IsExternal bool `xml:"IsExternal,omitempty"` ProviderType string `xml:"ProviderType,omitempty"` IsGroupRole bool `xml:"IsGroupRole,omitempty"` - StoredVmQuota int `xml:"StoredVmQuota,omitempty"` - DeployedVmQuota int `xml:"DeployedVmQuota,omitempty"` + StoredVmQuota int `xml:"StoredVmQuota"` + DeployedVmQuota int `xml:"DeployedVmQuota"` Role *Reference `xml:"Role,omitempty"` GroupReferences *GroupReference `xml:"GroupReferences,omitempty"` Password string `xml:"Password,omitempty"` @@ -2773,6 +3455,13 @@ type Group struct { ProviderType string `xml:"ProviderType"` // Role - reference to existing role Role *Reference `xml:"Role,omitempty"` + // UsersList - references to existing users of type User + UsersList *UsersList `xml:"UsersList,omitempty"` +} + +// UsersList is a tagged list of User Reference's +type UsersList struct { + UserReference []*Reference `xml:"UserReference,omitempty"` } // Type: AdminCatalogRecord @@ -2789,6 +3478,7 @@ type CatalogRecord struct { Description string `xml:"description,attr,omitempty"` IsPublished bool `xml:"isPublished,attr,omitempty"` IsShared bool `xml:"isShared,attr,omitempty"` + IsLocal bool `xml:"isLocal,attr,omitempty"` CreationDate string `xml:"creationDate,attr,omitempty"` OrgName string `xml:"orgName,attr,omitempty"` OwnerName string `xml:"ownerName,attr,omitempty"` @@ -2845,9 +3535,9 @@ type AccessSettingList struct { // LocalSubject is the user, group, or organization to which control access settings apply. type LocalSubject struct { - HREF string `xml:"href,attr"` // Required - The URL with the full identification of the subject - Name string `xml:"name,attr"` // The name of the subject. Not needed in input, but it is returned on reading - Type string `xml:"type,attr"` // Required - The MIME type of the subject. So far, we are using users, groups, and organizations + HREF string `xml:"href,attr"` // Required - The URL with the full identification of the subject + Name string `xml:"name,attr,omitempty"` // The name of the subject. Not needed in input, but it is returned on reading + Type string `xml:"type,attr,omitempty"` // The MIME type of the subject. So far, we are using users, groups, and organizations } // AccessSetting controls access to the resource. @@ -2889,3 +3579,242 @@ type VCloud struct { // RoleReferences // Networks } + +// UpdateVdcStorageProfiles is used to add a storage profile to an Org VDC or to remove one +type UpdateVdcStorageProfiles struct { + XMLName xml.Name `xml:"UpdateVdcStorageProfiles"` + Xmlns string `xml:"xmlns,attr,omitempty"` + Name string `xml:"name,attr"` + Description string `xml:"Description,omitempty"` + AddStorageProfile *VdcStorageProfileConfiguration `xml:"AddStorageProfile,omitempty"` + RemoveStorageProfile *Reference `xml:"RemoveStorageProfile,omitempty"` +} + +// Token is used for managing VCD API Tokens for a User in an Org +type Token struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Owner *OpenApiReference `json:"owner,omitempty"` + Org *OpenApiReference `json:"org,omitempty"` + Type string `json:"type,omitempty"` +} + +// ServiceAccount is used for managing a Service Account that belongs to a specific Org +type ServiceAccount struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + SoftwareID string `json:"softwareId,omitempty"` + SoftwareVersion string `json:"softwareVersion,omitempty"` + Role *OpenApiReference `json:"role,omitempty"` + URI string `json:"uri,omitempty"` + Org *OpenApiReference `json:"org,omitempty"` + Status string `json:"status,omitempty"` +} + +// ApiTokenRefresh contains the access token resulting from a refresh_token operation +type ApiTokenRefresh struct { + AccessToken string `json:"access_token,omitempty"` + TokenType string `json:"token_type,omitempty"` + ExpiresIn int `json:"expires_in,omitempty"` + RefreshToken string `json:"refresh_token,omitempty"` + UpdatedBy string `json:"updated_by,omitempty"` + UpdatedOn string `json:"updated_on,omitempty"` +} + +// ApiTokenParams contains the parameters required and returned by oauth/register operation +type ApiTokenParams struct { + ClientName string `json:"client_name"` + ClientID string `json:"client_id,omitempty"` + GrantTypes []string `json:"grant_types,omitempty"` + TokenEndpointAuthMethod string `json:"token_endpoint_auth_method,omitempty"` + ClientURI string `json:"client_uri,omitempty"` + SoftwareID string `json:"software_id,omitempty"` + SoftwareVersion string `json:"software_version,omitempty"` + Scope string `json:"scope,omitempty"` +} + +// ServiceAccountAuthParams is used to store the generated user code and device code that +// are needed for granting and activating a Service Account +type ServiceAccountAuthParams struct { + DeviceCode string `json:"device_code,omitempty"` + UserCode string `json:"user_code"` + VerificationURI string `json:"verification_uri,omitempty"` + ExpiresIn int `json:"expires_in,omitempty"` + Interval int `json:"interval,omitempty"` +} + +/**/ +type QueryResultTaskRecordType struct { + HREF string `xml:"href,attr,omitempty"` // Contains the URI to the resource. + ID string `xml:"id,attr,omitempty"` // The resource identifier, expressed in URN format. The value of this attribute uniquely identifies the resource, persists for the life of the resource, and is never reused. Yes Yes + Type string `xml:"type,attr,omitempty"` // Contains the type of the resource. + Org string `xml:"org,attr,omitempty"` // Organization reference or id + OrgName string `xml:"orgName,attr,omitempty"` // Organization name + Name string `xml:"name,attr,omitempty"` // The name of this task. + OperationFull string `xml:"operationFull,attr,omitempty"` // The full human-readable name of this task. + Message string `xml:"message,attr,omitempty"` // message + StartDate string `xml:"startDate,attr,omitempty"` // Start date + EndDate string `xml:"endDate,attr,omitempty"` // End date + Status string `xml:"status,attr,omitempty"` // Status + Progress int `xml:"progress,attr,omitempty"` // Progress of the task, expressed as a percentage. + OwnerName string `xml:"ownerName,attr,omitempty"` // Owner name + Object string `xml:"object,attr,omitempty"` // Object + ObjectType string `xml:"objectType,attr,omitempty"` // Object + ObjectName string `xml:"objectName,attr,omitempty"` // Object name + ServiceNamespace string `xml:"serviceNamespace,attr,omitempty"` // Service name space + Link *Link `xml:"Link,omitempty"` + Metadata *Metadata `xml:"Metadata,omitempty"` +} + +// QueryResultOrgVdcRecordType represents an Organisation record +type QueryResultOrgRecordType struct { + HREF string `xml:"href,attr,omitempty"` + Type string `xml:"type,attr,omitempty"` + ID string `xml:"id,attr,omitempty"` + Name string `xml:"name,attr"` + DisplayName string `xml:"displayName,attr,omitempty"` + IsEnabled bool `xml:"isEnabled,attr,omitempty"` + IsReadOnly bool `xml:"isReadOnly,attr,omitempty"` + CanPublishCatalogs bool `xml:"canPublishCatalogs,attr,omitempty"` + DeployedVMQuota *int `xml:"deployedVMQuota,attr,omitempty"` + StoredVMQuota *int `xml:"storedVMQuota,attr,omitempty"` + NumberOfCatalogs *int `xml:"numberOfCatalogs,attr,omitempty"` + NumberOfVdcs *int `xml:"numberOfVdcs,attr,omitempty"` + NumberOfVApps *int `xml:"numberOfVApps,attr,omitempty"` + NumberOfGroups *int `xml:"numberOfGroups,attr,omitempty"` + NumberOfDisks *int `xml:"numberOfDisks,attr,omitempty"` + Link *LinkList `xml:"Link,omitempty"` + Metadata *Metadata `xml:"Metadata,omitempty"` +} + +// ProviderVdcCreation contains the data needed to create a provider VDC. +// Note that this is a subset of the full structure of a provider VDC. +type ProviderVdcCreation struct { + Name string `json:"name"` + Description string `json:"description"` + ResourcePoolRefs *VimObjectRefs `json:"resourcePoolRefs"` + HighestSupportedHardwareVersion string `json:"highestSupportedHardwareVersion"` + IsEnabled bool `json:"isEnabled"` + VimServer []*Reference `json:"vimServer"` + StorageProfile []string `json:"storageProfile"` + NsxTManagerReference *Reference `json:"nsxTManagerReference"` + NetworkPool *Reference `json:"networkPool"` + AutoCreateNetworkPool bool `json:"autoCreateNetworkPool"` +} + +// AddResourcePool is used to add one or more resource pools to a provider VDC +type AddResourcePool struct { + VimObjectRef []*VimObjectRef `xml:"AddItem" json:"addItem"` +} + +// DeleteResourcePool is used to remove one or more resource pools from a provider VDC +type DeleteResourcePool struct { + ResourcePoolRefs []*Reference `xml:"DeleteItem" json:"deleteItem"` +} + +// AddStorageProfiles is used to add storage profiles to an existing provider VDC +type AddStorageProfiles struct { + AddStorageProfile []string `json:"addStorageProfile"` +} + +type EnableStorageProfile struct { + Enabled bool `json:"enabled"` +} + +type RemoveStorageProfile struct { + RemoveStorageProfile []*Reference `json:"removeStorageProfile"` +} + +// VirtualHardwareVersion describes supported hardware by the VMs created on the VDC +type VirtualHardwareVersion struct { + HardDiskAdapter []*HardDiskAdapter `xml:"HardDiskAdapter"` + Link Link `xml:"Link"` + MaxCPUs int `xml:"maxCPUs"` + MaxCoresPerSocket int `xml:"maxCoresPerSocket"` + MaxMemorySizeMb int `xml:"maxMemorySizeMb"` + MaxNICs int `xml:"maxNICs"` + Name string `xml:"name"` + SupportedMemorySizeGb []int `xml:"supportedMemorySizeGb"` + SupportedCoresPerSocket []int `xml:"supportedCoresPerSocket"` + SupportedOperatingSystems *SupportedOperatingSystemsInfoType `xml:"supportedOperatingSystems"` + + SupportsHotAdd *bool `xml:"supportsHotAdd"` + SupportsHotPlugPCI *bool `xml:"supportsHotPlugPCI"` + SupportsNestedHV *bool `xml:"supportsNestedHV"` +} + +// HardDiskAdapter describes a hard disk controller type +type HardDiskAdapter struct { + Id string `xml:"id,attr"` + LegacyId int `xml:"legacyId,attr"` + Name string `xml:"name,attr"` + MaximumDiskSizeGb int `xml:"maximumDiskSizeGb,attr"` + + BusNumberRanges struct { + Begin int `xml:"begin,attr"` + End int `xml:"end,attr"` + } `xml:"BusNumberRanges>Range"` + UnitNumberRanges struct { + Begin int `xml:"begin,attr"` + End int `xml:"end,attr"` + } `xml:"UnitNumberRanges>Range"` + + ReservedBusUnitNumber struct { + BusNumber int `xml:"busNumber,attr"` + UnitNumber int `xml:"unitNumber,attr"` + } `xml:"ReservedBusUnitNumber"` +} + +// SupportedOperatingSystemsInfoType describes what operating system families a hardware version supports +type SupportedOperatingSystemsInfoType struct { + Link *Link + OperatingSystemFamilyInfo []*OperatingSystemFamilyInfoType `xml:"OperatingSystemFamilyInfo"` +} + +// OperatingSystemFamilyInfoType describes operating systems of a given OS family +type OperatingSystemFamilyInfoType struct { + Name string `xml:"Name"` + OperatingSystemFamilyId *int `xml:"OperatingSystemFamilyId"` + OperatingSystems []*OperatingSystemInfoType `xml:"OperatingSystem"` +} + +// OperatingSystemInfoType describes a operating system +type OperatingSystemInfoType struct { + OperatingSystemId *int `xml:"OperatingSystemId,omitempty"` + DefaultHardDiskAdapterType string `xml:"DefaultHardDiskAdapterType"` + SupportedHardDiskAdapter []struct { + Ref string `xml:"ref,attr"` + } `xml:"SupportedHardDiskAdapter,omitempty"` + MinimumHardDiskSizeGigabytes *int `xml:"MinimumHardDiskSizeGigabytes"` + MinimumMemoryMegabytes *int `xml:"MinimumMemoryMegabytes"` + Name string `xml:"Name"` + InternalName string `xml:"InternalName"` + Supported *bool `xml:"Supported"` + SupportLevel string `xml:"SupportLevel"` + X64 *bool `xml:"x64"` + MaximumCpuCount *int `xml:"MaximumCpuCount"` + MaximumCoresPerSocket *int `xml:"MaximumCoresPerSocket"` + MaximumSocketCount *int `xml:"MaximumSocketCount"` + MinimumHardwareVersion *int `xml:"MinimumHardwareVersion"` + PersonalizationEnabled *bool `xml:"PersonalizationEnabled"` + PersonalizationAuto *bool `xml:"PersonalizationAuto"` + SysprepPackagingSupported *bool `xml:"SysprepPackagingSupported"` + SupportsMemHotAdd *bool `xml:"SupportsMemHotAdd"` + CimOsId *int `xml:"cimOsId"` + CimVersion *int `xml:"CimVersion"` + SupportedForCreate *bool `xml:"SupportedForCreate"` + + RecommendedNIC []struct { + Name string `xml:"name,attr"` + Id *int `xml:"id,attr,omitempty"` + } `xml:"RecommendedNIC"` + + SupportedNICType []struct { + Name string `xml:"name,attr"` + Id *int `xml:"id,attr,omitempty"` + } `xml:"SupportedNICType"` + + RecommendedFirmware string `xml:"RecommendedFirmware"` + SupportedFirmware []string `xml:"SupportedFirmware"` + SupportsTPM *bool `xml:"SupportsTPM"` +} diff --git a/types/v56/vm_types.go b/types/v56/vm_types.go index 718d72bd9..55c99b791 100644 --- a/types/v56/vm_types.go +++ b/types/v56/vm_types.go @@ -42,6 +42,8 @@ type Vm struct { // Section ovf:VirtualHardwareSection VirtualHardwareSection *VirtualHardwareSection `xml:"VirtualHardwareSection,omitempty"` + RuntimeInfoSection *RuntimeInfoSection `xml:"RuntimeInfoSection,omitempty"` + // FIXME: Upstream bug? Missing NetworkConnectionSection NetworkConnectionSection *NetworkConnectionSection `xml:"NetworkConnectionSection,omitempty"` @@ -49,8 +51,8 @@ type Vm struct { Snapshots *SnapshotSection `xml:"SnapshotSection,omitempty"` - // TODO: OVF Sections to be implemented - // Environment OVF_Environment `xml:"Environment,omitempty" + // The OVF environment defines how the guest software and the virtualization platform interact. + Environment *OvfEnvironment `xml:"Environment,omitempty"` VmSpecSection *VmSpecSection `xml:"VmSpecSection,omitempty"` @@ -63,6 +65,17 @@ type Vm struct { ProductSection *ProductSection `xml:"ProductSection,omitempty"` ComputePolicy *ComputePolicy `xml:"ComputePolicy,omitempty"` // accessible only from version API 33.0 Media *Reference `xml:"Media,omitempty"` // Reference to the media object to insert in a new VM. + BootOptions *BootOptions `xml:"BootOptions,omitempty"` // Accessible only from API version 37.1+ +} + +type RuntimeInfoSection struct { + Ns10 string `xml:"ns10,attr"` + Type string `xml:"type,attr"` + Href string `xml:"href,attr"` + Info string `xml:"Info"` + VMWareTools struct { + Version string `xml:"version,attr"` + } `xml:"VMWareTools"` } // VmSpecSection from Vm struct @@ -70,6 +83,7 @@ type VmSpecSection struct { Modified *bool `xml:"Modified,attr,omitempty"` Info string `xml:"ovf:Info"` OsType string `xml:"OsType,omitempty"` // The type of the OS. This parameter may be omitted when using the VmSpec to update the contents of an existing VM. + Firmware string `xml:"Firmware,omitempty"` // Available since API 37.1. VM's Firmware, can be either 'bios' or 'efi'. NumCpus *int `xml:"NumCpus,omitempty"` // Number of CPUs. This parameter may be omitted when using the VmSpec to update the contents of an existing VM. NumCoresPerSocket *int `xml:"NumCoresPerSocket,omitempty"` // Number of cores among which to distribute CPUs in this virtual machine. This parameter may be omitted when using the VmSpec to update the contents of an existing VM. CpuResourceMhz *CpuResourceMhz `xml:"CpuResourceMhz,omitempty"` // CPU compute resources. This parameter may be omitted when using the VmSpec to update the contents of an existing VM. @@ -82,11 +96,22 @@ type VmSpecSection struct { TimeSyncWithHost *bool `xml:"TimeSyncWithHost,omitempty"` // Synchronize the VM's time with the host. } +// BootOptions allows to specify boot options of a VM +type BootOptions struct { + BootDelay *int `xml:"BootDelay,omitempty"` // Delay between power-on and boot of the VM + EnterBiosSetup *bool `xml:"EnterBIOSSetup,omitempty"` // Set to false on the next boot + BootRetryEnabled *bool `xml:"BootRetryEnabled,omitempty"` // Available since API 37.1 + BootRetryDelay *int `xml:"BootRetryDelay,omitempty"` // Available since API 37.1. Doesn't have an effect if BootRetryEnabled is set to false + EfiSecureBootEnabled *bool `xml:"EfiSecureBootEnabled,omitempty"` // Available since API 37.1 + NetworkBootProtocol string `xml:"NetworkBootProtocol,omitempty"` // Available since API 37.1 +} + // RecomposeVAppParamsForEmptyVm represents a vApp structure which allows to create VM. type RecomposeVAppParamsForEmptyVm struct { XMLName xml.Name `xml:"RecomposeVAppParams"` XmlnsVcloud string `xml:"xmlns,attr"` XmlnsOvf string `xml:"xmlns:ovf,attr"` + PowerOn bool `xml:"powerOn,attr,omitempty"` // True if the VM should be powered-on after creation. Defaults to false. CreateItem *CreateItem `xml:"CreateItem,omitempty"` AllEULAsAccepted bool `xml:"AllEULAsAccepted,omitempty"` } @@ -100,7 +125,8 @@ type CreateItem struct { VmSpecSection *VmSpecSection `xml:"VmSpecSection,omitempty"` StorageProfile *Reference `xml:"StorageProfile,omitempty"` ComputePolicy *ComputePolicy `xml:"ComputePolicy,omitempty"` // accessible only from version API 33.0 - BootImage *Media `xml:"Media,omitempty"` // boot image as vApp template. Href, Id and name needed. + BootOptions *BootOptions `xml:"BootOptions,omitempty"` + BootImage *Media `xml:"Media,omitempty"` // boot image as vApp template. Href, Id and name needed. } // ComputePolicy represents structure to manage VM compute polices, part of RecomposeVAppParams structure. @@ -144,7 +170,104 @@ type SourcedVmTemplateParams struct { LocalityParams *LocalityParams `xml:"LocalityParams,omitempty"` // Locality parameters provide a hint that may help optimize placement of a VM and an independent a Disk so that the VM can make efficient use of the disk. Source *Reference `xml:"Source"` // A reference to an existing VM template VmCapabilities *VmCapabilities `xml:"VmCapabilities,omitempty"` // Describes the capabilities (hot swap, etc.) the instantiated VM should have. - VmGeneralParams *VMGeneralParams `xml:"VMGeneralParams,omitempty"` // Specify name, description, and other properties of a VM during instantiation. + VmGeneralParams *VMGeneralParams `xml:"VmGeneralParams,omitempty"` // Specify name, description, and other properties of a VM during instantiation. VmTemplateInstantiationParams *InstantiationParams `xml:"VmTemplateInstantiationParams,omitempty"` // Same as InstantiationParams used for VMs within a vApp StorageProfile *Reference `xml:"StorageProfile,omitempty"` // A reference to a storage profile to be used for the VM. The specified storage profile must exist in the organization vDC that contains the composed vApp. If not specified, the default storage profile for the vDC is used. } + +// The OVF environment enables the guest software to access information about the virtualization platform, such as +// the user-specified values for the properties defined in the OVF descriptor. +type OvfEnvironment struct { + XMLName xml.Name `xml:"Environment"` + Ve string `xml:"ve,attr,omitempty"` // Xml namespace + Id string `xml:"id,attr,omitempty"` // Identification of VM from OVF Descriptor. Describes this virtual system. + VCenterId string `xml:"vCenterId,attr,omitempty"` // VM moref in the vCenter + PlatformSection *PlatformSection `xml:"PlatformSection,omitempty"` // Describes the virtualization platform + PropertySection *PropertySection `xml:"PropertySection,omitempty"` // Property elements with key/value pairs + EthernetAdapterSection *EthernetAdapterSection `xml:"EthernetAdapterSection,omitempty"` // Contains adapters info and virtual networks attached +} + +// Provides information from the virtualization platform +type PlatformSection struct { + XMLName xml.Name `xml:"PlatformSection"` + Kind string `xml:"Kind,omitempty"` // Hypervisor kind is typically VMware ESXi + Version string `xml:"Version,omitempty"` // Hypervisor version + Vendor string `xml:"Vendor,omitempty"` // VMware, Inc. + Locale string `xml:"Locale,omitempty"` // Hypervisor locale +} + +// Contains a list of key/value pairs corresponding to properties defined in the OVF descriptor +// Operating system level configuration, such as host names, IP address, subnets, gateways, etc. +// Application-level configuration such as DNS name of active directory server, databases and +// other external services. +type PropertySection struct { + XMLName xml.Name `xml:"PropertySection"` + Properties []*OvfProperty `xml:"Property,omitempty"` +} + +type OvfProperty struct { + Key string `xml:"key,attr"` + Value string `xml:"value,attr"` +} + +// Contains adapters info and virtual networks attached +type EthernetAdapterSection struct { + XMLName xml.Name `xml:"EthernetAdapterSection"` + Adapters []*Adapter `xml:"Adapter,omitempty"` +} + +type Adapter struct { + Mac string `xml:"mac,attr"` + Network string `xml:"network,attr"` + UnitNumber string `xml:"unitNumber,attr"` +} + +// RequestVirtualHardwareSection is used to start a request in VM Extra Configuration set +type RequestVirtualHardwareSection struct { + // Extends OVF Section_Type + XMLName xml.Name `xml:"ovf:VirtualHardwareSection"` + Xmlns string `xml:"xmlns,attr,omitempty"` + Ovf string `xml:"xmlns:ovf,attr"` + Vssd string `xml:"xmlns:vssd,attr"` + Rasd string `xml:"xmlns:rasd,attr"` + Ns2 string `xml:"xmlns:ns2,attr"` + Ns3 string `xml:"xmlns:ns3,attr"` + Ns4 string `xml:"xmlns:ns4,attr"` + Vmw string `xml:"xmlns:vmw,attr"` + + Info string `xml:"ovf:Info"` + HREF string `xml:"href,attr,omitempty"` + Type string `xml:"type,attr,omitempty"` + System []InnerXML `xml:"ovf:System,omitempty"` + Item []InnerXML `xml:"ovf:Item,omitempty"` + + ExtraConfigs []*ExtraConfigMarshal `xml:"vmw:ExtraConfig,omitempty"` +} + +// ResponseVirtualHardwareSection is used to get a response +type ResponseVirtualHardwareSection struct { + // Extends OVF Section_Type + XMLName xml.Name `xml:"VirtualHardwareSection"` + Xmlns string `xml:"vcloud,attr,omitempty"` + Ovf string `xml:"xmlns:ovf,attr"` + Ns4 string `xml:"xmlns:ns4,attr"` + Vssd string `xml:"xmlns:vssd,attr"` + Rasd string `xml:"xmlns:rasd,attr"` + Vmw string `xml:"xmlns:vmw,attr"` + + Info string `xml:"Info"` + HREF string `xml:"href,attr,omitempty"` + Type string `xml:"type,attr,omitempty"` + + System []InnerXML `xml:"System,omitempty"` + Item []InnerXML `xml:"Item,omitempty"` + + ExtraConfigs []*ExtraConfig `xml:"ExtraConfig,omitempty"` +} + +// ExtraConfig describes an Extra Configuration item +type ExtraConfig struct { + Key string `xml:"key,attr"` + Value string `xml:"value,attr"` + Required bool `xml:"required,attr"` +} diff --git a/types/v56/vmmarshal.go b/types/v56/vmmarshal.go new file mode 100644 index 000000000..32cb18399 --- /dev/null +++ b/types/v56/vmmarshal.go @@ -0,0 +1,79 @@ +package types + +import ( + "encoding/xml" +) + +type ExtraConfigVirtualHardwareSectionMarshal struct { + NS10 string `xml:"xmlns:ns10,attr,omitempty"` + + Info string `xml:"ovf:Info"` + Items []*VirtualHardwareItemMarshal `xml:"ovf:Item,omitempty"` + ExtraConfigs []*ExtraConfigMarshal `xml:"vmw:ExtraConfig,omitempty"` +} +type ExtraConfigMarshal struct { + Key string `xml:"vmw:key,attr"` + Value string `xml:"vmw:value,attr"` + Required bool `xml:"ovf:required,attr"` +} + +type VirtualHardwareItemMarshal struct { + XMLName xml.Name `xml:"ovf:Item"` + Type string `xml:"ns10:type,attr,omitempty"` + Href string `xml:"ns10:href,attr,omitempty"` + + Address *NillableElementMarshal `xml:"rasd:Address"` + AddressOnParent *NillableElementMarshal `xml:"rasd:AddressOnParent"` + AllocationUnits *NillableElementMarshal `xml:"rasd:AllocationUnits"` + AutomaticAllocation *NillableElementMarshal `xml:"rasd:AutomaticAllocation"` + AutomaticDeallocation *NillableElementMarshal `xml:"rasd:AutomaticDeallocation"` + ConfigurationName *NillableElementMarshal `xml:"rasd:ConfigurationName"` + Connection []*VirtualHardwareConnectionMarshal `xml:"rasd:Connection,omitempty"` + ConsumerVisibility *NillableElementMarshal `xml:"rasd:ConsumerVisibility"` + Description *NillableElementMarshal `xml:"rasd:Description"` + ElementName *NillableElementMarshal `xml:"rasd:ElementName,omitempty"` + Generation *NillableElementMarshal `xml:"rasd:Generation"` + HostResource []*VirtualHardwareHostResourceMarshal `xml:"rasd:HostResource,omitempty"` + InstanceID int `xml:"rasd:InstanceID"` + Limit *NillableElementMarshal `xml:"rasd:Limit"` + MappingBehavior *NillableElementMarshal `xml:"rasd:MappingBehavior"` + OtherResourceType *NillableElementMarshal `xml:"rasd:OtherResourceType"` + Parent *NillableElementMarshal `xml:"rasd:Parent"` + PoolID *NillableElementMarshal `xml:"rasd:PoolID"` + Reservation *NillableElementMarshal `xml:"rasd:Reservation"` + ResourceSubType *NillableElementMarshal `xml:"rasd:ResourceSubType"` + ResourceType *NillableElementMarshal `xml:"rasd:ResourceType"` + VirtualQuantity *NillableElementMarshal `xml:"rasd:VirtualQuantity"` + VirtualQuantityUnits *NillableElementMarshal `xml:"rasd:VirtualQuantityUnits"` + Weight *NillableElementMarshal `xml:"rasd:Weight"` + + CoresPerSocket *CoresPerSocketMarshal `xml:"vmw:CoresPerSocket,omitempty"` + Link []*Link `xml:"Link,omitempty"` +} + +type NillableElementMarshal struct { + XmlnsXsi string `xml:"xmlns:xsi,attr,omitempty"` + XsiNil string `xml:"xsi:nil,attr,omitempty"` + Value string `xml:",chardata"` +} + +type CoresPerSocketMarshal struct { + OvfRequired string `xml:"ovf:required,attr,omitempty"` + Value string `xml:",chardata"` +} + +type VirtualHardwareConnectionMarshal struct { + IpAddressingMode string `xml:"ns10:ipAddressingMode,attr,omitempty"` + IPAddress string `xml:"ns10:ipAddress,attr,omitempty"` + PrimaryConnection bool `xml:"ns10:primaryNetworkConnection,attr,omitempty"` + Value string `xml:",chardata"` +} + +type VirtualHardwareHostResourceMarshal struct { + StorageProfile string `xml:"ns10:storageProfileHref,attr,omitempty"` + BusType int `xml:"ns10:busType,attr,omitempty"` + BusSubType string `xml:"ns10:busSubType,attr,omitempty"` + Capacity int `xml:"ns10:capacity,attr,omitempty"` + Iops string `xml:"ns10:iops,attr,omitempty"` + OverrideVmDefault string `xml:"ns10:storageProfileOverrideVmDefault,attr,omitempty"` +} diff --git a/util/logging.go b/util/logging.go index e899b5b3e..b49c11e7b 100644 --- a/util/logging.go +++ b/util/logging.go @@ -9,11 +9,12 @@ package util import ( "fmt" - "io/ioutil" + "io" "log" "net/http" "os" "path" + "path/filepath" "regexp" "runtime" "strings" @@ -33,12 +34,14 @@ const ( envLogOnScreen = "GOVCD_LOG_ON_SCREEN" // Name of the environment variable that enables logging of passwords + // #nosec G101 -- This is not a password envLogPasswords = "GOVCD_LOG_PASSWORDS" // Name of the environment variable that enables logging of HTTP requests envLogSkipHttpReq = "GOVCD_LOG_SKIP_HTTP_REQ" // Name of the environment variable that enables logging of HTTP responses + // #nosec G101 -- Not a credential envLogSkipHttpResp = "GOVCD_LOG_SKIP_HTTP_RESP" // Name of the environment variable with a custom list of of responses to skip from logging @@ -49,7 +52,7 @@ const ( ) var ( - // All go-vcloud director logging goes through this logger + // All go-vcloud-director logging goes through this logger Logger *log.Logger // It's true if we're using an user provided logger @@ -113,9 +116,9 @@ func newLogger(logpath string) *log.Logger { var err error var file *os.File if OverwriteLog { - file, err = os.OpenFile(logpath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0640) + file, err = os.OpenFile(filepath.Clean(logpath), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) } else { - file, err = os.OpenFile(logpath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0640) + file, err = os.OpenFile(filepath.Clean(logpath), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0600) } if err != nil { @@ -137,7 +140,7 @@ func SetLog() { return } if !EnableLogging { - Logger = log.New(ioutil.Discard, "", log.Ldate|log.Ltime) + Logger = log.New(io.Discard, "", log.Ldate|log.Ltime) return } @@ -160,34 +163,50 @@ func SetLog() { } } -// hidePasswords hides passwords that may be used in a request -func hidePasswords(in string, onScreen bool) string { +// hideSensitive hides passwords, tokens, and certificate details +func hideSensitive(in string, onScreen bool) string { if !onScreen && LogPasswords { return in } var out string + + // Filters out the below: + // Regular passwords re1 := regexp.MustCompile(`("[^\"]*[Pp]assword"\s*:\s*)"[^\"]+"`) out = re1.ReplaceAllString(in, `${1}"********"`) // Replace password in ADFS SAML request re2 := regexp.MustCompile(`(\s*)(.*)()`) out = re2.ReplaceAllString(out, `${1}******${3}`) - return out -} -// hideTokens hides SAML auth response token -func hideTokens(in string, onScreen bool) string { - if !onScreen && LogPasswords { - return in - } - var out string - // Filters out the below: // Token data between - re1 := regexp.MustCompile(`(.*)(.*)(.*)`) - out = re1.ReplaceAllString(in, `${1}******${3}`) + re3 := regexp.MustCompile(`(.*)(.*)(.*)`) + out = re3.ReplaceAllString(out, `${1}******${3}`) // Token data between - re2 := regexp.MustCompile(`(.*)(.*)(.*)`) - out = re2.ReplaceAllString(out, `${1}******${3}`) + re4 := regexp.MustCompile(`(.*)(.*)(.*)`) + out = re4.ReplaceAllString(out, `${1}******${3}`) + + // Data inside certificates and private keys + re5 := regexp.MustCompile(`(-----BEGIN CERTIFICATE-----)(.*)(-----END CERTIFICATE-----)`) + out = re5.ReplaceAllString(out, `${1}******${3}`) + re6 := regexp.MustCompile(`(-----BEGIN ENCRYPTED PRIVATE KEY-----)(.*)(-----END ENCRYPTED PRIVATE KEY-----)`) + out = re6.ReplaceAllString(out, `${1}******${3}`) + + // Token inside request body + re7 := regexp.MustCompile(`(refresh_token)=(\S+)`) + out = re7.ReplaceAllString(out, `${1}=*******`) + + // Bearer token inside JSON response + re8 := regexp.MustCompile(`("access_token":\s*)"[^"]*`) + out = re8.ReplaceAllString(out, `${1}*******`) + + // Token inside JSON response + re9 := regexp.MustCompile(`("refresh_token":\s*)"[^"]*`) + out = re9.ReplaceAllString(out, `${1}*******`) + + // API Token inside CSE JSON payloads + re10 := regexp.MustCompile(`("apiToken":\s*)"[^"]*`) + out = re10.ReplaceAllString(out, `${1}*******`) return out } @@ -197,6 +216,13 @@ func isBinary(data string, req *http.Request) bool { reContentRange := regexp.MustCompile(`(?i)content-range`) reMultipart := regexp.MustCompile(`(?i)multipart/form`) reMediaXml := regexp.MustCompile(`(?i)media+xml;`) + // Skip data transferred for vApp template or catalog item upload + if strings.Contains(req.URL.String(), "/transfer/") && + (strings.HasSuffix(req.URL.String(), ".vmdk") || strings.HasSuffix(req.URL.String(), "/file")) && + (req.Method == http.MethodPut || req.Method == http.MethodPost) { + return true + } + uiPlugin := regexp.MustCompile(`manifest\.json|bundle\.js`) for key, value := range req.Header { if reContentRange.MatchString(key) { return true @@ -210,7 +236,7 @@ func isBinary(data string, req *http.Request) bool { } } } - return false + return uiPlugin.MatchString(data) } // SanitizedHeader returns a http.Header with sensitive fields masked @@ -296,12 +322,13 @@ func ProcessRequestOutput(caller, operation, url, payload string, req *http.Requ if isBinary(payload, req) { payload = "[binary data]" } - if dataSize > 0 { - Logger.Printf("Request data: [%d]\n%s\n", dataSize, hidePasswords(payload, false)) - } + // Request header should be shown before Request data Logger.Printf("Req header:\n") logSanitizedHeader(req.Header) + if dataSize > 0 { + Logger.Printf("Request data: [%d]\n%s\n", dataSize, hideSensitive(payload, false)) + } } // Logs the essentials of a HTTP response @@ -345,9 +372,11 @@ func ProcessResponseOutput(caller string, resp *http.Response, result string) { dataSize := len(result) outTextSize := len(outText) if outTextSize != dataSize { - Logger.Printf("Response text: [%d -> %d]\n%s\n", dataSize, outTextSize, hideTokens(outText, false)) + Logger.Printf("Response text: [%d -> %d]\n%s\n", dataSize, outTextSize, hideSensitive(outText, false)) + } else if dataSize == 0 { + Logger.Printf("Response text: [%d]\n", dataSize) } else { - Logger.Printf("Response text: [%d]\n%s\n", dataSize, hideTokens(outText, false)) + Logger.Printf("Response text: [%d]\n%s\n", dataSize, hideSensitive(outText, false)) } } diff --git a/util/tar.go b/util/tar.go index 0234d7b8f..6f20fa74e 100644 --- a/util/tar.go +++ b/util/tar.go @@ -8,7 +8,6 @@ import ( "archive/tar" "errors" "io" - "io/ioutil" "net/http" "os" "path/filepath" @@ -17,22 +16,26 @@ import ( const TmpDirPrefix = "govcd" -// Extract files to system tmp dir with name govcd+random number. Created folder with files isn't deleted. +// Unpack extracts files to system tmp dir with name govcd+random number. Created folder with files isn't deleted. // Returns extracted files paths in array and path where folder with files created. func Unpack(tarFile string) ([]string, string, error) { var filePaths []string var dst string - reader, err := os.Open(tarFile) + reader, err := os.Open(filepath.Clean(tarFile)) if err != nil { return filePaths, dst, err } - defer reader.Close() + defer func() { + if err := reader.Close(); err != nil { + Logger.Printf("Error closing file: %s\n", err) + } + }() tarReader := tar.NewReader(reader) - dst, err = ioutil.TempDir("", TmpDirPrefix) + dst, err = os.MkdirTemp("", TmpDirPrefix) if err != nil { return filePaths, dst, err } @@ -70,7 +73,7 @@ func Unpack(tarFile string) ([]string, string, error) { // if its a dir and it doesn't exist create it case tar.TypeDir: if _, err := os.Stat(target); err != nil { - if err := os.MkdirAll(target, 0755); err != nil { + if err := os.MkdirAll(target, 0750); err != nil { return filePaths, dst, err } } @@ -87,26 +90,38 @@ func Unpack(tarFile string) ([]string, string, error) { // if it's a newFile create it case tar.TypeReg: - newFile, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) + newFile, err := os.OpenFile(filepath.Clean(target), os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) if err != nil { return filePaths, dst, err } // copy over contents - if _, err := io.Copy(newFile, tarReader); err != nil { - return filePaths, dst, err + for { + _, err := io.CopyN(newFile, tarReader, 1024) + if err != nil { + if errors.Is(err, io.EOF) { + break + } + return filePaths, dst, err + } } filePaths = append(filePaths, newFile.Name()) if err := isExtractedFileValid(newFile, expectedFileSize); err != nil { - newFile.Close() + errClose := newFile.Close() + if errClose != nil { + Logger.Printf("[DEBUG - Unpack] error closing newFile: %s", errClose) + } return filePaths, dst, err } - // manually close here after each newFile operation; defering would cause each newFile close + // manually close here after each newFile operation; deferring would cause each newFile close // to wait until all operations have completed. - newFile.Close() + errClose := newFile.Close() + if errClose != nil { + Logger.Printf("[DEBUG - Unpack] error closing newFile: %s", errClose) + } } } } @@ -133,11 +148,15 @@ func sanitizedName(filename string) string { // GetFileContentType returns the real file type func GetFileContentType(file string) (string, error) { // Open File - f, err := os.Open(file) + f, err := os.Open(filepath.Clean(file)) if err != nil { return "", err } - defer f.Close() + defer func() { + if err := f.Close(); err != nil { + Logger.Printf("Error closing file: %s\n", err) + } + }() // Only the first 512 bytes are used to sniff the content type. buffer := make([]byte, 512)