diff --git a/builtin/providers/openstack/resource_openstack_lbaas_pool_v2.go b/builtin/providers/openstack/resource_openstack_lbaas_pool_v2.go new file mode 100644 index 000000000000..e5de1996a298 --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_lbaas_pool_v2.go @@ -0,0 +1,322 @@ +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/pools" +) + +func resourcePoolV2() *schema.Resource { + return &schema.Resource{ + Create: resourcePoolV2Create, + Read: resourcePoolV2Read, + Delete: resourcePoolV2Delete, + + Schema: map[string]*schema.Schema{ + "region": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + DefaultFunc: schema.EnvDefaultFunc("OS_REGION_NAME", ""), + }, + + "tenant_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "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 != "lTCP" && value != "HTTP" && value != "HTTPS" { + errors = append(errors, fmt.Errorf( + "Only 'TCP', 'HTTP', and 'HTTPS' are supported values for 'protocol'")) + } + return + }, + }, + + // One of loadbalancer_id or listener_id must be provided + "loadbalancer_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + // One of loadbalancer_id or listener_id must be provided + "listener_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "lb_method": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + if value != "ROUND_ROBIN" && value != "LEAST_CONNECTIONS" && value != "SOURCE_IP" { + errors = append(errors, fmt.Errorf( + "Only 'ROUND_ROBIN', 'LEAST_CONNECTIONS', and 'SOURCE_IP' are supported values for 'lb_method'")) + } + return + }, + }, + + "persistence": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + if value != "SOURCE_IP" && value != "HTTP_COOKIE" && value != "APP_COOKIE" { + errors = append(errors, fmt.Errorf( + "Only 'SOURCE_IP', 'HTTP_COOKIE', and 'APP_COOKIE' are supported values for 'persistence'")) + } + return + }, + }, + + "cookie_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + }, + }, + + "admin_state_up": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + }, + + "id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + }, + } +} + +func resourcePoolV2Create(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) + var persistence pools.SessionPersistence + if p, ok := d.GetOk("persistence"); ok { + pV := (p.([]interface{}))[0].(map[string]interface{}) + + persistence = pools.SessionPersistence{ + Type: pV["type"].(string), + CookieName: pV["cookie_name"].(string), + } + } + createOpts := pools.CreateOpts{ + TenantID: d.Get("tenant_id").(string), + Name: d.Get("name").(string), + Description: d.Get("description").(string), + Protocol: pools.Protocol(d.Get("protocol").(string)), + LoadbalancerID: d.Get("loadbalancer_id").(string), + ListenerID: d.Get("listener_id").(string), + LBMethod: pools.LBMethod(d.Get("lb_method").(string)), + AdminStateUp: &adminStateUp, + } + // Must omit if not set + if persistence != (pools.SessionPersistence{}) { + createOpts.Persistence = &persistence + } + + log.Printf("[DEBUG] Create Options: %#v", createOpts) + pool, err := pools.Create(networkingClient, createOpts).Extract() + if err != nil { + return fmt.Errorf("Error creating OpenStack LBaaSV2 pool: %s", err) + } + log.Printf("[INFO] pool ID: %s", pool.ID) + + log.Printf("[DEBUG] Waiting for Openstack LBaaSV2 pool (%s) to become available.", pool.ID) + + stateConf := &resource.StateChangeConf{ + Pending: []string{"PENDING_CREATE"}, + Target: []string{"ACTIVE"}, + Refresh: waitForPoolActive(networkingClient, pool.ID), + Timeout: 2 * time.Minute, + Delay: 5 * time.Second, + MinTimeout: 3 * time.Second, + } + + _, err = stateConf.WaitForState() + if err != nil { + return err + } + + d.SetId(pool.ID) + + return resourcePoolV2Read(d, meta) +} + +func resourcePoolV2Read(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) + } + + pool, err := pools.Get(networkingClient, d.Id()).Extract() + if err != nil { + return CheckDeleted(d, err, "LBV2 Pool") + } + + log.Printf("[DEBUG] Retreived OpenStack LBaaSV2 Pool %s: %+v", d.Id(), pool) + + d.Set("lb_method", pool.LBMethod) + d.Set("protocol", pool.Protocol) + d.Set("description", pool.Description) + d.Set("tenant_id", pool.TenantID) + d.Set("admin_state_up", pool.AdminStateUp) + d.Set("name", pool.Name) + d.Set("id", pool.ID) + d.Set("persistence", pool.Persistence) + + return nil +} + +func resourcePoolV2Update(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 pools.UpdateOpts + if d.HasChange("lb_method") { + updateOpts.LBMethod = pools.LBMethod(d.Get("lb_method").(string)) + } + if d.HasChange("name") { + updateOpts.Name = d.Get("name").(string) + } + if d.HasChange("description") { + updateOpts.Description = d.Get("description").(string) + } + if d.HasChange("admin_state_up") { + asu := d.Get("admin_state_up").(bool) + updateOpts.AdminStateUp = &asu + } + + log.Printf("[DEBUG] Updating OpenStack LBaaSV2 Pool %s with options: %+v", d.Id(), updateOpts) + + _, err = pools.Update(networkingClient, d.Id(), updateOpts).Extract() + if err != nil { + return fmt.Errorf("Error updating OpenStack LBaaSV2 Pool: %s", err) + } + + return resourcePoolV2Read(d, meta) +} + +func resourcePoolV2Delete(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: waitForPoolDelete(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 Pool: %s", err) + } + + d.SetId("") + return nil +} + +func waitForPoolActive(networkingClient *gophercloud.ServiceClient, poolID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + pool, err := pools.Get(networkingClient, poolID).Extract() + if err != nil { + return nil, "", err + } + + // The pool resource has no Status attribute, so a successful Get is the best we can do + log.Printf("[DEBUG] OpenStack LBaaSV2 Pool: %+v", pool) + return pool, "ACTIVE", nil + } +} + +func waitForPoolDelete(networkingClient *gophercloud.ServiceClient, poolID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + log.Printf("[DEBUG] Attempting to delete OpenStack LBaaSV2 Pool %s", poolID) + + pool, err := pools.Get(networkingClient, poolID).Extract() + if err != nil { + errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError) + if !ok { + return pool, "ACTIVE", err + } + if errCode.Actual == 404 { + log.Printf("[DEBUG] Successfully deleted OpenStack LBaaSV2 Pool %s", poolID) + return pool, "DELETED", nil + } + } + + log.Printf("[DEBUG] Openstack LBaaSV2 Pool: %+v", pool) + err = pools.Delete(networkingClient, poolID).ExtractErr() + if err != nil { + errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError) + if !ok { + return pool, "ACTIVE", err + } + if errCode.Actual == 404 { + log.Printf("[DEBUG] Successfully deleted OpenStack LBaaSV2 Pool %s", poolID) + return pool, "DELETED", nil + } + } + + log.Printf("[DEBUG] OpenStack LBaaSV2 Pool %s still active.", poolID) + return pool, "ACTIVE", nil + } +} diff --git a/builtin/providers/openstack/resource_openstack_lbaas_pool_v2_test.go b/builtin/providers/openstack/resource_openstack_lbaas_pool_v2_test.go new file mode 100644 index 000000000000..6edf2063bb34 --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_lbaas_pool_v2_test.go @@ -0,0 +1,155 @@ +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/pools" +) + +func TestAccLBV2Pool_basic(t *testing.T) { + var pool pools.Pool + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckLBV2PoolDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: TestAccLBV2PoolConfig_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckLBV2PoolExists(t, "openstack_lbaas_pool_v2.pool_1", &pool), + ), + }, + resource.TestStep{ + Config: TestAccLBV2PoolConfig_update, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("openstack_lbaas_pool_v2.pool_1", "name", "tf_test_pool_update"), + ), + }, + }, + }) +} + +func testAccCheckLBV2PoolDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + networkingClient, err := config.networkingV2Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("(testAccCheckLBV2PoolDestroy) 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_pool_v2" { + continue + } + + _, err := pools.Get(networkingClient, rs.Primary.ID).Extract() + if err == nil { + return fmt.Errorf("Pool still exists: %s", rs.Primary.ID) + } + } + + return nil +} + +func testAccCheckLBV2PoolExists(t *testing.T, n string, pool *pools.Pool) 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("(testAccCheckLBV2PoolExists) Error creating OpenStack networking client: %s", err) + } + + found, err := pools.Get(networkingClient, rs.Primary.ID).Extract() + if err != nil { + return err + } + + if found.ID != rs.Primary.ID { + return fmt.Errorf("Member not found") + } + + *pool = *found + + return nil + } +} + +var TestAccLBV2PoolConfig_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" + } + + resource "openstack_lbaas_pool_v2" "pool_1" { + protocol = "HTTP" + lb_method = "ROUND_ROBIN" + listener_id = "${openstack_lbaas_listener_v2.listener_1.id}" + name = "tf_test_pool" + }`) + +var TestAccLBV2PoolConfig_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" + } + + resource "openstack_lbaas_pool_v2" "pool_1" { + protocol = "HTTP" + lb_method = "LEAST_CONNECTIONS" + listener_id = "${openstack_lbaas_listener_v2.listener_1.id}" + name = "tf_test_pool_update" + admin_state_up = "true" + }`) diff --git a/website/source/docs/providers/openstack/r/lb_pool_v2.html.markdown b/website/source/docs/providers/openstack/r/lb_pool_v2.html.markdown new file mode 100644 index 000000000000..f779c0f0d3ce --- /dev/null +++ b/website/source/docs/providers/openstack/r/lb_pool_v2.html.markdown @@ -0,0 +1,86 @@ +--- +layout: "openstack" +page_title: "OpenStack: openstack_lbaas_pool_v2" +sidebar_current: "docs-openstack-resource-lbaas-pool-v2" +description: |- + Manages a V2 pool resource within OpenStack. +--- + +# openstack\_lbaas\_pool\_v2 + +Manages a V2 pool resource within OpenStack. + +## Example Usage + +``` +resource "openstack_lbaas_pool_v2" "pool_1" { + protocol = "ProtocolHTTP" + lb_method = "LBMethodRoundRobin" + listener_id = "d9415786-5f1a-428b-b35f-2f1523e146d2" + persistence { + type = "HTTP_COOKIE" + cookie_name = "testCookie" + } +} +``` + +## 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 + pool. + +* `tenant_id` - (Optional) Required for admins. The UUID of the tenant who owns + the pool. Only administrative users can specify a tenant UUID + other than their own. Changing this creates a new pool. + +* `name` - (Optional) Human-readable name for the pool. + +* `description` - (Optional) Human-readable description for the pool. + +* `protocol` = (Required) The protocol - can either be TCP, HTTP or HTTPS. + Changing this creates a new pool. + +* `loadbalancer_id` - (Optional) The load balancer on which to provision this + pool. Changing this creates a new pool. + Note: One of LoadbalancerID or ListenerID must be provided. + +* `listener_id` - (Optional) The Listener on which the members of the pool + will be associated with. Changing this creates a new pool. + Note: One of LoadbalancerID or ListenerID must be provided. + +* `lb_method` - (Required) The algorithm used to distribute load between the + members of the pool. The current specification supports + LBMethodRoundRobin, LBMethodLeastConnections and LBMethodSourceIp as valid + values for this attribute. + +* `persistence` - Omit this field to prevent session persistence. Indicates + whether connections in the same session will be processed by the same Pool + member or not. Changing this creates a new pool. + +* `admin_state_up` - (Optional) The administrative state of the pool. + A valid value is true (UP) or false (DOWN). + +The `persistence` argument supports: + +* `type` - (Required) The type of persistence mode. The current specification + supports SOURCE_IP, HTTP_COOKIE, and APP_COOKIE. + +* `cookie_name` - (Required) The name of the cookie if persistence mode is set + appropriately. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The unique ID for the pool. +* `tenant_id` - See Argument Reference above. +* `name` - See Argument Reference above. +* `description` - See Argument Reference above. +* `protocol` - See Argument Reference above. +* `lb_method` - See Argument Reference above. +* `persistence` - See Argument Reference above. +* `admin_state_up` - See Argument Reference above.