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/govcd/api_vcd_test.go b/govcd/api_vcd_test.go index 80cf2e1fc..a8fffd2f1 100644 --- a/govcd/api_vcd_test.go +++ b/govcd/api_vcd_test.go @@ -179,19 +179,19 @@ type TestConfig struct { VimServer string `yaml:"vimServer,omitempty"` LdapServer string `yaml:"ldapServer,omitempty"` Nsxt struct { - Manager string `yaml:"manager"` - Tier0router string `yaml:"tier0router"` - Tier0routerVrf string `yaml:"tier0routerVrf"` - NsxtDvpg string `yaml:"nsxtDvpg"` - GatewayQosProfile string `yaml:"gatewayQosProfile"` - Vdc string `yaml:"vdc"` - ExternalNetwork string `yaml:"externalNetwork"` - EdgeGateway string `yaml:"edgeGateway"` - NsxtImportSegment string `yaml:"nsxtImportSegment"` - VdcGroup string `yaml:"vdcGroup"` - VdcGroupEdgeGateway string `yaml:"vdcGroupEdgeGateway"` - NsxtEdgeCluster string `yaml:"nsxtEdgeCluster"` - + 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"` + VdcGroup string `yaml:"vdcGroup"` + VdcGroupEdgeGateway string `yaml:"vdcGroupEdgeGateway"` + NsxtEdgeCluster string `yaml:"nsxtEdgeCluster"` + RoutedNetwork string `yaml:"routedNetwork"` NsxtAlbControllerUrl string `yaml:"nsxtAlbControllerUrl"` NsxtAlbControllerUser string `yaml:"nsxtAlbControllerUser"` NsxtAlbControllerPassword string `yaml:"nsxtAlbControllerPassword"` 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..63fe1f90e --- /dev/null +++ b/govcd/nsxt_edgegateway_static_route_test.go @@ -0,0 +1,107 @@ +//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) + + 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/openapi_endpoints.go b/govcd/openapi_endpoints.go index 2009d66b6..8f83e106f 100644 --- a/govcd/openapi_endpoints.go +++ b/govcd/openapi_endpoints.go @@ -38,6 +38,7 @@ var endpointMinApiVersions = map[string]string{ 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.OpenApiEndpointEdgeGatewayStaticRoutes: "37.0", // VCD 10.4.0+ (NSX-T only) types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGateways: "34.0", types.OpenApiPathVersion1_0_0 + types.OpenApiEndpointEdgeGatewayUsedIpAddresses: "34.0", diff --git a/types/v56/constants.go b/types/v56/constants.go index 60aab7d2c..5a2cecc74 100644 --- a/types/v56/constants.go +++ b/types/v56/constants.go @@ -380,6 +380,7 @@ const ( OpenApiEndpointEdgeGateways = "edgeGateways/" OpenApiEndpointEdgeGatewayQos = "edgeGateways/%s/qos" OpenApiEndpointEdgeGatewayDhcpForwarder = "edgeGateways/%s/dhcpForwarder" + OpenApiEndpointEdgeGatewayStaticRoutes = "edgeGateways/%s/routing/staticRoutes/" OpenApiEndpointEdgeGatewayUsedIpAddresses = "edgeGateways/%s/usedIpAddresses" OpenApiEndpointNsxtFirewallRules = "edgeGateways/%s/firewall/rules" OpenApiEndpointFirewallGroups = "firewallGroups/" diff --git a/types/v56/nsxt_types.go b/types/v56/nsxt_types.go index bfb3ff66f..399022398 100644 --- a/types/v56/nsxt_types.go +++ b/types/v56/nsxt_types.go @@ -1708,3 +1708,53 @@ type VcenterImportableDvpg struct { VirtualCenter *OpenApiReference `json:"virtualCenter"` Vlan string `json:"vlan"` } + +// 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"` +}