diff --git a/openstack/fw_group_v2.go b/openstack/fw_group_v2.go new file mode 100644 index 000000000..295926278 --- /dev/null +++ b/openstack/fw_group_v2.go @@ -0,0 +1,86 @@ +package openstack + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas_v2/groups" +) + +func fwGroupV2RefreshFunc(networkingClient *gophercloud.ServiceClient, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + group, err := groups.Get(networkingClient, id).Extract() + if err != nil { + return nil, "", err + } + + return group, group.Status, nil + } +} + +func fwGroupV2DeleteFunc(networkingClient *gophercloud.ServiceClient, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + group, err := groups.Get(networkingClient, id).Extract() + + if err != nil { + if _, ok := err.(gophercloud.ErrDefault404); ok { + return "", "DELETED", nil + } + return nil, "", fmt.Errorf("Unexpected error: %s", err) + } + + return group, "DELETING", nil + } +} + +func fwGroupV2IngressPolicyDeleteFunc(networkingClient *gophercloud.ServiceClient, d *schema.ResourceData, ctx context.Context, groupID string) diag.Diagnostics { + stateConf := &resource.StateChangeConf{ + Pending: []string{"PENDING_CREATE", "PENDING_UPDATE"}, + Target: []string{"ACTIVE", "INACTIVE", "DOWN"}, + Refresh: fwGroupV2RefreshFunc(networkingClient, d.Id()), + Timeout: d.Timeout(schema.TimeoutUpdate), + Delay: 0, + MinTimeout: 2 * time.Second, + } + + _, err := stateConf.WaitForStateContext(ctx) + if err != nil { + return diag.Errorf("Error waiting for openstack_fw_group_v2 %s to become active: %s", d.Id(), err) + } + + _, err = groups.RemoveIngressPolicy(networkingClient, groupID).Extract() + if err != nil { + return diag.Errorf("Error removing ingress firewall policy from openstack_fw_group_v2 %s: %s", d.Id(), err) + } + + return nil +} + +func fwGroupV2EgressPolicyDeleteFunc(networkingClient *gophercloud.ServiceClient, d *schema.ResourceData, ctx context.Context, groupID string) diag.Diagnostics { + stateConf := &resource.StateChangeConf{ + Pending: []string{"PENDING_CREATE", "PENDING_UPDATE"}, + Target: []string{"ACTIVE", "INACTIVE", "DOWN"}, + Refresh: fwGroupV2RefreshFunc(networkingClient, d.Id()), + Timeout: d.Timeout(schema.TimeoutUpdate), + Delay: 0, + MinTimeout: 2 * time.Second, + } + + _, err := stateConf.WaitForStateContext(ctx) + if err != nil { + return diag.Errorf("Error waiting for openstack_fw_group_v2 %s to become active: %s", d.Id(), err) + } + + _, err = groups.RemoveEgressPolicy(networkingClient, groupID).Extract() + if err != nil { + return diag.Errorf("Error removing egress firewall policy from openstack_fw_group_v2 %s: %s", d.Id(), err) + } + + return nil +} diff --git a/openstack/import_openstack_fw_group_v2_test.go b/openstack/import_openstack_fw_group_v2_test.go new file mode 100644 index 000000000..1477574ce --- /dev/null +++ b/openstack/import_openstack_fw_group_v2_test.go @@ -0,0 +1,32 @@ +package openstack + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccFWGroupV2_importBasic(t *testing.T) { + resourceName := "openstack_fw_group_v2.group_1" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccPreCheckNonAdminOnly(t) + testAccPreCheckFW(t) + }, + ProviderFactories: testAccProviders, + CheckDestroy: testAccCheckFWGroupV2Destroy, + Steps: []resource.TestStep{ + { + Config: testAccFWGroupV2Basic1, + }, + + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} diff --git a/openstack/provider.go b/openstack/provider.go index b35b9d835..6380f7d59 100644 --- a/openstack/provider.go +++ b/openstack/provider.go @@ -355,6 +355,7 @@ func Provider() *schema.Provider { "openstack_dns_transfer_request_v2": resourceDNSTransferRequestV2(), "openstack_dns_transfer_accept_v2": resourceDNSTransferAcceptV2(), "openstack_fw_firewall_v1": resourceFWFirewallV1(), + "openstack_fw_group_v2": resourceFWGroupV2(), "openstack_fw_policy_v1": resourceFWPolicyV1(), "openstack_fw_policy_v2": resourceFWPolicyV2(), "openstack_fw_rule_v1": resourceFWRuleV1(), diff --git a/openstack/resource_openstack_fw_group_v2.go b/openstack/resource_openstack_fw_group_v2.go new file mode 100644 index 000000000..8f6b9f4ef --- /dev/null +++ b/openstack/resource_openstack_fw_group_v2.go @@ -0,0 +1,365 @@ +package openstack + +import ( + "context" + "log" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas_v2/groups" +) + +func resourceFWGroupV2() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceFWGroupV2Create, + ReadContext: resourceFWGroupV2Read, + UpdateContext: resourceFWGroupV2Update, + DeleteContext: resourceFWGroupV2Delete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(10 * time.Minute), + Update: schema.DefaultTimeout(10 * time.Minute), + Delete: schema.DefaultTimeout(10 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "region": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "name": { + Type: schema.TypeString, + Optional: true, + }, + + "description": { + Type: schema.TypeString, + Optional: true, + }, + + "tenant_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + ConflictsWith: []string{"project_id"}, + }, + + "project_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + ConflictsWith: []string{"tenant_id"}, + }, + + "ingress_firewall_policy_id": { + Type: schema.TypeString, + Optional: true, + }, + + "egress_firewall_policy_id": { + Type: schema.TypeString, + Optional: true, + }, + + "admin_state_up": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + + "ports": { + Type: schema.TypeSet, + Optional: true, + Set: schema.HashString, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + + "shared": { + Type: schema.TypeBool, + Optional: true, + }, + + "status": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceFWGroupV2Create(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + config := meta.(*Config) + networkingClient, err := config.NetworkingV2Client(GetRegion(d, config)) + if err != nil { + return diag.Errorf("Error creating OpenStack networking client: %s", err) + } + + groupcreateOpts := groups.CreateOpts{ + Name: d.Get("name").(string), + TenantID: d.Get("tenant_id").(string), + ProjectID: d.Get("project_id").(string), + Description: d.Get("description").(string), + IngressFirewallPolicyID: d.Get("ingress_firewall_policy_id").(string), + EgressFirewallPolicyID: d.Get("egress_firewall_policy_id").(string), + } + + if r, ok := d.GetOk("shared"); ok { + shared := r.(bool) + groupcreateOpts.Shared = &shared + } + + if r, ok := d.GetOk("admin_state_up"); ok { + adminStateUp := r.(bool) + groupcreateOpts.AdminStateUp = &adminStateUp + } + + associatedPortsRaw := d.Get("ports").(*schema.Set).List() + if len(associatedPortsRaw) > 0 { + var portIds []string + for _, v := range associatedPortsRaw { + portIds = append(portIds, v.(string)) + } + + groupcreateOpts.Ports = portIds + } + + log.Printf("[DEBUG] openstack_fw_group_v2 create options: %#v", groupcreateOpts) + + group, err := groups.Create(networkingClient, groupcreateOpts).Extract() + if err != nil { + return diag.Errorf("Error creating openstack_fw_group_v2: %s", err) + } + + log.Printf("[DEBUG] Created openstack_fw_group_v2 %s: %#v", group.ID, group) + + stateConf := &resource.StateChangeConf{ + Pending: []string{"PENDING_CREATE"}, + Target: []string{"ACTIVE", "INACTIVE", "DOWN"}, + Refresh: fwGroupV2RefreshFunc(networkingClient, group.ID), + Timeout: d.Timeout(schema.TimeoutCreate), + Delay: 0, + MinTimeout: 2 * time.Second, + } + + _, err = stateConf.WaitForStateContext(ctx) + if err != nil { + return diag.Errorf("Error waiting for openstack_fw_group_v2 to become active: %s", err) + } + + d.SetId(group.ID) + + return resourceFWGroupV2Read(ctx, d, meta) +} + +func resourceFWGroupV2Read(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + config := meta.(*Config) + networkingClient, err := config.NetworkingV2Client(GetRegion(d, config)) + if err != nil { + return diag.Errorf("Error creating OpenStack networking client: %s", err) + } + + group, err := groups.Get(networkingClient, d.Id()).Extract() + if err != nil { + return diag.FromErr(CheckDeleted(d, err, "Error retrieving openstack_fw_group_v2")) + } + + log.Printf("[DEBUG] Retrieved openstack_fw_group_v2 %s: %#v", d.Id(), group) + + d.Set("name", group.Name) + d.Set("description", group.Description) + d.Set("tenant_id", group.TenantID) + d.Set("project_id", group.ProjectID) + d.Set("ingress_firewall_policy_id", group.IngressFirewallPolicyID) + d.Set("egress_firewall_policy_id", group.EgressFirewallPolicyID) + d.Set("admin_state_up", group.AdminStateUp) + d.Set("status", group.Status) + d.Set("ports", group.Ports) + d.Set("shared", group.Shared) + d.Set("region", GetRegion(d, config)) + + return nil +} + +func resourceFWGroupV2Update(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + config := meta.(*Config) + networkingClient, err := config.NetworkingV2Client(GetRegion(d, config)) + if err != nil { + return diag.Errorf("Error creating OpenStack networking client: %s", err) + } + + group, err := groups.Get(networkingClient, d.Id()).Extract() + if err != nil { + return diag.FromErr(CheckDeleted(d, err, "Error retrieving openstack_fw_group_v2")) + } + + var ( + hasChange bool + updateOpts groups.UpdateOpts + ) + + if d.HasChange("name") { + hasChange = true + name := d.Get("name").(string) + updateOpts.Name = &name + } + + if d.HasChange("description") { + hasChange = true + description := d.Get("description").(string) + updateOpts.Description = &description + } + + if d.HasChange("ingress_firewall_policy_id") { + ingressFirewallPolicyID := d.Get("ingress_firewall_policy_id").(string) + if ingressFirewallPolicyID == "" { + log.Printf("[DEBUG] Attempting to clear ingress policy of openstack_fw_group_v2: %s.", group.ID) + + err := fwGroupV2IngressPolicyDeleteFunc(networkingClient, d, ctx, group.ID) + if err != nil { + return err + } + } + if len(ingressFirewallPolicyID) > 0 { + hasChange = true + updateOpts.IngressFirewallPolicyID = &ingressFirewallPolicyID + } + } + + if d.HasChange("egress_firewall_policy_id") { + egressFirewallPolicyID := d.Get("egress_firewall_policy_id").(string) + if egressFirewallPolicyID == "" { + log.Printf("[DEBUG] Attempting to clear egress policy of openstack_fw_group_v2: %s.", group.ID) + + err := fwGroupV2EgressPolicyDeleteFunc(networkingClient, d, ctx, group.ID) + if err != nil { + return err + } + } + if len(egressFirewallPolicyID) > 0 { + hasChange = true + updateOpts.EgressFirewallPolicyID = &egressFirewallPolicyID + } + } + + if d.HasChange("shared") { + hasChange = true + shared := d.Get("shared").(bool) + updateOpts.Shared = &shared + } + + if d.HasChange("admin_state_up") { + hasChange = true + adminStateUp := d.Get("admin_state_up").(bool) + updateOpts.AdminStateUp = &adminStateUp + } + + var portIds []string + if d.HasChange("ports") { + hasChange = true + emptyList := make([]string, 0) + updateOpts.Ports = &emptyList + if _, ok := d.GetOk("ports"); ok { + associatedPortsRaw := d.Get("ports").(*schema.Set).List() + for _, v := range associatedPortsRaw { + portIds = append(portIds, v.(string)) + } + updateOpts.Ports = &portIds + } + } + + if hasChange { + log.Printf("[DEBUG] openstack_fw_group_v2 %s update options: %#v", d.Id(), updateOpts) + + _, err = groups.Update(networkingClient, d.Id(), updateOpts).Extract() + if err != nil { + return diag.Errorf("Error updating openstack_fw_group_v2 %s: %s", d.Id(), err) + } + + stateConf := &resource.StateChangeConf{ + Pending: []string{"PENDING_CREATE", "PENDING_UPDATE"}, + Target: []string{"ACTIVE", "INACTIVE", "DOWN"}, + Refresh: fwGroupV2RefreshFunc(networkingClient, d.Id()), + Timeout: d.Timeout(schema.TimeoutUpdate), + Delay: 0, + MinTimeout: 2 * time.Second, + } + + _, err = stateConf.WaitForStateContext(ctx) + if err != nil { + return diag.Errorf("Error waiting for openstack_fw_group_v2 %s to become active: %s", d.Id(), err) + } + } + + return resourceFWGroupV2Read(ctx, d, meta) +} + +func resourceFWGroupV2Delete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + config := meta.(*Config) + networkingClient, err := config.NetworkingV2Client(GetRegion(d, config)) + if err != nil { + return diag.Errorf("Error creating OpenStack networking client: %s", err) + } + + group, err := groups.Get(networkingClient, d.Id()).Extract() + if err != nil { + return diag.FromErr(CheckDeleted(d, err, "Error retrieving openstack_fw_group_v2")) + } + + if len(group.Ports) > 0 { + var updateGroupOpts groups.UpdateOpts + emptyPorts := []string{} + updateGroupOpts.Ports = &emptyPorts + _, err := groups.Update(networkingClient, group.ID, updateGroupOpts).Extract() + if err != nil { + return diag.Errorf("Error removing ports from openstack_fw_group_v2 %s: %s", d.Id(), err) + } + } + + if group.IngressFirewallPolicyID != "" { + diagErr := fwGroupV2IngressPolicyDeleteFunc(networkingClient, d, ctx, group.ID) + if diagErr != nil { + return diagErr + } + } + + if group.EgressFirewallPolicyID != "" { + diagErr := fwGroupV2EgressPolicyDeleteFunc(networkingClient, d, ctx, group.ID) + if diagErr != nil { + return diagErr + } + } + + err = groups.Delete(networkingClient, d.Id()).ExtractErr() + if err != nil { + return diag.Errorf("Error deleting openstack_fw_group_v2 %s: %s", d.Id(), err) + } + + stateConf := &resource.StateChangeConf{ + Pending: []string{"DELETING"}, + Target: []string{"DELETED"}, + Refresh: fwGroupV2DeleteFunc(networkingClient, d.Id()), + Timeout: d.Timeout(schema.TimeoutDelete), + Delay: 0, + MinTimeout: 2 * time.Second, + } + + _, err = stateConf.WaitForStateContext(ctx) + if err != nil { + return diag.Errorf("Error waiting for openstack_fw_firewall_v2 %s to delete: %s", d.Id(), err) + } + + return nil +} diff --git a/openstack/resource_openstack_fw_group_v2_test.go b/openstack/resource_openstack_fw_group_v2_test.go new file mode 100644 index 000000000..fa47451bd --- /dev/null +++ b/openstack/resource_openstack_fw_group_v2_test.go @@ -0,0 +1,493 @@ +package openstack + +import ( + "fmt" + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/fwaas_v2/groups" +) + +func TestAccFWGroupV2_basic(t *testing.T) { + var policyID *string + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccPreCheckNonAdminOnly(t) + testAccPreCheckFW(t) + }, + ProviderFactories: testAccProviders, + CheckDestroy: testAccCheckFWGroupV2Destroy, + Steps: []resource.TestStep{ + { + Config: testAccFWGroupV2Basic1, + Check: resource.ComposeTestCheckFunc( + testAccCheckFWGroupV2("openstack_fw_group_v2.group_1", "", "", policyID), + ), + }, + { + Config: testAccFWGroupV2Basic2, + Check: resource.ComposeTestCheckFunc( + testAccCheckFWGroupV2( + "openstack_fw_group_v2.group_1", "group_1", "terraform acceptance test", policyID), + ), + }, + { + Config: testAccFWGroupV2Basic3, + Check: resource.ComposeTestCheckFunc( + testAccCheckFWGroupV2( + "openstack_fw_group_v2.group_1", "new_name_group_1", "new description terraform acceptance test", policyID), + ), + }, + { + Config: testAccFWGroupV2Basic1, + Check: resource.ComposeTestCheckFunc( + testAccCheckFWGroupV2( + "openstack_fw_group_v2.group_1", "", "", policyID), + ), + }, + }, + }) +} + +func TestAccFWGroupV2_shared(t *testing.T) { + var policyID *string + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccPreCheckAdminOnly(t) + testAccPreCheckFW(t) + }, + ProviderFactories: testAccProviders, + CheckDestroy: testAccCheckFWGroupV2Destroy, + Steps: []resource.TestStep{ + { + Config: testAccFWGroupV2Shared, + Check: resource.ComposeTestCheckFunc( + testAccCheckFWGroupV2("openstack_fw_group_v2.group_1", "", "", policyID), + resource.TestCheckResourceAttr( + "openstack_fw_group_v2.group_1", "shared", "true"), + ), + }, + }, + }) +} + +func TestAccFWGroupV2_port(t *testing.T) { + var group groups.Group + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccPreCheckNonAdminOnly(t) + testAccPreCheckFW(t) + }, + ProviderFactories: testAccProviders, + CheckDestroy: testAccCheckFWGroupV2Destroy, + Steps: []resource.TestStep{ + { + Config: testAccFWGroupV2Port, + Check: resource.ComposeTestCheckFunc( + testAccCheckFWGroupV2Exists("openstack_fw_group_v2.group_1", &group), + testAccCheckFWGroupPortCount(&group, 1), + ), + }, + }, + }) +} + +func TestAccFWGroupV2_no_port(t *testing.T) { + var group groups.Group + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccPreCheckNonAdminOnly(t) + testAccPreCheckFW(t) + }, + ProviderFactories: testAccProviders, + CheckDestroy: testAccCheckFWGroupV2Destroy, + Steps: []resource.TestStep{ + { + Config: testAccFWGroupV2NoPort, + Check: resource.ComposeTestCheckFunc( + testAccCheckFWGroupV2Exists("openstack_fw_group_v2.group_1", &group), + resource.TestCheckResourceAttr("openstack_fw_group_v2.group_1", "description", "firewall group port test"), + testAccCheckFWGroupPortCount(&group, 0), + ), + }, + }, + }) +} + +func TestAccFWGroupV2_port_update(t *testing.T) { + var group groups.Group + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccPreCheckNonAdminOnly(t) + testAccPreCheckFW(t) + }, + ProviderFactories: testAccProviders, + CheckDestroy: testAccCheckFWGroupV2Destroy, + Steps: []resource.TestStep{ + { + Config: testAccFWGroupV2Port, + Check: resource.ComposeTestCheckFunc( + testAccCheckFWGroupV2Exists("openstack_fw_group_v2.group_1", &group), + testAccCheckFWGroupPortCount(&group, 1), + ), + }, + { + Config: testAccFWGroupV2PortAdd, + Check: resource.ComposeTestCheckFunc( + testAccCheckFWGroupV2Exists("openstack_fw_group_v2.group_1", &group), + testAccCheckFWGroupPortCount(&group, 2), + ), + }, + }, + }) +} + +func TestAccFWGroupV2_port_remove(t *testing.T) { + var group groups.Group + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccPreCheckNonAdminOnly(t) + testAccPreCheckFW(t) + }, + ProviderFactories: testAccProviders, + CheckDestroy: testAccCheckFWGroupV2Destroy, + Steps: []resource.TestStep{ + { + Config: testAccFWGroupV2Port, + Check: resource.ComposeTestCheckFunc( + testAccCheckFWGroupV2Exists("openstack_fw_group_v2.group_1", &group), + testAccCheckFWGroupPortCount(&group, 1), + ), + }, + { + Config: testAccFWGroupV2PortRemove, + Check: resource.ComposeTestCheckFunc( + testAccCheckFWGroupV2Exists("openstack_fw_group_v2.group_1", &group), + testAccCheckFWGroupPortCount(&group, 0), + ), + }, + }, + }) +} + +func testAccCheckFWGroupV2Destroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + networkingClient, err := config.NetworkingV2Client(osRegionName) + if err != nil { + return fmt.Errorf("Error creating OpenStack networking client: %s", err) + } + for _, rs := range s.RootModule().Resources { + if rs.Type != "openstack_fw_group_v2" { + continue + } + + _, err = groups.Get(networkingClient, rs.Primary.ID).Extract() + if err == nil { + return fmt.Errorf("Firewall group (%s) still exists", rs.Primary.ID) + } + if _, ok := err.(gophercloud.ErrDefault404); !ok { + return err + } + } + return nil +} + +func testAccCheckFWGroupV2Exists(n string, group *groups.Group) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + config := testAccProvider.Meta().(*Config) + networkingClient, err := config.NetworkingV2Client(osRegionName) + if err != nil { + return fmt.Errorf("Exists) Error creating OpenStack networking client: %s", err) + } + + found, err := groups.Get(networkingClient, rs.Primary.ID).Extract() + if err != nil { + return err + } + + if found.ID != rs.Primary.ID { + return fmt.Errorf("Firewall group not found. Expected %s, got %s", rs.Primary.ID, found.ID) + } + + *group = *found + + return nil + } +} + +func testAccCheckFWGroupPortCount(group *groups.Group, expected int) resource.TestCheckFunc { + return func(s *terraform.State) error { + if len(group.Ports) != expected { + return fmt.Errorf("Expected %d Ports, got %d", expected, len(group.Ports)) + } + + return nil + } +} + +func testAccCheckFWGroupV2(n, expectedName, expectedDescription string, IngressFirewallPolicyID *string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + config := testAccProvider.Meta().(*Config) + networkingClient, err := config.NetworkingV2Client(osRegionName) + if err != nil { + return fmt.Errorf("Exists) Error creating OpenStack networking client: %s", err) + } + + var found *groups.Group + for i := 0; i < 5; i++ { + // Firewall group creation is asynchronous. Retry some times + // if we get a 404 error. Fail on any other error. + found, err = groups.Get(networkingClient, rs.Primary.ID).Extract() + if err != nil { + if _, ok := err.(gophercloud.ErrDefault404); ok { + time.Sleep(time.Second) + continue + } + return err + } + break + } + + switch { + case found.Name != expectedName: + err = fmt.Errorf("Expected Name to be <%s> but found <%s>", expectedName, found.Name) + case found.Description != expectedDescription: + err = fmt.Errorf("Expected Description to be <%s> but found <%s>", + expectedDescription, found.Description) + case found.IngressFirewallPolicyID == "": + err = fmt.Errorf("Policy should not be empty") + } + + if err != nil { + return err + } + + IngressFirewallPolicyID = &found.IngressFirewallPolicyID + + return nil + } +} + +const testAccFWGroupV2Basic1 = ` +resource "openstack_fw_policy_v2" "ingress_firewall_policy_1" { + name = "ingress_firewall_policy_1" +} + +resource "openstack_fw_policy_v2" "egress_firewall_policy_1" { + name = "egress_firewall_policy_1" +} + +resource "openstack_fw_group_v2" "group_1" { + ingress_firewall_policy_id = "${openstack_fw_policy_v2.ingress_firewall_policy_1.id}" + egress_firewall_policy_id = "${openstack_fw_policy_v2.egress_firewall_policy_1.id}" +} +` + +const testAccFWGroupV2Shared = ` +resource "openstack_fw_policy_v2" "ingress_firewall_policy_1" { + name = "ingress_firewall_policy_1" +} + +resource "openstack_fw_policy_v2" "egress_firewall_policy_1" { + name = "egress_firewall_policy_1" +} + +resource "openstack_fw_group_v2" "group_1" { + ingress_firewall_policy_id = "${openstack_fw_policy_v2.ingress_firewall_policy_1.id}" + egress_firewall_policy_id = "${openstack_fw_policy_v2.egress_firewall_policy_1.id}" + shared = true +} +` + +const testAccFWGroupV2Basic2 = ` +resource "openstack_fw_policy_v2" "ingress_firewall_policy_1" { + name = "ingress_firewall_policy_1" +} + +resource "openstack_fw_policy_v2" "egress_firewall_policy_1" { + name = "egress_firewall_policy_1" +} + +resource "openstack_fw_group_v2" "group_1" { + name = "group_1" + description = "terraform acceptance test" + ingress_firewall_policy_id = "${openstack_fw_policy_v2.ingress_firewall_policy_1.id}" + egress_firewall_policy_id = "${openstack_fw_policy_v2.egress_firewall_policy_1.id}" + admin_state_up = true +} +` + +const testAccFWGroupV2Basic3 = ` +resource "openstack_fw_policy_v2" "ingress_firewall_policy_1" { + name = "ingress_firewall_policy_1" +} + +resource "openstack_fw_policy_v2" "egress_firewall_policy_1" { + name = "egress_firewall_policy_1" +} + +resource "openstack_fw_group_v2" "group_1" { + name = "new_name_group_1" + description = "new description terraform acceptance test" + ingress_firewall_policy_id = "${openstack_fw_policy_v2.ingress_firewall_policy_1.id}" + egress_firewall_policy_id = "${openstack_fw_policy_v2.egress_firewall_policy_1.id}" + admin_state_up = true +} +` + +const testAccFWGroupV2Port = ` +resource "openstack_networking_router_v2" "router_1" { + name = "router_1" + admin_state_up = true +} + +resource "openstack_networking_network_v2" "network_1" { + name = "network_1" + admin_state_up = "true" +} + +resource "openstack_networking_subnet_v2" "subnet_1" { + network_id = "${openstack_networking_network_v2.network_1.id}" + cidr = "10.20.30.0/24" + ip_version = 4 +} + +resource "openstack_networking_router_interface_v2" "router_interface_1" { + router_id = "${openstack_networking_router_v2.router_1.id}" + subnet_id = "${openstack_networking_subnet_v2.subnet_1.id}" +} + +resource "openstack_fw_policy_v2" "ingress_firewall_policy_1" { + name = "ingress_firewall_policy_1" +} + +resource "openstack_fw_group_v2" "group_1" { + name = "group_1" + description = "firewall group port test" + ports = [ + "${openstack_networking_router_interface_v2.router_interface_1.port_id}", + ] +} +` + +const testAccFWGroupV2PortAdd = ` +resource "openstack_networking_router_v2" "router_1" { + name = "router_1" + admin_state_up = "true" +} + +resource "openstack_networking_network_v2" "network_1" { + name = "network_1" + admin_state_up = "true" +} + +resource "openstack_networking_subnet_v2" "subnet_1" { + network_id = "${openstack_networking_network_v2.network_1.id}" + cidr = "10.20.30.0/24" + ip_version = 4 +} + +resource "openstack_networking_router_interface_v2" "router_interface_1" { + router_id = "${openstack_networking_router_v2.router_1.id}" + subnet_id = "${openstack_networking_subnet_v2.subnet_1.id}" +} + +resource "openstack_networking_router_v2" "router_2" { + name = "router_2" + admin_state_up = "true" +} + +resource "openstack_networking_network_v2" "network_2" { + name = "network_2" + admin_state_up = "true" +} + +resource "openstack_networking_subnet_v2" "subnet_2" { + network_id = "${openstack_networking_network_v2.network_2.id}" + cidr = "20.30.40.0/24" + ip_version = 4 +} + +resource "openstack_networking_router_interface_v2" "router_interface_2" { + router_id = "${openstack_networking_router_v2.router_2.id}" + subnet_id = "${openstack_networking_subnet_v2.subnet_2.id}" +} + +resource "openstack_fw_policy_v2" "ingress_firewall_policy_1" { + name = "ingress_firewall_policy_1" +} + +resource "openstack_fw_group_v2" "group_1" { + name = "group_1" + description = "firewall group port test" + ingress_firewall_policy_id = "${openstack_fw_policy_v2.ingress_firewall_policy_1.id}" + ports = [ + "${openstack_networking_router_interface_v2.router_interface_1.port_id}", + "${openstack_networking_router_interface_v2.router_interface_2.port_id}", + ] +} +` + +const testAccFWGroupV2PortRemove = ` +resource "openstack_fw_policy_v2" "ingress_firewall_policy_1" { + name = "ingress_firewall_policy_1" +} + +resource "openstack_fw_group_v2" "group_1" { + name = "group_1" + description = "firewall group port test" + ingress_firewall_policy_id = "${openstack_fw_policy_v2.ingress_firewall_policy_1.id}" + ports = [] +} +` + +const testAccFWGroupV2NoPort = ` +resource "openstack_fw_policy_v2" "ingress_firewall_policy_1" { + name = "ingress_firewall_policy_1" +} + +resource "openstack_fw_policy_v2" "egress_firewall_policy_1" { + name = "egress_firewall_policy_1" +} + +resource "openstack_fw_group_v2" "group_1" { + name = "group_1" + description = "firewall group port test" + ingress_firewall_policy_id = "${openstack_fw_policy_v2.ingress_firewall_policy_1.id}" + egress_firewall_policy_id = "${openstack_fw_policy_v2.egress_firewall_policy_1.id}" +} +` diff --git a/openstack/resource_openstack_fw_policy_v2.go b/openstack/resource_openstack_fw_policy_v2.go index 9e7690abd..4f733ade6 100644 --- a/openstack/resource_openstack_fw_policy_v2.go +++ b/openstack/resource_openstack_fw_policy_v2.go @@ -179,7 +179,7 @@ func resourceFWPolicyV2Update(ctx context.Context, d *schema.ResourceData, meta if hasChange { log.Printf("[DEBUG] openstack_fw_policy_v2 %s update options: %#v", d.Id(), updateOpts) - err = policies.Update(networkingClient, d.Id(), updateOpts).Err + _, err = policies.Update(networkingClient, d.Id(), updateOpts).Extract() if err != nil { return diag.Errorf("Error updating openstack_fw_policy_v2 %s: %s", d.Id(), err) } diff --git a/openstack/resource_openstack_fw_rule_v2.go b/openstack/resource_openstack_fw_rule_v2.go index ae65e3b74..3b8f756f0 100644 --- a/openstack/resource_openstack_fw_rule_v2.go +++ b/openstack/resource_openstack_fw_rule_v2.go @@ -294,7 +294,7 @@ func resourceFWRuleV2Update(ctx context.Context, d *schema.ResourceData, meta in if hasChange { log.Printf("[DEBUG] openstack_fw_rule_v2 %s update options: %#v", d.Id(), updateOpts) - err = rules.Update(networkingClient, d.Id(), updateOpts).Err + _, err = rules.Update(networkingClient, d.Id(), updateOpts).Extract() if err != nil { return diag.Errorf("Error updating openstack_fw_rule_v2 %s: %s", d.Id(), err) } diff --git a/website/docs/r/fw_group_v2.html.markdown b/website/docs/r/fw_group_v2.html.markdown new file mode 100644 index 000000000..ce51cb26a --- /dev/null +++ b/website/docs/r/fw_group_v2.html.markdown @@ -0,0 +1,129 @@ +--- +subcategory: "Networking / Neutron" +layout: "openstack" +page_title: "OpenStack: openstack_fw_group_v2" +sidebar_current: "docs-openstack-resource-fw-group-v2" +description: |- + Manages a v2 firewall group resource within OpenStack. +--- + +# openstack\_fw\_group\_v2 + +Manages a v2 firewall group resource within OpenStack. + +~> **Note:** Firewall v2 has no support for OVN currently. + +## Example Usage + +```hcl +resource "openstack_fw_rule_v2" "rule_1" { + name = "firewall_rule_2" + description = "drop TELNET traffic" + action = "deny" + protocol = "tcp" + destination_port = "23" + enabled = "true" +} + +resource "openstack_fw_rule_v2" "rule_2" { + name = "firewall_rule_1" + description = "drop NTP traffic" + action = "deny" + protocol = "udp" + destination_port = "123" + enabled = "false" +} + +resource "openstack_fw_policy_v2" "policy_1" { + name = "firewall_ingress_policy" + + rules = [ + openstack_fw_rule_v2.rule_1.id, + ] +} + +resource "openstack_fw_policy_v2" "policy_2" { + name = "firewall_egress_policy" + + rules = [ + openstack_fw_rule_v2.rule_2.id, + ] +} + +resource "openstack_fw_group_v2" "group_1" { + name = "firewall_group" + ingress_firewall_policy_id = openstack_fw_policy_v2.policy_1.id + egress_firewall_policy_id = openstack_fw_policy_v2.policy_2.id +} +``` + +## Argument Reference + +The following arguments are supported: + +* `region` - (Optional) The region in which to obtain the v2 networking client. + A networking client is needed to create a firewall group. If omitted, the + `region` argument of the provider is used. Changing this creates a new + firewall group. + +* `name` - (Optional) A name for the firewall group. Changing this + updates the `name` of an existing firewall. + +* `description` - (Optional) A description for the firewall group. Changing this + updates the `description` of an existing firewall group. + +* `tenant_id` - (Optional) - This argument conflict and interchangeable with + `project_id`. The owner of the firewall group. Required if admin wants to + create a firewall group for another tenant. Changing this creates a new + firewall group. + +* `project_id` - (Optional) - This argument conflict and interchangeable with + `tenant_id`. The owner of the firewall group. Required if admin wants to + create a firewall group for another project. Changing this creates a new + firewall group. + +* `ingress_firewall_policy_id` - (Optional) The ingress firewall policy resource + id for the firewall group. Changing this updates the + `ingress_firewall_policy_id` of an existing firewall group. + +* `egress_firewall_policy_id` - (Optional) The egress firewall policy resource + id for the firewall group. Changing this updates the + `egress_firewall_policy_id` of an existing firewall group. + +* `admin_state_up` - (Optional) Administrative up/down status for the firewall + group (must be "true" or "false" if provided - defaults to "true"). + Changing this updates the `admin_state_up` of an existing firewall group. + +* `ports` - (Optional) Port(s) to associate this firewall group + with. Must be a list of strings. Changing this updates the associated ports + of an existing firewall group. + +* `shared` - (Optional) Sharing status of the firewall group (must be "true" + or "false" if provided). If this is "true" the firewall group is visible to, + and can be used in, firewalls in other tenants. Changing this updates the + `shared` status of an existing firewall group. Only administrative users + can specify if the firewall group should be shared. + +## Attributes Reference + +The following attributes are exported: + +* `region` - See Argument Reference above. +* `name` - See Argument Reference above. +* `description` - See Argument Reference above. +* `tenant_id` - See Argument Reference above. +* `project_id` - See Argument Reference above. +* `ingress_firewall_policy_id` - See Argument Reference above. +* `egress_firewall_policy_id` - See Argument Reference above. +* `admin_state_up` - See Argument Reference above. +* `ports` - See Argument Reference above. +* `shared` - See Argument Reference above. +* `status` - The status of the firewall group. + +## Import + +Firewall groups can be imported using the `id`, e.g. + +``` +$ terraform import openstack_fw_group_v2.group_1 c9e39fb2-ce20-46c8-a964-25f3898c7a97 +``` diff --git a/website/docs/r/fw_policy_v2.html.markdown b/website/docs/r/fw_policy_v2.html.markdown index b2f5f3999..cffd893cd 100644 --- a/website/docs/r/fw_policy_v2.html.markdown +++ b/website/docs/r/fw_policy_v2.html.markdown @@ -38,8 +38,8 @@ resource "openstack_fw_policy_v2" "policy_1" { name = "firewall_policy" rules = [ - "${openstack_fw_rule_v2.rule_1.id}", - "${openstack_fw_rule_v2.rule_2.id}", + openstack_fw_rule_v2.rule_1.id, + openstack_fw_rule_v2.rule_2.id, ] } ``` diff --git a/website/openstack.erb b/website/openstack.erb index d0f29258e..4482f5e9b 100644 --- a/website/openstack.erb +++ b/website/openstack.erb @@ -465,6 +465,9 @@ > openstack_fw_firewall_v1 + > + openstack_fw_group_v2 + > openstack_fw_policy_v1