diff --git a/builtin/providers/openstack/provider.go b/builtin/providers/openstack/provider.go index 213acf989982..c6adedd3cbd2 100644 --- a/builtin/providers/openstack/provider.go +++ b/builtin/providers/openstack/provider.go @@ -84,6 +84,7 @@ func Provider() terraform.ResourceProvider { "openstack_networking_network_v2": resourceNetworkingNetworkV2(), "openstack_networking_subnet_v2": resourceNetworkingSubnetV2(), "openstack_networking_floatingip_v2": resourceNetworkingFloatingIPV2(), + "openstack_networking_port_v2": resourceNetworkingPortV2(), "openstack_networking_router_v2": resourceNetworkingRouterV2(), "openstack_networking_router_interface_v2": resourceNetworkingRouterInterfaceV2(), "openstack_objectstorage_container_v1": resourceObjectStorageContainerV1(), diff --git a/builtin/providers/openstack/resource_openstack_networking_network_v2_test.go b/builtin/providers/openstack/resource_openstack_networking_network_v2_test.go index ca09c6b98e0e..faa007e32fcf 100644 --- a/builtin/providers/openstack/resource_openstack_networking_network_v2_test.go +++ b/builtin/providers/openstack/resource_openstack_networking_network_v2_test.go @@ -8,8 +8,11 @@ import ( "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers" "github.com/rackspace/gophercloud/openstack/networking/v2/networks" + "github.com/rackspace/gophercloud/openstack/networking/v2/ports" "github.com/rackspace/gophercloud/openstack/networking/v2/subnets" ) @@ -104,6 +107,81 @@ func TestAccNetworkingV2Network_netstack(t *testing.T) { }) } +func TestAccNetworkingV2Network_fullstack(t *testing.T) { + region := os.Getenv(OS_REGION_NAME) + + var instance servers.Server + var network networks.Network + var port ports.Port + var secgroup secgroups.SecurityGroup + var subnet subnets.Subnet + + var testAccNetworkingV2Network_fullstack = fmt.Sprintf(` + resource "openstack_networking_network_v2" "foo" { + region = "%s" + name = "network_1" + admin_state_up = "true" + } + + resource "openstack_networking_subnet_v2" "foo" { + region = "%s" + name = "subnet_1" + network_id = "${openstack_networking_network_v2.foo.id}" + cidr = "192.168.199.0/24" + ip_version = 4 + } + + resource "openstack_compute_secgroup_v2" "foo" { + region = "%s" + name = "secgroup_1" + description = "a security group" + rule { + from_port = 22 + to_port = 22 + ip_protocol = "tcp" + cidr = "0.0.0.0/0" + } + } + + resource "openstack_networking_port_v2" "foo" { + region = "%s" + name = "port_1" + network_id = "${openstack_networking_network_v2.foo.id}" + admin_state_up = "true" + security_groups = ["${openstack_compute_secgroup_v2.foo.id}"] + + depends_on = ["openstack_networking_subnet_v2.foo"] + } + + resource "openstack_compute_instance_v2" "foo" { + region = "%s" + name = "terraform-test" + security_groups = ["${openstack_compute_secgroup_v2.foo.name}"] + + network { + port = "${openstack_networking_port_v2.foo.id}" + } + }`, region, region, region, region, region) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckNetworkingV2NetworkDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccNetworkingV2Network_fullstack, + Check: resource.ComposeTestCheckFunc( + testAccCheckNetworkingV2NetworkExists(t, "openstack_networking_network_v2.foo", &network), + testAccCheckNetworkingV2SubnetExists(t, "openstack_networking_subnet_v2.foo", &subnet), + testAccCheckComputeV2SecGroupExists(t, "openstack_compute_secgroup_v2.foo", &secgroup), + testAccCheckNetworkingV2PortExists(t, "openstack_networking_port_v2.foo", &port), + testAccCheckComputeV2InstanceExists(t, "openstack_compute_instance_v2.foo", &instance), + ), + }, + }, + }) +} + func testAccCheckNetworkingV2NetworkDestroy(s *terraform.State) error { config := testAccProvider.Meta().(*Config) networkingClient, err := config.networkingV2Client(OS_REGION_NAME) diff --git a/builtin/providers/openstack/resource_openstack_networking_port_v2.go b/builtin/providers/openstack/resource_openstack_networking_port_v2.go new file mode 100644 index 000000000000..701e42c05c1d --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_networking_port_v2.go @@ -0,0 +1,283 @@ +package openstack + +import ( + "fmt" + "log" + "strconv" + "time" + + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" + + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/networking/v2/ports" +) + +func resourceNetworkingPortV2() *schema.Resource { + return &schema.Resource{ + Create: resourceNetworkingPortV2Create, + Read: resourceNetworkingPortV2Read, + Update: resourceNetworkingPortV2Update, + Delete: resourceNetworkingPortV2Delete, + + Schema: map[string]*schema.Schema{ + "region": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + DefaultFunc: envDefaultFuncAllowMissing("OS_REGION_NAME"), + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: false, + }, + "network_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "admin_state_up": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: false, + Computed: true, + }, + "mac_address": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + }, + "tenant_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + }, + "device_owner": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + }, + "security_groups": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + ForceNew: false, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: func(v interface{}) int { + return hashcode.String(v.(string)) + }, + }, + "device_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + }, + }, + } +} + +func resourceNetworkingPortV2Create(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) + } + + createOpts := ports.CreateOpts{ + Name: d.Get("name").(string), + AdminStateUp: resourcePortAdminStateUpV2(d), + NetworkID: d.Get("network_id").(string), + MACAddress: d.Get("mac_address").(string), + TenantID: d.Get("tenant_id").(string), + DeviceOwner: d.Get("device_owner").(string), + SecurityGroups: resourcePortSecurityGroupsV2(d), + DeviceID: d.Get("device_id").(string), + } + + log.Printf("[DEBUG] Create Options: %#v", createOpts) + p, err := ports.Create(networkingClient, createOpts).Extract() + if err != nil { + return fmt.Errorf("Error creating OpenStack Neutron network: %s", err) + } + log.Printf("[INFO] Network ID: %s", p.ID) + + log.Printf("[DEBUG] Waiting for OpenStack Neutron Port (%s) to become available.", p.ID) + + stateConf := &resource.StateChangeConf{ + Target: "ACTIVE", + Refresh: waitForNetworkPortActive(networkingClient, p.ID), + Timeout: 2 * time.Minute, + Delay: 5 * time.Second, + MinTimeout: 3 * time.Second, + } + + _, err = stateConf.WaitForState() + + d.SetId(p.ID) + + return resourceNetworkingPortV2Read(d, meta) +} + +func resourceNetworkingPortV2Read(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) + } + + p, err := ports.Get(networkingClient, d.Id()).Extract() + if err != nil { + return CheckDeleted(d, err, "port") + } + + log.Printf("[DEBUG] Retreived Port %s: %+v", d.Id(), p) + + d.Set("name", p.Name) + d.Set("admin_state_up", strconv.FormatBool(p.AdminStateUp)) + d.Set("network_id", p.NetworkID) + d.Set("mac_address", p.MACAddress) + d.Set("tenant_id", p.TenantID) + d.Set("device_owner", p.DeviceOwner) + d.Set("security_groups", p.SecurityGroups) + d.Set("device_id", p.DeviceID) + + return nil +} + +func resourceNetworkingPortV2Update(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 ports.UpdateOpts + + if d.HasChange("name") { + updateOpts.Name = d.Get("name").(string) + } + + if d.HasChange("admin_state_up") { + updateOpts.AdminStateUp = resourcePortAdminStateUpV2(d) + } + + if d.HasChange("device_owner") { + updateOpts.DeviceOwner = d.Get("device_owner").(string) + } + + if d.HasChange("security_groups") { + updateOpts.SecurityGroups = resourcePortSecurityGroupsV2(d) + } + + if d.HasChange("device_id") { + updateOpts.DeviceID = d.Get("device_id").(string) + } + + log.Printf("[DEBUG] Updating Port %s with options: %+v", d.Id(), updateOpts) + + _, err = ports.Update(networkingClient, d.Id(), updateOpts).Extract() + if err != nil { + return fmt.Errorf("Error updating OpenStack Neutron Network: %s", err) + } + + return resourceNetworkingPortV2Read(d, meta) +} + +func resourceNetworkingPortV2Delete(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"}, + Target: "DELETED", + Refresh: waitForNetworkPortDelete(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 Neutron Network: %s", err) + } + + d.SetId("") + return nil +} + +func resourcePortSecurityGroupsV2(d *schema.ResourceData) []string { + rawSecurityGroups := d.Get("security_groups").(*schema.Set) + groups := make([]string, rawSecurityGroups.Len()) + for i, raw := range rawSecurityGroups.List() { + groups[i] = raw.(string) + } + return groups +} + +func resourcePortAdminStateUpV2(d *schema.ResourceData) *bool { + value := false + + if raw, ok := d.GetOk("admin_state_up"); ok && raw == "true" { + value = true + } + + return &value +} + +func waitForNetworkPortActive(networkingClient *gophercloud.ServiceClient, portId string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + p, err := ports.Get(networkingClient, portId).Extract() + if err != nil { + return nil, "", err + } + + log.Printf("[DEBUG] OpenStack Neutron Port: %+v", p) + if p.Status == "DOWN" || p.Status == "ACTIVE" { + return p, "ACTIVE", nil + } + + return p, p.Status, nil + } +} + +func waitForNetworkPortDelete(networkingClient *gophercloud.ServiceClient, portId string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + log.Printf("[DEBUG] Attempting to delete OpenStack Neutron Port %s", portId) + + p, err := ports.Get(networkingClient, portId).Extract() + if err != nil { + errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError) + if !ok { + return p, "ACTIVE", err + } + if errCode.Actual == 404 { + log.Printf("[DEBUG] Successfully deleted OpenStack Port %s", portId) + return p, "DELETED", nil + } + } + + err = ports.Delete(networkingClient, portId).ExtractErr() + if err != nil { + errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError) + if !ok { + return p, "ACTIVE", err + } + if errCode.Actual == 404 { + log.Printf("[DEBUG] Successfully deleted OpenStack Port %s", portId) + return p, "DELETED", nil + } + } + + log.Printf("[DEBUG] OpenStack Port %s still active.\n", portId) + return p, "ACTIVE", nil + } +} diff --git a/builtin/providers/openstack/resource_openstack_networking_port_v2_test.go b/builtin/providers/openstack/resource_openstack_networking_port_v2_test.go new file mode 100644 index 000000000000..edeb619011ec --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_networking_port_v2_test.go @@ -0,0 +1,102 @@ +package openstack + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + + "github.com/rackspace/gophercloud/openstack/networking/v2/networks" + "github.com/rackspace/gophercloud/openstack/networking/v2/ports" +) + +func TestAccNetworkingV2Port_basic(t *testing.T) { + region := os.Getenv(OS_REGION_NAME) + + var network networks.Network + var port ports.Port + + var testAccNetworkingV2Port_basic = fmt.Sprintf(` + resource "openstack_networking_network_v2" "foo" { + region = "%s" + name = "network_1" + admin_state_up = "true" + } + + resource "openstack_networking_port_v2" "foo" { + region = "%s" + name = "port_1" + network_id = "${openstack_networking_network_v2.foo.id}" + admin_state_up = "true" + }`, region, region) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckNetworkingV2PortDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccNetworkingV2Port_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckNetworkingV2NetworkExists(t, "openstack_networking_network_v2.foo", &network), + testAccCheckNetworkingV2PortExists(t, "openstack_networking_port_v2.foo", &port), + ), + }, + }, + }) +} + +func testAccCheckNetworkingV2PortDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + networkingClient, err := config.networkingV2Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("(testAccCheckNetworkingV2PortDestroy) Error creating OpenStack networking client: %s", err) + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != "openstack_networking_port_v2" { + continue + } + + _, err := ports.Get(networkingClient, rs.Primary.ID).Extract() + if err == nil { + return fmt.Errorf("Port still exists") + } + } + + return nil +} + +func testAccCheckNetworkingV2PortExists(t *testing.T, n string, port *ports.Port) 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("(testAccCheckNetworkingV2PortExists) Error creating OpenStack networking client: %s", err) + } + + found, err := ports.Get(networkingClient, rs.Primary.ID).Extract() + if err != nil { + return err + } + + if found.ID != rs.Primary.ID { + return fmt.Errorf("Port not found") + } + + *port = *found + + return nil + } +} diff --git a/website/source/docs/providers/openstack/r/networking_network_v2.html.markdown b/website/source/docs/providers/openstack/r/networking_network_v2.html.markdown index f62454e30f95..9a9eab935b7c 100644 --- a/website/source/docs/providers/openstack/r/networking_network_v2.html.markdown +++ b/website/source/docs/providers/openstack/r/networking_network_v2.html.markdown @@ -14,9 +14,46 @@ Manages a V2 Neutron network resource within OpenStack. ``` resource "openstack_networking_network_v2" "network_1" { - name = "tf_test_network" + name = "network_1" admin_state_up = "true" } + +resource "openstack_networking_subnet_v2" "subnet_1" { + name = "subnet_1" + network_id = "${openstack_networking_network_v2.network_1.id}" + cidr = "192.168.199.0/24" + ip_version = 4 +} + +resource "openstack_compute_secgroup_v2" "secgroup_1" { + name = "secgroup_1" + description = "a security group" + rule { + from_port = 22 + to_port = 22 + ip_protocol = "tcp" + cidr = "0.0.0.0/0" + } +} + +resource "openstack_networking_port_v2" "port_1" { + name = "port_1" + network_id = "${openstack_networking_network_v2.network_1.id}" + admin_state_up = "true" + security_groups = ["${openstack_compute_secgroup_v2.secgroup_1.id}"] + + depends_on = ["openstack_networking_subnet_v2.subnet_1"] +} + +resource "openstack_compute_instance_v2" "instance_1" { + name = "instance_1" + security_groups = ["${openstack_compute_secgroup_v2.secgroup_1.name}"] + + network { + port = "${openstack_networking_port_v2.port_1.id}" + } +} + ``` ## Argument Reference diff --git a/website/source/docs/providers/openstack/r/networking_port_v2.html.markdown b/website/source/docs/providers/openstack/r/networking_port_v2.html.markdown new file mode 100644 index 000000000000..d10130a56f22 --- /dev/null +++ b/website/source/docs/providers/openstack/r/networking_port_v2.html.markdown @@ -0,0 +1,73 @@ +--- +layout: "openstack" +page_title: "OpenStack: openstack_networking_port_v2" +sidebar_current: "docs-openstack-resource-networking-port-v2" +description: |- + Manages a V2 port resource within OpenStack. +--- + +# openstack\_networking\_port_v2 + +Manages a V2 port resource within OpenStack. + +## Example Usage + +``` +resource "openstack_networking_network_v2" "network_1" { + name = "network_1" + admin_state_up = "true" +} + +resource "openstack_networking_port_v2" "port_1" { + name = "port_1" + network_id = "${openstack_networking_network_v2.network_1.id}" + admin_state_up = "true" +} +``` + +## 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 a port. If omitted, the + `OS_REGION_NAME` environment variable is used. Changing this creates a new + port. + +* `name` - (Optional) A unique name for the port. Changing this + updates the `name` of an existing port. + +* `network_id` - (Required) The ID of the network to attach the port to. Changing + this creates a new port. + +* `admin_state_up` - (Optional) Administrative up/down status for the port + (must be "true" or "false" if provided). Changing this updates the + `admin_state_up` of an existing port. + +* `mac_address` - (Optional) Specify a specific MAC address for the port. Changing + this creates a new port. + +* `tenant_id` - (Optional) The owner of the Port. Required if admin wants + to create a port for another tenant. Changing this creates a new port. + +* `device_owner` - (Optional) The device owner of the Port. Changing this creates + a new port. + +* `security_groups` - (Optional) A list of security groups to apply to the port. + The security groups must be specified by ID and not name (as opposed to how + they are configured with the Compute Instance). + +* `device_id` - (Optional) The ID of the device attached to the port. Changing this + creates a new port. + +## Attributes Reference + +The following attributes are exported: + +* `region` - See Argument Reference above. +* `admin_state_up` - See Argument Reference above. +* `mac_address` - See Argument Reference above. +* `tenant_id` - See Argument Reference above. +* `device_owner` - See Argument Reference above. +* `security_groups` - See Argument Reference above. +* `device_id` - See Argument Reference above. diff --git a/website/source/layouts/openstack.erb b/website/source/layouts/openstack.erb index 2b92e052c35e..fee3fe91cf8e 100644 --- a/website/source/layouts/openstack.erb +++ b/website/source/layouts/openstack.erb @@ -49,6 +49,9 @@ > openstack_networking_network_v2 + > + openstack_networking_port_v2 + > openstack_networking_router_interface_v2