diff --git a/nsxt/provider.go b/nsxt/provider.go index 0937993ee..2c5a0a389 100644 --- a/nsxt/provider.go +++ b/nsxt/provider.go @@ -483,6 +483,7 @@ func Provider() *schema.Provider { "nsxt_policy_site": resourceNsxtPolicySite(), "nsxt_policy_global_manager": resourceNsxtPolicyGlobalManager(), "nsxt_policy_metadata_proxy": resourceNsxtPolicyMetadataProxy(), + "nsxt_policy_tier0_inter_vrf_routing": resourceNsxtPolicyTier0InterVRFRouting(), }, ConfigureFunc: providerConfigure, diff --git a/nsxt/resource_nsxt_policy_tier0_inter_vrf_routing.go b/nsxt/resource_nsxt_policy_tier0_inter_vrf_routing.go new file mode 100644 index 000000000..0ee86765d --- /dev/null +++ b/nsxt/resource_nsxt_policy_tier0_inter_vrf_routing.go @@ -0,0 +1,391 @@ +/* Copyright © 2024 Broadcom, Inc. All Rights Reserved. + SPDX-License-Identifier: MPL-2.0 */ + +package nsxt + +import ( + "fmt" + + "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/services/nsxt/infra/tier_0s" + "github.com/vmware/vsphere-automation-sdk-go/services/nsxt/model" +) + +var routeAdvertisementTypesValues = []string{ + "TIER0_STATIC", + "TIER0_CONNECTED", + "TIER0_NAT", + "TIER0_DNS_FORWARDER_IP", + "TIER0_IPSEC_LOCAL_ENDPOINT", + "TIER1_STATIC", + "TIER1_CONNECTED", + "TIER1_LB_SNAT", + "TIER1_LB_VIP", + "TIER1_NAT", + "TIER1_DNS_FORWARDER_IP", + "TIER1_IPSEC_LOCAL_ENDPOINT", +} + +func resourceNsxtPolicyTier0InterVRFRouting() *schema.Resource { + return &schema.Resource{ + Create: resourceNsxtPolicyTier0InterVRFRoutingCreate, + Read: resourceNsxtPolicyTier0InterVRFRoutingRead, + Update: resourceNsxtPolicyTier0InterVRFRoutingUpdate, + Delete: resourceNsxtPolicyTier0InterVRFRoutingDelete, + Importer: &schema.ResourceImporter{ + State: resourceNsxtPolicyTier0InterVRFRoutingImport, + }, + Schema: map[string]*schema.Schema{ + "nsx_id": getNsxIDSchema(), + "path": getPathSchema(), + "display_name": getDisplayNameSchema(), + "description": getDescriptionSchema(), + "revision": getRevisionSchema(), + "tag": getTagsSchema(), + "gateway_path": getPolicyPathSchema(true, true, "Policy path for the Gateway"), + "bgp_route_leaking": { + Type: schema.TypeList, + Description: "Import / export BGP routes", + Optional: true, + MaxItems: 2, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "address_family": { + Type: schema.TypeString, + Description: "Address family type", + Optional: true, + ValidateFunc: validation.StringInSlice([]string{"IPV4", "IPV6"}, false), + }, + "in_filter": { + Type: schema.TypeList, + Description: "route map path for IN direction", + Optional: true, + MaxItems: 1, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "out_filter": { + Type: schema.TypeList, + Description: "route map path for OUT direction", + Optional: true, + MaxItems: 1, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + }, + "static_route_advertisement": { + Type: schema.TypeList, + Description: "Advertise subnet to target peers as static routes", + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "advertisement_rule": { + Type: schema.TypeList, + Description: "Route advertisement rules", + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "action": { + Type: schema.TypeString, + Description: "Action to advertise routes", + Optional: true, + ValidateFunc: validation.StringInSlice([]string{"PERMIT", "DENY"}, false), + Default: "PERMIT", + }, + "name": { + Type: schema.TypeString, + Description: "Display name for rule", + Optional: true, + }, + "prefix_operator": { + Type: schema.TypeString, + Description: "Prefix operator to match subnets", + Optional: true, + ValidateFunc: validation.StringInSlice([]string{"GE", "EQ"}, false), + Default: "GE", + }, + "route_advertisement_types": { + Type: schema.TypeList, + Description: "Enable different types of route advertisements", + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringInSlice(routeAdvertisementTypesValues, false), + }, + }, + "subnets": { + Type: schema.TypeList, + Description: "Network CIDRs", + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validateCidr(), + }, + }, + }, + }, + }, + "in_filter_prefix_list": { + Type: schema.TypeList, + Description: "Paths of ordered Prefix list", + Optional: true, + MaxItems: 5, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + }, + "target_path": getPolicyPathSchema(true, false, "Policy path to tier0/vrf belongs to the same parent tier0"), + }, + } +} + +func getPolicyInterVRFRoutingFromSchema(d *schema.ResourceData) model.PolicyInterVrfRoutingConfig { + displayName := d.Get("display_name").(string) + description := d.Get("description").(string) + tags := getPolicyTagsFromSchema(d) + var bgpRouteLeaking []model.BgpRouteLeaking + bpgRouteLeakingList := d.Get("bgp_route_leaking") + if bpgRouteLeakingList != nil { + for _, brl := range bpgRouteLeakingList.([]interface{}) { + brlMap := brl.(map[string]interface{}) + addressFamily := brlMap["address_family"].(string) + var inFilter []string + if brlMap["in_filter"] != nil { + inFilter = interface2StringList(brlMap["in_filter"].([]interface{})) + } + var outFilter []string + if brlMap["out_filter"] != nil { + inFilter = interface2StringList(brlMap["out_filter"].([]interface{})) + } + bgpRouteLeaking = append(bgpRouteLeaking, model.BgpRouteLeaking{ + AddressFamily: &addressFamily, + InFilter: inFilter, + OutFilter: outFilter, + }) + } + } + + var staticRouteAdvertisement *model.PolicyStaticRouteAdvertisement + staticRouteAdvert := d.Get("static_route_advertisement") + if staticRouteAdvert != nil { + for _, sAdvert := range staticRouteAdvert.([]interface{}) { + // Should be one as list has MaxItems = 1 + sAdvertMap := sAdvert.(map[string]interface{}) + + var advertisementRules []model.PolicyRouteAdvertisementRule + advertRulesList := sAdvertMap["advertisement_rule"] + if advertRulesList != nil { + for _, advRule := range advertRulesList.([]interface{}) { + advRuleMap := advRule.(map[string]interface{}) + action := advRuleMap["action"].(string) + name := advRuleMap["name"].(string) + prefixOperator := advRuleMap["prefix_operator"].(string) + + var routeAdvertisementTypes []string + if advRuleMap["route_advertisement_types"] != nil { + routeAdvertisementTypes = interface2StringList(advRuleMap["route_advertisement_types"].([]interface{})) + } + + var subnets []string + if advRuleMap["subnets"] != nil { + subnets = interface2StringList(advRuleMap["subnets"].([]interface{})) + } + + advertisementRules = append(advertisementRules, model.PolicyRouteAdvertisementRule{ + Action: &action, + Name: &name, + PrefixOperator: &prefixOperator, + RouteAdvertisementTypes: routeAdvertisementTypes, + Subnets: subnets, + }) + } + } + var inFilterPrefixList []string + if sAdvertMap["in_filter_prefix_list"] != nil { + inFilterPrefixList = interface2StringList(sAdvertMap["in_filter_prefix_list"].([]interface{})) + } + staticRouteAdvertisement = &model.PolicyStaticRouteAdvertisement{ + AdvertisementRules: advertisementRules, + InFilterPrefixList: inFilterPrefixList, + } + } + } + + targetPath := d.Get("target_path").(string) + + return model.PolicyInterVrfRoutingConfig{ + DisplayName: &displayName, + Description: &description, + Tags: tags, + BgpRouteLeaking: bgpRouteLeaking, + StaticRouteAdvertisement: staticRouteAdvertisement, + TargetPath: &targetPath, + } +} + +func resourceNsxtPolicyTier0InterVRFRoutingCreate(d *schema.ResourceData, m interface{}) error { + connector := getPolicyConnector(m) + client := tier_0s.NewInterVrfRoutingClient(connector) + + gwPolicyPath := d.Get("gateway_path").(string) + isT0, gwID := parseGatewayPolicyPath(gwPolicyPath) + if gwID == "" { + return fmt.Errorf("gateway_path is not valid") + } + if !isT0 { + return fmt.Errorf("Tier0 gateway path expected, got %s", gwPolicyPath) + } + + id := d.Get("nsx_id").(string) + if id == "" { + id = newUUID() + } + + obj := getPolicyInterVRFRoutingFromSchema(d) + err := client.Patch(gwID, id, obj) + if err != nil { + return handleCreateError("InterVRFRouting", *obj.DisplayName, err) + } + + d.SetId(id) + d.Set("nsx_id", id) + + return resourceNsxtPolicyTier0InterVRFRoutingRead(d, m) +} + +func resourceNsxtPolicyTier0InterVRFRoutingRead(d *schema.ResourceData, m interface{}) error { + connector := getPolicyConnector(m) + client := tier_0s.NewInterVrfRoutingClient(connector) + + gwPolicyPath := d.Get("gateway_path").(string) + isT0, gwID := parseGatewayPolicyPath(gwPolicyPath) + if gwID == "" { + return fmt.Errorf("gateway_path is not valid") + } + if !isT0 { + return fmt.Errorf("Tier0 gateway path expected, got %s", gwPolicyPath) + } + + id := d.Get("nsx_id").(string) + + obj, err := client.Get(gwID, id) + if err != nil { + return handleReadError(d, "InterVRFRouting", id, err) + } + + d.Set("path", obj.Path) + d.Set("display_name", obj.DisplayName) + d.Set("description", obj.Description) + d.Set("revision", obj.Revision) + setPolicyTagsInSchema(d, obj.Tags) + + var brlList []interface{} + for _, brl := range obj.BgpRouteLeaking { + brlMap := make(map[string]interface{}) + brlMap["address_family"] = brl.AddressFamily + brlMap["in_filter"] = stringList2Interface(brl.InFilter) + brlMap["out_filter"] = stringList2Interface(brl.OutFilter) + + brlList = append(brlList, brlMap) + } + d.Set("bgp_route_leaking", brlList) + + var sraList []interface{} + if obj.StaticRouteAdvertisement != nil { + sra := make(map[string]interface{}) + var advRuleList []interface{} + for _, advRule := range obj.StaticRouteAdvertisement.AdvertisementRules { + elem := make(map[string]interface{}) + elem["action"] = advRule.Action + elem["name"] = advRule.Name + elem["prefix_operator"] = advRule.PrefixOperator + elem["route_advertisement_types"] = stringList2Interface(advRule.RouteAdvertisementTypes) + elem["subnets"] = stringList2Interface(advRule.Subnets) + + advRuleList = append(advRuleList, elem) + } + sra["advertisement_rule"] = advRuleList + sra["in_filter_prefix_list"] = stringList2Interface(obj.StaticRouteAdvertisement.InFilterPrefixList) + + sraList = []interface{}{sra} + } + d.Set("static_route_advertisement", sraList) + d.Set("target_path", obj.TargetPath) + + return nil +} + +func resourceNsxtPolicyTier0InterVRFRoutingUpdate(d *schema.ResourceData, m interface{}) error { + connector := getPolicyConnector(m) + client := tier_0s.NewInterVrfRoutingClient(connector) + + gwPolicyPath := d.Get("gateway_path").(string) + isT0, gwID := parseGatewayPolicyPath(gwPolicyPath) + if gwID == "" { + return fmt.Errorf("gateway_path is not valid") + } + if !isT0 { + return fmt.Errorf("Tier0 gateway path expected, got %s", gwPolicyPath) + } + + id := d.Get("nsx_id").(string) + + obj := getPolicyInterVRFRoutingFromSchema(d) + revision := int64(d.Get("revision").(int)) + obj.Revision = &revision + + err := client.Patch(gwID, id, obj) + if err != nil { + return handleUpdateError("InterVRFRouting", *obj.DisplayName, err) + } + + return resourceNsxtPolicyTier0InterVRFRoutingRead(d, m) +} + +func resourceNsxtPolicyTier0InterVRFRoutingDelete(d *schema.ResourceData, m interface{}) error { + connector := getPolicyConnector(m) + client := tier_0s.NewInterVrfRoutingClient(connector) + + gwPolicyPath := d.Get("gateway_path").(string) + isT0, gwID := parseGatewayPolicyPath(gwPolicyPath) + if gwID == "" { + return fmt.Errorf("gateway_path is not valid") + } + if !isT0 { + return fmt.Errorf("Tier0 gateway path expected, got %s", gwPolicyPath) + } + + id := d.Get("nsx_id").(string) + + err := client.Delete(gwID, id) + if err != nil { + return handleDeleteError("InterVRFRouting", id, err) + } + + return nil +} + +func resourceNsxtPolicyTier0InterVRFRoutingImport(d *schema.ResourceData, m interface{}) ([]*schema.ResourceData, error) { + importID := d.Id() + + rd, err := nsxtPolicyPathResourceImporterHelper(d, m) + if err == nil { + d.Set("nsx_id", d.Id()) + gwPath, err := getParameterFromPolicyPath("", "/inter-vrf-routing/", importID) + if err != nil { + return nil, fmt.Errorf("incorrect policy path for inter vrf routing resource: %v", err) + } + d.Set("gateway_path", gwPath) + return rd, nil + } + return nil, fmt.Errorf("not a policy path of an InterVRFRouting resource") +} diff --git a/nsxt/resource_nsxt_policy_tier0_inter_vrf_routing_test.go b/nsxt/resource_nsxt_policy_tier0_inter_vrf_routing_test.go new file mode 100644 index 000000000..a200a4f4e --- /dev/null +++ b/nsxt/resource_nsxt_policy_tier0_inter_vrf_routing_test.go @@ -0,0 +1,135 @@ +/* Copyright © 2024 Broadcom, 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" + "github.com/vmware/vsphere-automation-sdk-go/services/nsxt/infra/tier_0s" +) + +func TestAccResourceNsxtPolicyTier0InterVRFRouting_basic(t *testing.T) { + name := getAccTestResourceName() + updateName := getAccTestResourceName() + testResourceName := "nsxt_policy_tier0_inter_vrf_routing.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccOnlyLocalManager(t) + testAccPreCheck(t) + testAccNSXVersion(t, "4.1.0") + }, + Providers: testAccProviders, + CheckDestroy: func(state *terraform.State) error { + return testAccNsxtPolicyTier0InterVRFRoutingCheckDestroy(state, updateName) + }, + Steps: []resource.TestStep{ + { + Config: testAccNsxtPolicyTier0InterVRFRoutingTemplate(name, "[\"192.168.240.0/24\", \"192.168.241.0/24\"]"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(testResourceName, "display_name", name), + resource.TestCheckResourceAttrSet(testResourceName, "path"), + resource.TestCheckResourceAttrSet(testResourceName, "revision"), + resource.TestCheckResourceAttrSet(testResourceName, "gateway_path"), + resource.TestCheckResourceAttrSet(testResourceName, "target_path"), + resource.TestCheckResourceAttr(testResourceName, "bgp_route_leaking.0.address_family", "IPV4"), + resource.TestCheckResourceAttr(testResourceName, "static_route_advertisement.0.advertisement_rule.0.name", "test"), + resource.TestCheckResourceAttr(testResourceName, "static_route_advertisement.0.advertisement_rule.0.subnets.#", "2"), + ), + }, + { + Config: testAccNsxtPolicyTier0InterVRFRoutingTemplate(updateName, "[\"192.168.240.0/24\", \"192.168.241.0/24\", \"192.168.242.0/24\"]"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(testResourceName, "display_name", updateName), + resource.TestCheckResourceAttrSet(testResourceName, "path"), + resource.TestCheckResourceAttrSet(testResourceName, "revision"), + resource.TestCheckResourceAttrSet(testResourceName, "gateway_path"), + resource.TestCheckResourceAttrSet(testResourceName, "target_path"), + resource.TestCheckResourceAttr(testResourceName, "bgp_route_leaking.0.address_family", "IPV4"), + resource.TestCheckResourceAttr(testResourceName, "static_route_advertisement.0.advertisement_rule.0.name", "test"), + resource.TestCheckResourceAttr(testResourceName, "static_route_advertisement.0.advertisement_rule.0.subnets.#", "3"), + ), + }, + }, + }) +} + +func TestAccResourceNsxtPolicyTier0InterVRFRouting_importBasic(t *testing.T) { + name := getAccTestResourceName() + testResourceName := "nsxt_policy_tier0_inter_vrf_routing.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccOnlyLocalManager(t) + testAccPreCheck(t) + testAccNSXVersion(t, "4.1.0") + }, + Providers: testAccProviders, + CheckDestroy: func(state *terraform.State) error { + return testAccNsxtPolicyTier0InterVRFRoutingCheckDestroy(state, name) + }, + Steps: []resource.TestStep{ + { + Config: testAccNsxtPolicyTier0InterVRFRoutingTemplate(name, "[\"192.168.240.0/24\", \"192.168.241.0/24\"]"), + }, + { + ResourceName: testResourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: testAccResourceNsxtPolicyImportIDRetriever(testResourceName), + }, + }, + }) +} + +func testAccNsxtPolicyTier0InterVRFRoutingCheckDestroy(state *terraform.State, name string) error { + connector := getPolicyConnector(testAccProvider.Meta().(nsxtClients)) + client := tier_0s.NewInterVrfRoutingClient(connector) + for _, rs := range state.RootModule().Resources { + + if rs.Type != "nsxt_policy_tier0_inter_vrf_routing" { + continue + } + + resourceID := rs.Primary.Attributes["id"] + gwID := getPolicyIDFromPath(rs.Primary.Attributes["gateway_path"]) + _, err := client.Get(gwID, resourceID) + if err == nil { + return fmt.Errorf("policy Tier0InterVRFRouting %s still exists", name) + } + } + return nil +} + +func testAccNsxtPolicyTier0InterVRFRoutingTemplate(name, subnets string) string { + rName := getAccTestResourceName() + + return testAccNsxtPolicyTier0WithVRFTemplate(rName, false, false, false) + fmt.Sprintf(` +resource "nsxt_policy_tier0_inter_vrf_routing" "test" { + display_name = "%s" + gateway_path = nsxt_policy_tier0_gateway.parent.path + target_path = nsxt_policy_tier0_gateway.test.path + + bgp_route_leaking { + address_family = "IPV4" + } + static_route_advertisement { + advertisement_rule { + name = "test" + action = "PERMIT" + prefix_operator = "GE" + route_advertisement_types = ["TIER0_CONNECTED", "TIER0_NAT"] + subnets = %s + } + in_filter_prefix_list = [ + join("/", [nsxt_policy_tier0_gateway.parent.path, "prefix-lists/IPSEC_LOCAL_IP"]), + join("/", [nsxt_policy_tier0_gateway.parent.path, "prefix-lists/DNS_FORWARDER_IP"]) + ] + } +} +`, name, subnets) +} diff --git a/website/docs/r/policy_tier0_inter_vrf_routing.html.markdown b/website/docs/r/policy_tier0_inter_vrf_routing.html.markdown new file mode 100644 index 000000000..f5fd71ba6 --- /dev/null +++ b/website/docs/r/policy_tier0_inter_vrf_routing.html.markdown @@ -0,0 +1,80 @@ +--- +subcategory: "Beta" +layout: "nsxt" +page_title: "NSXT: nsxt_policy_tier0_inter_vrf_routing" +description: A resource to configure tier0 inter VRF routing + +--- + +# nsxt_policy_tier0_inter_vrf_routing + +This resource provides a method for the management of tier0 inter VRF routing. +This resource is supported with NSX 4.1.0 onwards. + +## Example Usage + +```hcl +resource "nsxt_policy_tier0_inter_vrf_routing" "test" { + display_name = "test-vrf-route" + gateway_path = nsxt_policy_tier0_gateway.t0.path + target_path = nsxt_policy_tier0_gateway.t0-vrf.path + + bgp_route_leaking { + address_family = "IPV4" + } + static_route_advertisement { + advertisement_rule { + name = "test" + action = "PERMIT" + prefix_operator = "GE" + route_advertisement_types = ["TIER0_CONNECTED", "TIER0_NAT"] + subnets = ["192.168.240.0/24", "192.168.241.0/24"] + } + in_filter_prefix_list = [ + join("/", [nsxt_policy_tier0_gateway.parent.path, "prefix-lists/IPSEC_LOCAL_IP"]), + join("/", [nsxt_policy_tier0_gateway.parent.path, "prefix-lists/DNS_FORWARDER_IP"]) + ] + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `display_name` - (Required) Display name of the resource. +* `description` - (Optional) Description of the resource. +* `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. +* `gateway_path` - (Required) The NSX Policy path to the Tier0 Gateway for this NAT Rule. +* `bgp_route_leaking` - (Optional) Import / export BGP routes + * `address_family` - (Optional) Address family type. Valid values are "IPV4", "IPV6". + * `in_filter` - (Optional) List of route map paths for IN direction. + * `out_filter` - (Optional) List of route map paths for OUT direction. +* `static_route_advertisement` - (Optional) Advertise subnet to target peers as static routes. + * `advertisement_rule` - (Optional) Route advertisement rules. + * `action` - (Optional) Action to advertise routes. Valid values are "PERMIT", "DENY". Default is "PERMIT". + * `name` - (Optional) Display name for rule. + * `prefix_operator` - (Optional) Prefix operator to match subnets. Valid values are "GE", "EQ". Default is "GE". + * `route_advertisement_types` - (Optional) Enable different types of route advertisements. Valid values are "TIER0_STATIC", "TIER0_CONNECTED", "TIER0_NAT", "TIER0_DNS_FORWARDER_IP", "TIER0_IPSEC_LOCAL_ENDPOINT", "TIER1_STATIC", "TIER1_CONNECTED", "TIER1_LB_SNAT", "TIER1_LB_VIP", "TIER1_NAT", "TIER1_DNS_FORWARDER_IP", "TIER1_IPSEC_LOCAL_ENDPOINT". + * `subnets` - (Optional) Network CIDRs. + * `in_filter_prefix_list` - (Optional) Paths of ordered Prefix list. +* `target_path` - (Required) Policy path to tier0/vrf belongs to the same parent tier0. + +## Attributes Reference + +In addition to arguments listed above, the following attributes are exported: + +* `id` - ID of the resource. +* `revision` - Indicates current revision number of the object as seen by NSX-T API server. This attribute can be useful for debugging. + +## Importing + +An existing policy NAT Rule can be [imported][docs-import] into this resource, via the following command: + +[docs-import]: https://www.terraform.io/cli/import + +``` +terraform import nsxt_policy_tier0_inter_vrf_routing.vrfroute POLICY_PATH +``` +The above command imports the policy tier0 inter VRF routing configuration named `vrfroute` for policy path `POLICY_PATH`.