diff --git a/nsxt/data_source_nsxt_policy_transport_zone.go b/nsxt/data_source_nsxt_policy_transport_zone.go index 00758242e..3b8306733 100644 --- a/nsxt/data_source_nsxt_policy_transport_zone.go +++ b/nsxt/data_source_nsxt_policy_transport_zone.go @@ -19,6 +19,7 @@ var policyTransportZoneTransportTypes = [](string){ lm_model.PolicyTransportZone_TZ_TYPE_OVERLAY_STANDARD, lm_model.PolicyTransportZone_TZ_TYPE_OVERLAY_ENS, lm_model.PolicyTransportZone_TZ_TYPE_VLAN_BACKED, + lm_model.PolicyTransportZone_TZ_TYPE_OVERLAY_BACKED, lm_model.PolicyTransportZone_TZ_TYPE_UNKNOWN, } diff --git a/nsxt/provider.go b/nsxt/provider.go index 7ebdd6672..b4689284b 100644 --- a/nsxt/provider.go +++ b/nsxt/provider.go @@ -407,6 +407,7 @@ func Provider() *schema.Provider { "nsxt_policy_spoof_guard_profile": resourceNsxtPolicySpoofGuardProfile(), "nsxt_policy_gateway_qos_profile": resourceNsxtPolicyGatewayQosProfile(), "nsxt_policy_project": resourceNsxtPolicyProject(), + "nsxt_policy_transport_zone": resourceNsxtPolicyTransportZone(), "nsxt_edge_cluster": resourceNsxtEdgeCluster(), "nsxt_compute_manager": resourceNsxtComputeManager(), "nsxt_manager_cluster": resourceNsxtManagerCluster(), diff --git a/nsxt/resource_nsxt_policy_transport_zone.go b/nsxt/resource_nsxt_policy_transport_zone.go new file mode 100644 index 000000000..70e4b6b33 --- /dev/null +++ b/nsxt/resource_nsxt_policy_transport_zone.go @@ -0,0 +1,285 @@ +/* Copyright © 2023 VMware, Inc. All Rights Reserved. + SPDX-License-Identifier: MPL-2.0 */ + +package nsxt + +import ( + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/vmware/vsphere-automation-sdk-go/runtime/protocol/client" + "github.com/vmware/vsphere-automation-sdk-go/services/nsxt/infra" + "github.com/vmware/vsphere-automation-sdk-go/services/nsxt/infra/sites/enforcement_points" + "github.com/vmware/vsphere-automation-sdk-go/services/nsxt/model" +) + +var defaultInfraSitePath = "/infra/sites/default" + +func resourceNsxtPolicyTransportZone() *schema.Resource { + return &schema.Resource{ + Create: resourceNsxtPolicyTransportZoneCreate, + Read: resourceNsxtPolicyTransportZoneRead, + Update: resourceNsxtPolicyTransportZoneUpdate, + Delete: resourceNsxtPolicyTransportZoneDelete, + Importer: &schema.ResourceImporter{ + State: resourceNsxtPolicyTransportZoneImporter, + }, + + Schema: map[string]*schema.Schema{ + "nsx_id": getNsxIDSchema(), + "display_name": getDataSourceDisplayNameSchema(), + "description": getDataSourceDescriptionSchema(), + "path": getPathSchema(), + "revision": getRevisionSchema(), + "tag": getTagsSchema(), + "is_default": { + Type: schema.TypeBool, + Description: "Indicates whether the transport zone is default", + Optional: true, + Default: false, + }, + "transport_type": { + Type: schema.TypeString, + Description: "Type of Transport Zone", + Required: true, + ValidateFunc: validation.StringInSlice(policyTransportZoneTransportTypes, false), + ForceNew: true, + }, + "uplink_teaming_policy_names": { + Type: schema.TypeList, + Description: "Names of the switching uplink teaming policies that are supported by this transport zone.", + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "site_path": { + Type: schema.TypeString, + Description: "Path to the site this Transport Zone belongs to", + Optional: true, + ForceNew: true, + Default: defaultInfraSitePath, + ValidateFunc: validatePolicyPath(), + }, + "enforcement_point": { + Type: schema.TypeString, + Description: "ID of the enforcement point this Transport Zone belongs to", + Optional: true, + ForceNew: true, + Computed: true, + }, + }, + } +} + +func resourceNsxtPolicyTransportZoneExists(siteID, epID, tzID string, connector client.Connector) (bool, error) { + var err error + + // Check site existence first + siteClient := infra.NewSitesClient(connector) + _, err = siteClient.Get(siteID) + if err != nil { + msg := fmt.Sprintf("Failed to read site %s", siteID) + return false, logAPIError(msg, err) + } + + // Check (ep, tz) existence. In case of ep not found, NSX returns BAD_REQUEST + tzClient := enforcement_points.NewTransportZonesClient(connector) + _, err = tzClient.Get(siteID, epID, tzID) + if err == nil { + return true, nil + } + + if isNotFoundError(err) { + return false, nil + } + + return false, logAPIError("Error retrieving resource", err) +} + +func getSitePathFromChildResourcePath(childPath string) (string, error) { + startIndex := strings.Index(childPath, "enforcement-points") + if startIndex <= 0 { + return "", fmt.Errorf("failed to find site path from path %s", childPath) + } + sitePath := childPath[:startIndex-1] + if !isPolicyPath(sitePath) { + return "", fmt.Errorf("site path %s is invalid", sitePath) + } + return sitePath, nil +} + +func policyTransportZonePatch(siteID, epID, tzID string, d *schema.ResourceData, m interface{}) error { + connector := getPolicyConnector(m) + + displayName := d.Get("display_name").(string) + description := d.Get("description").(string) + tags := getPolicyTagsFromSchema(d) + isDefault := d.Get("is_default").(bool) + transportType := d.Get("transport_type").(string) + uplinkTeamingNames := getStringListFromSchemaList(d, "uplink_teaming_policy_names") + + if len(uplinkTeamingNames) > 0 && transportType != model.PolicyTransportZone_TZ_TYPE_VLAN_BACKED { + // uplink_teaming_policy_names only valid for VLAN_BACKED TZ + return fmt.Errorf("cannot use uplink_teaming_policy_names with transport_type %s", transportType) + } + + obj := model.PolicyTransportZone{ + DisplayName: &displayName, + Description: &description, + Tags: tags, + IsDefault: &isDefault, + TzType: &transportType, + UplinkTeamingPolicyNames: uplinkTeamingNames, + } + + // Create the resource using PATCH + tzClient := enforcement_points.NewTransportZonesClient(connector) + _, err := tzClient.Patch(siteID, epID, tzID, obj) + return err +} + +func policyTransportZoneIDTuple(d *schema.ResourceData, m interface{}) (id, siteID, epID string, err error) { + id = d.Id() + if id == "" { + err = fmt.Errorf("error obtaining TransportZone ID") + return + } + sitePath := d.Get("site_path").(string) + siteID = getResourceIDFromResourcePath(sitePath, "sites") + if siteID == "" { + err = fmt.Errorf("error obtaining Site ID from site path %s", sitePath) + return + } + epID = d.Get("enforcement_point").(string) + if epID == "" { + epID = getPolicyEnforcementPoint(m) + } + return +} + +func resourceNsxtPolicyTransportZoneCreate(d *schema.ResourceData, m interface{}) error { + connector := getPolicyConnector(m) + + id := d.Get("nsx_id").(string) + if id == "" { + id = newUUID() + } + sitePath := d.Get("site_path").(string) + siteID := getResourceIDFromResourcePath(sitePath, "sites") + if siteID == "" { + return fmt.Errorf("error obtaining Site ID from site path %s", sitePath) + } + epID := d.Get("enforcement_point").(string) + if epID == "" { + epID = getPolicyEnforcementPoint(m) + } + exists, err := resourceNsxtPolicyTransportZoneExists(siteID, epID, id, connector) + if err != nil { + return err + } + if exists { + return fmt.Errorf("resource with ID %s already exists", id) + } + + // Create the resource using PATCH + log.Printf("[INFO] Creating TransportZone with ID %s under site %s enforcement point %s", id, siteID, epID) + err = policyTransportZonePatch(siteID, epID, id, d, m) + if err != nil { + return handleCreateError("TransportZone", id, err) + } + + d.SetId(id) + d.Set("nsx_id", id) + + return resourceNsxtPolicyTransportZoneRead(d, m) +} + +func resourceNsxtPolicyTransportZoneRead(d *schema.ResourceData, m interface{}) error { + connector := getPolicyConnector(m) + tzClient := enforcement_points.NewTransportZonesClient(connector) + + id, siteID, epID, err := policyTransportZoneIDTuple(d, m) + if err != nil { + return err + } + + obj, err := tzClient.Get(siteID, epID, id) + if err != nil { + return handleReadError(d, "TransportZone", id, err) + } + sitePath, err := getSitePathFromChildResourcePath(*obj.ParentPath) + if err != nil { + return handleReadError(d, "TransportZone", id, err) + } + + d.Set("site_path", sitePath) + d.Set("enforcement_point", epID) + d.Set("display_name", obj.DisplayName) + d.Set("description", obj.Description) + setPolicyTagsInSchema(d, obj.Tags) + d.Set("nsx_id", id) + d.Set("path", obj.Path) + d.Set("revision", obj.Revision) + d.Set("is_default", obj.IsDefault) + d.Set("transport_type", obj.TzType) + d.Set("uplink_teaming_policy_names", obj.UplinkTeamingPolicyNames) + + return nil +} + +func resourceNsxtPolicyTransportZoneUpdate(d *schema.ResourceData, m interface{}) error { + id, siteID, epID, err := policyTransportZoneIDTuple(d, m) + if err != nil { + return err + } + + log.Printf("[INFO] Updateing TransportZone with ID %s", id) + err = policyTransportZonePatch(siteID, epID, id, d, m) + if err != nil { + return handleUpdateError("TransportZone", id, err) + } + + return resourceNsxtPolicyTransportZoneRead(d, m) +} + +func resourceNsxtPolicyTransportZoneDelete(d *schema.ResourceData, m interface{}) error { + connector := getPolicyConnector(m) + tzClient := enforcement_points.NewTransportZonesClient(connector) + + id, siteID, epID, err := policyTransportZoneIDTuple(d, m) + if err != nil { + return err + } + + log.Printf("[INFO] Deleting TransportZone with ID %s", id) + err = tzClient.Delete(siteID, epID, id) + if err != nil { + return handleDeleteError("TransportZone", id, err) + } + + return nil +} + +func resourceNsxtPolicyTransportZoneImporter(d *schema.ResourceData, m interface{}) ([]*schema.ResourceData, error) { + importID := d.Id() + rd, err := nsxtPolicyPathResourceImporterHelper(d, m) + if err != nil { + return rd, err + } + + epID, err := getParameterFromPolicyPath("/enforcement-points/", "/transport-zones/", importID) + if err != nil { + return nil, err + } + d.Set("enforcement_point", epID) + sitePath, err := getSitePathFromChildResourcePath(importID) + if err != nil { + return rd, err + } + d.Set("site_path", sitePath) + return rd, nil +} diff --git a/nsxt/resource_nsxt_policy_transport_zone_test.go b/nsxt/resource_nsxt_policy_transport_zone_test.go new file mode 100644 index 000000000..5df72a570 --- /dev/null +++ b/nsxt/resource_nsxt_policy_transport_zone_test.go @@ -0,0 +1,198 @@ +/* Copyright © 2023 VMware, Inc. All Rights Reserved. + SPDX-License-Identifier: MPL-2.0 */ + +package nsxt + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + + lm_model "github.com/vmware/vsphere-automation-sdk-go/services/nsxt/model" +) + +var accTestPolicyTransportZoneCreateAttributes = map[string]string{ + "display_name": getAccTestResourceName(), + "description": "terraform created", + "transport_type": lm_model.PolicyTransportZone_TZ_TYPE_VLAN_BACKED, +} + +var accTestPolicyTransportZoneUpdateAttributes = map[string]string{ + "display_name": getAccTestResourceName(), + "description": "terraform updated", + "transport_type": lm_model.PolicyTransportZone_TZ_TYPE_OVERLAY_BACKED, +} + +func TestAccResourceNsxtPolicyTransportZone_basic(t *testing.T) { + testResourceName := "nsxt_policy_transport_zone.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccOnlyLocalManager(t) + + }, + Providers: testAccProviders, + CheckDestroy: func(state *terraform.State) error { + return testAccNsxtPolicyTransportZoneCheckDestroy(state, accTestPolicyTransportZoneUpdateAttributes["display_name"]) + }, + Steps: []resource.TestStep{ + { + Config: testAccNsxtPolicyTransportZoneCreate(), + Check: resource.ComposeTestCheckFunc( + testAccNsxtPolicyTransportZoneExists(accTestPolicyTransportZoneCreateAttributes["display_name"], testResourceName), + resource.TestCheckResourceAttr(testResourceName, "display_name", accTestPolicyTransportZoneCreateAttributes["display_name"]), + resource.TestCheckResourceAttr(testResourceName, "description", accTestPolicyTransportZoneCreateAttributes["description"]), + resource.TestCheckResourceAttr(testResourceName, "transport_type", accTestPolicyTransportZoneCreateAttributes["transport_type"]), + + resource.TestCheckResourceAttrSet(testResourceName, "nsx_id"), + resource.TestCheckResourceAttrSet(testResourceName, "path"), + resource.TestCheckResourceAttrSet(testResourceName, "revision"), + resource.TestCheckResourceAttrSet(testResourceName, "is_default"), + resource.TestCheckResourceAttrSet(testResourceName, "enforcement_point"), + resource.TestCheckResourceAttrSet(testResourceName, "site_path"), + resource.TestCheckResourceAttr(testResourceName, "tag.#", "1"), + ), + }, + { + Config: testAccNsxtPolicyTransportZoneUpdate(), + Check: resource.ComposeTestCheckFunc( + testAccNsxtPolicyTransportZoneExists(accTestPolicyTransportZoneUpdateAttributes["display_name"], testResourceName), + resource.TestCheckResourceAttr(testResourceName, "display_name", accTestPolicyTransportZoneUpdateAttributes["display_name"]), + resource.TestCheckResourceAttr(testResourceName, "description", accTestPolicyTransportZoneUpdateAttributes["description"]), + resource.TestCheckResourceAttr(testResourceName, "transport_type", accTestPolicyTransportZoneUpdateAttributes["transport_type"]), + + resource.TestCheckResourceAttrSet(testResourceName, "nsx_id"), + resource.TestCheckResourceAttrSet(testResourceName, "path"), + resource.TestCheckResourceAttrSet(testResourceName, "revision"), + resource.TestCheckResourceAttrSet(testResourceName, "is_default"), + resource.TestCheckResourceAttrSet(testResourceName, "enforcement_point"), + resource.TestCheckResourceAttrSet(testResourceName, "site_path"), + resource.TestCheckResourceAttr(testResourceName, "tag.#", "0"), + ), + }, + }, + }) +} + +func TestAccResourceNsxtPolicyTransportZone_import_basic(t *testing.T) { + testResourceName := "nsxt_policy_transport_zone.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccOnlyLocalManager(t) + }, + Providers: testAccProviders, + CheckDestroy: func(state *terraform.State) error { + return testAccNsxtPolicyTransportZoneCheckDestroy(state, accTestPolicyTransportZoneCreateAttributes["display_name"]) + }, + Steps: []resource.TestStep{ + { + Config: testAccNsxtPolicyTransportZoneCreate(), + }, + { + ResourceName: testResourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: testAccNsxtPolicyTransportZoneImporterGetID, + }, + }, + }) +} + +func testAccNsxtPolicyTransportZoneImporterGetID(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources["nsxt_policy_transport_zone.test"] + if !ok { + return "", fmt.Errorf("PolicyTransportZone resource %s not found in resources", "nsxt_policy_transport_zone.test") + } + resourceID := rs.Primary.ID + if resourceID == "" { + return "", fmt.Errorf("PolicyTransportZone resource ID not set in resources ") + } + path := rs.Primary.Attributes["path"] + if path == "" { + return "", fmt.Errorf("PolicyTransportZone path not set in resources ") + } + return path, nil +} + +func testAccNsxtPolicyTransportZoneExists(displayName string, resourceName string) resource.TestCheckFunc { + return func(state *terraform.State) error { + + connector := getPolicyConnector(testAccProvider.Meta().(nsxtClients)) + + rs, ok := state.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("PolicyTransportZone resource %s not found in resources", resourceName) + } + + tzID := rs.Primary.Attributes["id"] + if tzID == "" { + return fmt.Errorf("PolicyTransportZone resource ID not set in resources") + } + epID := rs.Primary.Attributes["enforcement_point"] + sitePath := rs.Primary.Attributes["site_path"] + siteID := getPolicyIDFromPath(sitePath) + exists, err := resourceNsxtPolicyTransportZoneExists(siteID, epID, tzID, connector) + if err != nil { + return err + } + if !exists { + return fmt.Errorf("PolicyTransportZone %s does not exist", tzID) + } + + return nil + } +} + +func testAccNsxtPolicyTransportZoneCheckDestroy(state *terraform.State, displayName string) error { + connector := getPolicyConnector(testAccProvider.Meta().(nsxtClients)) + for _, rs := range state.RootModule().Resources { + + if rs.Type != "nsxt_policy_transport_zone" { + continue + } + + tzID := rs.Primary.Attributes["id"] + epID := rs.Primary.Attributes["enforcement_point"] + sitePath := rs.Primary.Attributes["site_path"] + siteID := getPolicyIDFromPath(sitePath) + exists, err := resourceNsxtPolicyTransportZoneExists(siteID, epID, tzID, connector) + if err != nil { + return err + } + + if exists { + return fmt.Errorf("PolicyTransportZone %s still exists", displayName) + } + } + return nil +} + +func testAccNsxtPolicyTransportZoneCreate() string { + attrMap := accTestPolicyTransportZoneCreateAttributes + return fmt.Sprintf(` +resource "nsxt_policy_transport_zone" "test" { + display_name = "%s" + description = "%s" + transport_type = "%s" + + tag { + scope = "scope1" + tag = "tag1" + } +}`, attrMap["display_name"], attrMap["description"], attrMap["transport_type"]) +} + +func testAccNsxtPolicyTransportZoneUpdate() string { + attrMap := accTestPolicyTransportZoneUpdateAttributes + return fmt.Sprintf(` +resource "nsxt_policy_transport_zone" "test" { + display_name = "%s" + description = "%s" + transport_type = "%s" +}`, attrMap["display_name"], attrMap["description"], attrMap["transport_type"]) +} diff --git a/website/docs/d/policy_transport_zone.html.markdown b/website/docs/d/policy_transport_zone.html.markdown index a5dd422d2..4ab7a6f54 100644 --- a/website/docs/d/policy_transport_zone.html.markdown +++ b/website/docs/d/policy_transport_zone.html.markdown @@ -42,8 +42,8 @@ data "nsxt_policy_transport_zone" "overlay_transport_zone" { * `id` - (Optional) The ID of Transport Zone to retrieve. * `display_name` - (Optional) The Display Name prefix of the Transport Zone to retrieve. -* `transport_type` - (Optional) Transport type of requested Transport Zone, one of `OVERLAY_STANDARD`, `OVERLAY_ENS`, `VLAN_BACKED` and `UNKNOWN`. -* `is_default` - (Optional) May be set together with `transport_type` in order to retrieve default Transport Zone for for this transport type. +* `transport_type` - (Optional) Transport type of requested Transport Zone, one of `OVERLAY_STANDARD`, `OVERLAY_ENS`, `OVERLAY_BACKED`, `VLAN_BACKED` and `UNKNOWN`. +* `is_default` - (Optional) May be set together with `transport_type` in order to retrieve default Transport Zone for this transport type. * `site_path` - (Optional) The path of the site which the Transport Zone belongs to, this configuration is required for global manager only. `path` field of the existing `nsxt_policy_site` can be used here. ## Attributes Reference diff --git a/website/docs/r/policy_transport_zone.html.markdown b/website/docs/r/policy_transport_zone.html.markdown new file mode 100644 index 000000000..2a05d9a5d --- /dev/null +++ b/website/docs/r/policy_transport_zone.html.markdown @@ -0,0 +1,67 @@ +--- +subcategory: "Fabric" +layout: "nsxt" +page_title: "NSXT: nsxt_policy_transport_zone" +description: A resource to configure Policy Transport Zone. +--- + +# nsxt_policy_transport_zone + +This resource provides a method for the management of Policy based Transport Zones (TZ). A Transport Zone defines the scope to which a network can extend in NSX. For example an overlay based Transport Zone is associated with both hypervisors and segments and defines which hypervisors will be able to serve the defined segment. Virtual machines on the hypervisor associated with a Transport Zone can be attached to segments in that same Transport Zone. + +This data source is applicable to NSX Policy Manager. + +## Example Usage + +```hcl +resource "nsxt_policy_transport_zone" "overlay_transport_zone" { + display_name = "1-transportzone-87" + transport_type = "OVERLAY_BACKED" +} +``` + +```hcl +resource "nsxt_policy_transport_zone" "vlan_transport_zone" { + display_name = "1-transportzone-87" + description = "VLAN transport zone" + transport_type = "VLAN_BACKED" + is_default = true + uplink_teaming_policy_names = ["teaming-1"] + site_path = "/infra/sites/default" + enforcement_point = "default" + + tag { + scope = "app" + tag = "web" + } +} +``` + +## Argument Reference + +* `display_name` - (Optional) The Display Name of the Transport Zone. +* `description` - (Optional) Description of the Transport Zone. +* `tag` - (Optional) A list of scope + tag pairs to associate with this resource. +* `nsx_id` - (Optional) The NSX ID of this resource. If set, this ID will be used to create the policy resource. +* `transport_type` - (Required) Transport type of requested Transport Zone, one of `OVERLAY_STANDARD`, `OVERLAY_ENS`, `OVERLAY_BACKED`, `VLAN_BACKED` and `UNKNOWN`. +* `is_default` - (Optional) Set this Transport Zone as the default zone of given `transport_type`. Default value is `false`. When setting a Transport Zone with `is_default`: `true`, no existing Transport Zone of same `transport_type` should be set as default. +* `uplink_teaming_policy_names` - (Optional) The names of switching uplink teaming policies that all transport nodes in this transport zone support. Uplinkin teaming policies are only valid for `VLAN_BACKED` transport zones. +* `site_path` - (Optional) The path of the site which the Transport Zone belongs to. `path` field of the existing `nsxt_policy_site` can be used here. +* `enforcement_point` - (Optional) The ID of enforcement point under given `site_path` to manage the Transport Zone. + +## Attributes Reference + +In addition to arguments listed above, the following attributes are exported: + +* `revision` - Indicates current revision number of the object as seen by NSX-T API server. This attribute can be useful for debugging. +* `path` - The NSX path of the policy resource. + +## Importing + +An existing Transport Zone can be [imported][docs-import] into this resource, via the following command: + +[docs-import]: https://www.terraform.io/cli/import +``` +terraform import nsxt_policy_transport_zone.overlay-tz POLICY_PATH +``` +The above command imports the Transport Zone named `overlay-tz` with the policy path `POLICY_PATH`.