diff --git a/builtin/providers/openstack/resource_openstack_lbaas_member_v2.go b/builtin/providers/openstack/resource_openstack_lbaas_member_v2.go new file mode 100644 index 000000000000..2af4fee4cda7 --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_lbaas_member_v2.go @@ -0,0 +1,273 @@ +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 resourceMemberV2() *schema.Resource { + return &schema.Resource{ + Create: resourceMemberV2Create, + Read: resourceMemberV2Read, + Delete: resourceMemberV2Delete, + + Schema: map[string]*schema.Schema{ + "region": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + DefaultFunc: schema.EnvDefaultFunc("OS_REGION_NAME", ""), + }, + + "name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "tenant_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "address": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "protocol_port": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + ForceNew: true, + }, + + "weight": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Computed: true, + ForceNew: true, + ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { + value := v.(int) + if value < 1 { + errors = append(errors, fmt.Errorf( + "Only numbers greater than 0 are supported values for 'weight'")) + } + return + }, + }, + + "subnet_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "admin_state_up": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + }, + + "pool_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + }, + } +} + +func resourceMemberV2Create(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) + } + + subnetID := d.Get("subnet_id").(string) + adminStateUp := d.Get("admin_state_up").(bool) + createOpts := pools.MemberCreateOpts{ + Name: d.Get("name").(string), + TenantID: d.Get("tenant_id").(string), + Address: d.Get("address").(string), + ProtocolPort: d.Get("protocol_port").(int), + Weight: d.Get("weight").(int), + AdminStateUp: &adminStateUp, + } + // Must omit if not set + if subnetID != "" { + createOpts.SubnetID = subnetID + } + + poolID := d.Get("pool_id").(string) + + log.Printf("[DEBUG] Create Options: %#v", createOpts) + member, err := pools.CreateAssociateMember(networkingClient, poolID, createOpts).ExtractMember() + if err != nil { + return fmt.Errorf("Error creating OpenStack LBaaSV2 member: %s", err) + } + log.Printf("[INFO] member ID: %s", member.ID) + + log.Printf("[DEBUG] Waiting for Openstack LBaaSV2 member (%s) to become available.", member.ID) + + stateConf := &resource.StateChangeConf{ + Pending: []string{"PENDING_CREATE"}, + Target: []string{"ACTIVE"}, + Refresh: waitForMemberActive(networkingClient, poolID, member.ID), + Timeout: 2 * time.Minute, + Delay: 5 * time.Second, + MinTimeout: 3 * time.Second, + } + + _, err = stateConf.WaitForState() + if err != nil { + return err + } + + d.SetId(member.ID) + + return resourceMemberV2Read(d, meta) +} + +func resourceMemberV2Read(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) + } + + member, err := pools.GetAssociateMember(networkingClient, d.Get("pool_id").(string), d.Id()).ExtractMember() + if err != nil { + return CheckDeleted(d, err, "LBV2 Member") + } + + log.Printf("[DEBUG] Retreived OpenStack LBaaSV2 Member %s: %+v", d.Id(), member) + + d.Set("name", member.Name) + d.Set("weight", member.Weight) + d.Set("admin_state_up", member.AdminStateUp) + d.Set("tenant_id", member.TenantID) + d.Set("subnet_id", member.SubnetID) + d.Set("address", member.Address) + d.Set("protocol_port", member.ProtocolPort) + d.Set("id", member.ID) + + return nil +} + +func resourceMemberV2Update(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.MemberUpdateOpts + if d.HasChange("name") { + updateOpts.Name = d.Get("name").(string) + } + if d.HasChange("weight") { + updateOpts.Weight = d.Get("weight").(int) + } + if d.HasChange("admin_state_up") { + asu := d.Get("admin_state_up").(bool) + updateOpts.AdminStateUp = &asu + } + + log.Printf("[DEBUG] Updating OpenStack LBaaSV2 Member %s with options: %+v", d.Id(), updateOpts) + + _, err = pools.UpdateAssociateMember(networkingClient, d.Get("pool_id").(string), d.Id(), updateOpts).ExtractMember() + if err != nil { + return fmt.Errorf("Error updating OpenStack LBaaSV2 Member: %s", err) + } + + return resourceMemberV2Read(d, meta) +} + +func resourceMemberV2Delete(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: waitForMemberDelete(networkingClient, d.Get("pool_id").(string), 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 Member: %s", err) + } + + d.SetId("") + return nil +} + +func waitForMemberActive(networkingClient *gophercloud.ServiceClient, poolID string, memberID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + member, err := pools.GetAssociateMember(networkingClient, poolID, memberID).ExtractMember() + if err != nil { + return nil, "", err + } + + // The member resource has no Status attribute, so a successful Get is the best we can do + log.Printf("[DEBUG] OpenStack LBaaSV2 Member: %+v", member) + return member, "ACTIVE", nil + } +} + +func waitForMemberDelete(networkingClient *gophercloud.ServiceClient, poolID string, memberID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + log.Printf("[DEBUG] Attempting to delete OpenStack LBaaSV2 Member %s", memberID) + + member, err := pools.GetAssociateMember(networkingClient, poolID, memberID).ExtractMember() + if err != nil { + errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError) + if !ok { + return member, "ACTIVE", err + } + if errCode.Actual == 404 { + log.Printf("[DEBUG] Successfully deleted OpenStack LBaaSV2 Member %s", memberID) + return member, "DELETED", nil + } + } + + log.Printf("[DEBUG] Openstack LBaaSV2 Member: %+v", member) + err = pools.DeleteMember(networkingClient, poolID, memberID).ExtractErr() + if err != nil { + errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError) + if !ok { + return member, "ACTIVE", err + } + if errCode.Actual == 404 { + log.Printf("[DEBUG] Successfully deleted OpenStack LBaaSV2 Member %s", memberID) + return member, "DELETED", nil + } + } + + log.Printf("[DEBUG] OpenStack LBaaSV2 Member %s still active.", memberID) + return member, "ACTIVE", nil + } +} diff --git a/builtin/providers/openstack/resource_openstack_lbaas_member_v2_test.go b/builtin/providers/openstack/resource_openstack_lbaas_member_v2_test.go new file mode 100644 index 000000000000..232db24dcd2b --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_lbaas_member_v2_test.go @@ -0,0 +1,171 @@ +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 TestAccLBV2Member_basic(t *testing.T) { + var member pools.Member + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckLBV2MemberDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: TestAccLBV2MemberConfig_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckLBV2MemberExists(t, "openstack_lbaas_member_v2.member_1", &member), + ), + }, + resource.TestStep{ + Config: TestAccLBV2MemberConfig_update, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("openstack_lbaas_member_v2.member_1", "weight", "10"), + ), + }, + }, + }) +} + +func testAccCheckLBV2MemberDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + networkingClient, err := config.networkingV2Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("(testAccCheckLBV2MemberDestroy) 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_member_v2" { + continue + } + + log.Printf("[FINDME] rs.Primary.Attributes: %#v", rs.Primary.Attributes) + _, err := pools.GetAssociateMember(networkingClient, rs.Primary.Attributes["pool_id"], rs.Primary.ID).ExtractMember() + if err == nil { + return fmt.Errorf("Member still exists: %s", rs.Primary.ID) + } + } + + return nil +} + +func testAccCheckLBV2MemberExists(t *testing.T, n string, member *pools.Member) 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("(testAccCheckLBV2MemberExists) Error creating OpenStack networking client: %s", err) + } + + found, err := pools.GetAssociateMember(networkingClient, rs.Primary.Attributes["pool_id"], rs.Primary.ID).ExtractMember() + if err != nil { + return err + } + + if found.ID != rs.Primary.ID { + return fmt.Errorf("Member not found") + } + + *member = *found + + return nil + } +} + +var TestAccLBV2MemberConfig_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" +} + +resource "openstack_lbaas_member_v2" "member_1" { + address = "192.168.199.10" + pool_id = "${openstack_lbaas_pool_v2.pool_1.id}" + protocol_port = 8080 + subnet_id = "${openstack_networking_subnet_v2.subnet_1.id}" +}`) + +var TestAccLBV2MemberConfig_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 = "ROUND_ROBIN" + listener_id = "${openstack_lbaas_listener_v2.listener_1.id}" + name = "tf_test_pool" +} + +resource "openstack_lbaas_member_v2" "member_1" { + address = "192.168.199.10" + pool_id = "${openstack_lbaas_pool_v2.pool_1.id}" + protocol_port = 8080 + subnet_id = "${openstack_networking_subnet_v2.subnet_1.id}" + weight = 10 + admin_state_up = "true" +}`) diff --git a/website/source/docs/providers/openstack/r/lb_member_v2.html.markdown b/website/source/docs/providers/openstack/r/lb_member_v2.html.markdown new file mode 100644 index 000000000000..214ef7a2231d --- /dev/null +++ b/website/source/docs/providers/openstack/r/lb_member_v2.html.markdown @@ -0,0 +1,63 @@ +--- +layout: "openstack" +page_title: "OpenStack: openstack_lbaas_member_v2" +sidebar_current: "docs-openstack-resource-lbaas-member-v2" +description: |- + Manages a V2 member resource within OpenStack. +--- + +# openstack\_lbaas\_member\_v2 + +Manages a V2 member resource within OpenStack. + +## Example Usage + +``` +resource "openstack_lbaas_member_v2" "member_1" { + address = "192.168.199.23" + protocol_port = 8080 +} +``` + +## 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 + member. + +* `name` - (Optional) Human-readable name for the member. + +* `tenant_id` - (Optional) Required for admins. The UUID of the tenant who owns + the member. Only administrative users can specify a tenant UUID + other than their own. Changing this creates a new member. + +* `address` - (Required) The IP address of the member to receive traffic from + the load balancer. Changing this creates a new member. + +* `protocol_port` - (Required) The port on which to listen for client traffic. + Changing this creates a new member. + +* `weight` - (Optional) A positive integer value that indicates the relative + portion of traffic that this member should receive from the pool. For + example, a member with a weight of 10 receives five times as much traffic + as a member with a weight of 2. + +* `admin_state_up` - (Optional) The administrative state of the member. + A valid value is true (UP) or false (DOWN). + +## Attributes Reference + +The following attributes are exported: + +* `id` - The unique ID for the member. +* `name` - See Argument Reference above. +* `weight` - See Argument Reference above. +* `admin_state_up` - See Argument Reference above. +* `tenant_id` - See Argument Reference above. +* `subnet_id` - See Argument Reference above. +* `pool_id` - See Argument Reference above. +* `address` - See Argument Reference above. +* `protocol_port` - See Argument Reference above.