diff --git a/builtin/providers/openstack/provider.go b/builtin/providers/openstack/provider.go index 6a708a28ff13..a43242333d91 100644 --- a/builtin/providers/openstack/provider.go +++ b/builtin/providers/openstack/provider.go @@ -64,6 +64,9 @@ func Provider() terraform.ResourceProvider { "openstack_compute_keypair_v2": resourceComputeKeypairV2(), "openstack_compute_secgroup_v2": resourceComputeSecGroupV2(), "openstack_compute_floatingip_v2": resourceComputeFloatingIPV2(), + "openstack_fw_firewall_v1": resourceFWFirewallV1(), + "openstack_fw_policy_v1": resourceFWPolicyV1(), + "openstack_fw_rule_v1": resourceFWRuleV1(), "openstack_lb_monitor_v1": resourceLBMonitorV1(), "openstack_lb_pool_v1": resourceLBPoolV1(), "openstack_lb_vip_v1": resourceLBVipV1(), diff --git a/builtin/providers/openstack/resource_openstack_fw_firewall_v1.go b/builtin/providers/openstack/resource_openstack_fw_firewall_v1.go new file mode 100644 index 000000000000..16b2014a720a --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_fw_firewall_v1.go @@ -0,0 +1,266 @@ +package openstack + +import ( + "fmt" + "log" + "time" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls" +) + +func resourceFWFirewallV1() *schema.Resource { + return &schema.Resource{ + Create: resourceFWFirewallV1Create, + Read: resourceFWFirewallV1Read, + Update: resourceFWFirewallV1Update, + Delete: resourceFWFirewallV1Delete, + + Schema: map[string]*schema.Schema{ + "region": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + DefaultFunc: envDefaultFunc("OS_REGION_NAME"), + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "policy_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "admin_state_up": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "tenant_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + }, + } +} + +func resourceFWFirewallV1Create(d *schema.ResourceData, meta interface{}) error { + + config := meta.(*Config) + networkingClient, err := config.networkingV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack networking client: %s", err) + } + + adminStateUp := d.Get("admin_state_up").(bool) + + firewallConfiguration := firewalls.CreateOpts{ + Name: d.Get("name").(string), + Description: d.Get("description").(string), + PolicyID: d.Get("policy_id").(string), + AdminStateUp: &adminStateUp, + TenantID: d.Get("tenant_id").(string), + } + + log.Printf("[DEBUG] Create firewall: %#v", firewallConfiguration) + + firewall, err := firewalls.Create(networkingClient, firewallConfiguration).Extract() + if err != nil { + return err + } + + log.Printf("[DEBUG] Firewall created: %#v", firewall) + + stateConf := &resource.StateChangeConf{ + Pending: []string{"PENDING_CREATE"}, + Target: "ACTIVE", + Refresh: waitForFirewallActive(networkingClient, firewall.ID), + Timeout: 30 * time.Second, + Delay: 0, + MinTimeout: 2 * time.Second, + } + + _, err = stateConf.WaitForState() + + d.SetId(firewall.ID) + + return resourceFWFirewallV1Read(d, meta) +} + +func resourceFWFirewallV1Read(d *schema.ResourceData, meta interface{}) error { + log.Printf("[DEBUG] Retrieve information about firewall: %s", d.Id()) + + config := meta.(*Config) + networkingClient, err := config.networkingV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack networking client: %s", err) + } + + firewall, err := firewalls.Get(networkingClient, d.Id()).Extract() + + if err != nil { + return CheckDeleted(d, err, "LB pool") + } + + if t, exists := d.GetOk("name"); exists && t != "" { + d.Set("name", firewall.Name) + } else { + d.Set("name", "") + } + + if t, exists := d.GetOk("description"); exists && t != "" { + d.Set("description", firewall.Description) + } else { + d.Set("description", "") + } + + if t, exists := d.GetOk("policy_id"); exists && t != "" { + d.Set("policy_id", firewall.PolicyID) + } else { + d.Set("policy_id", "") + } + + if t, exists := d.GetOk("admin_state_up"); exists && t != "" { + d.Set("admin_state_up", firewall.AdminStateUp) + } else { + d.Set("admin_state_up", "") + } + + if t, exists := d.GetOk("tenant_id"); exists && t != "" { + d.Set("tenant_id", firewall.TenantID) + } else { + d.Set("tenant_id", "") + } + + return nil +} + +func resourceFWFirewallV1Update(d *schema.ResourceData, meta interface{}) error { + + config := meta.(*Config) + networkingClient, err := config.networkingV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack networking client: %s", err) + } + + opts := firewalls.UpdateOpts{} + + if d.HasChange("name") { + opts.Name = d.Get("name").(string) + } + + if d.HasChange("description") { + opts.Description = d.Get("description").(string) + } + + if d.HasChange("policy_id") { + opts.PolicyID = d.Get("policy_id").(string) + } + + if d.HasChange("admin_state_up") { + adminStateUp := d.Get("admin_state_up").(bool) + opts.AdminStateUp = &adminStateUp + } + + log.Printf("[DEBUG] Updating firewall with id %s: %#v", d.Id(), opts) + + stateConf := &resource.StateChangeConf{ + Pending: []string{"PENDING_CREATE", "PENDING_UPDATE"}, + Target: "ACTIVE", + Refresh: waitForFirewallActive(networkingClient, d.Id()), + Timeout: 30 * time.Second, + Delay: 0, + MinTimeout: 2 * time.Second, + } + + _, err = stateConf.WaitForState() + + err = firewalls.Update(networkingClient, d.Id(), opts).Err + if err != nil { + return err + } + + return resourceFWFirewallV1Read(d, meta) +} + +func resourceFWFirewallV1Delete(d *schema.ResourceData, meta interface{}) error { + log.Printf("[DEBUG] Destroy firewall: %s", d.Id()) + + config := meta.(*Config) + networkingClient, err := config.networkingV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack networking client: %s", err) + } + + stateConf := &resource.StateChangeConf{ + Pending: []string{"PENDING_CREATE", "PENDING_UPDATE"}, + Target: "ACTIVE", + Refresh: waitForFirewallActive(networkingClient, d.Id()), + Timeout: 30 * time.Second, + Delay: 0, + MinTimeout: 2 * time.Second, + } + + _, err = stateConf.WaitForState() + + err = firewalls.Delete(networkingClient, d.Id()).Err + + if err != nil { + return err + } + + stateConf = &resource.StateChangeConf{ + Pending: []string{"DELETING"}, + Target: "DELETED", + Refresh: waitForFirewallDeletion(networkingClient, d.Id()), + Timeout: 2 * time.Minute, + Delay: 0, + MinTimeout: 2 * time.Second, + } + + _, err = stateConf.WaitForState() + + return err +} + +func waitForFirewallActive(networkingClient *gophercloud.ServiceClient, id string) resource.StateRefreshFunc { + + return func() (interface{}, string, error) { + fw, err := firewalls.Get(networkingClient, id).Extract() + log.Printf("[DEBUG] Get firewall %s => %#v", id, fw) + + if err != nil { + return nil, "", err + } + return fw, fw.Status, nil + } +} + +func waitForFirewallDeletion(networkingClient *gophercloud.ServiceClient, id string) resource.StateRefreshFunc { + + return func() (interface{}, string, error) { + fw, err := firewalls.Get(networkingClient, id).Extract() + log.Printf("[DEBUG] Get firewall %s => %#v", id, fw) + + if err != nil { + httpStatus := err.(*gophercloud.UnexpectedResponseCodeError) + log.Printf("[DEBUG] Get firewall %s status is %d", id, httpStatus.Actual) + + if httpStatus.Actual == 404 { + log.Printf("[DEBUG] Firewall %s is actually deleted", id) + return "", "DELETED", nil + } + return nil, "", fmt.Errorf("Unexpected status code %d", httpStatus.Actual) + } + + log.Printf("[DEBUG] Firewall %s deletion is pending", id) + return fw, "DELETING", nil + } +} diff --git a/builtin/providers/openstack/resource_openstack_fw_firewall_v1_test.go b/builtin/providers/openstack/resource_openstack_fw_firewall_v1_test.go new file mode 100644 index 000000000000..34112f778f09 --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_fw_firewall_v1_test.go @@ -0,0 +1,139 @@ +package openstack + +import ( + "fmt" + "testing" + "time" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls" +) + +func TestAccFWFirewallV1(t *testing.T) { + + var policyID *string + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckFWFirewallV1Destroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testFirewallConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckFWFirewallV1Exists("openstack_fw_firewall_v1.accept_test", "", "", policyID), + ), + }, + resource.TestStep{ + Config: testFirewallConfigUpdated, + Check: resource.ComposeTestCheckFunc( + testAccCheckFWFirewallV1Exists("openstack_fw_firewall_v1.accept_test", "accept_test", "terraform acceptance test", policyID), + ), + }, + }, + }) +} + +func testAccCheckFWFirewallV1Destroy(s *terraform.State) error { + + config := testAccProvider.Meta().(*Config) + networkingClient, err := config.networkingV2Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("(testAccCheckOpenstackFirewallDestroy) Error creating OpenStack networking client: %s", err) + } + for _, rs := range s.RootModule().Resources { + if rs.Type != "openstack_firewall" { + continue + } + _, err = firewalls.Get(networkingClient, rs.Primary.ID).Extract() + if err == nil { + return fmt.Errorf("Firewall (%s) still exists.", rs.Primary.ID) + } + httpError, ok := err.(*gophercloud.UnexpectedResponseCodeError) + if !ok || httpError.Actual != 404 { + return httpError + } + } + return nil +} + +func testAccCheckFWFirewallV1Exists(n, expectedName, expectedDescription string, policyID *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(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("(testAccCheckFirewallExists) Error creating OpenStack networking client: %s", err) + } + + var found *firewalls.Firewall + for i := 0; i < 5; i++ { + // Firewall creation is asynchronous. Retry some times + // if we get a 404 error. Fail on any other error. + found, err = firewalls.Get(networkingClient, rs.Primary.ID).Extract() + if err != nil { + httpError, ok := err.(*gophercloud.UnexpectedResponseCodeError) + if !ok || httpError.Actual != 404 { + time.Sleep(time.Second) + continue + } + } + break + } + + if err != nil { + return err + } + + if found.Name != expectedName { + return fmt.Errorf("Expected Name to be <%s> but found <%s>", expectedName, found.Name) + } + if found.Description != expectedDescription { + return fmt.Errorf("Expected Description to be <%s> but found <%s>", expectedDescription, found.Description) + } + if found.PolicyID == "" { + return fmt.Errorf("Policy should not be empty") + } + if policyID != nil && found.PolicyID == *policyID { + return fmt.Errorf("Policy had not been correctly updated. Went from <%s> to <%s>", expectedName, found.Name) + } + + policyID = &found.PolicyID + + return nil + } +} + +const testFirewallConfig = ` +resource "openstack_fw_firewall_v1" "accept_test" { + policy_id = "${openstack_fw_policy_v1.accept_test_policy_1.id}" +} + +resource "openstack_fw_policy_v1" "accept_test_policy_1" { + name = "policy-1" +} +` + +const testFirewallConfigUpdated = ` +resource "openstack_fw_firewall_v1" "accept_test" { + name = "accept_test" + description = "terraform acceptance test" + policy_id = "${openstack_fw_policy_v1.accept_test_policy_2.id}" +} + +resource "openstack_fw_policy_v1" "accept_test_policy_2" { + name = "policy-2" +} +` diff --git a/builtin/providers/openstack/resource_openstack_fw_policy_v1.go b/builtin/providers/openstack/resource_openstack_fw_policy_v1.go new file mode 100644 index 000000000000..e58b994fc7e0 --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_fw_policy_v1.go @@ -0,0 +1,225 @@ +package openstack + +import ( + "fmt" + "log" + "time" + + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/schema" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/policies" +) + +func resourceFWPolicyV1() *schema.Resource { + return &schema.Resource{ + Create: resourceFWPolicyV1Create, + Read: resourceFWPolicyV1Read, + Update: resourceFWPolicyV1Update, + Delete: resourceFWPolicyV1Delete, + + Schema: map[string]*schema.Schema{ + "region": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + DefaultFunc: envDefaultFunc("OS_REGION_NAME"), + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "audited": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "shared": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "tenant_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "rules": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: func(v interface{}) int { + return hashcode.String(v.(string)) + }, + }, + }, + } +} + +func resourceFWPolicyV1Create(d *schema.ResourceData, meta interface{}) error { + + config := meta.(*Config) + networkingClient, err := config.networkingV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack networking client: %s", err) + } + + v := d.Get("rules").(*schema.Set) + + log.Printf("[DEBUG] Rules found : %#v", v) + log.Printf("[DEBUG] Rules count : %d", v.Len()) + + rules := make([]string, v.Len()) + for i, v := range v.List() { + rules[i] = v.(string) + } + + audited := d.Get("audited").(bool) + shared := d.Get("shared").(bool) + + opts := policies.CreateOpts{ + Name: d.Get("name").(string), + Description: d.Get("description").(string), + Audited: &audited, + Shared: &shared, + TenantID: d.Get("tenant_id").(string), + Rules: rules, + } + + log.Printf("[DEBUG] Create firewall policy: %#v", opts) + + policy, err := policies.Create(networkingClient, opts).Extract() + if err != nil { + return err + } + + log.Printf("[DEBUG] Firewall policy craeted: %#v", policy) + + d.SetId(policy.ID) + + return resourceFWPolicyV1Read(d, meta) +} + +func resourceFWPolicyV1Read(d *schema.ResourceData, meta interface{}) error { + log.Printf("[DEBUG] Retrieve information about firewall policy: %s", d.Id()) + + config := meta.(*Config) + networkingClient, err := config.networkingV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack networking client: %s", err) + } + + policy, err := policies.Get(networkingClient, d.Id()).Extract() + + if err != nil { + return CheckDeleted(d, err, "LB pool") + } + + if t, exists := d.GetOk("name"); exists && t != "" { + d.Set("name", policy.Name) + } else { + d.Set("name", "") + } + + if t, exists := d.GetOk("description"); exists && t != "" { + d.Set("description", policy.Description) + } else { + d.Set("description", "") + } + + if t, exists := d.GetOk("shared"); exists && t != "" { + d.Set("shared", policy.Shared) + } else { + d.Set("shared", "") + } + + if t, exists := d.GetOk("audited"); exists && t != "" { + d.Set("audited", policy.Audited) + } else { + d.Set("audited", "") + } + + if t, exists := d.GetOk("tenant_id"); exists && t != "" { + d.Set("tenant_id", policy.TenantID) + } else { + d.Set("tenant_id", "") + } + + return nil +} + +func resourceFWPolicyV1Update(d *schema.ResourceData, meta interface{}) error { + + config := meta.(*Config) + networkingClient, err := config.networkingV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack networking client: %s", err) + } + + opts := policies.UpdateOpts{} + + if d.HasChange("name") { + opts.Name = d.Get("name").(string) + } + + if d.HasChange("description") { + opts.Description = d.Get("description").(string) + } + + if d.HasChange("rules") { + v := d.Get("rules").(*schema.Set) + + log.Printf("[DEBUG] Rules found : %#v", v) + log.Printf("[DEBUG] Rules count : %d", v.Len()) + + rules := make([]string, v.Len()) + for i, v := range v.List() { + rules[i] = v.(string) + } + opts.Rules = rules + } + + log.Printf("[DEBUG] Updating firewall policy with id %s: %#v", d.Id(), opts) + + err = policies.Update(networkingClient, d.Id(), opts).Err + if err != nil { + return err + } + + return resourceFWPolicyV1Read(d, meta) +} + +func resourceFWPolicyV1Delete(d *schema.ResourceData, meta interface{}) error { + log.Printf("[DEBUG] Destroy firewall policy: %s", d.Id()) + + config := meta.(*Config) + networkingClient, err := config.networkingV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack networking client: %s", err) + } + + for i := 0; i < 15; i++ { + + err = policies.Delete(networkingClient, d.Id()).Err + if err == nil { + break + } + + httpError, ok := err.(*gophercloud.UnexpectedResponseCodeError) + if !ok || httpError.Actual != 409 { + return err + } + + // This error usualy means that the policy is attached + // to a firewall. At this point, the firewall is probably + // being delete. So, we retry a few times. + + time.Sleep(time.Second * 2) + } + + return err +} diff --git a/builtin/providers/openstack/resource_openstack_fw_policy_v1_test.go b/builtin/providers/openstack/resource_openstack_fw_policy_v1_test.go new file mode 100644 index 000000000000..1a37a383f732 --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_fw_policy_v1_test.go @@ -0,0 +1,165 @@ +package openstack + +import ( + "fmt" + "testing" + "time" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/policies" +) + +func TestAccFWPolicyV1(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckFWPolicyV1Destroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testFirewallPolicyConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckFWPolicyV1Exists( + "openstack_fw_policy_v1.accept_test", + "", "", 0), + ), + }, + resource.TestStep{ + Config: testFirewallPolicyConfigAddRules, + Check: resource.ComposeTestCheckFunc( + testAccCheckFWPolicyV1Exists( + "openstack_fw_policy_v1.accept_test", + "accept_test", "terraform acceptance test", 2), + ), + }, + resource.TestStep{ + Config: testFirewallPolicyUpdateDeleteRule, + Check: resource.ComposeTestCheckFunc( + testAccCheckFWPolicyV1Exists( + "openstack_fw_policy_v1.accept_test", + "accept_test", "terraform acceptance test", 1), + ), + }, + }, + }) +} + +func testAccCheckFWPolicyV1Destroy(s *terraform.State) error { + + config := testAccProvider.Meta().(*Config) + networkingClient, err := config.networkingV2Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("(testAccCheckOpenstackFirewallPolicyDestroy) Error creating OpenStack networking client: %s", err) + } + for _, rs := range s.RootModule().Resources { + if rs.Type != "openstack_fw_policy_v1" { + continue + } + _, err = policies.Get(networkingClient, rs.Primary.ID).Extract() + if err == nil { + return fmt.Errorf("Firewall policy (%s) still exists.", rs.Primary.ID) + } + httpError, ok := err.(*gophercloud.UnexpectedResponseCodeError) + if !ok || httpError.Actual != 404 { + return httpError + } + } + return nil +} + +func testAccCheckFWPolicyV1Exists(n, name, description string, ruleCount int) 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(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("(testAccCheckFirewallPolicyExists) Error creating OpenStack networking client: %s", err) + } + + var found *policies.Policy + for i := 0; i < 5; i++ { + // Firewall policy creation is asynchronous. Retry some times + // if we get a 404 error. Fail on any other error. + found, err = policies.Get(networkingClient, rs.Primary.ID).Extract() + if err != nil { + httpError, ok := err.(*gophercloud.UnexpectedResponseCodeError) + if !ok || httpError.Actual != 404 { + time.Sleep(time.Second) + continue + } + } + break + } + + if err != nil { + return err + } + + if name != found.Name { + return fmt.Errorf("Expected name <%s>, but found <%s>", name, found.Name) + } + + if description != found.Description { + return fmt.Errorf("Expected description <%s>, but found <%s>", description, found.Description) + } + + if ruleCount != len(found.Rules) { + return fmt.Errorf("Expected rule count <%d>, but found <%d>", ruleCount, len(found.Rules)) + } + + return nil + } +} + +const testFirewallPolicyConfig = ` +resource "openstack_fw_policy_v1" "accept_test" { + +} +` + +const testFirewallPolicyConfigAddRules = ` +resource "openstack_fw_policy_v1" "accept_test" { + name = "accept_test" + description = "terraform acceptance test" + rules = [ + "${openstack_fw_rule_v1.accept_test_udp_deny.id}", + "${openstack_fw_rule_v1.accept_test_tcp_allow.id}" + ] +} + +resource "openstack_fw_rule_v1" "accept_test_tcp_allow" { + protocol = "tcp" + action = "allow" +} + +resource "openstack_fw_rule_v1" "accept_test_udp_deny" { + protocol = "udp" + action = "deny" +} +` + +const testFirewallPolicyUpdateDeleteRule = ` +resource "openstack_fw_policy_v1" "accept_test" { + name = "accept_test" + description = "terraform acceptance test" + rules = [ + "${openstack_fw_rule_v1.accept_test_udp_deny.id}" + ] +} + +resource "openstack_fw_rule_v1" "accept_test_udp_deny" { + protocol = "udp" + action = "deny" +} +` diff --git a/builtin/providers/openstack/resource_openstack_fw_rule_v1.go b/builtin/providers/openstack/resource_openstack_fw_rule_v1.go new file mode 100644 index 000000000000..f4dbb2fc7eae --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_fw_rule_v1.go @@ -0,0 +1,262 @@ +package openstack + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/policies" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/rules" +) + +func resourceFWRuleV1() *schema.Resource { + return &schema.Resource{ + Create: resourceFWRuleV1Create, + Read: resourceFWRuleV1Read, + Update: resourceFWRuleV1Update, + Delete: resourceFWRuleV1Delete, + + Schema: map[string]*schema.Schema{ + "region": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + DefaultFunc: envDefaultFunc("OS_REGION_NAME"), + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "protocol": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "action": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "ip_version": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Default: 4, + }, + "source_ip_address": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "destination_ip_address": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "source_port": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "destination_port": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "enabled": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "tenant_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + }, + } +} + +func resourceFWRuleV1Create(d *schema.ResourceData, meta interface{}) error { + + config := meta.(*Config) + networkingClient, err := config.networkingV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack networking client: %s", err) + } + + enabled := d.Get("enabled").(bool) + + ruleConfiguration := rules.CreateOpts{ + Name: d.Get("name").(string), + Description: d.Get("description").(string), + Protocol: d.Get("protocol").(string), + Action: d.Get("action").(string), + IPVersion: d.Get("ip_version").(int), + SourceIPAddress: d.Get("source_ip_address").(string), + DestinationIPAddress: d.Get("destination_ip_address").(string), + SourcePort: d.Get("source_port").(string), + DestinationPort: d.Get("destination_port").(string), + Enabled: &enabled, + TenantID: d.Get("tenant_id").(string), + } + + log.Printf("[DEBUG] Create firewall rule: %#v", ruleConfiguration) + + rule, err := rules.Create(networkingClient, ruleConfiguration).Extract() + + if err != nil { + return err + } + + log.Printf("[DEBUG] Firewall rule with id %s : %#v", rule.ID, rule) + + d.SetId(rule.ID) + + return resourceFWRuleV1Read(d, meta) +} + +func resourceFWRuleV1Read(d *schema.ResourceData, meta interface{}) error { + log.Printf("[DEBUG] Retrieve information about firewall rule: %s", d.Id()) + + config := meta.(*Config) + networkingClient, err := config.networkingV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack networking client: %s", err) + } + + rule, err := rules.Get(networkingClient, d.Id()).Extract() + + if err != nil { + return CheckDeleted(d, err, "LB pool") + } + + d.Set("protocol", rule.Protocol) + d.Set("action", rule.Action) + + if t, exists := d.GetOk("name"); exists && t != "" { + d.Set("name", rule.Name) + } else { + d.Set("name", "") + } + + if t, exists := d.GetOk("description"); exists && t != "" { + d.Set("description", rule.Description) + } else { + d.Set("description", "") + } + + if t, exists := d.GetOk("ip_version"); exists && t != "" { + d.Set("ip_version", rule.IPVersion) + } else { + d.Set("ip_version", "") + } + + if t, exists := d.GetOk("source_ip_address"); exists && t != "" { + d.Set("source_ip_address", rule.SourceIPAddress) + } else { + d.Set("source_ip_address", "") + } + + if t, exists := d.GetOk("destination_ip_address"); exists && t != "" { + d.Set("destination_ip_address", rule.DestinationIPAddress) + } else { + d.Set("destination_ip_address", "") + } + + if t, exists := d.GetOk("source_port"); exists && t != "" { + d.Set("source_port", rule.SourcePort) + } else { + d.Set("source_port", "") + } + + if t, exists := d.GetOk("destination_port"); exists && t != "" { + d.Set("destination_port", rule.DestinationPort) + } else { + d.Set("destination_port", "") + } + + if t, exists := d.GetOk("enabled"); exists && t != "" { + d.Set("enabled", rule.Enabled) + } else { + d.Set("enabled", "") + } + + return nil +} + +func resourceFWRuleV1Update(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + networkingClient, err := config.networkingV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack networking client: %s", err) + } + + opts := rules.UpdateOpts{} + + if d.HasChange("name") { + opts.Name = d.Get("name").(string) + } + if d.HasChange("description") { + opts.Description = d.Get("description").(string) + } + if d.HasChange("protocol") { + opts.Protocol = d.Get("protocol").(string) + } + if d.HasChange("action") { + opts.Action = d.Get("action").(string) + } + if d.HasChange("ip_version") { + opts.IPVersion = d.Get("ip_version").(int) + } + if d.HasChange("source_ip_address") { + sourceIPAddress := d.Get("source_ip_address").(string) + opts.SourceIPAddress = &sourceIPAddress + } + if d.HasChange("destination_ip_address") { + destinationIPAddress := d.Get("destination_ip_address").(string) + opts.DestinationIPAddress = &destinationIPAddress + } + if d.HasChange("source_port") { + sourcePort := d.Get("source_port").(string) + opts.SourcePort = &sourcePort + } + if d.HasChange("destination_port") { + destinationPort := d.Get("destination_port").(string) + opts.DestinationPort = &destinationPort + } + if d.HasChange("enabled") { + enabled := d.Get("enabled").(bool) + opts.Enabled = &enabled + } + + log.Printf("[DEBUG] Updating firewall rules: %#v", opts) + + err = rules.Update(networkingClient, d.Id(), opts).Err + if err != nil { + return err + } + + return resourceFWRuleV1Read(d, meta) +} + +func resourceFWRuleV1Delete(d *schema.ResourceData, meta interface{}) error { + log.Printf("[DEBUG] Destroy firewall rule: %s", d.Id()) + + config := meta.(*Config) + networkingClient, err := config.networkingV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack networking client: %s", err) + } + + rule, err := rules.Get(networkingClient, d.Id()).Extract() + if err != nil { + return err + } + + if rule.PolicyID != "" { + err := policies.RemoveRule(networkingClient, rule.PolicyID, rule.ID) + if err != nil { + return err + } + } + + return rules.Delete(networkingClient, d.Id()).Err +} diff --git a/builtin/providers/openstack/resource_openstack_fw_rule_v1_test.go b/builtin/providers/openstack/resource_openstack_fw_rule_v1_test.go new file mode 100644 index 000000000000..ba96bb8b19e0 --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_fw_rule_v1_test.go @@ -0,0 +1,185 @@ +package openstack + +import ( + "fmt" + "reflect" + "testing" + "time" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/rules" +) + +func TestAccFWRuleV1(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckFWRuleV1Destroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testFirewallRuleMinimalConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckFWRuleV1Exists( + "openstack_fw_rule_v1.accept_test_minimal", + &rules.Rule{ + Protocol: "udp", + Action: "deny", + IPVersion: 4, + Enabled: true, + }), + ), + }, + resource.TestStep{ + Config: testFirewallRuleConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckFWRuleV1Exists( + "openstack_fw_rule_v1.accept_test", + &rules.Rule{ + Name: "accept_test", + Protocol: "udp", + Action: "deny", + Description: "Terraform accept test", + IPVersion: 4, + SourceIPAddress: "1.2.3.4", + DestinationIPAddress: "4.3.2.0/24", + SourcePort: "444", + DestinationPort: "555", + Enabled: true, + }), + ), + }, + resource.TestStep{ + Config: testFirewallRuleUpdateAllFieldsConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckFWRuleV1Exists( + "openstack_fw_rule_v1.accept_test", + &rules.Rule{ + Name: "accept_test_updated_2", + Protocol: "tcp", + Action: "allow", + Description: "Terraform accept test updated", + IPVersion: 4, + SourceIPAddress: "1.2.3.0/24", + DestinationIPAddress: "4.3.2.8", + SourcePort: "666", + DestinationPort: "777", + Enabled: false, + }), + ), + }, + }, + }) +} + +func testAccCheckFWRuleV1Destroy(s *terraform.State) error { + + config := testAccProvider.Meta().(*Config) + networkingClient, err := config.networkingV2Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("(testAccCheckOpenstackFirewallRuleDestroy) Error creating OpenStack networking client: %s", err) + } + for _, rs := range s.RootModule().Resources { + if rs.Type != "openstack_firewall_rule" { + continue + } + _, err = rules.Get(networkingClient, rs.Primary.ID).Extract() + if err == nil { + return fmt.Errorf("Firewall rule (%s) still exists.", rs.Primary.ID) + } + httpError, ok := err.(*gophercloud.UnexpectedResponseCodeError) + if !ok || httpError.Actual != 404 { + return httpError + } + } + return nil +} + +func testAccCheckFWRuleV1Exists(n string, expected *rules.Rule) 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(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("(testAccCheckFirewallRuleExists) Error creating OpenStack networking client: %s", err) + } + + var found *rules.Rule + for i := 0; i < 5; i++ { + // Firewall rule creation is asynchronous. Retry some times + // if we get a 404 error. Fail on any other error. + found, err = rules.Get(networkingClient, rs.Primary.ID).Extract() + if err != nil { + httpError, ok := err.(*gophercloud.UnexpectedResponseCodeError) + if !ok || httpError.Actual != 404 { + time.Sleep(time.Second) + continue + } + } + break + } + + if err != nil { + return err + } + + expected.ID = found.ID + // Erase the tenant id because we don't want to compare + // it as long it is not present in the expected + found.TenantID = "" + + if !reflect.DeepEqual(expected, found) { + return fmt.Errorf("Expected:\n%#v\nFound:\n%#v", expected, found) + } + + return nil + } +} + +const testFirewallRuleMinimalConfig = ` +resource "openstack_fw_rule_v1" "accept_test_minimal" { + protocol = "udp" + action = "deny" +} +` + +const testFirewallRuleConfig = ` +resource "openstack_fw_rule_v1" "accept_test" { + name = "accept_test" + description = "Terraform accept test" + protocol = "udp" + action = "deny" + ip_version = 4 + source_ip_address = "1.2.3.4" + destination_ip_address = "4.3.2.0/24" + source_port = "444" + destination_port = "555" + enabled = true +} +` + +const testFirewallRuleUpdateAllFieldsConfig = ` +resource "openstack_fw_rule_v1" "accept_test" { + name = "accept_test_updated_2" + description = "Terraform accept test updated" + protocol = "tcp" + action = "allow" + ip_version = 4 + source_ip_address = "1.2.3.0/24" + destination_ip_address = "4.3.2.8" + source_port = "666" + destination_port = "777" + enabled = false +} +`