package openstack import ( "bytes" "fmt" "log" "time" "github.com/hashicorp/terraform/helper/hashcode" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" "github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud/openstack/networking/v2/ports" ) func resourceNetworkingPortV2() *schema.Resource { return &schema.Resource{ Create: resourceNetworkingPortV2Create, Read: resourceNetworkingPortV2Read, Update: resourceNetworkingPortV2Update, Delete: resourceNetworkingPortV2Delete, Importer: &schema.ResourceImporter{ State: schema.ImportStatePassthrough, }, Timeouts: &schema.ResourceTimeout{ Create: schema.DefaultTimeout(10 * time.Minute), Delete: schema.DefaultTimeout(10 * time.Minute), }, 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: false, }, "network_id": &schema.Schema{ Type: schema.TypeString, Required: true, ForceNew: true, }, "admin_state_up": &schema.Schema{ Type: schema.TypeBool, 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_group_ids": &schema.Schema{ Type: schema.TypeSet, Optional: true, ForceNew: false, Computed: true, Elem: &schema.Schema{Type: schema.TypeString}, Set: schema.HashString, }, "device_id": &schema.Schema{ Type: schema.TypeString, Optional: true, ForceNew: true, Computed: true, }, "fixed_ip": &schema.Schema{ Type: schema.TypeList, Optional: true, ForceNew: false, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "subnet_id": &schema.Schema{ Type: schema.TypeString, Required: true, }, "ip_address": &schema.Schema{ Type: schema.TypeString, Optional: true, }, }, }, }, "allowed_address_pairs": &schema.Schema{ Type: schema.TypeSet, Optional: true, ForceNew: false, Computed: true, Set: allowedAddressPairsHash, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "ip_address": &schema.Schema{ Type: schema.TypeString, Required: true, }, "mac_address": &schema.Schema{ Type: schema.TypeString, Optional: true, Computed: true, }, }, }, }, "value_specs": &schema.Schema{ Type: schema.TypeMap, Optional: true, ForceNew: true, }, "all_fixed_ips": &schema.Schema{ Type: schema.TypeList, Computed: true, Elem: &schema.Schema{Type: schema.TypeString}, }, }, } } func resourceNetworkingPortV2Create(d *schema.ResourceData, meta interface{}) error { config := meta.(*Config) networkingClient, err := config.networkingV2Client(GetRegion(d)) if err != nil { return fmt.Errorf("Error creating OpenStack networking client: %s", err) } createOpts := PortCreateOpts{ 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), FixedIPs: resourcePortFixedIpsV2(d), AllowedAddressPairs: resourceAllowedAddressPairsV2(d), }, MapValueSpecs(d), } 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: []string{"ACTIVE"}, Refresh: waitForNetworkPortActive(networkingClient, p.ID), Timeout: d.Timeout(schema.TimeoutCreate), 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(GetRegion(d)) 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] Retrieved Port %s: %+v", d.Id(), p) d.Set("name", p.Name) d.Set("admin_state_up", 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_group_ids", p.SecurityGroups) d.Set("device_id", p.DeviceID) // Create a slice of all returned Fixed IPs. // This will be in the order returned by the API, // which is usually alpha-numeric. var ips []string for _, ipObject := range p.FixedIPs { ips = append(ips, ipObject.IPAddress) } d.Set("all_fixed_ips", ips) // Convert AllowedAddressPairs to list of map var pairs []map[string]interface{} for _, pairObject := range p.AllowedAddressPairs { pair := make(map[string]interface{}) pair["ip_address"] = pairObject.IPAddress pair["mac_address"] = pairObject.MACAddress pairs = append(pairs, pair) } d.Set("allowed_address_pairs", pairs) d.Set("region", GetRegion(d)) return nil } func resourceNetworkingPortV2Update(d *schema.ResourceData, meta interface{}) error { config := meta.(*Config) networkingClient, err := config.networkingV2Client(GetRegion(d)) if err != nil { return fmt.Errorf("Error creating OpenStack networking client: %s", err) } // security_group_ids and allowed_address_pairs are able to send empty arrays // to denote the removal of each. But their default zero-value is translated // to "null", which has been reported to cause problems in vendor-modified // OpenStack clouds. Therefore, we must set them in each request update. updateOpts := ports.UpdateOpts{ AllowedAddressPairs: resourceAllowedAddressPairsV2(d), SecurityGroups: resourcePortSecurityGroupsV2(d), } 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("device_id") { updateOpts.DeviceID = d.Get("device_id").(string) } if d.HasChange("fixed_ip") { updateOpts.FixedIPs = resourcePortFixedIpsV2(d) } 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(GetRegion(d)) if err != nil { return fmt.Errorf("Error creating OpenStack networking client: %s", err) } stateConf := &resource.StateChangeConf{ Pending: []string{"ACTIVE"}, Target: []string{"DELETED"}, Refresh: waitForNetworkPortDelete(networkingClient, d.Id()), Timeout: d.Timeout(schema.TimeoutDelete), 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_group_ids").(*schema.Set) groups := make([]string, rawSecurityGroups.Len()) for i, raw := range rawSecurityGroups.List() { groups[i] = raw.(string) } return groups } func resourcePortFixedIpsV2(d *schema.ResourceData) interface{} { rawIP := d.Get("fixed_ip").([]interface{}) if len(rawIP) == 0 { return nil } ip := make([]ports.IP, len(rawIP)) for i, raw := range rawIP { rawMap := raw.(map[string]interface{}) ip[i] = ports.IP{ SubnetID: rawMap["subnet_id"].(string), IPAddress: rawMap["ip_address"].(string), } } return ip } func resourceAllowedAddressPairsV2(d *schema.ResourceData) []ports.AddressPair { // ports.AddressPair rawPairs := d.Get("allowed_address_pairs").(*schema.Set).List() pairs := make([]ports.AddressPair, len(rawPairs)) for i, raw := range rawPairs { rawMap := raw.(map[string]interface{}) pairs[i] = ports.AddressPair{ IPAddress: rawMap["ip_address"].(string), MACAddress: rawMap["mac_address"].(string), } } return pairs } func resourcePortAdminStateUpV2(d *schema.ResourceData) *bool { value := false if raw, ok := d.GetOk("admin_state_up"); ok && raw == true { value = true } return &value } func allowedAddressPairsHash(v interface{}) int { var buf bytes.Buffer m := v.(map[string]interface{}) buf.WriteString(fmt.Sprintf("%s", m["ip_address"].(string))) return hashcode.String(buf.String()) } 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 { if _, ok := err.(gophercloud.ErrDefault404); ok { log.Printf("[DEBUG] Successfully deleted OpenStack Port %s", portId) return p, "DELETED", nil } return p, "ACTIVE", err } err = ports.Delete(networkingClient, portId).ExtractErr() if err != nil { if _, ok := err.(gophercloud.ErrDefault404); ok { log.Printf("[DEBUG] Successfully deleted OpenStack Port %s", portId) return p, "DELETED", nil } return p, "ACTIVE", err } log.Printf("[DEBUG] OpenStack Port %s still active.\n", portId) return p, "ACTIVE", nil } }