From e15935912508a5c09a7803ca58e347212e2e48ab Mon Sep 17 00:00:00 2001 From: dkalleg Date: Fri, 3 Jun 2016 19:09:42 -0700 Subject: [PATCH] OpenStack LBaaS v2 support - Listener resource CRUD, Tests and Docs for managing a Listener resource --- .../resource_openstack_lbaas_listener_v2.go | 317 ++++++++++++++++++ ...source_openstack_lbaas_listener_v2_test.go | 145 ++++++++ .../openstack/r/lb_listener_v2.html.markdown | 78 +++++ 3 files changed, 540 insertions(+) create mode 100644 builtin/providers/openstack/resource_openstack_lbaas_listener_v2.go create mode 100644 builtin/providers/openstack/resource_openstack_lbaas_listener_v2_test.go create mode 100644 website/source/docs/providers/openstack/r/lb_listener_v2.html.markdown diff --git a/builtin/providers/openstack/resource_openstack_lbaas_listener_v2.go b/builtin/providers/openstack/resource_openstack_lbaas_listener_v2.go new file mode 100644 index 000000000000..ce99c2287828 --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_lbaas_listener_v2.go @@ -0,0 +1,317 @@ +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/lbaas_v2/listeners" +) + +func resourceListenerV2() *schema.Resource { + return &schema.Resource{ + Create: resourceListenerV2Create, + Read: resourceListenerV2Read, + Delete: resourceListenerV2Delete, + + Schema: map[string]*schema.Schema{ + "region": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + DefaultFunc: schema.EnvDefaultFunc("OS_REGION_NAME", ""), + }, + + "protocol": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + if value != "TCP" && value != "HTTP" && value != "HTTPS" { + errors = append(errors, fmt.Errorf( + "Only 'TCP', 'HTTP', and 'HTTPS' are supported values for 'protocol'")) + } + return + }, + }, + + "protocol_port": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + ForceNew: true, + }, + + "tenant_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "loadbalancer_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "default_pool_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "connection_limit": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + }, + + "default_tls_container_ref": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "sni_container_refs": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + + "admin_state_up": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + }, + + "id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + }, + } +} + +func resourceListenerV2Create(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) + connLimit := d.Get("connection_limit").(int) + var sniContainerRefs []string + if raw, ok := d.GetOk("sni_container_refs"); ok { + for _, v := range raw.([]interface{}) { + sniContainerRefs = append(sniContainerRefs, v.(string)) + } + } + createOpts := listeners.CreateOpts{ + Protocol: listeners.Protocol(d.Get("protocol").(string)), + ProtocolPort: d.Get("protocol_port").(int), + TenantID: d.Get("tenant_id").(string), + LoadbalancerID: d.Get("loadbalancer_id").(string), + Name: d.Get("name").(string), + DefaultPoolID: d.Get("default_pool_id").(string), + Description: d.Get("description").(string), + ConnLimit: &connLimit, + DefaultTlsContainerRef: d.Get("default_tls_container_ref").(string), + SniContainerRefs: sniContainerRefs, + AdminStateUp: &adminStateUp, + } + + log.Printf("[DEBUG] Create Options: %#v", createOpts) + listener, err := listeners.Create(networkingClient, createOpts).Extract() + if err != nil { + return fmt.Errorf("Error creating OpenStack LBaaSV2 listener: %s", err) + } + log.Printf("[INFO] Listener ID: %s", listener.ID) + + log.Printf("[DEBUG] Waiting for Openstack LBaaSV2 listener (%s) to become available.", listener.ID) + + stateConf := &resource.StateChangeConf{ + Pending: []string{"PENDING_CREATE"}, + Target: []string{"ACTIVE"}, + Refresh: waitForListenerActive(networkingClient, listener.ID), + Timeout: 2 * time.Minute, + Delay: 5 * time.Second, + MinTimeout: 3 * time.Second, + } + + _, err = stateConf.WaitForState() + if err != nil { + return err + } + + d.SetId(listener.ID) + + return resourceListenerV2Read(d, meta) +} + +func resourceListenerV2Read(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) + } + + listener, err := listeners.Get(networkingClient, d.Id()).Extract() + if err != nil { + return CheckDeleted(d, err, "LBV2 listener") + } + + log.Printf("[DEBUG] Retreived OpenStack LBaaSV2 listener %s: %+v", d.Id(), listener) + + d.Set("id", listener.ID) + d.Set("name", listener.Name) + d.Set("protocol", listener.Protocol) + d.Set("tenant_id", listener.TenantID) + d.Set("description", listener.Description) + d.Set("protocol_port", listener.ProtocolPort) + d.Set("admin_state_up", listener.AdminStateUp) + d.Set("default_pool_id", listener.DefaultPoolID) + d.Set("connection_limit", listener.ConnLimit) + d.Set("sni_container_refs", listener.SniContainerRefs) + d.Set("default_tls_container_ref", listener.DefaultTlsContainerRef) + + return nil +} + +func resourceListenerV2Update(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) + } + + var updateOpts listeners.UpdateOpts + if d.HasChange("name") { + updateOpts.Name = d.Get("name").(string) + } + if d.HasChange("description") { + updateOpts.Description = d.Get("description").(string) + } + if d.HasChange("connection_limit") { + connLimit := d.Get("connection_limit").(int) + updateOpts.ConnLimit = &connLimit + } + if d.HasChange("default_tls_container_ref") { + updateOpts.DefaultTlsContainerRef = d.Get("default_tls_container_ref").(string) + } + if d.HasChange("sni_container_refs") { + var sniContainerRefs []string + if raw, ok := d.GetOk("sni_container_refs"); ok { + for _, v := range raw.([]interface{}) { + sniContainerRefs = append(sniContainerRefs, v.(string)) + } + } + updateOpts.SniContainerRefs = sniContainerRefs + } + if d.HasChange("admin_state_up") { + asu := d.Get("admin_state_up").(bool) + updateOpts.AdminStateUp = &asu + } + + log.Printf("[DEBUG] Updating OpenStack LBaaSV2 Listener %s with options: %+v", d.Id(), updateOpts) + + _, err = listeners.Update(networkingClient, d.Id(), updateOpts).Extract() + if err != nil { + return fmt.Errorf("Error updating OpenStack LBaaSV2 Listener: %s", err) + } + + return resourceListenerV2Read(d, meta) + +} + +func resourceListenerV2Delete(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) + } + + stateConf := &resource.StateChangeConf{ + Pending: []string{"ACTIVE", "PENDING_DELETE"}, + Target: []string{"DELETED"}, + Refresh: waitForListenerDelete(networkingClient, d.Id()), + Timeout: 2 * time.Minute, + Delay: 5 * time.Second, + MinTimeout: 3 * time.Second, + } + + _, err = stateConf.WaitForState() + if err != nil { + return fmt.Errorf("Error deleting OpenStack LBaaSV2 listener: %s", err) + } + + d.SetId("") + return nil +} + +func waitForListenerActive(networkingClient *gophercloud.ServiceClient, listenerID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + listener, err := listeners.Get(networkingClient, listenerID).Extract() + if err != nil { + return nil, "", err + } + + // The listener resource has no Status attribute, so a successful Get is the best we can do + log.Printf("[DEBUG] OpenStack LBaaSV2 listener: %+v", listener) + return listener, "ACTIVE", nil + } +} + +func waitForListenerDelete(networkingClient *gophercloud.ServiceClient, listenerID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + log.Printf("[DEBUG] Attempting to delete OpenStack LBaaSV2 listener %s", listenerID) + + listener, err := listeners.Get(networkingClient, listenerID).Extract() + if err != nil { + errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError) + if !ok { + return listener, "ACTIVE", err + } + if errCode.Actual == 404 { + log.Printf("[DEBUG] Successfully deleted OpenStack LBaaSV2 listener %s", listenerID) + return listener, "DELETED", nil + } + } + + log.Printf("[DEBUG] Openstack LBaaSV2 listener: %+v", listener) + err = listeners.Delete(networkingClient, listenerID).ExtractErr() + if err != nil { + errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError) + if !ok { + return listener, "ACTIVE", err + } + if errCode.Actual == 404 { + log.Printf("[DEBUG] Successfully deleted OpenStack LBaaSV2 listener %s", listenerID) + return listener, "DELETED", nil + } + } + + log.Printf("[DEBUG] OpenStack LBaaSV2 listener %s still active.", listenerID) + return listener, "ACTIVE", nil + } +} diff --git a/builtin/providers/openstack/resource_openstack_lbaas_listener_v2_test.go b/builtin/providers/openstack/resource_openstack_lbaas_listener_v2_test.go new file mode 100644 index 000000000000..ca0e0ae2674c --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_lbaas_listener_v2_test.go @@ -0,0 +1,145 @@ +package openstack + +import ( + "fmt" + "log" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas_v2/listeners" +) + +func TestAccLBV2Listener_basic(t *testing.T) { + var listener listeners.Listener + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckLBV2ListenerDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: TestAccLBV2ListenerConfig_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckLBV2ListenerExists(t, "openstack_lbaas_listener_v2.listener_1", &listener), + ), + }, + resource.TestStep{ + Config: TestAccLBV2ListenerConfig_update, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("openstack_lbaas_listener_v2.listener_1", "name", "tf_test_listener_updated"), + resource.TestCheckResourceAttr("openstack_lbaas_listener_v2.listener_1", "connection_limit", "100"), + ), + }, + }, + }) +} + +func testAccCheckLBV2ListenerDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + networkingClient, err := config.networkingV2Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("(testAccCheckLBV2ListenerDestroy) Error creating OpenStack networking client: %s", err) + } + + for _, rs := range s.RootModule().Resources { + log.Printf("[FINDME] rs TYPE is: %T", rs) + + if rs.Type != "openstack_lbaas_listener_v2" { + continue + } + + _, err := listeners.Get(networkingClient, rs.Primary.ID).Extract() + if err == nil { + return fmt.Errorf("Listener still exists: %s", rs.Primary.ID) + } + } + + return nil +} + +func testAccCheckLBV2ListenerExists(t *testing.T, n string, listener *listeners.Listener) 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("(testAccCheckLBV2ListenerExists) Error creating OpenStack networking client: %s", err) + } + + found, err := listeners.Get(networkingClient, rs.Primary.ID).Extract() + if err != nil { + return err + } + + if found.ID != rs.Primary.ID { + return fmt.Errorf("Member not found") + } + + *listener = *found + + return nil + } +} + +var TestAccLBV2ListenerConfig_basic = fmt.Sprintf(` + resource "openstack_networking_network_v2" "network_1" { + name = "tf_test_network" + admin_state_up = "true" + } + + resource "openstack_networking_subnet_v2" "subnet_1" { + network_id = "${openstack_networking_network_v2.network_1.id}" + cidr = "192.168.199.0/24" + ip_version = 4 + name = "tf_test_subnet" + } + + resource "openstack_lbaas_loadbalancer_v2" "loadbalancer_1" { + vip_subnet_id = "${openstack_networking_subnet_v2.subnet_1.id}" + name = "tf_test_loadbalancer_v2" + } + + resource "openstack_lbaas_listener_v2" "listener_1" { + protocol = "HTTP" + protocol_port = 8080 + loadbalancer_id = "${openstack_lbaas_loadbalancer_v2.loadbalancer_1.id}" + name = "tf_test_listener" + } + `) + +var TestAccLBV2ListenerConfig_update = fmt.Sprintf(` + resource "openstack_networking_network_v2" "network_1" { + name = "tf_test_network" + admin_state_up = "true" + } + + resource "openstack_networking_subnet_v2" "subnet_1" { + network_id = "${openstack_networking_network_v2.network_1.id}" + cidr = "192.168.199.0/24" + ip_version = 4 + name = "tf_test_subnet" + } + + resource "openstack_lbaas_loadbalancer_v2" "loadbalancer_1" { + vip_subnet_id = "${openstack_networking_subnet_v2.subnet_1.id}" + name = "tf_test_loadbalancer_v2" + } + + resource "openstack_lbaas_listener_v2" "listener_1" { + protocol = "HTTP" + protocol_port = 8080 + loadbalancer_id = "${openstack_lbaas_loadbalancer_v2.loadbalancer_1.id}" + name = "tf_test_listener_updated" + connection_limit = 100 + admin_state_up = "true" + } +`) diff --git a/website/source/docs/providers/openstack/r/lb_listener_v2.html.markdown b/website/source/docs/providers/openstack/r/lb_listener_v2.html.markdown new file mode 100644 index 000000000000..ac042223e5fa --- /dev/null +++ b/website/source/docs/providers/openstack/r/lb_listener_v2.html.markdown @@ -0,0 +1,78 @@ +--- +layout: "openstack" +page_title: "OpenStack: openstack_lbaas_listener_v2" +sidebar_current: "docs-openstack-resource-lbaas-listener-v2" +description: |- + Manages a V2 listener resource within OpenStack. +--- + +# openstack\_lbaas\_listener\_v2 + +Manages a V2 listener resource within OpenStack. + +## Example Usage + +``` +resource "openstack_lbaas_listener_v2" "listener_1" { + protocol = "HTTP" + protocol_port = 8080 + loadbalancer_id = "d9415786-5f1a-428b-b35f-2f1523e146d2" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `region` - (Required) The region in which to obtain the V2 Networking client. + A Networking client is needed to create an . If omitted, the + `OS_REGION_NAME` environment variable is used. Changing this creates a new + Listener. + +* `protocol` = (Required) The protocol - can either be TCP, HTTP or HTTPS. + Changing this creates a new Listener. + +* `protocol_port` = (Required) The port on which to listen for client traffic. + Changing this creates a new Listener. + +* `tenant_id` - (Optional) Required for admins. The UUID of the tenant who owns + the Listener. Only administrative users can specify a tenant UUID + other than their own. Changing this creates a new Listener. + +* `loadbalancer_id` - (Required) The load balancer on which to provision this + Listener. Changing this creates a new Listener. + +* `name` - (Optional) Human-readable name for the Listener. Does not have + to be unique. + +* `default_pool_id` - (Optional) The ID of the default pool with which the + Listener is associated. Changing this creates a new Listener. + +* `description` - (Optional) Human-readable description for the Listener. + +* `connection_limit` - (Optional) The maximum number of connections allowed + for the Listener. + +* `default_tls_container_ref` - (Optional) A reference to a container of TLS + secrets. + +* `sni_container_refs` - (Optional) A list of references to TLS secrets. + +* `admin_state_up` - (Optional) The administrative state of the Listener. + A valid value is true (UP) or false (DOWN). + +## Attributes Reference + +The following attributes are exported: + +* `id` - The unique ID for the Listener. +* `protocol` - See Argument Reference above. +* `protocol_port` - See Argument Reference above. +* `tenant_id` - See Argument Reference above. +* `name` - See Argument Reference above. +* `default_port_id` - See Argument Reference above. +* `description` - See Argument Reference above. +* `connection_limit` - See Argument Reference above. +* `default_tls_container_ref` - See Argument Reference above. +* `sni_container_refs` - See Argument Reference above. +* `admin_state_up` - See Argument Reference above.