diff --git a/builtin/bins/provider-openstack/main.go b/builtin/bins/provider-openstack/main.go new file mode 100644 index 0000000000..f897f1c557 --- /dev/null +++ b/builtin/bins/provider-openstack/main.go @@ -0,0 +1,12 @@ +package main + +import ( + "github.com/hashicorp/terraform/builtin/providers/openstack" + "github.com/hashicorp/terraform/plugin" +) + +func main() { + plugin.Serve(&plugin.ServeOpts{ + ProviderFunc: openstack.Provider, + }) +} diff --git a/builtin/providers/openstack/config.go b/builtin/providers/openstack/config.go new file mode 100644 index 0000000000..d05662017c --- /dev/null +++ b/builtin/providers/openstack/config.go @@ -0,0 +1,67 @@ +package openstack + +import ( + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack" +) + +type Config struct { + Username string + UserID string + Password string + APIKey string + IdentityEndpoint string + TenantID string + TenantName string + DomainID string + DomainName string + + osClient *gophercloud.ProviderClient +} + +func (c *Config) loadAndValidate() error { + ao := gophercloud.AuthOptions{ + Username: c.Username, + UserID: c.UserID, + Password: c.Password, + APIKey: c.APIKey, + IdentityEndpoint: c.IdentityEndpoint, + TenantID: c.TenantID, + TenantName: c.TenantName, + DomainID: c.DomainID, + DomainName: c.DomainName, + } + + client, err := openstack.AuthenticatedClient(ao) + if err != nil { + return err + } + + c.osClient = client + + return nil +} + +func (c *Config) blockStorageV1Client(region string) (*gophercloud.ServiceClient, error) { + return openstack.NewBlockStorageV1(c.osClient, gophercloud.EndpointOpts{ + Region: region, + }) +} + +func (c *Config) computeV2Client(region string) (*gophercloud.ServiceClient, error) { + return openstack.NewComputeV2(c.osClient, gophercloud.EndpointOpts{ + Region: region, + }) +} + +func (c *Config) networkingV2Client(region string) (*gophercloud.ServiceClient, error) { + return openstack.NewNetworkV2(c.osClient, gophercloud.EndpointOpts{ + Region: region, + }) +} + +func (c *Config) objectStorageV1Client(region string) (*gophercloud.ServiceClient, error) { + return openstack.NewObjectStorageV1(c.osClient, gophercloud.EndpointOpts{ + Region: region, + }) +} diff --git a/builtin/providers/openstack/provider.go b/builtin/providers/openstack/provider.go new file mode 100644 index 0000000000..a43242333d --- /dev/null +++ b/builtin/providers/openstack/provider.go @@ -0,0 +1,113 @@ +package openstack + +import ( + "os" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +// Provider returns a schema.Provider for OpenStack. +func Provider() terraform.ResourceProvider { + return &schema.Provider{ + Schema: map[string]*schema.Schema{ + "auth_url": &schema.Schema{ + Type: schema.TypeString, + Required: true, + DefaultFunc: envDefaultFunc("OS_AUTH_URL"), + }, + "user_name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + DefaultFunc: envDefaultFunc("OS_USERNAME"), + }, + "user_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "", + }, + "tenant_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "", + }, + "tenant_name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + DefaultFunc: envDefaultFunc("OS_TENANT_NAME"), + }, + "password": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + DefaultFunc: envDefaultFunc("OS_PASSWORD"), + }, + "api_key": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "", + }, + "domain_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "", + }, + "domain_name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "", + }, + }, + + ResourcesMap: map[string]*schema.Resource{ + "openstack_blockstorage_volume_v1": resourceBlockStorageVolumeV1(), + "openstack_compute_instance_v2": resourceComputeInstanceV2(), + "openstack_compute_keypair_v2": resourceComputeKeypairV2(), + "openstack_compute_secgroup_v2": resourceComputeSecGroupV2(), + "openstack_compute_floatingip_v2": resourceComputeFloatingIPV2(), + "openstack_fw_firewall_v1": resourceFWFirewallV1(), + "openstack_fw_policy_v1": resourceFWPolicyV1(), + "openstack_fw_rule_v1": resourceFWRuleV1(), + "openstack_lb_monitor_v1": resourceLBMonitorV1(), + "openstack_lb_pool_v1": resourceLBPoolV1(), + "openstack_lb_vip_v1": resourceLBVipV1(), + "openstack_networking_network_v2": resourceNetworkingNetworkV2(), + "openstack_networking_subnet_v2": resourceNetworkingSubnetV2(), + "openstack_networking_floatingip_v2": resourceNetworkingFloatingIPV2(), + "openstack_networking_router_v2": resourceNetworkingRouterV2(), + "openstack_networking_router_interface_v2": resourceNetworkingRouterInterfaceV2(), + "openstack_objectstorage_container_v1": resourceObjectStorageContainerV1(), + }, + + ConfigureFunc: configureProvider, + } +} + +func configureProvider(d *schema.ResourceData) (interface{}, error) { + config := Config{ + IdentityEndpoint: d.Get("auth_url").(string), + Username: d.Get("user_name").(string), + UserID: d.Get("user_id").(string), + Password: d.Get("password").(string), + APIKey: d.Get("api_key").(string), + TenantID: d.Get("tenant_id").(string), + TenantName: d.Get("tenant_name").(string), + DomainID: d.Get("domain_id").(string), + DomainName: d.Get("domain_name").(string), + } + + if err := config.loadAndValidate(); err != nil { + return nil, err + } + + return &config, nil +} + +func envDefaultFunc(k string) schema.SchemaDefaultFunc { + return func() (interface{}, error) { + if v := os.Getenv(k); v != "" { + return v, nil + } + + return nil, nil + } +} diff --git a/builtin/providers/openstack/provider_test.go b/builtin/providers/openstack/provider_test.go new file mode 100644 index 0000000000..7b3e65dd43 --- /dev/null +++ b/builtin/providers/openstack/provider_test.go @@ -0,0 +1,66 @@ +package openstack + +import ( + "os" + "testing" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +var ( + OS_REGION_NAME = "" + OS_POOL_NAME = "" +) + +var testAccProviders map[string]terraform.ResourceProvider +var testAccProvider *schema.Provider + +func init() { + testAccProvider = Provider().(*schema.Provider) + testAccProviders = map[string]terraform.ResourceProvider{ + "openstack": testAccProvider, + } +} + +func TestProvider(t *testing.T) { + if err := Provider().(*schema.Provider).InternalValidate(); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestProvider_impl(t *testing.T) { + var _ terraform.ResourceProvider = Provider() +} + +func testAccPreCheck(t *testing.T) { + v := os.Getenv("OS_AUTH_URL") + if v == "" { + t.Fatal("OS_AUTH_URL must be set for acceptance tests") + } + + v = os.Getenv("OS_REGION_NAME") + if v == "" { + t.Fatal("OS_REGION_NAME must be set for acceptance tests") + } + OS_REGION_NAME = v + + v1 := os.Getenv("OS_IMAGE_ID") + v2 := os.Getenv("OS_IMAGE_NAME") + + if v1 == "" && v2 == "" { + t.Fatal("OS_IMAGE_ID or OS_IMAGE_NAME must be set for acceptance tests") + } + + v = os.Getenv("OS_POOL_NAME") + if v == "" { + t.Fatal("OS_POOL_NAME must be set for acceptance tests") + } + OS_POOL_NAME = v + + v1 = os.Getenv("OS_FLAVOR_ID") + v2 = os.Getenv("OS_FLAVOR_NAME") + if v1 == "" && v2 == "" { + t.Fatal("OS_FLAVOR_ID or OS_FLAVOR_NAME must be set for acceptance tests") + } +} diff --git a/builtin/providers/openstack/resource_openstack_blockstorage_volume_v1.go b/builtin/providers/openstack/resource_openstack_blockstorage_volume_v1.go new file mode 100644 index 0000000000..dc26385908 --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_blockstorage_volume_v1.go @@ -0,0 +1,314 @@ +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/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach" +) + +func resourceBlockStorageVolumeV1() *schema.Resource { + return &schema.Resource{ + Create: resourceBlockStorageVolumeV1Create, + Read: resourceBlockStorageVolumeV1Read, + Update: resourceBlockStorageVolumeV1Update, + Delete: resourceBlockStorageVolumeV1Delete, + + Schema: map[string]*schema.Schema{ + "region": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + DefaultFunc: envDefaultFunc("OS_REGION_NAME"), + }, + "size": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + ForceNew: true, + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: false, + }, + "description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: false, + }, + "metadata": &schema.Schema{ + Type: schema.TypeMap, + Optional: true, + ForceNew: false, + }, + "snapshot_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "source_vol_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "image_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "volume_type": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "attachment": &schema.Schema{ + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "instance_id": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "device": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + }, + Set: resourceVolumeAttachmentHash, + }, + }, + } +} + +func resourceBlockStorageVolumeV1Create(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + blockStorageClient, err := config.blockStorageV1Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack block storage client: %s", err) + } + + createOpts := &volumes.CreateOpts{ + Description: d.Get("description").(string), + Name: d.Get("name").(string), + Size: d.Get("size").(int), + SnapshotID: d.Get("snapshot_id").(string), + SourceVolID: d.Get("source_vol_id").(string), + ImageID: d.Get("image_id").(string), + VolumeType: d.Get("volume_type").(string), + Metadata: resourceContainerMetadataV2(d), + } + + log.Printf("[DEBUG] Create Options: %#v", createOpts) + v, err := volumes.Create(blockStorageClient, createOpts).Extract() + if err != nil { + return fmt.Errorf("Error creating OpenStack volume: %s", err) + } + log.Printf("[INFO] Volume ID: %s", v.ID) + + // Store the ID now + d.SetId(v.ID) + + // Wait for the volume to become available. + log.Printf( + "[DEBUG] Waiting for volume (%s) to become available", + v.ID) + + stateConf := &resource.StateChangeConf{ + Target: "available", + Refresh: VolumeV1StateRefreshFunc(blockStorageClient, v.ID), + Timeout: 10 * time.Minute, + Delay: 10 * time.Second, + MinTimeout: 3 * time.Second, + } + + _, err = stateConf.WaitForState() + if err != nil { + return fmt.Errorf( + "Error waiting for volume (%s) to become ready: %s", + v.ID, err) + } + + return resourceBlockStorageVolumeV1Read(d, meta) +} + +func resourceBlockStorageVolumeV1Read(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + blockStorageClient, err := config.blockStorageV1Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack block storage client: %s", err) + } + + v, err := volumes.Get(blockStorageClient, d.Id()).Extract() + if err != nil { + return CheckDeleted(d, err, "volume") + } + + log.Printf("[DEBUG] Retreived volume %s: %+v", d.Id(), v) + + d.Set("size", v.Size) + d.Set("description", v.Description) + d.Set("name", v.Name) + d.Set("snapshot_id", v.SnapshotID) + d.Set("source_vol_id", v.SourceVolID) + d.Set("volume_type", v.VolumeType) + d.Set("metadata", v.Metadata) + + if len(v.Attachments) > 0 { + attachments := make([]map[string]interface{}, len(v.Attachments)) + for i, attachment := range v.Attachments { + attachments[i] = make(map[string]interface{}) + attachments[i]["id"] = attachment["id"] + attachments[i]["instance_id"] = attachment["server_id"] + attachments[i]["device"] = attachment["device"] + log.Printf("[DEBUG] attachment: %v", attachment) + } + d.Set("attachment", attachments) + } + + return nil +} + +func resourceBlockStorageVolumeV1Update(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + blockStorageClient, err := config.blockStorageV1Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack block storage client: %s", err) + } + + updateOpts := volumes.UpdateOpts{ + Name: d.Get("name").(string), + Description: d.Get("description").(string), + } + + if d.HasChange("metadata") { + updateOpts.Metadata = resourceVolumeMetadataV1(d) + } + + _, err = volumes.Update(blockStorageClient, d.Id(), updateOpts).Extract() + if err != nil { + return fmt.Errorf("Error updating OpenStack volume: %s", err) + } + + return resourceBlockStorageVolumeV1Read(d, meta) +} + +func resourceBlockStorageVolumeV1Delete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + blockStorageClient, err := config.blockStorageV1Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack block storage client: %s", err) + } + + v, err := volumes.Get(blockStorageClient, d.Id()).Extract() + if err != nil { + return CheckDeleted(d, err, "volume") + } + + // make sure this volume is detached from all instances before deleting + if len(v.Attachments) > 0 { + log.Printf("[DEBUG] detaching volumes") + if computeClient, err := config.computeV2Client(d.Get("region").(string)); err != nil { + return err + } else { + for _, volumeAttachment := range v.Attachments { + log.Printf("[DEBUG] Attachment: %v", volumeAttachment) + if err := volumeattach.Delete(computeClient, volumeAttachment["server_id"].(string), volumeAttachment["id"].(string)).ExtractErr(); err != nil { + return err + } + } + + stateConf := &resource.StateChangeConf{ + Pending: []string{"in-use", "attaching"}, + Target: "available", + Refresh: VolumeV1StateRefreshFunc(blockStorageClient, d.Id()), + Timeout: 10 * time.Minute, + Delay: 10 * time.Second, + MinTimeout: 3 * time.Second, + } + + _, err = stateConf.WaitForState() + if err != nil { + return fmt.Errorf( + "Error waiting for volume (%s) to become available: %s", + d.Id(), err) + } + } + } + + err = volumes.Delete(blockStorageClient, d.Id()).ExtractErr() + if err != nil { + return fmt.Errorf("Error deleting OpenStack volume: %s", err) + } + + // Wait for the volume to delete before moving on. + log.Printf("[DEBUG] Waiting for volume (%s) to delete", d.Id()) + + stateConf := &resource.StateChangeConf{ + Pending: []string{"deleting", "available"}, + Target: "deleted", + Refresh: VolumeV1StateRefreshFunc(blockStorageClient, d.Id()), + Timeout: 10 * time.Minute, + Delay: 10 * time.Second, + MinTimeout: 3 * time.Second, + } + + _, err = stateConf.WaitForState() + if err != nil { + return fmt.Errorf( + "Error waiting for volume (%s) to delete: %s", + d.Id(), err) + } + + d.SetId("") + return nil +} + +func resourceVolumeMetadataV1(d *schema.ResourceData) map[string]string { + m := make(map[string]string) + for key, val := range d.Get("metadata").(map[string]interface{}) { + m[key] = val.(string) + } + return m +} + +// VolumeV1StateRefreshFunc returns a resource.StateRefreshFunc that is used to watch +// an OpenStack volume. +func VolumeV1StateRefreshFunc(client *gophercloud.ServiceClient, volumeID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + v, err := volumes.Get(client, volumeID).Extract() + if err != nil { + errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError) + if !ok { + return nil, "", err + } + if errCode.Actual == 404 { + return v, "deleted", nil + } + return nil, "", err + } + + return v, v.Status, nil + } +} + +func resourceVolumeAttachmentHash(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + if m["instance_id"] != nil { + buf.WriteString(fmt.Sprintf("%s-", m["instance_id"].(string))) + } + return hashcode.String(buf.String()) +} diff --git a/builtin/providers/openstack/resource_openstack_blockstorage_volume_v1_test.go b/builtin/providers/openstack/resource_openstack_blockstorage_volume_v1_test.go new file mode 100644 index 0000000000..5404fd3912 --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_blockstorage_volume_v1_test.go @@ -0,0 +1,138 @@ +package openstack + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + + "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes" +) + +func TestAccBlockStorageV1Volume_basic(t *testing.T) { + var volume volumes.Volume + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckBlockStorageV1VolumeDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccBlockStorageV1Volume_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckBlockStorageV1VolumeExists(t, "openstack_blockstorage_volume_v1.volume_1", &volume), + resource.TestCheckResourceAttr("openstack_blockstorage_volume_v1.volume_1", "name", "tf-test-volume"), + testAccCheckBlockStorageV1VolumeMetadata(&volume, "foo", "bar"), + ), + }, + resource.TestStep{ + Config: testAccBlockStorageV1Volume_update, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("openstack_blockstorage_volume_v1.volume_1", "name", "tf-test-volume-updated"), + testAccCheckBlockStorageV1VolumeMetadata(&volume, "foo", "bar"), + ), + }, + }, + }) +} + +func testAccCheckBlockStorageV1VolumeDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + blockStorageClient, err := config.blockStorageV1Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("Error creating OpenStack block storage client: %s", err) + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != "openstack_blockstorage_volume_v1" { + continue + } + + _, err := volumes.Get(blockStorageClient, rs.Primary.ID).Extract() + if err == nil { + return fmt.Errorf("Volume still exists") + } + } + + return nil +} + +func testAccCheckBlockStorageV1VolumeExists(t *testing.T, n string, volume *volumes.Volume) 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) + blockStorageClient, err := config.blockStorageV1Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("Error creating OpenStack block storage client: %s", err) + } + + found, err := volumes.Get(blockStorageClient, rs.Primary.ID).Extract() + if err != nil { + return err + } + + if found.ID != rs.Primary.ID { + return fmt.Errorf("Volume not found") + } + + *volume = *found + + return nil + } +} + +func testAccCheckBlockStorageV1VolumeMetadata( + volume *volumes.Volume, k string, v string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if volume.Metadata == nil { + return fmt.Errorf("No metadata") + } + + for key, value := range volume.Metadata { + if k != key { + continue + } + + if v == value { + return nil + } + + return fmt.Errorf("Bad value for %s: %s", k, value) + } + + return fmt.Errorf("Metadata not found: %s", k) + } +} + +var testAccBlockStorageV1Volume_basic = fmt.Sprintf(` + resource "openstack_blockstorage_volume_v1" "volume_1" { + region = "%s" + name = "tf-test-volume" + description = "first test volume" + metadata{ + foo = "bar" + } + size = 1 + }`, + OS_REGION_NAME) + +var testAccBlockStorageV1Volume_update = fmt.Sprintf(` + resource "openstack_blockstorage_volume_v1" "volume_1" { + region = "%s" + name = "tf-test-volume-updated" + description = "first test volume" + metadata{ + foo = "bar" + } + size = 1 + }`, + OS_REGION_NAME) diff --git a/builtin/providers/openstack/resource_openstack_compute_floatingip_v2.go b/builtin/providers/openstack/resource_openstack_compute_floatingip_v2.go new file mode 100644 index 0000000000..bb2facc4ee --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_compute_floatingip_v2.go @@ -0,0 +1,107 @@ +package openstack + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip" +) + +func resourceComputeFloatingIPV2() *schema.Resource { + return &schema.Resource{ + Create: resourceComputeFloatingIPV2Create, + Read: resourceComputeFloatingIPV2Read, + Update: nil, + Delete: resourceComputeFloatingIPV2Delete, + + Schema: map[string]*schema.Schema{ + "region": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + DefaultFunc: envDefaultFunc("OS_REGION_NAME"), + }, + + "pool": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + DefaultFunc: envDefaultFunc("OS_POOL_NAME"), + }, + + "address": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "fixed_ip": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "instance_id": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceComputeFloatingIPV2Create(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + computeClient, err := config.computeV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack compute client: %s", err) + } + + createOpts := &floatingip.CreateOpts{ + Pool: d.Get("pool").(string), + } + log.Printf("[DEBUG] Create Options: %#v", createOpts) + newFip, err := floatingip.Create(computeClient, createOpts).Extract() + if err != nil { + return fmt.Errorf("Error creating Floating IP: %s", err) + } + + d.SetId(newFip.ID) + + return resourceComputeFloatingIPV2Read(d, meta) +} + +func resourceComputeFloatingIPV2Read(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + computeClient, err := config.computeV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack compute client: %s", err) + } + + fip, err := floatingip.Get(computeClient, d.Id()).Extract() + if err != nil { + return CheckDeleted(d, err, "floating ip") + } + + log.Printf("[DEBUG] Retrieved Floating IP %s: %+v", d.Id(), fip) + + d.Set("pool", fip.Pool) + d.Set("instance_id", fip.InstanceID) + d.Set("address", fip.IP) + d.Set("fixed_ip", fip.FixedIP) + + return nil +} + +func resourceComputeFloatingIPV2Delete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + computeClient, err := config.computeV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack compute client: %s", err) + } + + log.Printf("[DEBUG] Deleting Floating IP %s", d.Id()) + if err := floatingip.Delete(computeClient, d.Id()).ExtractErr(); err != nil { + return fmt.Errorf("Error deleting Floating IP: %s", err) + } + + return nil +} diff --git a/builtin/providers/openstack/resource_openstack_compute_floatingip_v2_test.go b/builtin/providers/openstack/resource_openstack_compute_floatingip_v2_test.go new file mode 100644 index 0000000000..c246d1a510 --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_compute_floatingip_v2_test.go @@ -0,0 +1,86 @@ +package openstack + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/floatingip" +) + +func TestAccComputeV2FloatingIP_basic(t *testing.T) { + var floatingIP floatingip.FloatingIP + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeV2FloatingIPDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeV2FloatingIP_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeV2FloatingIPExists(t, "openstack_compute_floatingip_v2.foo", &floatingIP), + ), + }, + }, + }) +} + +func testAccCheckComputeV2FloatingIPDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + computeClient, err := config.computeV2Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("(testAccCheckComputeV2FloatingIPDestroy) Error creating OpenStack compute client: %s", err) + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != "openstack_compute_floatingip_v2" { + continue + } + + _, err := floatingip.Get(computeClient, rs.Primary.ID).Extract() + if err == nil { + return fmt.Errorf("FloatingIP still exists") + } + } + + return nil +} + +func testAccCheckComputeV2FloatingIPExists(t *testing.T, n string, kp *floatingip.FloatingIP) 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) + computeClient, err := config.computeV2Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("(testAccCheckComputeV2FloatingIPExists) Error creating OpenStack compute client: %s", err) + } + + found, err := floatingip.Get(computeClient, rs.Primary.ID).Extract() + if err != nil { + return err + } + + if found.ID != rs.Primary.ID { + return fmt.Errorf("FloatingIP not found") + } + + *kp = *found + + return nil + } +} + +var testAccComputeV2FloatingIP_basic = fmt.Sprintf(` + resource "openstack_compute_floatingip_v2" "foo" { + }`) diff --git a/builtin/providers/openstack/resource_openstack_compute_instance_v2.go b/builtin/providers/openstack/resource_openstack_compute_instance_v2.go new file mode 100644 index 0000000000..5a4c16c7e5 --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_compute_instance_v2.go @@ -0,0 +1,1029 @@ +package openstack + +import ( + "bytes" + "crypto/sha1" + "encoding/hex" + "fmt" + "log" + "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/compute/v2/extensions/bootfromvolume" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach" + "github.com/rackspace/gophercloud/openstack/compute/v2/flavors" + "github.com/rackspace/gophercloud/openstack/compute/v2/images" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips" + "github.com/rackspace/gophercloud/openstack/networking/v2/networks" + "github.com/rackspace/gophercloud/openstack/networking/v2/ports" + "github.com/rackspace/gophercloud/pagination" +) + +func resourceComputeInstanceV2() *schema.Resource { + return &schema.Resource{ + Create: resourceComputeInstanceV2Create, + Read: resourceComputeInstanceV2Read, + Update: resourceComputeInstanceV2Update, + Delete: resourceComputeInstanceV2Delete, + + Schema: map[string]*schema.Schema{ + "region": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + DefaultFunc: envDefaultFunc("OS_REGION_NAME"), + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: false, + }, + "image_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + DefaultFunc: envDefaultFunc("OS_IMAGE_ID"), + }, + "image_name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + DefaultFunc: envDefaultFunc("OS_IMAGE_NAME"), + }, + "flavor_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: false, + Computed: true, + DefaultFunc: envDefaultFunc("OS_FLAVOR_ID"), + }, + "flavor_name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: false, + Computed: true, + DefaultFunc: envDefaultFunc("OS_FLAVOR_NAME"), + }, + "floating_ip": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: false, + }, + "user_data": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + // just stash the hash for state & diff comparisons + StateFunc: func(v interface{}) string { + switch v.(type) { + case string: + hash := sha1.Sum([]byte(v.(string))) + return hex.EncodeToString(hash[:]) + default: + return "" + } + }, + }, + "security_groups": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + ForceNew: false, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "availability_zone": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "network": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "uuid": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "port": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "fixed_ip": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + "metadata": &schema.Schema{ + Type: schema.TypeMap, + Optional: true, + ForceNew: false, + }, + "config_drive": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + }, + "admin_pass": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: false, + }, + "access_ip_v4": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Optional: true, + ForceNew: false, + }, + "access_ip_v6": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Optional: true, + ForceNew: false, + }, + "key_pair": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "block_device": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "uuid": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "source_type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "volume_size": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + }, + "destination_type": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "boot_index": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + }, + }, + }, + }, + "volume": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "volume_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "device": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + }, + }, + Set: resourceComputeVolumeAttachmentHash, + }, + }, + } +} + +func resourceComputeInstanceV2Create(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + computeClient, err := config.computeV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack compute client: %s", err) + } + + var createOpts servers.CreateOptsBuilder + + imageId, err := getImageID(computeClient, d) + if err != nil { + return err + } + + flavorId, err := getFlavorID(computeClient, d) + if err != nil { + return err + } + + createOpts = &servers.CreateOpts{ + Name: d.Get("name").(string), + ImageRef: imageId, + FlavorRef: flavorId, + SecurityGroups: resourceInstanceSecGroupsV2(d), + AvailabilityZone: d.Get("availability_zone").(string), + Networks: resourceInstanceNetworksV2(d), + Metadata: resourceInstanceMetadataV2(d), + ConfigDrive: d.Get("config_drive").(bool), + AdminPass: d.Get("admin_pass").(string), + UserData: []byte(d.Get("user_data").(string)), + } + + if keyName, ok := d.Get("key_pair").(string); ok && keyName != "" { + createOpts = &keypairs.CreateOptsExt{ + createOpts, + keyName, + } + } + + if blockDeviceRaw, ok := d.Get("block_device").(map[string]interface{}); ok && blockDeviceRaw != nil { + blockDevice := resourceInstanceBlockDeviceV2(d, blockDeviceRaw) + createOpts = &bootfromvolume.CreateOptsExt{ + createOpts, + blockDevice, + } + } + + log.Printf("[DEBUG] Create Options: %#v", createOpts) + server, err := servers.Create(computeClient, createOpts).Extract() + if err != nil { + return fmt.Errorf("Error creating OpenStack server: %s", err) + } + log.Printf("[INFO] Instance ID: %s", server.ID) + + // Store the ID now + d.SetId(server.ID) + + // Wait for the instance to become running so we can get some attributes + // that aren't available until later. + log.Printf( + "[DEBUG] Waiting for instance (%s) to become running", + server.ID) + + stateConf := &resource.StateChangeConf{ + Pending: []string{"BUILD"}, + Target: "ACTIVE", + Refresh: ServerV2StateRefreshFunc(computeClient, server.ID), + Timeout: 10 * time.Minute, + Delay: 10 * time.Second, + MinTimeout: 3 * time.Second, + } + + _, err = stateConf.WaitForState() + if err != nil { + return fmt.Errorf( + "Error waiting for instance (%s) to become ready: %s", + server.ID, err) + } + floatingIP := d.Get("floating_ip").(string) + if floatingIP != "" { + networkingClient, err := config.networkingV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack compute client: %s", err) + } + + allFloatingIPs, err := getFloatingIPs(networkingClient) + if err != nil { + return fmt.Errorf("Error listing OpenStack floating IPs: %s", err) + } + err = assignFloatingIP(networkingClient, extractFloatingIPFromIP(allFloatingIPs, floatingIP), server.ID) + if err != nil { + fmt.Errorf("Error assigning floating IP to OpenStack compute instance: %s", err) + } + } + + // were volume attachments specified? + if v := d.Get("volume"); v != nil { + vols := v.(*schema.Set).List() + if len(vols) > 0 { + if blockClient, err := config.blockStorageV1Client(d.Get("region").(string)); err != nil { + return fmt.Errorf("Error creating OpenStack block storage client: %s", err) + } else { + if err := attachVolumesToInstance(computeClient, blockClient, d.Id(), vols); err != nil { + return err + } + } + } + } + + return resourceComputeInstanceV2Read(d, meta) +} + +func resourceComputeInstanceV2Read(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + computeClient, err := config.computeV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack compute client: %s", err) + } + + server, err := servers.Get(computeClient, d.Id()).Extract() + if err != nil { + return CheckDeleted(d, err, "server") + } + + log.Printf("[DEBUG] Retreived Server %s: %+v", d.Id(), server) + + d.Set("name", server.Name) + d.Set("access_ip_v4", server.AccessIPv4) + d.Set("access_ip_v6", server.AccessIPv6) + + hostv4 := server.AccessIPv4 + if hostv4 == "" { + if publicAddressesRaw, ok := server.Addresses["public"]; ok { + publicAddresses := publicAddressesRaw.([]interface{}) + for _, paRaw := range publicAddresses { + pa := paRaw.(map[string]interface{}) + if pa["version"].(float64) == 4 { + hostv4 = pa["addr"].(string) + break + } + } + } + } + + // If no host found, just get the first IPv4 we find + if hostv4 == "" { + for _, networkAddresses := range server.Addresses { + for _, element := range networkAddresses.([]interface{}) { + address := element.(map[string]interface{}) + if address["version"].(float64) == 4 { + hostv4 = address["addr"].(string) + break + } + } + } + } + d.Set("access_ip_v4", hostv4) + log.Printf("hostv4: %s", hostv4) + + hostv6 := server.AccessIPv6 + if hostv6 == "" { + if publicAddressesRaw, ok := server.Addresses["public"]; ok { + publicAddresses := publicAddressesRaw.([]interface{}) + for _, paRaw := range publicAddresses { + pa := paRaw.(map[string]interface{}) + if pa["version"].(float64) == 6 { + hostv6 = fmt.Sprintf("[%s]", pa["addr"].(string)) + break + } + } + } + } + + // If no hostv6 found, just get the first IPv6 we find + if hostv6 == "" { + for _, networkAddresses := range server.Addresses { + for _, element := range networkAddresses.([]interface{}) { + address := element.(map[string]interface{}) + if address["version"].(float64) == 6 { + hostv6 = fmt.Sprintf("[%s]", address["addr"].(string)) + break + } + } + } + } + d.Set("access_ip_v6", hostv6) + log.Printf("hostv6: %s", hostv6) + + preferredv := "" + if hostv4 != "" { + preferredv = hostv4 + } else if hostv6 != "" { + preferredv = hostv6 + } + + if preferredv != "" { + // Initialize the connection info + d.SetConnInfo(map[string]string{ + "type": "ssh", + "host": preferredv, + }) + } + + d.Set("metadata", server.Metadata) + + secGrpNames := []string{} + for _, sg := range server.SecurityGroups { + secGrpNames = append(secGrpNames, sg["name"].(string)) + } + d.Set("security_groups", secGrpNames) + + flavorId, ok := server.Flavor["id"].(string) + if !ok { + return fmt.Errorf("Error setting OpenStack server's flavor: %v", server.Flavor) + } + d.Set("flavor_id", flavorId) + + flavor, err := flavors.Get(computeClient, flavorId).Extract() + if err != nil { + return err + } + d.Set("flavor_name", flavor.Name) + + imageId, ok := server.Image["id"].(string) + if !ok { + return fmt.Errorf("Error setting OpenStack server's image: %v", server.Image) + } + d.Set("image_id", imageId) + + image, err := images.Get(computeClient, imageId).Extract() + if err != nil { + return err + } + d.Set("image_name", image.Name) + + // volume attachments + vas, err := getVolumeAttachments(computeClient, d.Id()) + if err != nil { + return err + } + if len(vas) > 0 { + attachments := make([]map[string]interface{}, len(vas)) + for i, attachment := range vas { + attachments[i] = make(map[string]interface{}) + attachments[i]["id"] = attachment.ID + attachments[i]["volume_id"] = attachment.VolumeID + attachments[i]["device"] = attachment.Device + } + log.Printf("[INFO] Volume attachments: %v", attachments) + d.Set("volume", attachments) + } + + return nil +} + +func resourceComputeInstanceV2Update(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + computeClient, err := config.computeV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack compute client: %s", err) + } + + var updateOpts servers.UpdateOpts + if d.HasChange("name") { + updateOpts.Name = d.Get("name").(string) + } + if d.HasChange("access_ip_v4") { + updateOpts.AccessIPv4 = d.Get("access_ip_v4").(string) + } + if d.HasChange("access_ip_v6") { + updateOpts.AccessIPv4 = d.Get("access_ip_v6").(string) + } + + if updateOpts != (servers.UpdateOpts{}) { + _, err := servers.Update(computeClient, d.Id(), updateOpts).Extract() + if err != nil { + return fmt.Errorf("Error updating OpenStack server: %s", err) + } + } + + if d.HasChange("metadata") { + var metadataOpts servers.MetadataOpts + metadataOpts = make(servers.MetadataOpts) + newMetadata := d.Get("metadata").(map[string]interface{}) + for k, v := range newMetadata { + metadataOpts[k] = v.(string) + } + + _, err := servers.UpdateMetadata(computeClient, d.Id(), metadataOpts).Extract() + if err != nil { + return fmt.Errorf("Error updating OpenStack server (%s) metadata: %s", d.Id(), err) + } + } + + if d.HasChange("security_groups") { + oldSGRaw, newSGRaw := d.GetChange("security_groups") + oldSGSlice, newSGSlice := oldSGRaw.([]interface{}), newSGRaw.([]interface{}) + oldSGSet := schema.NewSet(func(v interface{}) int { return hashcode.String(v.(string)) }, oldSGSlice) + newSGSet := schema.NewSet(func(v interface{}) int { return hashcode.String(v.(string)) }, newSGSlice) + secgroupsToAdd := newSGSet.Difference(oldSGSet) + secgroupsToRemove := oldSGSet.Difference(newSGSet) + + log.Printf("[DEBUG] Security groups to add: %v", secgroupsToAdd) + + log.Printf("[DEBUG] Security groups to remove: %v", secgroupsToRemove) + + for _, g := range secgroupsToAdd.List() { + err := secgroups.AddServerToGroup(computeClient, d.Id(), g.(string)).ExtractErr() + if err != nil { + return fmt.Errorf("Error adding security group to OpenStack server (%s): %s", d.Id(), err) + } + log.Printf("[DEBUG] Added security group (%s) to instance (%s)", g.(string), d.Id()) + } + + for _, g := range secgroupsToRemove.List() { + err := secgroups.RemoveServerFromGroup(computeClient, d.Id(), g.(string)).ExtractErr() + if err != nil { + errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError) + if !ok { + return fmt.Errorf("Error removing security group from OpenStack server (%s): %s", d.Id(), err) + } + if errCode.Actual == 404 { + continue + } else { + return fmt.Errorf("Error removing security group from OpenStack server (%s): %s", d.Id(), err) + } + } else { + log.Printf("[DEBUG] Removed security group (%s) from instance (%s)", g.(string), d.Id()) + } + } + } + + if d.HasChange("admin_pass") { + if newPwd, ok := d.Get("admin_pass").(string); ok { + err := servers.ChangeAdminPassword(computeClient, d.Id(), newPwd).ExtractErr() + if err != nil { + return fmt.Errorf("Error changing admin password of OpenStack server (%s): %s", d.Id(), err) + } + } + } + + if d.HasChange("floating_ip") { + floatingIP := d.Get("floating_ip").(string) + if floatingIP != "" { + networkingClient, err := config.networkingV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack compute client: %s", err) + } + + allFloatingIPs, err := getFloatingIPs(networkingClient) + if err != nil { + return fmt.Errorf("Error listing OpenStack floating IPs: %s", err) + } + err = assignFloatingIP(networkingClient, extractFloatingIPFromIP(allFloatingIPs, floatingIP), d.Id()) + if err != nil { + fmt.Errorf("Error assigning floating IP to OpenStack compute instance: %s", err) + } + } + } + + if d.HasChange("volume") { + // old attachments and new attachments + oldAttachments, newAttachments := d.GetChange("volume") + + // for each old attachment, detach the volume + oldAttachmentSet := oldAttachments.(*schema.Set).List() + if len(oldAttachmentSet) > 0 { + if blockClient, err := config.blockStorageV1Client(d.Get("region").(string)); err != nil { + return err + } else { + if err := detachVolumesFromInstance(computeClient, blockClient, d.Id(), oldAttachmentSet); err != nil { + return err + } + } + } + + // for each new attachment, attach the volume + newAttachmentSet := newAttachments.(*schema.Set).List() + if len(newAttachmentSet) > 0 { + if blockClient, err := config.blockStorageV1Client(d.Get("region").(string)); err != nil { + return err + } else { + if err := attachVolumesToInstance(computeClient, blockClient, d.Id(), newAttachmentSet); err != nil { + return err + } + } + } + + d.SetPartial("volume") + } + + if d.HasChange("flavor_id") || d.HasChange("flavor_name") { + flavorId, err := getFlavorID(computeClient, d) + if err != nil { + return err + } + resizeOpts := &servers.ResizeOpts{ + FlavorRef: flavorId, + } + log.Printf("[DEBUG] Resize configuration: %#v", resizeOpts) + err = servers.Resize(computeClient, d.Id(), resizeOpts).ExtractErr() + if err != nil { + return fmt.Errorf("Error resizing OpenStack server: %s", err) + } + + // Wait for the instance to finish resizing. + log.Printf("[DEBUG] Waiting for instance (%s) to finish resizing", d.Id()) + + stateConf := &resource.StateChangeConf{ + Pending: []string{"RESIZE"}, + Target: "VERIFY_RESIZE", + Refresh: ServerV2StateRefreshFunc(computeClient, d.Id()), + Timeout: 3 * time.Minute, + Delay: 10 * time.Second, + MinTimeout: 3 * time.Second, + } + + _, err = stateConf.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for instance (%s) to resize: %s", d.Id(), err) + } + + // Confirm resize. + log.Printf("[DEBUG] Confirming resize") + err = servers.ConfirmResize(computeClient, d.Id()).ExtractErr() + if err != nil { + return fmt.Errorf("Error confirming resize of OpenStack server: %s", err) + } + + stateConf = &resource.StateChangeConf{ + Pending: []string{"VERIFY_RESIZE"}, + Target: "ACTIVE", + Refresh: ServerV2StateRefreshFunc(computeClient, d.Id()), + Timeout: 3 * time.Minute, + Delay: 10 * time.Second, + MinTimeout: 3 * time.Second, + } + + _, err = stateConf.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for instance (%s) to confirm resize: %s", d.Id(), err) + } + } + + return resourceComputeInstanceV2Read(d, meta) +} + +func resourceComputeInstanceV2Delete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + computeClient, err := config.computeV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack compute client: %s", err) + } + + err = servers.Delete(computeClient, d.Id()).ExtractErr() + if err != nil { + return fmt.Errorf("Error deleting OpenStack server: %s", err) + } + + // Wait for the instance to delete before moving on. + log.Printf("[DEBUG] Waiting for instance (%s) to delete", d.Id()) + + stateConf := &resource.StateChangeConf{ + Pending: []string{"ACTIVE"}, + Target: "DELETED", + Refresh: ServerV2StateRefreshFunc(computeClient, d.Id()), + Timeout: 10 * time.Minute, + Delay: 10 * time.Second, + MinTimeout: 3 * time.Second, + } + + _, err = stateConf.WaitForState() + if err != nil { + return fmt.Errorf( + "Error waiting for instance (%s) to delete: %s", + d.Id(), err) + } + + d.SetId("") + return nil +} + +// ServerV2StateRefreshFunc returns a resource.StateRefreshFunc that is used to watch +// an OpenStack instance. +func ServerV2StateRefreshFunc(client *gophercloud.ServiceClient, instanceID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + s, err := servers.Get(client, instanceID).Extract() + if err != nil { + errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError) + if !ok { + return nil, "", err + } + if errCode.Actual == 404 { + return s, "DELETED", nil + } + return nil, "", err + } + + return s, s.Status, nil + } +} + +func resourceInstanceSecGroupsV2(d *schema.ResourceData) []string { + rawSecGroups := d.Get("security_groups").([]interface{}) + secgroups := make([]string, len(rawSecGroups)) + for i, raw := range rawSecGroups { + secgroups[i] = raw.(string) + } + return secgroups +} + +func resourceInstanceNetworksV2(d *schema.ResourceData) []servers.Network { + rawNetworks := d.Get("network").([]interface{}) + networks := make([]servers.Network, len(rawNetworks)) + for i, raw := range rawNetworks { + rawMap := raw.(map[string]interface{}) + networks[i] = servers.Network{ + UUID: rawMap["uuid"].(string), + Port: rawMap["port"].(string), + FixedIP: rawMap["fixed_ip"].(string), + } + } + return networks +} + +func resourceInstanceMetadataV2(d *schema.ResourceData) map[string]string { + m := make(map[string]string) + for key, val := range d.Get("metadata").(map[string]interface{}) { + m[key] = val.(string) + } + return m +} + +func resourceInstanceBlockDeviceV2(d *schema.ResourceData, bd map[string]interface{}) []bootfromvolume.BlockDevice { + sourceType := bootfromvolume.SourceType(bd["source_type"].(string)) + bfvOpts := []bootfromvolume.BlockDevice{ + bootfromvolume.BlockDevice{ + UUID: bd["uuid"].(string), + SourceType: sourceType, + VolumeSize: bd["volume_size"].(int), + DestinationType: bd["destination_type"].(string), + BootIndex: bd["boot_index"].(int), + }, + } + + return bfvOpts +} + +func extractFloatingIPFromIP(ips []floatingips.FloatingIP, IP string) *floatingips.FloatingIP { + for _, floatingIP := range ips { + if floatingIP.FloatingIP == IP { + return &floatingIP + } + } + return nil +} + +func assignFloatingIP(networkingClient *gophercloud.ServiceClient, floatingIP *floatingips.FloatingIP, instanceID string) error { + networkID, err := getFirstNetworkID(networkingClient, instanceID) + if err != nil { + return err + } + portID, err := getInstancePortID(networkingClient, instanceID, networkID) + _, err = floatingips.Update(networkingClient, floatingIP.ID, floatingips.UpdateOpts{ + PortID: portID, + }).Extract() + return err +} + +func getFirstNetworkID(networkingClient *gophercloud.ServiceClient, instanceID string) (string, error) { + pager := networks.List(networkingClient, networks.ListOpts{}) + + var networkdID string + err := pager.EachPage(func(page pagination.Page) (bool, error) { + networkList, err := networks.ExtractNetworks(page) + if err != nil { + return false, err + } + + if len(networkList) > 0 { + networkdID = networkList[0].ID + return false, nil + } + return false, fmt.Errorf("No network found for the instance %s", instanceID) + }) + if err != nil { + return "", err + } + return networkdID, nil + +} + +func getInstancePortID(networkingClient *gophercloud.ServiceClient, instanceID, networkID string) (string, error) { + pager := ports.List(networkingClient, ports.ListOpts{ + DeviceID: instanceID, + NetworkID: networkID, + }) + + var portID string + err := pager.EachPage(func(page pagination.Page) (bool, error) { + portList, err := ports.ExtractPorts(page) + if err != nil { + return false, err + } + for _, port := range portList { + portID = port.ID + return false, nil + } + return true, nil + }) + + if err != nil { + return "", err + } + return portID, nil +} + +func getFloatingIPs(networkingClient *gophercloud.ServiceClient) ([]floatingips.FloatingIP, error) { + pager := floatingips.List(networkingClient, floatingips.ListOpts{}) + + ips := []floatingips.FloatingIP{} + err := pager.EachPage(func(page pagination.Page) (bool, error) { + floatingipList, err := floatingips.ExtractFloatingIPs(page) + if err != nil { + return false, err + } + for _, f := range floatingipList { + ips = append(ips, f) + } + return true, nil + }) + + if err != nil { + return nil, err + } + return ips, nil +} + +func getImageID(client *gophercloud.ServiceClient, d *schema.ResourceData) (string, error) { + imageId := d.Get("image_id").(string) + + if imageId != "" { + return imageId, nil + } + + imageCount := 0 + imageName := d.Get("image_name").(string) + if imageName != "" { + pager := images.ListDetail(client, &images.ListOpts{ + Name: imageName, + }) + pager.EachPage(func(page pagination.Page) (bool, error) { + imageList, err := images.ExtractImages(page) + if err != nil { + return false, err + } + + for _, i := range imageList { + if i.Name == imageName { + imageCount++ + imageId = i.ID + } + } + return true, nil + }) + + switch imageCount { + case 0: + return "", fmt.Errorf("Unable to find image: %s", imageName) + case 1: + return imageId, nil + default: + return "", fmt.Errorf("Found %d images matching %s", imageCount, imageName) + } + } + return "", fmt.Errorf("Neither an image ID nor an image name were able to be determined.") +} + +func getFlavorID(client *gophercloud.ServiceClient, d *schema.ResourceData) (string, error) { + flavorId := d.Get("flavor_id").(string) + + if flavorId != "" { + return flavorId, nil + } + + flavorCount := 0 + flavorName := d.Get("flavor_name").(string) + if flavorName != "" { + pager := flavors.ListDetail(client, nil) + pager.EachPage(func(page pagination.Page) (bool, error) { + flavorList, err := flavors.ExtractFlavors(page) + if err != nil { + return false, err + } + + for _, f := range flavorList { + if f.Name == flavorName { + flavorCount++ + flavorId = f.ID + } + } + return true, nil + }) + + switch flavorCount { + case 0: + return "", fmt.Errorf("Unable to find flavor: %s", flavorName) + case 1: + return flavorId, nil + default: + return "", fmt.Errorf("Found %d flavors matching %s", flavorCount, flavorName) + } + } + return "", fmt.Errorf("Neither a flavor ID nor a flavor name were able to be determined.") +} + +func resourceComputeVolumeAttachmentHash(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%s-", m["volume_id"].(string))) + buf.WriteString(fmt.Sprintf("%s-", m["device"].(string))) + return hashcode.String(buf.String()) +} + +func attachVolumesToInstance(computeClient *gophercloud.ServiceClient, blockClient *gophercloud.ServiceClient, serverId string, vols []interface{}) error { + if len(vols) > 0 { + for _, v := range vols { + va := v.(map[string]interface{}) + volumeId := va["volume_id"].(string) + device := va["device"].(string) + + s := "" + if serverId != "" { + s = serverId + } else if va["server_id"] != "" { + s = va["server_id"].(string) + } else { + return fmt.Errorf("Unable to determine server ID to attach volume.") + } + + vaOpts := &volumeattach.CreateOpts{ + Device: device, + VolumeID: volumeId, + } + + if _, err := volumeattach.Create(computeClient, s, vaOpts).Extract(); err != nil { + return err + } + + stateConf := &resource.StateChangeConf{ + Pending: []string{"attaching", "available"}, + Target: "in-use", + Refresh: VolumeV1StateRefreshFunc(blockClient, va["volume_id"].(string)), + Timeout: 30 * time.Minute, + Delay: 5 * time.Second, + MinTimeout: 2 * time.Second, + } + + if _, err := stateConf.WaitForState(); err != nil { + return err + } + + log.Printf("[INFO] Attached volume %s to instance %s", volumeId, serverId) + } + } + return nil +} + +func detachVolumesFromInstance(computeClient *gophercloud.ServiceClient, blockClient *gophercloud.ServiceClient, serverId string, vols []interface{}) error { + if len(vols) > 0 { + for _, v := range vols { + va := v.(map[string]interface{}) + aId := va["id"].(string) + + if err := volumeattach.Delete(computeClient, serverId, aId).ExtractErr(); err != nil { + return err + } + + stateConf := &resource.StateChangeConf{ + Pending: []string{"detaching", "in-use"}, + Target: "available", + Refresh: VolumeV1StateRefreshFunc(blockClient, va["volume_id"].(string)), + Timeout: 30 * time.Minute, + Delay: 5 * time.Second, + MinTimeout: 2 * time.Second, + } + + if _, err := stateConf.WaitForState(); err != nil { + return err + } + log.Printf("[INFO] Detached volume %s from instance %s", va["volume_id"], serverId) + } + } + + return nil +} + +func getVolumeAttachments(computeClient *gophercloud.ServiceClient, serverId string) ([]volumeattach.VolumeAttachment, error) { + var attachments []volumeattach.VolumeAttachment + err := volumeattach.List(computeClient, serverId).EachPage(func(page pagination.Page) (bool, error) { + actual, err := volumeattach.ExtractVolumeAttachments(page) + if err != nil { + return false, err + } + + attachments = actual + return true, nil + }) + + if err != nil { + return nil, err + } + + return attachments, nil +} diff --git a/builtin/providers/openstack/resource_openstack_compute_instance_v2_test.go b/builtin/providers/openstack/resource_openstack_compute_instance_v2_test.go new file mode 100644 index 0000000000..f4c6c8557d --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_compute_instance_v2_test.go @@ -0,0 +1,185 @@ +package openstack + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + + "github.com/rackspace/gophercloud/openstack/blockstorage/v1/volumes" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/volumeattach" + "github.com/rackspace/gophercloud/openstack/compute/v2/servers" + "github.com/rackspace/gophercloud/pagination" +) + +func TestAccComputeV2Instance_basic(t *testing.T) { + var instance servers.Server + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeV2InstanceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeV2Instance_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeV2InstanceExists(t, "openstack_compute_instance_v2.foo", &instance), + testAccCheckComputeV2InstanceMetadata(&instance, "foo", "bar"), + ), + }, + }, + }) +} + +func TestAccComputeV2Instance_volumeAttach(t *testing.T) { + var instance servers.Server + var volume volumes.Volume + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeV2InstanceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeV2Instance_volumeAttach, + Check: resource.ComposeTestCheckFunc( + testAccCheckBlockStorageV1VolumeExists(t, "openstack_blockstorage_volume_v1.myvol", &volume), + testAccCheckComputeV2InstanceExists(t, "openstack_compute_instance_v2.foo", &instance), + testAccCheckComputeV2InstanceVolumeAttachment(&instance, &volume), + ), + }, + }, + }) +} + +func testAccCheckComputeV2InstanceDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + computeClient, err := config.computeV2Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("(testAccCheckComputeV2InstanceDestroy) Error creating OpenStack compute client: %s", err) + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != "openstack_compute_instance_v2" { + continue + } + + _, err := servers.Get(computeClient, rs.Primary.ID).Extract() + if err == nil { + return fmt.Errorf("Instance still exists") + } + } + + return nil +} + +func testAccCheckComputeV2InstanceExists(t *testing.T, n string, instance *servers.Server) 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) + computeClient, err := config.computeV2Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("(testAccCheckComputeV2InstanceExists) Error creating OpenStack compute client: %s", err) + } + + found, err := servers.Get(computeClient, rs.Primary.ID).Extract() + if err != nil { + return err + } + + if found.ID != rs.Primary.ID { + return fmt.Errorf("Instance not found") + } + + *instance = *found + + return nil + } +} + +func testAccCheckComputeV2InstanceMetadata( + instance *servers.Server, k string, v string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if instance.Metadata == nil { + return fmt.Errorf("No metadata") + } + + for key, value := range instance.Metadata { + if k != key { + continue + } + + if v == value.(string) { + return nil + } + + return fmt.Errorf("Bad value for %s: %s", k, value) + } + + return fmt.Errorf("Metadata not found: %s", k) + } +} + +func testAccCheckComputeV2InstanceVolumeAttachment( + instance *servers.Server, volume *volumes.Volume) resource.TestCheckFunc { + return func(s *terraform.State) error { + var attachments []volumeattach.VolumeAttachment + + config := testAccProvider.Meta().(*Config) + computeClient, err := config.computeV2Client(OS_REGION_NAME) + if err != nil { + return err + } + err = volumeattach.List(computeClient, instance.ID).EachPage(func(page pagination.Page) (bool, error) { + actual, err := volumeattach.ExtractVolumeAttachments(page) + if err != nil { + return false, fmt.Errorf("Unable to lookup attachment: %s", err) + } + + attachments = actual + return true, nil + }) + + for _, attachment := range attachments { + if attachment.VolumeID == volume.ID { + return nil + } + } + + return fmt.Errorf("Volume not found: %s", volume.ID) + } +} + +var testAccComputeV2Instance_basic = fmt.Sprintf(` + resource "openstack_compute_instance_v2" "foo" { + region = "%s" + name = "terraform-test" + metadata { + foo = "bar" + } + }`, + OS_REGION_NAME) + +var testAccComputeV2Instance_volumeAttach = fmt.Sprintf(` + resource "openstack_blockstorage_volume_v1" "myvol" { + name = "myvol" + size = 1 + } + + resource "openstack_compute_instance_v2" "foo" { + region = "%s" + name = "terraform-test" + volume { + volume_id = "${openstack_blockstorage_volume_v1.myvol.id}" + } + }`, + OS_REGION_NAME) diff --git a/builtin/providers/openstack/resource_openstack_compute_keypair_v2.go b/builtin/providers/openstack/resource_openstack_compute_keypair_v2.go new file mode 100644 index 0000000000..db6bed5b2f --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_compute_keypair_v2.go @@ -0,0 +1,92 @@ +package openstack + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs" +) + +func resourceComputeKeypairV2() *schema.Resource { + return &schema.Resource{ + Create: resourceComputeKeypairV2Create, + Read: resourceComputeKeypairV2Read, + Delete: resourceComputeKeypairV2Delete, + + Schema: map[string]*schema.Schema{ + "region": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + DefaultFunc: envDefaultFunc("OS_REGION_NAME"), + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "public_key": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + }, + } +} + +func resourceComputeKeypairV2Create(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + computeClient, err := config.computeV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack compute client: %s", err) + } + + createOpts := keypairs.CreateOpts{ + Name: d.Get("name").(string), + PublicKey: d.Get("public_key").(string), + } + + log.Printf("[DEBUG] Create Options: %#v", createOpts) + kp, err := keypairs.Create(computeClient, createOpts).Extract() + if err != nil { + return fmt.Errorf("Error creating OpenStack keypair: %s", err) + } + + d.SetId(kp.Name) + + return resourceComputeKeypairV2Read(d, meta) +} + +func resourceComputeKeypairV2Read(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + computeClient, err := config.computeV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack compute client: %s", err) + } + + kp, err := keypairs.Get(computeClient, d.Id()).Extract() + if err != nil { + return CheckDeleted(d, err, "keypair") + } + + d.Set("name", kp.Name) + d.Set("public_key", kp.PublicKey) + + return nil +} + +func resourceComputeKeypairV2Delete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + computeClient, err := config.computeV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack compute client: %s", err) + } + + err = keypairs.Delete(computeClient, d.Id()).ExtractErr() + if err != nil { + return fmt.Errorf("Error deleting OpenStack keypair: %s", err) + } + d.SetId("") + return nil +} diff --git a/builtin/providers/openstack/resource_openstack_compute_keypair_v2_test.go b/builtin/providers/openstack/resource_openstack_compute_keypair_v2_test.go new file mode 100644 index 0000000000..da090bcd83 --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_compute_keypair_v2_test.go @@ -0,0 +1,90 @@ +package openstack + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/keypairs" +) + +func TestAccComputeV2Keypair_basic(t *testing.T) { + var keypair keypairs.KeyPair + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeV2KeypairDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeV2Keypair_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeV2KeypairExists(t, "openstack_compute_keypair_v2.foo", &keypair), + ), + }, + }, + }) +} + +func testAccCheckComputeV2KeypairDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + computeClient, err := config.computeV2Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("(testAccCheckComputeV2KeypairDestroy) Error creating OpenStack compute client: %s", err) + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != "openstack_compute_keypair_v2" { + continue + } + + _, err := keypairs.Get(computeClient, rs.Primary.ID).Extract() + if err == nil { + return fmt.Errorf("Keypair still exists") + } + } + + return nil +} + +func testAccCheckComputeV2KeypairExists(t *testing.T, n string, kp *keypairs.KeyPair) 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) + computeClient, err := config.computeV2Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("(testAccCheckComputeV2KeypairExists) Error creating OpenStack compute client: %s", err) + } + + found, err := keypairs.Get(computeClient, rs.Primary.ID).Extract() + if err != nil { + return err + } + + if found.Name != rs.Primary.ID { + return fmt.Errorf("Keypair not found") + } + + *kp = *found + + return nil + } +} + +var testAccComputeV2Keypair_basic = fmt.Sprintf(` + resource "openstack_compute_keypair_v2" "foo" { + region = "%s" + name = "test-keypair-tf" + public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDAjpC1hwiOCCmKEWxJ4qzTTsJbKzndLo1BCz5PcwtUnflmU+gHJtWMZKpuEGVi29h0A/+ydKek1O18k10Ff+4tyFjiHDQAT9+OfgWf7+b1yK+qDip3X1C0UPMbwHlTfSGWLGZquwhvEFx9k3h/M+VtMvwR1lJ9LUyTAImnNjWG7TAIPmui30HvM2UiFEmqkr4ijq45MyX2+fLIePLRIFuu1p4whjHAQYufqyno3BS48icQb4p6iVEZPo4AE2o9oIyQvj2mx4dk5Y8CgSETOZTYDOR3rU2fZTRDRgPJDH9FWvQjF5tA0p3d9CoWWd2s6GKKbfoUIi8R/Db1BSPJwkqB jrp-hp-pc" + }`, + OS_REGION_NAME) diff --git a/builtin/providers/openstack/resource_openstack_compute_secgroup_v2.go b/builtin/providers/openstack/resource_openstack_compute_secgroup_v2.go new file mode 100644 index 0000000000..ca646d77d2 --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_compute_secgroup_v2.go @@ -0,0 +1,294 @@ +package openstack + +import ( + "bytes" + "fmt" + "log" + + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/schema" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups" +) + +func resourceComputeSecGroupV2() *schema.Resource { + return &schema.Resource{ + Create: resourceComputeSecGroupV2Create, + Read: resourceComputeSecGroupV2Read, + Update: resourceComputeSecGroupV2Update, + Delete: resourceComputeSecGroupV2Delete, + + Schema: map[string]*schema.Schema{ + "region": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + DefaultFunc: envDefaultFunc("OS_REGION_NAME"), + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: false, + }, + "description": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: false, + }, + "rule": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "from_port": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + ForceNew: false, + }, + "to_port": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + ForceNew: false, + }, + "ip_protocol": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: false, + }, + "cidr": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: false, + }, + "from_group_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: false, + }, + "self": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: false, + ForceNew: false, + }, + }, + }, + }, + }, + } +} + +func resourceComputeSecGroupV2Create(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + computeClient, err := config.computeV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack compute client: %s", err) + } + + createOpts := secgroups.CreateOpts{ + Name: d.Get("name").(string), + Description: d.Get("description").(string), + } + + log.Printf("[DEBUG] Create Options: %#v", createOpts) + sg, err := secgroups.Create(computeClient, createOpts).Extract() + if err != nil { + return fmt.Errorf("Error creating OpenStack security group: %s", err) + } + + d.SetId(sg.ID) + + createRuleOptsList := resourceSecGroupRulesV2(d) + for _, createRuleOpts := range createRuleOptsList { + _, err := secgroups.CreateRule(computeClient, createRuleOpts).Extract() + if err != nil { + return fmt.Errorf("Error creating OpenStack security group rule: %s", err) + } + } + + return resourceComputeSecGroupV2Read(d, meta) +} + +func resourceComputeSecGroupV2Read(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + computeClient, err := config.computeV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack compute client: %s", err) + } + + sg, err := secgroups.Get(computeClient, d.Id()).Extract() + if err != nil { + return CheckDeleted(d, err, "security group") + } + + d.Set("name", sg.Name) + d.Set("description", sg.Description) + rtm := rulesToMap(sg.Rules) + for _, v := range rtm { + if v["group"] == d.Get("name") { + v["self"] = "1" + } else { + v["self"] = "0" + } + } + log.Printf("[DEBUG] rulesToMap(sg.Rules): %+v", rtm) + d.Set("rule", rtm) + + return nil +} + +func resourceComputeSecGroupV2Update(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + computeClient, err := config.computeV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack compute client: %s", err) + } + + updateOpts := secgroups.UpdateOpts{ + Name: d.Get("name").(string), + Description: d.Get("description").(string), + } + + log.Printf("[DEBUG] Updating Security Group (%s) with options: %+v", d.Id(), updateOpts) + + _, err = secgroups.Update(computeClient, d.Id(), updateOpts).Extract() + if err != nil { + return fmt.Errorf("Error updating OpenStack security group (%s): %s", d.Id(), err) + } + + if d.HasChange("rule") { + oldSGRaw, newSGRaw := d.GetChange("rule") + oldSGRSlice, newSGRSlice := oldSGRaw.([]interface{}), newSGRaw.([]interface{}) + oldSGRSet := schema.NewSet(secgroupRuleV2Hash, oldSGRSlice) + newSGRSet := schema.NewSet(secgroupRuleV2Hash, newSGRSlice) + secgrouprulesToAdd := newSGRSet.Difference(oldSGRSet) + secgrouprulesToRemove := oldSGRSet.Difference(newSGRSet) + + log.Printf("[DEBUG] Security group rules to add: %v", secgrouprulesToAdd) + + log.Printf("[DEBUG] Security groups rules to remove: %v", secgrouprulesToRemove) + + for _, rawRule := range secgrouprulesToAdd.List() { + createRuleOpts := resourceSecGroupRuleCreateOptsV2(d, rawRule) + rule, err := secgroups.CreateRule(computeClient, createRuleOpts).Extract() + if err != nil { + return fmt.Errorf("Error adding rule to OpenStack security group (%s): %s", d.Id(), err) + } + log.Printf("[DEBUG] Added rule (%s) to OpenStack security group (%s) ", rule.ID, d.Id()) + } + + for _, r := range secgrouprulesToRemove.List() { + rule := resourceSecGroupRuleV2(d, r) + err := secgroups.DeleteRule(computeClient, rule.ID).ExtractErr() + if err != nil { + errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError) + if !ok { + return fmt.Errorf("Error removing rule (%s) from OpenStack security group (%s): %s", rule.ID, d.Id(), err) + } + if errCode.Actual == 404 { + continue + } else { + return fmt.Errorf("Error removing rule (%s) from OpenStack security group (%s)", rule.ID, d.Id()) + } + } else { + log.Printf("[DEBUG] Removed rule (%s) from OpenStack security group (%s): %s", rule.ID, d.Id(), err) + } + } + } + + return resourceComputeSecGroupV2Read(d, meta) +} + +func resourceComputeSecGroupV2Delete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + computeClient, err := config.computeV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack compute client: %s", err) + } + + err = secgroups.Delete(computeClient, d.Id()).ExtractErr() + if err != nil { + return fmt.Errorf("Error deleting OpenStack security group: %s", err) + } + d.SetId("") + return nil +} + +func resourceSecGroupRulesV2(d *schema.ResourceData) []secgroups.CreateRuleOpts { + rawRules := (d.Get("rule")).([]interface{}) + createRuleOptsList := make([]secgroups.CreateRuleOpts, len(rawRules)) + for i, raw := range rawRules { + rawMap := raw.(map[string]interface{}) + groupId := rawMap["from_group_id"].(string) + if rawMap["self"].(bool) { + groupId = d.Id() + } + createRuleOptsList[i] = secgroups.CreateRuleOpts{ + ParentGroupID: d.Id(), + FromPort: rawMap["from_port"].(int), + ToPort: rawMap["to_port"].(int), + IPProtocol: rawMap["ip_protocol"].(string), + CIDR: rawMap["cidr"].(string), + FromGroupID: groupId, + } + } + return createRuleOptsList +} + +func resourceSecGroupRuleCreateOptsV2(d *schema.ResourceData, raw interface{}) secgroups.CreateRuleOpts { + rawMap := raw.(map[string]interface{}) + groupId := rawMap["from_group_id"].(string) + if rawMap["self"].(bool) { + groupId = d.Id() + } + return secgroups.CreateRuleOpts{ + ParentGroupID: d.Id(), + FromPort: rawMap["from_port"].(int), + ToPort: rawMap["to_port"].(int), + IPProtocol: rawMap["ip_protocol"].(string), + CIDR: rawMap["cidr"].(string), + FromGroupID: groupId, + } +} + +func resourceSecGroupRuleV2(d *schema.ResourceData, raw interface{}) secgroups.Rule { + rawMap := raw.(map[string]interface{}) + return secgroups.Rule{ + ID: rawMap["id"].(string), + ParentGroupID: d.Id(), + FromPort: rawMap["from_port"].(int), + ToPort: rawMap["to_port"].(int), + IPProtocol: rawMap["ip_protocol"].(string), + IPRange: secgroups.IPRange{CIDR: rawMap["cidr"].(string)}, + } +} + +func rulesToMap(sgrs []secgroups.Rule) []map[string]interface{} { + sgrMap := make([]map[string]interface{}, len(sgrs)) + for i, sgr := range sgrs { + sgrMap[i] = map[string]interface{}{ + "id": sgr.ID, + "from_port": sgr.FromPort, + "to_port": sgr.ToPort, + "ip_protocol": sgr.IPProtocol, + "cidr": sgr.IPRange.CIDR, + "group": sgr.Group.Name, + } + } + return sgrMap +} + +func secgroupRuleV2Hash(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%d-", m["from_port"].(int))) + buf.WriteString(fmt.Sprintf("%d-", m["to_port"].(int))) + buf.WriteString(fmt.Sprintf("%s-", m["ip_protocol"].(string))) + buf.WriteString(fmt.Sprintf("%s-", m["cidr"].(string))) + + return hashcode.String(buf.String()) +} diff --git a/builtin/providers/openstack/resource_openstack_compute_secgroup_v2_test.go b/builtin/providers/openstack/resource_openstack_compute_secgroup_v2_test.go new file mode 100644 index 0000000000..e78865b8a5 --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_compute_secgroup_v2_test.go @@ -0,0 +1,90 @@ +package openstack + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + + "github.com/rackspace/gophercloud/openstack/compute/v2/extensions/secgroups" +) + +func TestAccComputeV2SecGroup_basic(t *testing.T) { + var secgroup secgroups.SecurityGroup + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeV2SecGroupDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeV2SecGroup_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeV2SecGroupExists(t, "openstack_compute_secgroup_v2.foo", &secgroup), + ), + }, + }, + }) +} + +func testAccCheckComputeV2SecGroupDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + computeClient, err := config.computeV2Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("(testAccCheckComputeV2SecGroupDestroy) Error creating OpenStack compute client: %s", err) + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != "openstack_compute_secgroup_v2" { + continue + } + + _, err := secgroups.Get(computeClient, rs.Primary.ID).Extract() + if err == nil { + return fmt.Errorf("Security group still exists") + } + } + + return nil +} + +func testAccCheckComputeV2SecGroupExists(t *testing.T, n string, secgroup *secgroups.SecurityGroup) 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) + computeClient, err := config.computeV2Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("(testAccCheckComputeV2SecGroupExists) Error creating OpenStack compute client: %s", err) + } + + found, err := secgroups.Get(computeClient, rs.Primary.ID).Extract() + if err != nil { + return err + } + + if found.ID != rs.Primary.ID { + return fmt.Errorf("Security group not found") + } + + *secgroup = *found + + return nil + } +} + +var testAccComputeV2SecGroup_basic = fmt.Sprintf(` + resource "openstack_compute_secgroup_v2" "foo" { + region = "%s" + name = "test_group_1" + description = "first test security group" + }`, + OS_REGION_NAME) diff --git a/builtin/providers/openstack/resource_openstack_fw_firewall_v1.go b/builtin/providers/openstack/resource_openstack_fw_firewall_v1.go new file mode 100644 index 0000000000..8505ac3b3a --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_fw_firewall_v1.go @@ -0,0 +1,242 @@ +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/fwaas/firewalls" +) + +func resourceFWFirewallV1() *schema.Resource { + return &schema.Resource{ + Create: resourceFWFirewallV1Create, + Read: resourceFWFirewallV1Read, + Update: resourceFWFirewallV1Update, + Delete: resourceFWFirewallV1Delete, + + Schema: map[string]*schema.Schema{ + "region": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + DefaultFunc: envDefaultFunc("OS_REGION_NAME"), + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "policy_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "admin_state_up": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "tenant_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + }, + } +} + +func resourceFWFirewallV1Create(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) + + firewallConfiguration := firewalls.CreateOpts{ + Name: d.Get("name").(string), + Description: d.Get("description").(string), + PolicyID: d.Get("policy_id").(string), + AdminStateUp: &adminStateUp, + TenantID: d.Get("tenant_id").(string), + } + + log.Printf("[DEBUG] Create firewall: %#v", firewallConfiguration) + + firewall, err := firewalls.Create(networkingClient, firewallConfiguration).Extract() + if err != nil { + return err + } + + log.Printf("[DEBUG] Firewall created: %#v", firewall) + + stateConf := &resource.StateChangeConf{ + Pending: []string{"PENDING_CREATE"}, + Target: "ACTIVE", + Refresh: waitForFirewallActive(networkingClient, firewall.ID), + Timeout: 30 * time.Second, + Delay: 0, + MinTimeout: 2 * time.Second, + } + + _, err = stateConf.WaitForState() + + d.SetId(firewall.ID) + + return resourceFWFirewallV1Read(d, meta) +} + +func resourceFWFirewallV1Read(d *schema.ResourceData, meta interface{}) error { + log.Printf("[DEBUG] Retrieve information about firewall: %s", d.Id()) + + config := meta.(*Config) + networkingClient, err := config.networkingV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack networking client: %s", err) + } + + firewall, err := firewalls.Get(networkingClient, d.Id()).Extract() + + if err != nil { + return CheckDeleted(d, err, "LB pool") + } + + d.Set("name", firewall.Name) + d.Set("description", firewall.Description) + d.Set("policy_id", firewall.PolicyID) + d.Set("admin_state_up", firewall.AdminStateUp) + d.Set("tenant_id", firewall.TenantID) + + return nil +} + +func resourceFWFirewallV1Update(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) + } + + opts := firewalls.UpdateOpts{} + + if d.HasChange("name") { + opts.Name = d.Get("name").(string) + } + + if d.HasChange("description") { + opts.Description = d.Get("description").(string) + } + + if d.HasChange("policy_id") { + opts.PolicyID = d.Get("policy_id").(string) + } + + if d.HasChange("admin_state_up") { + adminStateUp := d.Get("admin_state_up").(bool) + opts.AdminStateUp = &adminStateUp + } + + log.Printf("[DEBUG] Updating firewall with id %s: %#v", d.Id(), opts) + + stateConf := &resource.StateChangeConf{ + Pending: []string{"PENDING_CREATE", "PENDING_UPDATE"}, + Target: "ACTIVE", + Refresh: waitForFirewallActive(networkingClient, d.Id()), + Timeout: 30 * time.Second, + Delay: 0, + MinTimeout: 2 * time.Second, + } + + _, err = stateConf.WaitForState() + + err = firewalls.Update(networkingClient, d.Id(), opts).Err + if err != nil { + return err + } + + return resourceFWFirewallV1Read(d, meta) +} + +func resourceFWFirewallV1Delete(d *schema.ResourceData, meta interface{}) error { + log.Printf("[DEBUG] Destroy firewall: %s", d.Id()) + + 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{"PENDING_CREATE", "PENDING_UPDATE"}, + Target: "ACTIVE", + Refresh: waitForFirewallActive(networkingClient, d.Id()), + Timeout: 30 * time.Second, + Delay: 0, + MinTimeout: 2 * time.Second, + } + + _, err = stateConf.WaitForState() + + err = firewalls.Delete(networkingClient, d.Id()).Err + + if err != nil { + return err + } + + stateConf = &resource.StateChangeConf{ + Pending: []string{"DELETING"}, + Target: "DELETED", + Refresh: waitForFirewallDeletion(networkingClient, d.Id()), + Timeout: 2 * time.Minute, + Delay: 0, + MinTimeout: 2 * time.Second, + } + + _, err = stateConf.WaitForState() + + return err +} + +func waitForFirewallActive(networkingClient *gophercloud.ServiceClient, id string) resource.StateRefreshFunc { + + return func() (interface{}, string, error) { + fw, err := firewalls.Get(networkingClient, id).Extract() + log.Printf("[DEBUG] Get firewall %s => %#v", id, fw) + + if err != nil { + return nil, "", err + } + return fw, fw.Status, nil + } +} + +func waitForFirewallDeletion(networkingClient *gophercloud.ServiceClient, id string) resource.StateRefreshFunc { + + return func() (interface{}, string, error) { + fw, err := firewalls.Get(networkingClient, id).Extract() + log.Printf("[DEBUG] Get firewall %s => %#v", id, fw) + + if err != nil { + httpStatus := err.(*gophercloud.UnexpectedResponseCodeError) + log.Printf("[DEBUG] Get firewall %s status is %d", id, httpStatus.Actual) + + if httpStatus.Actual == 404 { + log.Printf("[DEBUG] Firewall %s is actually deleted", id) + return "", "DELETED", nil + } + return nil, "", fmt.Errorf("Unexpected status code %d", httpStatus.Actual) + } + + log.Printf("[DEBUG] Firewall %s deletion is pending", id) + return fw, "DELETING", nil + } +} diff --git a/builtin/providers/openstack/resource_openstack_fw_firewall_v1_test.go b/builtin/providers/openstack/resource_openstack_fw_firewall_v1_test.go new file mode 100644 index 0000000000..34112f778f --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_fw_firewall_v1_test.go @@ -0,0 +1,139 @@ +package openstack + +import ( + "fmt" + "testing" + "time" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/firewalls" +) + +func TestAccFWFirewallV1(t *testing.T) { + + var policyID *string + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckFWFirewallV1Destroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testFirewallConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckFWFirewallV1Exists("openstack_fw_firewall_v1.accept_test", "", "", policyID), + ), + }, + resource.TestStep{ + Config: testFirewallConfigUpdated, + Check: resource.ComposeTestCheckFunc( + testAccCheckFWFirewallV1Exists("openstack_fw_firewall_v1.accept_test", "accept_test", "terraform acceptance test", policyID), + ), + }, + }, + }) +} + +func testAccCheckFWFirewallV1Destroy(s *terraform.State) error { + + config := testAccProvider.Meta().(*Config) + networkingClient, err := config.networkingV2Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("(testAccCheckOpenstackFirewallDestroy) Error creating OpenStack networking client: %s", err) + } + for _, rs := range s.RootModule().Resources { + if rs.Type != "openstack_firewall" { + continue + } + _, err = firewalls.Get(networkingClient, rs.Primary.ID).Extract() + if err == nil { + return fmt.Errorf("Firewall (%s) still exists.", rs.Primary.ID) + } + httpError, ok := err.(*gophercloud.UnexpectedResponseCodeError) + if !ok || httpError.Actual != 404 { + return httpError + } + } + return nil +} + +func testAccCheckFWFirewallV1Exists(n, expectedName, expectedDescription string, policyID *string) 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("(testAccCheckFirewallExists) Error creating OpenStack networking client: %s", err) + } + + var found *firewalls.Firewall + for i := 0; i < 5; i++ { + // Firewall creation is asynchronous. Retry some times + // if we get a 404 error. Fail on any other error. + found, err = firewalls.Get(networkingClient, rs.Primary.ID).Extract() + if err != nil { + httpError, ok := err.(*gophercloud.UnexpectedResponseCodeError) + if !ok || httpError.Actual != 404 { + time.Sleep(time.Second) + continue + } + } + break + } + + if err != nil { + return err + } + + if found.Name != expectedName { + return fmt.Errorf("Expected Name to be <%s> but found <%s>", expectedName, found.Name) + } + if found.Description != expectedDescription { + return fmt.Errorf("Expected Description to be <%s> but found <%s>", expectedDescription, found.Description) + } + if found.PolicyID == "" { + return fmt.Errorf("Policy should not be empty") + } + if policyID != nil && found.PolicyID == *policyID { + return fmt.Errorf("Policy had not been correctly updated. Went from <%s> to <%s>", expectedName, found.Name) + } + + policyID = &found.PolicyID + + return nil + } +} + +const testFirewallConfig = ` +resource "openstack_fw_firewall_v1" "accept_test" { + policy_id = "${openstack_fw_policy_v1.accept_test_policy_1.id}" +} + +resource "openstack_fw_policy_v1" "accept_test_policy_1" { + name = "policy-1" +} +` + +const testFirewallConfigUpdated = ` +resource "openstack_fw_firewall_v1" "accept_test" { + name = "accept_test" + description = "terraform acceptance test" + policy_id = "${openstack_fw_policy_v1.accept_test_policy_2.id}" +} + +resource "openstack_fw_policy_v1" "accept_test_policy_2" { + name = "policy-2" +} +` diff --git a/builtin/providers/openstack/resource_openstack_fw_policy_v1.go b/builtin/providers/openstack/resource_openstack_fw_policy_v1.go new file mode 100644 index 0000000000..0560bfcef6 --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_fw_policy_v1.go @@ -0,0 +1,200 @@ +package openstack + +import ( + "fmt" + "log" + "time" + + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/schema" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/policies" +) + +func resourceFWPolicyV1() *schema.Resource { + return &schema.Resource{ + Create: resourceFWPolicyV1Create, + Read: resourceFWPolicyV1Read, + Update: resourceFWPolicyV1Update, + Delete: resourceFWPolicyV1Delete, + + Schema: map[string]*schema.Schema{ + "region": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + DefaultFunc: envDefaultFunc("OS_REGION_NAME"), + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "audited": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "shared": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "tenant_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "rules": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: func(v interface{}) int { + return hashcode.String(v.(string)) + }, + }, + }, + } +} + +func resourceFWPolicyV1Create(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) + } + + v := d.Get("rules").(*schema.Set) + + log.Printf("[DEBUG] Rules found : %#v", v) + log.Printf("[DEBUG] Rules count : %d", v.Len()) + + rules := make([]string, v.Len()) + for i, v := range v.List() { + rules[i] = v.(string) + } + + audited := d.Get("audited").(bool) + shared := d.Get("shared").(bool) + + opts := policies.CreateOpts{ + Name: d.Get("name").(string), + Description: d.Get("description").(string), + Audited: &audited, + Shared: &shared, + TenantID: d.Get("tenant_id").(string), + Rules: rules, + } + + log.Printf("[DEBUG] Create firewall policy: %#v", opts) + + policy, err := policies.Create(networkingClient, opts).Extract() + if err != nil { + return err + } + + log.Printf("[DEBUG] Firewall policy created: %#v", policy) + + d.SetId(policy.ID) + + return resourceFWPolicyV1Read(d, meta) +} + +func resourceFWPolicyV1Read(d *schema.ResourceData, meta interface{}) error { + log.Printf("[DEBUG] Retrieve information about firewall policy: %s", d.Id()) + + config := meta.(*Config) + networkingClient, err := config.networkingV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack networking client: %s", err) + } + + policy, err := policies.Get(networkingClient, d.Id()).Extract() + + if err != nil { + return CheckDeleted(d, err, "LB pool") + } + + d.Set("name", policy.Name) + d.Set("description", policy.Description) + d.Set("shared", policy.Shared) + d.Set("audited", policy.Audited) + d.Set("tenant_id", policy.TenantID) + return nil +} + +func resourceFWPolicyV1Update(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) + } + + opts := policies.UpdateOpts{} + + if d.HasChange("name") { + opts.Name = d.Get("name").(string) + } + + if d.HasChange("description") { + opts.Description = d.Get("description").(string) + } + + if d.HasChange("rules") { + v := d.Get("rules").(*schema.Set) + + log.Printf("[DEBUG] Rules found : %#v", v) + log.Printf("[DEBUG] Rules count : %d", v.Len()) + + rules := make([]string, v.Len()) + for i, v := range v.List() { + rules[i] = v.(string) + } + opts.Rules = rules + } + + log.Printf("[DEBUG] Updating firewall policy with id %s: %#v", d.Id(), opts) + + err = policies.Update(networkingClient, d.Id(), opts).Err + if err != nil { + return err + } + + return resourceFWPolicyV1Read(d, meta) +} + +func resourceFWPolicyV1Delete(d *schema.ResourceData, meta interface{}) error { + log.Printf("[DEBUG] Destroy firewall policy: %s", d.Id()) + + config := meta.(*Config) + networkingClient, err := config.networkingV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack networking client: %s", err) + } + + for i := 0; i < 15; i++ { + + err = policies.Delete(networkingClient, d.Id()).Err + if err == nil { + break + } + + httpError, ok := err.(*gophercloud.UnexpectedResponseCodeError) + if !ok || httpError.Actual != 409 { + return err + } + + // This error usualy means that the policy is attached + // to a firewall. At this point, the firewall is probably + // being delete. So, we retry a few times. + + time.Sleep(time.Second * 2) + } + + return err +} diff --git a/builtin/providers/openstack/resource_openstack_fw_policy_v1_test.go b/builtin/providers/openstack/resource_openstack_fw_policy_v1_test.go new file mode 100644 index 0000000000..1a37a383f7 --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_fw_policy_v1_test.go @@ -0,0 +1,165 @@ +package openstack + +import ( + "fmt" + "testing" + "time" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/policies" +) + +func TestAccFWPolicyV1(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckFWPolicyV1Destroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testFirewallPolicyConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckFWPolicyV1Exists( + "openstack_fw_policy_v1.accept_test", + "", "", 0), + ), + }, + resource.TestStep{ + Config: testFirewallPolicyConfigAddRules, + Check: resource.ComposeTestCheckFunc( + testAccCheckFWPolicyV1Exists( + "openstack_fw_policy_v1.accept_test", + "accept_test", "terraform acceptance test", 2), + ), + }, + resource.TestStep{ + Config: testFirewallPolicyUpdateDeleteRule, + Check: resource.ComposeTestCheckFunc( + testAccCheckFWPolicyV1Exists( + "openstack_fw_policy_v1.accept_test", + "accept_test", "terraform acceptance test", 1), + ), + }, + }, + }) +} + +func testAccCheckFWPolicyV1Destroy(s *terraform.State) error { + + config := testAccProvider.Meta().(*Config) + networkingClient, err := config.networkingV2Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("(testAccCheckOpenstackFirewallPolicyDestroy) Error creating OpenStack networking client: %s", err) + } + for _, rs := range s.RootModule().Resources { + if rs.Type != "openstack_fw_policy_v1" { + continue + } + _, err = policies.Get(networkingClient, rs.Primary.ID).Extract() + if err == nil { + return fmt.Errorf("Firewall policy (%s) still exists.", rs.Primary.ID) + } + httpError, ok := err.(*gophercloud.UnexpectedResponseCodeError) + if !ok || httpError.Actual != 404 { + return httpError + } + } + return nil +} + +func testAccCheckFWPolicyV1Exists(n, name, description string, ruleCount int) 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("(testAccCheckFirewallPolicyExists) Error creating OpenStack networking client: %s", err) + } + + var found *policies.Policy + for i := 0; i < 5; i++ { + // Firewall policy creation is asynchronous. Retry some times + // if we get a 404 error. Fail on any other error. + found, err = policies.Get(networkingClient, rs.Primary.ID).Extract() + if err != nil { + httpError, ok := err.(*gophercloud.UnexpectedResponseCodeError) + if !ok || httpError.Actual != 404 { + time.Sleep(time.Second) + continue + } + } + break + } + + if err != nil { + return err + } + + if name != found.Name { + return fmt.Errorf("Expected name <%s>, but found <%s>", name, found.Name) + } + + if description != found.Description { + return fmt.Errorf("Expected description <%s>, but found <%s>", description, found.Description) + } + + if ruleCount != len(found.Rules) { + return fmt.Errorf("Expected rule count <%d>, but found <%d>", ruleCount, len(found.Rules)) + } + + return nil + } +} + +const testFirewallPolicyConfig = ` +resource "openstack_fw_policy_v1" "accept_test" { + +} +` + +const testFirewallPolicyConfigAddRules = ` +resource "openstack_fw_policy_v1" "accept_test" { + name = "accept_test" + description = "terraform acceptance test" + rules = [ + "${openstack_fw_rule_v1.accept_test_udp_deny.id}", + "${openstack_fw_rule_v1.accept_test_tcp_allow.id}" + ] +} + +resource "openstack_fw_rule_v1" "accept_test_tcp_allow" { + protocol = "tcp" + action = "allow" +} + +resource "openstack_fw_rule_v1" "accept_test_udp_deny" { + protocol = "udp" + action = "deny" +} +` + +const testFirewallPolicyUpdateDeleteRule = ` +resource "openstack_fw_policy_v1" "accept_test" { + name = "accept_test" + description = "terraform acceptance test" + rules = [ + "${openstack_fw_rule_v1.accept_test_udp_deny.id}" + ] +} + +resource "openstack_fw_rule_v1" "accept_test_udp_deny" { + protocol = "udp" + action = "deny" +} +` diff --git a/builtin/providers/openstack/resource_openstack_fw_rule_v1.go b/builtin/providers/openstack/resource_openstack_fw_rule_v1.go new file mode 100644 index 0000000000..f0f5affcc0 --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_fw_rule_v1.go @@ -0,0 +1,223 @@ +package openstack + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/policies" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/rules" +) + +func resourceFWRuleV1() *schema.Resource { + return &schema.Resource{ + Create: resourceFWRuleV1Create, + Read: resourceFWRuleV1Read, + Update: resourceFWRuleV1Update, + Delete: resourceFWRuleV1Delete, + + Schema: map[string]*schema.Schema{ + "region": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + DefaultFunc: envDefaultFunc("OS_REGION_NAME"), + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "protocol": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "action": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "ip_version": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Default: 4, + }, + "source_ip_address": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "destination_ip_address": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "source_port": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "destination_port": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "enabled": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "tenant_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + }, + } +} + +func resourceFWRuleV1Create(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) + } + + enabled := d.Get("enabled").(bool) + + ruleConfiguration := rules.CreateOpts{ + Name: d.Get("name").(string), + Description: d.Get("description").(string), + Protocol: d.Get("protocol").(string), + Action: d.Get("action").(string), + IPVersion: d.Get("ip_version").(int), + SourceIPAddress: d.Get("source_ip_address").(string), + DestinationIPAddress: d.Get("destination_ip_address").(string), + SourcePort: d.Get("source_port").(string), + DestinationPort: d.Get("destination_port").(string), + Enabled: &enabled, + TenantID: d.Get("tenant_id").(string), + } + + log.Printf("[DEBUG] Create firewall rule: %#v", ruleConfiguration) + + rule, err := rules.Create(networkingClient, ruleConfiguration).Extract() + + if err != nil { + return err + } + + log.Printf("[DEBUG] Firewall rule with id %s : %#v", rule.ID, rule) + + d.SetId(rule.ID) + + return resourceFWRuleV1Read(d, meta) +} + +func resourceFWRuleV1Read(d *schema.ResourceData, meta interface{}) error { + log.Printf("[DEBUG] Retrieve information about firewall rule: %s", d.Id()) + + config := meta.(*Config) + networkingClient, err := config.networkingV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack networking client: %s", err) + } + + rule, err := rules.Get(networkingClient, d.Id()).Extract() + + if err != nil { + return CheckDeleted(d, err, "LB pool") + } + + d.Set("protocol", rule.Protocol) + d.Set("action", rule.Action) + + d.Set("name", rule.Name) + d.Set("description", rule.Description) + d.Set("ip_version", rule.IPVersion) + d.Set("source_ip_address", rule.SourceIPAddress) + d.Set("destination_ip_address", rule.DestinationIPAddress) + d.Set("source_port", rule.SourcePort) + d.Set("destination_port", rule.DestinationPort) + d.Set("enabled", rule.Enabled) + + return nil +} + +func resourceFWRuleV1Update(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) + } + + opts := rules.UpdateOpts{} + + if d.HasChange("name") { + opts.Name = d.Get("name").(string) + } + if d.HasChange("description") { + opts.Description = d.Get("description").(string) + } + if d.HasChange("protocol") { + opts.Protocol = d.Get("protocol").(string) + } + if d.HasChange("action") { + opts.Action = d.Get("action").(string) + } + if d.HasChange("ip_version") { + opts.IPVersion = d.Get("ip_version").(int) + } + if d.HasChange("source_ip_address") { + sourceIPAddress := d.Get("source_ip_address").(string) + opts.SourceIPAddress = &sourceIPAddress + } + if d.HasChange("destination_ip_address") { + destinationIPAddress := d.Get("destination_ip_address").(string) + opts.DestinationIPAddress = &destinationIPAddress + } + if d.HasChange("source_port") { + sourcePort := d.Get("source_port").(string) + opts.SourcePort = &sourcePort + } + if d.HasChange("destination_port") { + destinationPort := d.Get("destination_port").(string) + opts.DestinationPort = &destinationPort + } + if d.HasChange("enabled") { + enabled := d.Get("enabled").(bool) + opts.Enabled = &enabled + } + + log.Printf("[DEBUG] Updating firewall rules: %#v", opts) + + err = rules.Update(networkingClient, d.Id(), opts).Err + if err != nil { + return err + } + + return resourceFWRuleV1Read(d, meta) +} + +func resourceFWRuleV1Delete(d *schema.ResourceData, meta interface{}) error { + log.Printf("[DEBUG] Destroy firewall rule: %s", d.Id()) + + config := meta.(*Config) + networkingClient, err := config.networkingV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack networking client: %s", err) + } + + rule, err := rules.Get(networkingClient, d.Id()).Extract() + if err != nil { + return err + } + + if rule.PolicyID != "" { + err := policies.RemoveRule(networkingClient, rule.PolicyID, rule.ID) + if err != nil { + return err + } + } + + return rules.Delete(networkingClient, d.Id()).Err +} diff --git a/builtin/providers/openstack/resource_openstack_fw_rule_v1_test.go b/builtin/providers/openstack/resource_openstack_fw_rule_v1_test.go new file mode 100644 index 0000000000..ba96bb8b19 --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_fw_rule_v1_test.go @@ -0,0 +1,185 @@ +package openstack + +import ( + "fmt" + "reflect" + "testing" + "time" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/fwaas/rules" +) + +func TestAccFWRuleV1(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckFWRuleV1Destroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testFirewallRuleMinimalConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckFWRuleV1Exists( + "openstack_fw_rule_v1.accept_test_minimal", + &rules.Rule{ + Protocol: "udp", + Action: "deny", + IPVersion: 4, + Enabled: true, + }), + ), + }, + resource.TestStep{ + Config: testFirewallRuleConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckFWRuleV1Exists( + "openstack_fw_rule_v1.accept_test", + &rules.Rule{ + Name: "accept_test", + Protocol: "udp", + Action: "deny", + Description: "Terraform accept test", + IPVersion: 4, + SourceIPAddress: "1.2.3.4", + DestinationIPAddress: "4.3.2.0/24", + SourcePort: "444", + DestinationPort: "555", + Enabled: true, + }), + ), + }, + resource.TestStep{ + Config: testFirewallRuleUpdateAllFieldsConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckFWRuleV1Exists( + "openstack_fw_rule_v1.accept_test", + &rules.Rule{ + Name: "accept_test_updated_2", + Protocol: "tcp", + Action: "allow", + Description: "Terraform accept test updated", + IPVersion: 4, + SourceIPAddress: "1.2.3.0/24", + DestinationIPAddress: "4.3.2.8", + SourcePort: "666", + DestinationPort: "777", + Enabled: false, + }), + ), + }, + }, + }) +} + +func testAccCheckFWRuleV1Destroy(s *terraform.State) error { + + config := testAccProvider.Meta().(*Config) + networkingClient, err := config.networkingV2Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("(testAccCheckOpenstackFirewallRuleDestroy) Error creating OpenStack networking client: %s", err) + } + for _, rs := range s.RootModule().Resources { + if rs.Type != "openstack_firewall_rule" { + continue + } + _, err = rules.Get(networkingClient, rs.Primary.ID).Extract() + if err == nil { + return fmt.Errorf("Firewall rule (%s) still exists.", rs.Primary.ID) + } + httpError, ok := err.(*gophercloud.UnexpectedResponseCodeError) + if !ok || httpError.Actual != 404 { + return httpError + } + } + return nil +} + +func testAccCheckFWRuleV1Exists(n string, expected *rules.Rule) 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("(testAccCheckFirewallRuleExists) Error creating OpenStack networking client: %s", err) + } + + var found *rules.Rule + for i := 0; i < 5; i++ { + // Firewall rule creation is asynchronous. Retry some times + // if we get a 404 error. Fail on any other error. + found, err = rules.Get(networkingClient, rs.Primary.ID).Extract() + if err != nil { + httpError, ok := err.(*gophercloud.UnexpectedResponseCodeError) + if !ok || httpError.Actual != 404 { + time.Sleep(time.Second) + continue + } + } + break + } + + if err != nil { + return err + } + + expected.ID = found.ID + // Erase the tenant id because we don't want to compare + // it as long it is not present in the expected + found.TenantID = "" + + if !reflect.DeepEqual(expected, found) { + return fmt.Errorf("Expected:\n%#v\nFound:\n%#v", expected, found) + } + + return nil + } +} + +const testFirewallRuleMinimalConfig = ` +resource "openstack_fw_rule_v1" "accept_test_minimal" { + protocol = "udp" + action = "deny" +} +` + +const testFirewallRuleConfig = ` +resource "openstack_fw_rule_v1" "accept_test" { + name = "accept_test" + description = "Terraform accept test" + protocol = "udp" + action = "deny" + ip_version = 4 + source_ip_address = "1.2.3.4" + destination_ip_address = "4.3.2.0/24" + source_port = "444" + destination_port = "555" + enabled = true +} +` + +const testFirewallRuleUpdateAllFieldsConfig = ` +resource "openstack_fw_rule_v1" "accept_test" { + name = "accept_test_updated_2" + description = "Terraform accept test updated" + protocol = "tcp" + action = "allow" + ip_version = 4 + source_ip_address = "1.2.3.0/24" + destination_ip_address = "4.3.2.8" + source_port = "666" + destination_port = "777" + enabled = false +} +` diff --git a/builtin/providers/openstack/resource_openstack_lb_monitor_v1.go b/builtin/providers/openstack/resource_openstack_lb_monitor_v1.go new file mode 100644 index 0000000000..cdfd54ccc1 --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_lb_monitor_v1.go @@ -0,0 +1,192 @@ +package openstack + +import ( + "fmt" + "log" + "strconv" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors" +) + +func resourceLBMonitorV1() *schema.Resource { + return &schema.Resource{ + Create: resourceLBMonitorV1Create, + Read: resourceLBMonitorV1Read, + Update: resourceLBMonitorV1Update, + Delete: resourceLBMonitorV1Delete, + + Schema: map[string]*schema.Schema{ + "region": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + DefaultFunc: envDefaultFunc("OS_REGION_NAME"), + }, + "tenant_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "delay": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + ForceNew: false, + }, + "timeout": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + ForceNew: false, + }, + "max_retries": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + ForceNew: false, + }, + "url_path": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: false, + }, + "http_method": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: false, + }, + "expected_codes": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: false, + }, + "admin_state_up": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: false, + }, + }, + } +} + +func resourceLBMonitorV1Create(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 := monitors.CreateOpts{ + TenantID: d.Get("tenant_id").(string), + Type: d.Get("type").(string), + Delay: d.Get("delay").(int), + Timeout: d.Get("timeout").(int), + MaxRetries: d.Get("max_retries").(int), + URLPath: d.Get("url_path").(string), + ExpectedCodes: d.Get("expected_codes").(string), + HTTPMethod: d.Get("http_method").(string), + } + + asuRaw := d.Get("admin_state_up").(string) + if asuRaw != "" { + asu, err := strconv.ParseBool(asuRaw) + if err != nil { + return fmt.Errorf("admin_state_up, if provided, must be either 'true' or 'false'") + } + createOpts.AdminStateUp = &asu + } + + log.Printf("[DEBUG] Create Options: %#v", createOpts) + m, err := monitors.Create(networkingClient, createOpts).Extract() + if err != nil { + return fmt.Errorf("Error creating OpenStack LB Monitor: %s", err) + } + log.Printf("[INFO] LB Monitor ID: %s", m.ID) + + d.SetId(m.ID) + + return resourceLBMonitorV1Read(d, meta) +} + +func resourceLBMonitorV1Read(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) + } + + m, err := monitors.Get(networkingClient, d.Id()).Extract() + if err != nil { + return CheckDeleted(d, err, "LB monitor") + } + + log.Printf("[DEBUG] Retreived OpenStack LB Monitor %s: %+v", d.Id(), m) + + d.Set("type", m.Type) + d.Set("delay", m.Delay) + d.Set("timeout", m.Timeout) + d.Set("max_retries", m.MaxRetries) + d.Set("tenant_id", m.TenantID) + d.Set("url_path", m.URLPath) + d.Set("http_method", m.HTTPMethod) + d.Set("expected_codes", m.ExpectedCodes) + d.Set("admin_state_up", strconv.FormatBool(m.AdminStateUp)) + + return nil +} + +func resourceLBMonitorV1Update(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) + } + + updateOpts := monitors.UpdateOpts{ + Delay: d.Get("delay").(int), + Timeout: d.Get("timeout").(int), + MaxRetries: d.Get("max_retries").(int), + URLPath: d.Get("url_path").(string), + HTTPMethod: d.Get("http_method").(string), + ExpectedCodes: d.Get("expected_codes").(string), + } + + if d.HasChange("admin_state_up") { + asuRaw := d.Get("admin_state_up").(string) + if asuRaw != "" { + asu, err := strconv.ParseBool(asuRaw) + if err != nil { + return fmt.Errorf("admin_state_up, if provided, must be either 'true' or 'false'") + } + updateOpts.AdminStateUp = &asu + } + } + + log.Printf("[DEBUG] Updating OpenStack LB Monitor %s with options: %+v", d.Id(), updateOpts) + + _, err = monitors.Update(networkingClient, d.Id(), updateOpts).Extract() + if err != nil { + return fmt.Errorf("Error updating OpenStack LB Monitor: %s", err) + } + + return resourceLBMonitorV1Read(d, meta) +} + +func resourceLBMonitorV1Delete(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) + } + + err = monitors.Delete(networkingClient, d.Id()).ExtractErr() + if err != nil { + return fmt.Errorf("Error deleting OpenStack LB Monitor: %s", err) + } + + d.SetId("") + return nil +} diff --git a/builtin/providers/openstack/resource_openstack_lb_monitor_v1_test.go b/builtin/providers/openstack/resource_openstack_lb_monitor_v1_test.go new file mode 100644 index 0000000000..5aaf61d2c6 --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_lb_monitor_v1_test.go @@ -0,0 +1,110 @@ +package openstack + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/monitors" +) + +func TestAccLBV1Monitor_basic(t *testing.T) { + var monitor monitors.Monitor + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckLBV1MonitorDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccLBV1Monitor_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckLBV1MonitorExists(t, "openstack_lb_monitor_v1.monitor_1", &monitor), + ), + }, + resource.TestStep{ + Config: testAccLBV1Monitor_update, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("openstack_lb_monitor_v1.monitor_1", "delay", "20"), + ), + }, + }, + }) +} + +func testAccCheckLBV1MonitorDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + networkingClient, err := config.networkingV2Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("(testAccCheckLBV1MonitorDestroy) Error creating OpenStack networking client: %s", err) + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != "openstack_lb_monitor_v1" { + continue + } + + _, err := monitors.Get(networkingClient, rs.Primary.ID).Extract() + if err == nil { + return fmt.Errorf("LB monitor still exists") + } + } + + return nil +} + +func testAccCheckLBV1MonitorExists(t *testing.T, n string, monitor *monitors.Monitor) 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("(testAccCheckLBV1MonitorExists) Error creating OpenStack networking client: %s", err) + } + + found, err := monitors.Get(networkingClient, rs.Primary.ID).Extract() + if err != nil { + return err + } + + if found.ID != rs.Primary.ID { + return fmt.Errorf("Monitor not found") + } + + *monitor = *found + + return nil + } +} + +var testAccLBV1Monitor_basic = fmt.Sprintf(` + resource "openstack_lb_monitor_v1" "monitor_1" { + region = "%s" + type = "PING" + delay = 30 + timeout = 5 + max_retries = 3 + admin_state_up = "true" + }`, + OS_REGION_NAME) + +var testAccLBV1Monitor_update = fmt.Sprintf(` + resource "openstack_lb_monitor_v1" "monitor_1" { + region = "%s" + type = "PING" + delay = 20 + timeout = 5 + max_retries = 3 + admin_state_up = "true" + }`, + OS_REGION_NAME) diff --git a/builtin/providers/openstack/resource_openstack_lb_pool_v1.go b/builtin/providers/openstack/resource_openstack_lb_pool_v1.go new file mode 100644 index 0000000000..6b69f2fac1 --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_lb_pool_v1.go @@ -0,0 +1,327 @@ +package openstack + +import ( + "bytes" + "fmt" + "log" + + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/schema" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/members" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools" + "github.com/rackspace/gophercloud/pagination" +) + +func resourceLBPoolV1() *schema.Resource { + return &schema.Resource{ + Create: resourceLBPoolV1Create, + Read: resourceLBPoolV1Read, + Update: resourceLBPoolV1Update, + Delete: resourceLBPoolV1Delete, + + Schema: map[string]*schema.Schema{ + "region": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + DefaultFunc: envDefaultFunc("OS_REGION_NAME"), + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: false, + }, + "protocol": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "subnet_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "lb_method": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: false, + }, + "tenant_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "member": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "region": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + DefaultFunc: envDefaultFunc("OS_REGION_NAME"), + }, + "tenant_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "address": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "port": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + ForceNew: true, + }, + "admin_state_up": &schema.Schema{ + Type: schema.TypeBool, + Required: true, + ForceNew: false, + }, + }, + }, + Set: resourceLBMemberV1Hash, + }, + "monitor_ids": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + ForceNew: false, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: func(v interface{}) int { + return hashcode.String(v.(string)) + }, + }, + }, + } +} + +func resourceLBPoolV1Create(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 := pools.CreateOpts{ + Name: d.Get("name").(string), + Protocol: d.Get("protocol").(string), + SubnetID: d.Get("subnet_id").(string), + LBMethod: d.Get("lb_method").(string), + TenantID: d.Get("tenant_id").(string), + } + + log.Printf("[DEBUG] Create Options: %#v", createOpts) + p, err := pools.Create(networkingClient, createOpts).Extract() + if err != nil { + return fmt.Errorf("Error creating OpenStack LB pool: %s", err) + } + log.Printf("[INFO] LB Pool ID: %s", p.ID) + + d.SetId(p.ID) + + if mIDs := resourcePoolMonitorIDsV1(d); mIDs != nil { + for _, mID := range mIDs { + _, err := pools.AssociateMonitor(networkingClient, p.ID, mID).Extract() + if err != nil { + return fmt.Errorf("Error associating monitor (%s) with OpenStack LB pool (%s): %s", mID, p.ID, err) + } + } + } + + if memberOpts := resourcePoolMembersV1(d); memberOpts != nil { + for _, memberOpt := range memberOpts { + _, err := members.Create(networkingClient, memberOpt).Extract() + if err != nil { + return fmt.Errorf("Error creating OpenStack LB member: %s", err) + } + } + } + + return resourceLBPoolV1Read(d, meta) +} + +func resourceLBPoolV1Read(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 := pools.Get(networkingClient, d.Id()).Extract() + if err != nil { + return CheckDeleted(d, err, "LB pool") + } + + log.Printf("[DEBUG] Retreived OpenStack LB Pool %s: %+v", d.Id(), p) + + d.Set("name", p.Name) + d.Set("protocol", p.Protocol) + d.Set("subnet_id", p.SubnetID) + d.Set("lb_method", p.LBMethod) + d.Set("tenant_id", p.TenantID) + d.Set("monitor_ids", p.MonitorIDs) + d.Set("member_ids", p.MemberIDs) + + return nil +} + +func resourceLBPoolV1Update(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("name") { + updateOpts.Name = d.Get("name").(string) + } + if d.HasChange("lb_method") { + updateOpts.LBMethod = d.Get("lb_method").(string) + } + + log.Printf("[DEBUG] Updating OpenStack LB 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 LB Pool: %s", err) + } + + if d.HasChange("monitor_ids") { + oldMIDsRaw, newMIDsRaw := d.GetChange("security_groups") + oldMIDsSet, newMIDsSet := oldMIDsRaw.(*schema.Set), newMIDsRaw.(*schema.Set) + monitorsToAdd := newMIDsSet.Difference(oldMIDsSet) + monitorsToRemove := oldMIDsSet.Difference(newMIDsSet) + + log.Printf("[DEBUG] Monitors to add: %v", monitorsToAdd) + + log.Printf("[DEBUG] Monitors to remove: %v", monitorsToRemove) + + for _, m := range monitorsToAdd.List() { + _, err := pools.AssociateMonitor(networkingClient, d.Id(), m.(string)).Extract() + if err != nil { + return fmt.Errorf("Error associating monitor (%s) with OpenStack server (%s): %s", m.(string), d.Id(), err) + } + log.Printf("[DEBUG] Associated monitor (%s) with pool (%s)", m.(string), d.Id()) + } + + for _, m := range monitorsToRemove.List() { + _, err := pools.DisassociateMonitor(networkingClient, d.Id(), m.(string)).Extract() + if err != nil { + return fmt.Errorf("Error disassociating monitor (%s) from OpenStack server (%s): %s", m.(string), d.Id(), err) + } + log.Printf("[DEBUG] Disassociated monitor (%s) from pool (%s)", m.(string), d.Id()) + } + } + + if d.HasChange("member") { + oldMembersRaw, newMembersRaw := d.GetChange("member") + oldMembersSet, newMembersSet := oldMembersRaw.(*schema.Set), newMembersRaw.(*schema.Set) + membersToAdd := newMembersSet.Difference(oldMembersSet) + membersToRemove := oldMembersSet.Difference(newMembersSet) + + log.Printf("[DEBUG] Members to add: %v", membersToAdd) + + log.Printf("[DEBUG] Members to remove: %v", membersToRemove) + + for _, m := range membersToRemove.List() { + oldMember := resourcePoolMemberV1(d, m) + listOpts := members.ListOpts{ + PoolID: d.Id(), + Address: oldMember.Address, + ProtocolPort: oldMember.ProtocolPort, + } + err = members.List(networkingClient, listOpts).EachPage(func(page pagination.Page) (bool, error) { + extractedMembers, err := members.ExtractMembers(page) + if err != nil { + return false, err + } + for _, member := range extractedMembers { + err := members.Delete(networkingClient, member.ID).ExtractErr() + if err != nil { + return false, fmt.Errorf("Error deleting member (%s) from OpenStack LB pool (%s): %s", member.ID, d.Id(), err) + } + log.Printf("[DEBUG] Deleted member (%s) from pool (%s)", member.ID, d.Id()) + } + return true, nil + }) + } + + for _, m := range membersToAdd.List() { + createOpts := resourcePoolMemberV1(d, m) + newMember, err := members.Create(networkingClient, createOpts).Extract() + if err != nil { + return fmt.Errorf("Error creating LB member: %s", err) + } + log.Printf("[DEBUG] Created member (%s) in OpenStack LB pool (%s)", newMember.ID, d.Id()) + } + } + + return resourceLBPoolV1Read(d, meta) +} + +func resourceLBPoolV1Delete(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) + } + + err = pools.Delete(networkingClient, d.Id()).ExtractErr() + if err != nil { + return fmt.Errorf("Error deleting OpenStack LB Pool: %s", err) + } + + d.SetId("") + return nil +} + +func resourcePoolMonitorIDsV1(d *schema.ResourceData) []string { + mIDsRaw := d.Get("monitor_ids").(*schema.Set) + mIDs := make([]string, mIDsRaw.Len()) + for i, raw := range mIDsRaw.List() { + mIDs[i] = raw.(string) + } + return mIDs +} + +func resourcePoolMembersV1(d *schema.ResourceData) []members.CreateOpts { + memberOptsRaw := (d.Get("member")).(*schema.Set) + memberOpts := make([]members.CreateOpts, memberOptsRaw.Len()) + for i, raw := range memberOptsRaw.List() { + rawMap := raw.(map[string]interface{}) + memberOpts[i] = members.CreateOpts{ + TenantID: rawMap["tenant_id"].(string), + Address: rawMap["address"].(string), + ProtocolPort: rawMap["port"].(int), + PoolID: d.Id(), + } + } + return memberOpts +} + +func resourcePoolMemberV1(d *schema.ResourceData, raw interface{}) members.CreateOpts { + rawMap := raw.(map[string]interface{}) + return members.CreateOpts{ + TenantID: rawMap["tenant_id"].(string), + Address: rawMap["address"].(string), + ProtocolPort: rawMap["port"].(int), + PoolID: d.Id(), + } +} + +func resourceLBMemberV1Hash(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%s-", m["region"].(string))) + buf.WriteString(fmt.Sprintf("%s-", m["tenant_id"].(string))) + buf.WriteString(fmt.Sprintf("%s-", m["address"].(string))) + buf.WriteString(fmt.Sprintf("%d-", m["port"].(int))) + + return hashcode.String(buf.String()) +} diff --git a/builtin/providers/openstack/resource_openstack_lb_pool_v1_test.go b/builtin/providers/openstack/resource_openstack_lb_pool_v1_test.go new file mode 100644 index 0000000000..1889c23845 --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_lb_pool_v1_test.go @@ -0,0 +1,134 @@ +package openstack + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/pools" +) + +func TestAccLBV1Pool_basic(t *testing.T) { + var pool pools.Pool + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckLBV1PoolDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccLBV1Pool_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckLBV1PoolExists(t, "openstack_lb_pool_v1.pool_1", &pool), + ), + }, + resource.TestStep{ + Config: testAccLBV1Pool_update, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("openstack_lb_pool_v1.pool_1", "name", "tf_test_lb_pool_updated"), + ), + }, + }, + }) +} + +func testAccCheckLBV1PoolDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + networkingClient, err := config.networkingV2Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("(testAccCheckLBV1PoolDestroy) Error creating OpenStack networking client: %s", err) + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != "openstack_lb_pool_v1" { + continue + } + + _, err := pools.Get(networkingClient, rs.Primary.ID).Extract() + if err == nil { + return fmt.Errorf("LB Pool still exists") + } + } + + return nil +} + +func testAccCheckLBV1PoolExists(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("(testAccCheckLBV1PoolExists) 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("Pool not found") + } + + *pool = *found + + return nil + } +} + +var testAccLBV1Pool_basic = fmt.Sprintf(` + resource "openstack_networking_network_v2" "network_1" { + region = "%s" + name = "network_1" + admin_state_up = "true" + } + + resource "openstack_networking_subnet_v2" "subnet_1" { + region = "%s" + network_id = "${openstack_networking_network_v2.network_1.id}" + cidr = "192.168.199.0/24" + ip_version = 4 + } + + resource "openstack_lb_pool_v1" "pool_1" { + region = "%s" + name = "tf_test_lb_pool" + protocol = "HTTP" + subnet_id = "${openstack_networking_subnet_v2.subnet_1.id}" + lb_method = "ROUND_ROBIN" + }`, + OS_REGION_NAME, OS_REGION_NAME, OS_REGION_NAME) + +var testAccLBV1Pool_update = fmt.Sprintf(` + resource "openstack_networking_network_v2" "network_1" { + region = "%s" + name = "network_1" + admin_state_up = "true" + } + + resource "openstack_networking_subnet_v2" "subnet_1" { + region = "%s" + network_id = "${openstack_networking_network_v2.network_1.id}" + cidr = "192.168.199.0/24" + ip_version = 4 + } + + resource "openstack_lb_pool_v1" "pool_1" { + region = "%s" + name = "tf_test_lb_pool_updated" + protocol = "HTTP" + subnet_id = "${openstack_networking_subnet_v2.subnet_1.id}" + lb_method = "ROUND_ROBIN" + }`, + OS_REGION_NAME, OS_REGION_NAME, OS_REGION_NAME) diff --git a/builtin/providers/openstack/resource_openstack_lb_vip_v1.go b/builtin/providers/openstack/resource_openstack_lb_vip_v1.go new file mode 100644 index 0000000000..bd2ae135e4 --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_lb_vip_v1.go @@ -0,0 +1,258 @@ +package openstack + +import ( + "fmt" + "log" + "strconv" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips" +) + +func resourceLBVipV1() *schema.Resource { + return &schema.Resource{ + Create: resourceLBVipV1Create, + Read: resourceLBVipV1Read, + Update: resourceLBVipV1Update, + Delete: resourceLBVipV1Delete, + + Schema: map[string]*schema.Schema{ + "region": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + DefaultFunc: envDefaultFunc("OS_REGION_NAME"), + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: false, + }, + "subnet_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "protocol": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "port": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + ForceNew: true, + }, + "pool_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: false, + }, + "tenant_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "address": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: false, + }, + "persistence": &schema.Schema{ + Type: schema.TypeMap, + Optional: true, + ForceNew: false, + }, + "conn_limit": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + ForceNew: false, + }, + "admin_state_up": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: false, + }, + }, + } +} + +func resourceLBVipV1Create(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 := vips.CreateOpts{ + Name: d.Get("name").(string), + SubnetID: d.Get("subnet_id").(string), + Protocol: d.Get("protocol").(string), + ProtocolPort: d.Get("port").(int), + PoolID: d.Get("pool_id").(string), + TenantID: d.Get("tenant_id").(string), + Address: d.Get("address").(string), + Description: d.Get("description").(string), + Persistence: resourceVipPersistenceV1(d), + ConnLimit: gophercloud.MaybeInt(d.Get("conn_limit").(int)), + } + + asuRaw := d.Get("admin_state_up").(string) + if asuRaw != "" { + asu, err := strconv.ParseBool(asuRaw) + if err != nil { + return fmt.Errorf("admin_state_up, if provided, must be either 'true' or 'false'") + } + createOpts.AdminStateUp = &asu + } + + log.Printf("[DEBUG] Create Options: %#v", createOpts) + p, err := vips.Create(networkingClient, createOpts).Extract() + if err != nil { + return fmt.Errorf("Error creating OpenStack LB VIP: %s", err) + } + log.Printf("[INFO] LB VIP ID: %s", p.ID) + + d.SetId(p.ID) + + return resourceLBVipV1Read(d, meta) +} + +func resourceLBVipV1Read(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 := vips.Get(networkingClient, d.Id()).Extract() + if err != nil { + return CheckDeleted(d, err, "LB VIP") + } + + log.Printf("[DEBUG] Retreived OpenStack LB VIP %s: %+v", d.Id(), p) + + d.Set("name", p.Name) + d.Set("subnet_id", p.SubnetID) + d.Set("protocol", p.Protocol) + d.Set("port", p.ProtocolPort) + d.Set("pool_id", p.PoolID) + + if t, exists := d.GetOk("tenant_id"); exists && t != "" { + d.Set("tenant_id", p.TenantID) + } else { + d.Set("tenant_id", "") + } + + if t, exists := d.GetOk("address"); exists && t != "" { + d.Set("address", p.Address) + } else { + d.Set("address", "") + } + + if t, exists := d.GetOk("description"); exists && t != "" { + d.Set("description", p.Description) + } else { + d.Set("description", "") + } + + if t, exists := d.GetOk("persistence"); exists && t != "" { + d.Set("persistence", p.Description) + } + + if t, exists := d.GetOk("conn_limit"); exists && t != "" { + d.Set("conn_limit", p.ConnLimit) + } else { + d.Set("conn_limit", "") + } + + if t, exists := d.GetOk("admin_state_up"); exists && t != "" { + d.Set("admin_state_up", strconv.FormatBool(p.AdminStateUp)) + } else { + d.Set("admin_state_up", "") + } + + return nil +} + +func resourceLBVipV1Update(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 vips.UpdateOpts + if d.HasChange("name") { + updateOpts.Name = d.Get("name").(string) + } + if d.HasChange("pool_id") { + updateOpts.PoolID = d.Get("pool_id").(string) + } + if d.HasChange("description") { + updateOpts.Description = d.Get("description").(string) + } + if d.HasChange("persistence") { + updateOpts.Persistence = resourceVipPersistenceV1(d) + } + if d.HasChange("conn_limit") { + updateOpts.ConnLimit = gophercloud.MaybeInt(d.Get("conn_limit").(int)) + } + if d.HasChange("admin_state_up") { + asuRaw := d.Get("admin_state_up").(string) + if asuRaw != "" { + asu, err := strconv.ParseBool(asuRaw) + if err != nil { + return fmt.Errorf("admin_state_up, if provided, must be either 'true' or 'false'") + } + updateOpts.AdminStateUp = &asu + } + } + + log.Printf("[DEBUG] Updating OpenStack LB VIP %s with options: %+v", d.Id(), updateOpts) + + _, err = vips.Update(networkingClient, d.Id(), updateOpts).Extract() + if err != nil { + return fmt.Errorf("Error updating OpenStack LB VIP: %s", err) + } + + return resourceLBVipV1Read(d, meta) +} + +func resourceLBVipV1Delete(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) + } + + err = vips.Delete(networkingClient, d.Id()).ExtractErr() + if err != nil { + return fmt.Errorf("Error deleting OpenStack LB VIP: %s", err) + } + + d.SetId("") + return nil +} + +func resourceVipPersistenceV1(d *schema.ResourceData) *vips.SessionPersistence { + rawP := d.Get("persistence").(interface{}) + rawMap := rawP.(map[string]interface{}) + if len(rawMap) != 0 { + p := vips.SessionPersistence{} + if t, ok := rawMap["type"]; ok { + p.Type = t.(string) + } + if c, ok := rawMap["cookie_name"]; ok { + p.CookieName = c.(string) + } + return &p + } + return nil +} diff --git a/builtin/providers/openstack/resource_openstack_lb_vip_v1_test.go b/builtin/providers/openstack/resource_openstack_lb_vip_v1_test.go new file mode 100644 index 0000000000..f30cd9d56d --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_lb_vip_v1_test.go @@ -0,0 +1,152 @@ +package openstack + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/lbaas/vips" +) + +func TestAccLBV1VIP_basic(t *testing.T) { + var vip vips.VirtualIP + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckLBV1VIPDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccLBV1VIP_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckLBV1VIPExists(t, "openstack_lb_vip_v1.vip_1", &vip), + ), + }, + resource.TestStep{ + Config: testAccLBV1VIP_update, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("openstack_lb_vip_v1.vip_1", "name", "tf_test_lb_vip_updated"), + ), + }, + }, + }) +} + +func testAccCheckLBV1VIPDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + networkingClient, err := config.networkingV2Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("(testAccCheckLBV1VIPDestroy) Error creating OpenStack networking client: %s", err) + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != "openstack_lb_vip_v1" { + continue + } + + _, err := vips.Get(networkingClient, rs.Primary.ID).Extract() + if err == nil { + return fmt.Errorf("LB VIP still exists") + } + } + + return nil +} + +func testAccCheckLBV1VIPExists(t *testing.T, n string, vip *vips.VirtualIP) 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("(testAccCheckLBV1VIPExists) Error creating OpenStack networking client: %s", err) + } + + found, err := vips.Get(networkingClient, rs.Primary.ID).Extract() + if err != nil { + return err + } + + if found.ID != rs.Primary.ID { + return fmt.Errorf("VIP not found") + } + + *vip = *found + + return nil + } +} + +var testAccLBV1VIP_basic = fmt.Sprintf(` + resource "openstack_networking_network_v2" "network_1" { + region = "%s" + name = "network_1" + admin_state_up = "true" + } + + resource "openstack_networking_subnet_v2" "subnet_1" { + region = "%s" + network_id = "${openstack_networking_network_v2.network_1.id}" + cidr = "192.168.199.0/24" + ip_version = 4 + } + + resource "openstack_lb_pool_v1" "pool_1" { + region = "%s" + name = "tf_test_lb_pool" + protocol = "HTTP" + subnet_id = "${openstack_networking_subnet_v2.subnet_1.id}" + lb_method = "ROUND_ROBIN" + } + + resource "openstack_lb_vip_v1" "vip_1" { + region = "RegionOne" + name = "tf_test_lb_vip" + subnet_id = "${openstack_networking_subnet_v2.subnet_1.id}" + protocol = "HTTP" + port = 80 + pool_id = "${openstack_lb_pool_v1.pool_1.id}" + }`, + OS_REGION_NAME, OS_REGION_NAME, OS_REGION_NAME) + +var testAccLBV1VIP_update = fmt.Sprintf(` + resource "openstack_networking_network_v2" "network_1" { + region = "%s" + name = "network_1" + admin_state_up = "true" + } + + resource "openstack_networking_subnet_v2" "subnet_1" { + region = "%s" + network_id = "${openstack_networking_network_v2.network_1.id}" + cidr = "192.168.199.0/24" + ip_version = 4 + } + + resource "openstack_lb_pool_v1" "pool_1" { + region = "%s" + name = "tf_test_lb_pool" + protocol = "HTTP" + subnet_id = "${openstack_networking_subnet_v2.subnet_1.id}" + lb_method = "ROUND_ROBIN" + } + + resource "openstack_lb_vip_v1" "vip_1" { + region = "RegionOne" + name = "tf_test_lb_vip_updated" + subnet_id = "${openstack_networking_subnet_v2.subnet_1.id}" + protocol = "HTTP" + port = 80 + pool_id = "${openstack_lb_pool_v1.pool_1.id}" + }`, + OS_REGION_NAME, OS_REGION_NAME, OS_REGION_NAME) diff --git a/builtin/providers/openstack/resource_openstack_networking_floatingip_v2.go b/builtin/providers/openstack/resource_openstack_networking_floatingip_v2.go new file mode 100644 index 0000000000..cbc4307648 --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_networking_floatingip_v2.go @@ -0,0 +1,162 @@ +package openstack + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips" + "github.com/rackspace/gophercloud/openstack/networking/v2/networks" + "github.com/rackspace/gophercloud/pagination" +) + +func resourceNetworkingFloatingIPV2() *schema.Resource { + return &schema.Resource{ + Create: resourceNetworkFloatingIPV2Create, + Read: resourceNetworkFloatingIPV2Read, + Delete: resourceNetworkFloatingIPV2Delete, + + Schema: map[string]*schema.Schema{ + "region": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + DefaultFunc: envDefaultFunc("OS_REGION_NAME"), + }, + "address": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "pool": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceNetworkFloatingIPV2Create(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + networkClient, err := config.networkingV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack network client: %s", err) + } + + poolID, err := getNetworkID(d, meta, d.Get("pool").(string)) + if err != nil { + return fmt.Errorf("Error retrieving floating IP pool name: %s", err) + } + if len(poolID) == 0 { + return fmt.Errorf("No network found with name: %s", d.Get("pool").(string)) + } + createOpts := floatingips.CreateOpts{ + FloatingNetworkID: poolID, + } + log.Printf("[DEBUG] Create Options: %#v", createOpts) + floatingIP, err := floatingips.Create(networkClient, createOpts).Extract() + if err != nil { + return fmt.Errorf("Error allocating floating IP: %s", err) + } + + d.SetId(floatingIP.ID) + + return resourceNetworkFloatingIPV2Read(d, meta) +} + +func resourceNetworkFloatingIPV2Read(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + networkClient, err := config.networkingV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack network client: %s", err) + } + + floatingIP, err := floatingips.Get(networkClient, d.Id()).Extract() + if err != nil { + return CheckDeleted(d, err, "floating IP") + } + + d.Set("address", floatingIP.FloatingIP) + poolName, err := getNetworkName(d, meta, floatingIP.FloatingNetworkID) + if err != nil { + return fmt.Errorf("Error retrieving floating IP pool name: %s", err) + } + d.Set("pool", poolName) + + return nil +} + +func resourceNetworkFloatingIPV2Delete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + networkClient, err := config.networkingV2Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack network client: %s", err) + } + + err = floatingips.Delete(networkClient, d.Id()).ExtractErr() + if err != nil { + return fmt.Errorf("Error deleting floating IP: %s", err) + } + d.SetId("") + return nil +} + +func getNetworkID(d *schema.ResourceData, meta interface{}, networkName string) (string, error) { + config := meta.(*Config) + networkClient, err := config.networkingV2Client(d.Get("region").(string)) + if err != nil { + return "", fmt.Errorf("Error creating OpenStack network client: %s", err) + } + + opts := networks.ListOpts{Name: networkName} + pager := networks.List(networkClient, opts) + networkID := "" + + err = pager.EachPage(func(page pagination.Page) (bool, error) { + networkList, err := networks.ExtractNetworks(page) + if err != nil { + return false, err + } + + for _, n := range networkList { + if n.Name == networkName { + networkID = n.ID + return false, nil + } + } + + return true, nil + }) + + return networkID, err +} + +func getNetworkName(d *schema.ResourceData, meta interface{}, networkID string) (string, error) { + config := meta.(*Config) + networkClient, err := config.networkingV2Client(d.Get("region").(string)) + if err != nil { + return "", fmt.Errorf("Error creating OpenStack network client: %s", err) + } + + opts := networks.ListOpts{ID: networkID} + pager := networks.List(networkClient, opts) + networkName := "" + + err = pager.EachPage(func(page pagination.Page) (bool, error) { + networkList, err := networks.ExtractNetworks(page) + if err != nil { + return false, err + } + + for _, n := range networkList { + if n.ID == networkID { + networkName = n.Name + return false, nil + } + } + + return true, nil + }) + + return networkName, err +} diff --git a/builtin/providers/openstack/resource_openstack_networking_floatingip_v2_test.go b/builtin/providers/openstack/resource_openstack_networking_floatingip_v2_test.go new file mode 100644 index 0000000000..cd08ea5121 --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_networking_floatingip_v2_test.go @@ -0,0 +1,89 @@ +package openstack + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/floatingips" +) + +func TestAccNetworkingV2FloatingIP_basic(t *testing.T) { + var floatingIP floatingips.FloatingIP + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckNetworkingV2FloatingIPDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccNetworkingV2FloatingIP_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckNetworkingV2FloatingIPExists(t, "openstack_networking_floatingip_v2.foo", &floatingIP), + ), + }, + }, + }) +} + +func testAccCheckNetworkingV2FloatingIPDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + networkClient, err := config.networkingV2Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("(testAccCheckNetworkingV2FloatingIPDestroy) Error creating OpenStack floating IP: %s", err) + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != "openstack_networking_floatingip_v2" { + continue + } + + _, err := floatingips.Get(networkClient, rs.Primary.ID).Extract() + if err == nil { + return fmt.Errorf("FloatingIP still exists") + } + } + + return nil +} + +func testAccCheckNetworkingV2FloatingIPExists(t *testing.T, n string, kp *floatingips.FloatingIP) 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) + networkClient, err := config.networkingV2Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("(testAccCheckNetworkingV2FloatingIPExists) Error creating OpenStack networking client: %s", err) + } + + found, err := floatingips.Get(networkClient, rs.Primary.ID).Extract() + if err != nil { + return err + } + + if found.ID != rs.Primary.ID { + return fmt.Errorf("FloatingIP not found") + } + + *kp = *found + + return nil + } +} + +var testAccNetworkingV2FloatingIP_basic = fmt.Sprintf(` + resource "openstack_networking_floatingip_v2" "foo" { + region = "%s" + pool = "%s" + }`, + OS_REGION_NAME, OS_POOL_NAME) diff --git a/builtin/providers/openstack/resource_openstack_networking_network_v2.go b/builtin/providers/openstack/resource_openstack_networking_network_v2.go new file mode 100644 index 0000000000..2ac4ab94eb --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_networking_network_v2.go @@ -0,0 +1,170 @@ +package openstack + +import ( + "fmt" + "log" + "strconv" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/rackspace/gophercloud/openstack/networking/v2/networks" +) + +func resourceNetworkingNetworkV2() *schema.Resource { + return &schema.Resource{ + Create: resourceNetworkingNetworkV2Create, + Read: resourceNetworkingNetworkV2Read, + Update: resourceNetworkingNetworkV2Update, + Delete: resourceNetworkingNetworkV2Delete, + + Schema: map[string]*schema.Schema{ + "region": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + DefaultFunc: envDefaultFunc("OS_REGION_NAME"), + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: false, + }, + "admin_state_up": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: false, + }, + "shared": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: false, + }, + "tenant_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + }, + } +} + +func resourceNetworkingNetworkV2Create(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 := networks.CreateOpts{ + Name: d.Get("name").(string), + TenantID: d.Get("tenant_id").(string), + } + + asuRaw := d.Get("admin_state_up").(string) + if asuRaw != "" { + asu, err := strconv.ParseBool(asuRaw) + if err != nil { + return fmt.Errorf("admin_state_up, if provided, must be either 'true' or 'false'") + } + createOpts.AdminStateUp = &asu + } + + sharedRaw := d.Get("shared").(string) + if sharedRaw != "" { + shared, err := strconv.ParseBool(sharedRaw) + if err != nil { + return fmt.Errorf("shared, if provided, must be either 'true' or 'false': %v", err) + } + createOpts.Shared = &shared + } + + log.Printf("[DEBUG] Create Options: %#v", createOpts) + n, err := networks.Create(networkingClient, createOpts).Extract() + if err != nil { + return fmt.Errorf("Error creating OpenStack Neutron network: %s", err) + } + log.Printf("[INFO] Network ID: %s", n.ID) + + d.SetId(n.ID) + + return resourceNetworkingNetworkV2Read(d, meta) +} + +func resourceNetworkingNetworkV2Read(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) + } + + n, err := networks.Get(networkingClient, d.Id()).Extract() + if err != nil { + return CheckDeleted(d, err, "network") + } + + log.Printf("[DEBUG] Retreived Network %s: %+v", d.Id(), n) + + d.Set("name", n.Name) + d.Set("admin_state_up", strconv.FormatBool(n.AdminStateUp)) + d.Set("shared", strconv.FormatBool(n.Shared)) + d.Set("tenant_id", n.TenantID) + + return nil +} + +func resourceNetworkingNetworkV2Update(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 networks.UpdateOpts + if d.HasChange("name") { + updateOpts.Name = d.Get("name").(string) + } + if d.HasChange("admin_state_up") { + asuRaw := d.Get("admin_state_up").(string) + if asuRaw != "" { + asu, err := strconv.ParseBool(asuRaw) + if err != nil { + return fmt.Errorf("admin_state_up, if provided, must be either 'true' or 'false'") + } + updateOpts.AdminStateUp = &asu + } + } + if d.HasChange("shared") { + sharedRaw := d.Get("shared").(string) + if sharedRaw != "" { + shared, err := strconv.ParseBool(sharedRaw) + if err != nil { + return fmt.Errorf("shared, if provided, must be either 'true' or 'false': %v", err) + } + updateOpts.Shared = &shared + } + } + + log.Printf("[DEBUG] Updating Network %s with options: %+v", d.Id(), updateOpts) + + _, err = networks.Update(networkingClient, d.Id(), updateOpts).Extract() + if err != nil { + return fmt.Errorf("Error updating OpenStack Neutron Network: %s", err) + } + + return resourceNetworkingNetworkV2Read(d, meta) +} + +func resourceNetworkingNetworkV2Delete(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) + } + + err = networks.Delete(networkingClient, d.Id()).ExtractErr() + if err != nil { + return fmt.Errorf("Error deleting OpenStack Neutron Network: %s", err) + } + + d.SetId("") + return nil +} diff --git a/builtin/providers/openstack/resource_openstack_networking_network_v2_test.go b/builtin/providers/openstack/resource_openstack_networking_network_v2_test.go new file mode 100644 index 0000000000..5bff605320 --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_networking_network_v2_test.go @@ -0,0 +1,104 @@ +package openstack + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + + "github.com/rackspace/gophercloud/openstack/networking/v2/networks" +) + +func TestAccNetworkingV2Network_basic(t *testing.T) { + var network networks.Network + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckNetworkingV2NetworkDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccNetworkingV2Network_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckNetworkingV2NetworkExists(t, "openstack_networking_network_v2.foo", &network), + ), + }, + resource.TestStep{ + Config: testAccNetworkingV2Network_update, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("openstack_networking_network_v2.foo", "name", "network_2"), + ), + }, + }, + }) +} + +func testAccCheckNetworkingV2NetworkDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + networkingClient, err := config.networkingV2Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("(testAccCheckNetworkingV2NetworkDestroy) Error creating OpenStack networking client: %s", err) + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != "openstack_networking_network_v2" { + continue + } + + _, err := networks.Get(networkingClient, rs.Primary.ID).Extract() + if err == nil { + return fmt.Errorf("Network still exists") + } + } + + return nil +} + +func testAccCheckNetworkingV2NetworkExists(t *testing.T, n string, network *networks.Network) 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("(testAccCheckNetworkingV2NetworkExists) Error creating OpenStack networking client: %s", err) + } + + found, err := networks.Get(networkingClient, rs.Primary.ID).Extract() + if err != nil { + return err + } + + if found.ID != rs.Primary.ID { + return fmt.Errorf("Network not found") + } + + *network = *found + + return nil + } +} + +var testAccNetworkingV2Network_basic = fmt.Sprintf(` + resource "openstack_networking_network_v2" "foo" { + region = "%s" + name = "network_1" + admin_state_up = "true" + }`, + OS_REGION_NAME) + +var testAccNetworkingV2Network_update = fmt.Sprintf(` + resource "openstack_networking_network_v2" "foo" { + region = "%s" + name = "network_2" + admin_state_up = "true" + }`, + OS_REGION_NAME) diff --git a/builtin/providers/openstack/resource_openstack_networking_router_interface_v2.go b/builtin/providers/openstack/resource_openstack_networking_router_interface_v2.go new file mode 100644 index 0000000000..e67ff6f58f --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_networking_router_interface_v2.go @@ -0,0 +1,107 @@ +package openstack + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers" + "github.com/rackspace/gophercloud/openstack/networking/v2/ports" +) + +func resourceNetworkingRouterInterfaceV2() *schema.Resource { + return &schema.Resource{ + Create: resourceNetworkingRouterInterfaceV2Create, + Read: resourceNetworkingRouterInterfaceV2Read, + Delete: resourceNetworkingRouterInterfaceV2Delete, + + Schema: map[string]*schema.Schema{ + "region": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + DefaultFunc: envDefaultFunc("OS_REGION_NAME"), + }, + "router_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "subnet_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceNetworkingRouterInterfaceV2Create(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 := routers.InterfaceOpts{ + SubnetID: d.Get("subnet_id").(string), + } + + log.Printf("[DEBUG] Create Options: %#v", createOpts) + n, err := routers.AddInterface(networkingClient, d.Get("router_id").(string), createOpts).Extract() + if err != nil { + return fmt.Errorf("Error creating OpenStack Neutron router interface: %s", err) + } + log.Printf("[INFO] Router interface Port ID: %s", n.PortID) + + d.SetId(n.PortID) + + return resourceNetworkingRouterInterfaceV2Read(d, meta) +} + +func resourceNetworkingRouterInterfaceV2Read(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) + } + + n, err := ports.Get(networkingClient, d.Id()).Extract() + if err != nil { + httpError, ok := err.(*gophercloud.UnexpectedResponseCodeError) + if !ok { + return fmt.Errorf("Error retrieving OpenStack Neutron Router Interface: %s", err) + } + + if httpError.Actual == 404 { + d.SetId("") + return nil + } + return fmt.Errorf("Error retrieving OpenStack Neutron Router Interface: %s", err) + } + + log.Printf("[DEBUG] Retreived Router Interface %s: %+v", d.Id(), n) + + return nil +} + +func resourceNetworkingRouterInterfaceV2Delete(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) + } + + removeOpts := routers.InterfaceOpts{ + SubnetID: d.Get("subnet_id").(string), + } + + _, err = routers.RemoveInterface(networkingClient, d.Get("router_id").(string), removeOpts).Extract() + if err != nil { + return fmt.Errorf("Error deleting OpenStack Neutron Router Interface: %s", err) + } + + d.SetId("") + return nil +} diff --git a/builtin/providers/openstack/resource_openstack_networking_router_interface_v2_test.go b/builtin/providers/openstack/resource_openstack_networking_router_interface_v2_test.go new file mode 100644 index 0000000000..be3b12c0b5 --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_networking_router_interface_v2_test.go @@ -0,0 +1,100 @@ +package openstack + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + + "github.com/rackspace/gophercloud/openstack/networking/v2/ports" +) + +func TestAccNetworkingV2RouterInterface_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckNetworkingV2RouterInterfaceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccNetworkingV2RouterInterface_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckNetworkingV2RouterInterfaceExists(t, "openstack_networking_router_interface_v2.int_1"), + ), + }, + }, + }) +} + +func testAccCheckNetworkingV2RouterInterfaceDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + networkingClient, err := config.networkingV2Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("(testAccCheckNetworkingV2RouterInterfaceDestroy) Error creating OpenStack networking client: %s", err) + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != "openstack_networking_router_interface_v2" { + continue + } + + _, err := ports.Get(networkingClient, rs.Primary.ID).Extract() + if err == nil { + return fmt.Errorf("Router interface still exists") + } + } + + return nil +} + +func testAccCheckNetworkingV2RouterInterfaceExists(t *testing.T, n string) 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("(testAccCheckNetworkingV2RouterInterfaceExists) 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("Router interface not found") + } + + return nil + } +} + +var testAccNetworkingV2RouterInterface_basic = fmt.Sprintf(` +resource "openstack_networking_router_v2" "router_1" { + name = "router_1" + admin_state_up = "true" +} + +resource "openstack_networking_router_interface_v2" "int_1" { + subnet_id = "${openstack_networking_subnet_v2.subnet_1.id}" + router_id = "${openstack_networking_router_v2.router_1.id}" +} + +resource "openstack_networking_network_v2" "network_1" { + name = "network_1" + 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 +}`) diff --git a/builtin/providers/openstack/resource_openstack_networking_router_v2.go b/builtin/providers/openstack/resource_openstack_networking_router_v2.go new file mode 100644 index 0000000000..3b6df4816a --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_networking_router_v2.go @@ -0,0 +1,169 @@ +package openstack + +import ( + "fmt" + "log" + "strconv" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/rackspace/gophercloud" + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers" +) + +func resourceNetworkingRouterV2() *schema.Resource { + return &schema.Resource{ + Create: resourceNetworkingRouterV2Create, + Read: resourceNetworkingRouterV2Read, + Update: resourceNetworkingRouterV2Update, + Delete: resourceNetworkingRouterV2Delete, + + Schema: map[string]*schema.Schema{ + "region": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + DefaultFunc: envDefaultFunc("OS_REGION_NAME"), + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: false, + }, + "admin_state_up": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: false, + }, + "external_gateway": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: false, + }, + "tenant_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + }, + } +} + +func resourceNetworkingRouterV2Create(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 := routers.CreateOpts{ + Name: d.Get("name").(string), + TenantID: d.Get("tenant_id").(string), + } + + asuRaw := d.Get("admin_state_up").(string) + if asuRaw != "" { + asu, err := strconv.ParseBool(asuRaw) + if err != nil { + return fmt.Errorf("admin_state_up, if provided, must be either 'true' or 'false'") + } + createOpts.AdminStateUp = &asu + } + + externalGateway := d.Get("external_gateway").(string) + if externalGateway != "" { + gatewayInfo := routers.GatewayInfo{ + NetworkID: externalGateway, + } + createOpts.GatewayInfo = &gatewayInfo + } + + log.Printf("[DEBUG] Create Options: %#v", createOpts) + n, err := routers.Create(networkingClient, createOpts).Extract() + if err != nil { + return fmt.Errorf("Error creating OpenStack Neutron router: %s", err) + } + log.Printf("[INFO] Router ID: %s", n.ID) + + d.SetId(n.ID) + + return resourceNetworkingRouterV2Read(d, meta) +} + +func resourceNetworkingRouterV2Read(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) + } + + n, err := routers.Get(networkingClient, d.Id()).Extract() + if err != nil { + httpError, ok := err.(*gophercloud.UnexpectedResponseCodeError) + if !ok { + return fmt.Errorf("Error retrieving OpenStack Neutron Router: %s", err) + } + + if httpError.Actual == 404 { + d.SetId("") + return nil + } + return fmt.Errorf("Error retrieving OpenStack Neutron Router: %s", err) + } + + log.Printf("[DEBUG] Retreived Router %s: %+v", d.Id(), n) + + d.Set("name", n.Name) + d.Set("admin_state_up", strconv.FormatBool(n.AdminStateUp)) + d.Set("tenant_id", n.TenantID) + d.Set("external_gateway", n.GatewayInfo.NetworkID) + + return nil +} + +func resourceNetworkingRouterV2Update(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 routers.UpdateOpts + if d.HasChange("name") { + updateOpts.Name = d.Get("name").(string) + } + if d.HasChange("admin_state_up") { + asuRaw := d.Get("admin_state_up").(string) + if asuRaw != "" { + asu, err := strconv.ParseBool(asuRaw) + if err != nil { + return fmt.Errorf("admin_state_up, if provided, must be either 'true' or 'false'") + } + updateOpts.AdminStateUp = &asu + } + } + + log.Printf("[DEBUG] Updating Router %s with options: %+v", d.Id(), updateOpts) + + _, err = routers.Update(networkingClient, d.Id(), updateOpts).Extract() + if err != nil { + return fmt.Errorf("Error updating OpenStack Neutron Router: %s", err) + } + + return resourceNetworkingRouterV2Read(d, meta) +} + +func resourceNetworkingRouterV2Delete(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) + } + + err = routers.Delete(networkingClient, d.Id()).ExtractErr() + if err != nil { + return fmt.Errorf("Error deleting OpenStack Neutron Router: %s", err) + } + + d.SetId("") + return nil +} diff --git a/builtin/providers/openstack/resource_openstack_networking_router_v2_test.go b/builtin/providers/openstack/resource_openstack_networking_router_v2_test.go new file mode 100644 index 0000000000..248f4e721f --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_networking_router_v2_test.go @@ -0,0 +1,100 @@ +package openstack + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + + "github.com/rackspace/gophercloud/openstack/networking/v2/extensions/layer3/routers" +) + +func TestAccNetworkingV2Router_basic(t *testing.T) { + var router routers.Router + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckNetworkingV2RouterDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccNetworkingV2Router_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckNetworkingV2RouterExists(t, "openstack_networking_router_v2.foo", &router), + ), + }, + resource.TestStep{ + Config: testAccNetworkingV2Router_update, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("openstack_networking_router_v2.foo", "name", "router_2"), + ), + }, + }, + }) +} + +func testAccCheckNetworkingV2RouterDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + networkingClient, err := config.networkingV2Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("(testAccCheckNetworkingV2RouterDestroy) Error creating OpenStack networking client: %s", err) + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != "openstack_networking_router_v2" { + continue + } + + _, err := routers.Get(networkingClient, rs.Primary.ID).Extract() + if err == nil { + return fmt.Errorf("Router still exists") + } + } + + return nil +} + +func testAccCheckNetworkingV2RouterExists(t *testing.T, n string, router *routers.Router) 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("(testAccCheckNetworkingV2RouterExists) Error creating OpenStack networking client: %s", err) + } + + found, err := routers.Get(networkingClient, rs.Primary.ID).Extract() + if err != nil { + return err + } + + if found.ID != rs.Primary.ID { + return fmt.Errorf("Router not found") + } + + *router = *found + + return nil + } +} + +var testAccNetworkingV2Router_basic = fmt.Sprintf(` + resource "openstack_networking_router_v2" "foo" { + name = "router" + admin_state_up = "true" + }`) + +var testAccNetworkingV2Router_update = fmt.Sprintf(` + resource "openstack_networking_router_v2" "foo" { + name = "router_2" + admin_state_up = "true" + }`) diff --git a/builtin/providers/openstack/resource_openstack_networking_subnet_v2.go b/builtin/providers/openstack/resource_openstack_networking_subnet_v2.go new file mode 100644 index 0000000000..2f898bb4c5 --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_networking_subnet_v2.go @@ -0,0 +1,272 @@ +package openstack + +import ( + "fmt" + "log" + "strconv" + + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/schema" + "github.com/rackspace/gophercloud/openstack/networking/v2/subnets" +) + +func resourceNetworkingSubnetV2() *schema.Resource { + return &schema.Resource{ + Create: resourceNetworkingSubnetV2Create, + Read: resourceNetworkingSubnetV2Read, + Update: resourceNetworkingSubnetV2Update, + Delete: resourceNetworkingSubnetV2Delete, + + Schema: map[string]*schema.Schema{ + "region": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + DefaultFunc: envDefaultFunc("OS_REGION_NAME"), + }, + "network_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "cidr": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: false, + }, + "tenant_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "allocation_pools": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "start": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "end": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + "gateway_ip": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: false, + }, + "ip_version": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + ForceNew: true, + }, + "enable_dhcp": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: false, + }, + "dns_nameservers": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + ForceNew: false, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: func(v interface{}) int { + return hashcode.String(v.(string)) + }, + }, + "host_routes": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + ForceNew: false, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "destination_cidr": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + "next_hop": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + }, + } +} + +func resourceNetworkingSubnetV2Create(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 := subnets.CreateOpts{ + NetworkID: d.Get("network_id").(string), + CIDR: d.Get("cidr").(string), + Name: d.Get("name").(string), + TenantID: d.Get("tenant_id").(string), + AllocationPools: resourceSubnetAllocationPoolsV2(d), + GatewayIP: d.Get("gateway_ip").(string), + IPVersion: d.Get("ip_version").(int), + DNSNameservers: resourceSubnetDNSNameserversV2(d), + HostRoutes: resourceSubnetHostRoutesV2(d), + } + + edRaw := d.Get("enable_dhcp").(string) + if edRaw != "" { + ed, err := strconv.ParseBool(edRaw) + if err != nil { + return fmt.Errorf("enable_dhcp, if provided, must be either 'true' or 'false'") + } + createOpts.EnableDHCP = &ed + } + + log.Printf("[DEBUG] Create Options: %#v", createOpts) + s, err := subnets.Create(networkingClient, createOpts).Extract() + if err != nil { + return fmt.Errorf("Error creating OpenStack Neutron subnet: %s", err) + } + log.Printf("[INFO] Subnet ID: %s", s.ID) + + d.SetId(s.ID) + + return resourceNetworkingSubnetV2Read(d, meta) +} + +func resourceNetworkingSubnetV2Read(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) + } + + s, err := subnets.Get(networkingClient, d.Id()).Extract() + if err != nil { + return CheckDeleted(d, err, "subnet") + } + + log.Printf("[DEBUG] Retreived Subnet %s: %+v", d.Id(), s) + + d.Set("newtork_id", s.NetworkID) + d.Set("cidr", s.CIDR) + d.Set("ip_version", s.IPVersion) + d.Set("name", s.Name) + d.Set("tenant_id", s.TenantID) + d.Set("allocation_pools", s.AllocationPools) + d.Set("gateway_ip", s.GatewayIP) + d.Set("enable_dhcp", strconv.FormatBool(s.EnableDHCP)) + d.Set("dns_nameservers", s.DNSNameservers) + d.Set("host_routes", s.HostRoutes) + + return nil +} + +func resourceNetworkingSubnetV2Update(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 subnets.UpdateOpts + + if d.HasChange("name") { + updateOpts.Name = d.Get("name").(string) + } + + if d.HasChange("gateway_ip") { + updateOpts.GatewayIP = d.Get("gateway_ip").(string) + } + + if d.HasChange("dns_nameservers") { + updateOpts.DNSNameservers = resourceSubnetDNSNameserversV2(d) + } + + if d.HasChange("host_routes") { + updateOpts.HostRoutes = resourceSubnetHostRoutesV2(d) + } + + if d.HasChange("enable_dhcp") { + edRaw := d.Get("enable_dhcp").(string) + if edRaw != "" { + ed, err := strconv.ParseBool(edRaw) + if err != nil { + return fmt.Errorf("enable_dhcp, if provided, must be either 'true' or 'false'") + } + updateOpts.EnableDHCP = &ed + } + } + + log.Printf("[DEBUG] Updating Subnet %s with options: %+v", d.Id(), updateOpts) + + _, err = subnets.Update(networkingClient, d.Id(), updateOpts).Extract() + if err != nil { + return fmt.Errorf("Error updating OpenStack Neutron Subnet: %s", err) + } + + return resourceNetworkingSubnetV2Read(d, meta) +} + +func resourceNetworkingSubnetV2Delete(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) + } + + err = subnets.Delete(networkingClient, d.Id()).ExtractErr() + if err != nil { + return fmt.Errorf("Error deleting OpenStack Neutron Subnet: %s", err) + } + + d.SetId("") + return nil +} + +func resourceSubnetAllocationPoolsV2(d *schema.ResourceData) []subnets.AllocationPool { + rawAPs := d.Get("allocation_pools").([]interface{}) + aps := make([]subnets.AllocationPool, len(rawAPs)) + for i, raw := range rawAPs { + rawMap := raw.(map[string]interface{}) + aps[i] = subnets.AllocationPool{ + Start: rawMap["start"].(string), + End: rawMap["end"].(string), + } + } + return aps +} + +func resourceSubnetDNSNameserversV2(d *schema.ResourceData) []string { + rawDNSN := d.Get("dns_nameservers").(*schema.Set) + dnsn := make([]string, rawDNSN.Len()) + for i, raw := range rawDNSN.List() { + dnsn[i] = raw.(string) + } + return dnsn +} + +func resourceSubnetHostRoutesV2(d *schema.ResourceData) []subnets.HostRoute { + rawHR := d.Get("host_routes").([]interface{}) + hr := make([]subnets.HostRoute, len(rawHR)) + for i, raw := range rawHR { + rawMap := raw.(map[string]interface{}) + hr[i] = subnets.HostRoute{ + DestinationCIDR: rawMap["destination_cidr"].(string), + NextHop: rawMap["next_hop"].(string), + } + } + return hr +} diff --git a/builtin/providers/openstack/resource_openstack_networking_subnet_v2_test.go b/builtin/providers/openstack/resource_openstack_networking_subnet_v2_test.go new file mode 100644 index 0000000000..d7f6116e9f --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_networking_subnet_v2_test.go @@ -0,0 +1,119 @@ +package openstack + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + + "github.com/rackspace/gophercloud/openstack/networking/v2/subnets" +) + +func TestAccNetworkingV2Subnet_basic(t *testing.T) { + var subnet subnets.Subnet + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckNetworkingV2SubnetDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccNetworkingV2Subnet_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckNetworkingV2SubnetExists(t, "openstack_networking_subnet_v2.subnet_1", &subnet), + ), + }, + resource.TestStep{ + Config: testAccNetworkingV2Subnet_update, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("openstack_networking_subnet_v2.subnet_1", "name", "tf-test-subnet"), + resource.TestCheckResourceAttr("openstack_networking_subnet_v2.subnet_1", "gateway_ip", "192.68.0.1"), + ), + }, + }, + }) +} + +func testAccCheckNetworkingV2SubnetDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + networkingClient, err := config.networkingV2Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("(testAccCheckNetworkingV2SubnetDestroy) Error creating OpenStack networking client: %s", err) + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != "openstack_networking_subnet_v2" { + continue + } + + _, err := subnets.Get(networkingClient, rs.Primary.ID).Extract() + if err == nil { + return fmt.Errorf("Subnet still exists") + } + } + + return nil +} + +func testAccCheckNetworkingV2SubnetExists(t *testing.T, n string, subnet *subnets.Subnet) 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("(testAccCheckNetworkingV2SubnetExists) Error creating OpenStack networking client: %s", err) + } + + found, err := subnets.Get(networkingClient, rs.Primary.ID).Extract() + if err != nil { + return err + } + + if found.ID != rs.Primary.ID { + return fmt.Errorf("Subnet not found") + } + + *subnet = *found + + return nil + } +} + +var testAccNetworkingV2Subnet_basic = fmt.Sprintf(` + resource "openstack_networking_network_v2" "network_1" { + region = "%s" + name = "network_1" + admin_state_up = "true" + } + + resource "openstack_networking_subnet_v2" "subnet_1" { + region = "%s" + network_id = "${openstack_networking_network_v2.network_1.id}" + cidr = "192.168.199.0/24" + ip_version = 4 + }`, OS_REGION_NAME, OS_REGION_NAME) + +var testAccNetworkingV2Subnet_update = fmt.Sprintf(` + resource "openstack_networking_network_v2" "network_1" { + region = "%s" + name = "network_1" + admin_state_up = "true" + } + + resource "openstack_networking_subnet_v2" "subnet_1" { + region = "%s" + name = "tf-test-subnet" + network_id = "${openstack_networking_network_v2.network_1.id}" + cidr = "192.168.199.0/24" + ip_version = 4 + gateway_ip = "192.68.0.1" + }`, OS_REGION_NAME, OS_REGION_NAME) diff --git a/builtin/providers/openstack/resource_openstack_objectstorage_container_v1.go b/builtin/providers/openstack/resource_openstack_objectstorage_container_v1.go new file mode 100644 index 0000000000..31666a3565 --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_objectstorage_container_v1.go @@ -0,0 +1,148 @@ +package openstack + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers" +) + +func resourceObjectStorageContainerV1() *schema.Resource { + return &schema.Resource{ + Create: resourceObjectStorageContainerV1Create, + Read: resourceObjectStorageContainerV1Read, + Update: resourceObjectStorageContainerV1Update, + Delete: resourceObjectStorageContainerV1Delete, + + Schema: map[string]*schema.Schema{ + "region": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + DefaultFunc: envDefaultFunc("OS_REGION_NAME"), + }, + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: false, + }, + "container_read": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: false, + }, + "container_sync_to": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: false, + }, + "container_sync_key": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: false, + }, + "container_write": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: false, + }, + "content_type": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: false, + }, + "metadata": &schema.Schema{ + Type: schema.TypeMap, + Optional: true, + ForceNew: false, + }, + }, + } +} + +func resourceObjectStorageContainerV1Create(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + objectStorageClient, err := config.objectStorageV1Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack object storage client: %s", err) + } + + cn := d.Get("name").(string) + + createOpts := &containers.CreateOpts{ + ContainerRead: d.Get("container_read").(string), + ContainerSyncTo: d.Get("container_sync_to").(string), + ContainerSyncKey: d.Get("container_sync_key").(string), + ContainerWrite: d.Get("container_write").(string), + ContentType: d.Get("content_type").(string), + Metadata: resourceContainerMetadataV2(d), + } + + log.Printf("[DEBUG] Create Options: %#v", createOpts) + _, err = containers.Create(objectStorageClient, cn, createOpts).Extract() + if err != nil { + return fmt.Errorf("Error creating OpenStack container: %s", err) + } + log.Printf("[INFO] Container ID: %s", cn) + + // Store the ID now + d.SetId(cn) + + return resourceObjectStorageContainerV1Read(d, meta) +} + +func resourceObjectStorageContainerV1Read(d *schema.ResourceData, meta interface{}) error { + return nil +} + +func resourceObjectStorageContainerV1Update(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + objectStorageClient, err := config.objectStorageV1Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack object storage client: %s", err) + } + + updateOpts := containers.UpdateOpts{ + ContainerRead: d.Get("container_read").(string), + ContainerSyncTo: d.Get("container_sync_to").(string), + ContainerSyncKey: d.Get("container_sync_key").(string), + ContainerWrite: d.Get("container_write").(string), + ContentType: d.Get("content_type").(string), + } + + if d.HasChange("metadata") { + updateOpts.Metadata = resourceContainerMetadataV2(d) + } + + _, err = containers.Update(objectStorageClient, d.Id(), updateOpts).Extract() + if err != nil { + return fmt.Errorf("Error updating OpenStack container: %s", err) + } + + return resourceObjectStorageContainerV1Read(d, meta) +} + +func resourceObjectStorageContainerV1Delete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + objectStorageClient, err := config.objectStorageV1Client(d.Get("region").(string)) + if err != nil { + return fmt.Errorf("Error creating OpenStack object storage client: %s", err) + } + + _, err = containers.Delete(objectStorageClient, d.Id()).Extract() + if err != nil { + return fmt.Errorf("Error deleting OpenStack container: %s", err) + } + + d.SetId("") + return nil +} + +func resourceContainerMetadataV2(d *schema.ResourceData) map[string]string { + m := make(map[string]string) + for key, val := range d.Get("metadata").(map[string]interface{}) { + m[key] = val.(string) + } + return m +} diff --git a/builtin/providers/openstack/resource_openstack_objectstorage_container_v1_test.go b/builtin/providers/openstack/resource_openstack_objectstorage_container_v1_test.go new file mode 100644 index 0000000000..9377ad2fb0 --- /dev/null +++ b/builtin/providers/openstack/resource_openstack_objectstorage_container_v1_test.go @@ -0,0 +1,77 @@ +package openstack + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/rackspace/gophercloud/openstack/objectstorage/v1/containers" +) + +func TestAccObjectStorageV1Container_basic(t *testing.T) { + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckObjectStorageV1ContainerDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccObjectStorageV1Container_basic, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("openstack_objectstorage_container_v1.container_1", "name", "tf-test-container"), + resource.TestCheckResourceAttr("openstack_objectstorage_container_v1.container_1", "content_type", "application/json"), + ), + }, + resource.TestStep{ + Config: testAccObjectStorageV1Container_update, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("openstack_objectstorage_container_v1.container_1", "content_type", "text/plain"), + ), + }, + }, + }) +} + +func testAccCheckObjectStorageV1ContainerDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + objectStorageClient, err := config.objectStorageV1Client(OS_REGION_NAME) + if err != nil { + return fmt.Errorf("Error creating OpenStack object storage client: %s", err) + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != "openstack_objectstorage_container_v1" { + continue + } + + _, err := containers.Get(objectStorageClient, rs.Primary.ID).Extract() + if err == nil { + return fmt.Errorf("Container still exists") + } + } + + return nil +} + +var testAccObjectStorageV1Container_basic = fmt.Sprintf(` + resource "openstack_objectstorage_container_v1" "container_1" { + region = "%s" + name = "tf-test-container" + metadata { + test = "true" + } + content_type = "application/json" + }`, + OS_REGION_NAME) + +var testAccObjectStorageV1Container_update = fmt.Sprintf(` + resource "openstack_objectstorage_container_v1" "container_1" { + region = "%s" + name = "tf-test-container" + metadata { + test = "true" + } + content_type = "text/plain" + }`, + OS_REGION_NAME) diff --git a/builtin/providers/openstack/util.go b/builtin/providers/openstack/util.go new file mode 100644 index 0000000000..93a8bfbc52 --- /dev/null +++ b/builtin/providers/openstack/util.go @@ -0,0 +1,22 @@ +package openstack + +import ( + "fmt" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/rackspace/gophercloud" +) + +// CheckDeleted checks the error to see if it's a 404 (Not Found) and, if so, +// sets the resource ID to the empty string instead of throwing an error. +func CheckDeleted(d *schema.ResourceData, err error, msg string) error { + errCode, ok := err.(*gophercloud.UnexpectedResponseCodeError) + if !ok { + return fmt.Errorf("%s: %s", msg, err) + } + if errCode.Actual == 404 { + d.SetId("") + return nil + } + return fmt.Errorf("%s: %s", msg, err) +} diff --git a/website/source/docs/providers/index.html.markdown b/website/source/docs/providers/index.html.markdown index f03c17c54b..5365d0e86e 100644 --- a/website/source/docs/providers/index.html.markdown +++ b/website/source/docs/providers/index.html.markdown @@ -14,7 +14,7 @@ etc. Almost any infrastructure noun can be represented as a resource in Terrafor Terraform is agnostic to the underlying platforms by supporting providers. A provider is responsible for understanding API interactions and exposing resources. Providers -generally are an IaaS (e.g. AWS, DigitalOcean, GCE), PaaS (e.g. Heroku, CloudFoundry), +generally are an IaaS (e.g. AWS, DigitalOcean, GCE, OpenStack), PaaS (e.g. Heroku, CloudFoundry), or SaaS services (e.g. Atlas, DNSimple, CloudFlare). Use the navigation to the left to read about the available providers. diff --git a/website/source/docs/providers/openstack/index.html.markdown b/website/source/docs/providers/openstack/index.html.markdown new file mode 100644 index 0000000000..da6d1fb79d --- /dev/null +++ b/website/source/docs/providers/openstack/index.html.markdown @@ -0,0 +1,54 @@ +--- +layout: "openstack" +page_title: "Provider: OpenStack" +sidebar_current: "docs-openstack-index" +description: |- + The OpenStack provider is used to interact with the many resources supported by OpenStack. The provider needs to be configured with the proper credentials before it can be used. +--- + +# OpenStack Provider + +The OpenStack provider is used to interact with the +many resources supported by OpenStack. The provider needs to be configured +with the proper credentials before it can be used. + +Use the navigation to the left to read about the available resources. + +## Example Usage + +``` +# Configure the OpenStack Provider +provider "openstack" { + user_name = "admin" + tenant_name = "admin" + password = "pwd" + auth_url = "http://myauthurl:5000/v2.0" +} + +# Create a web server +resource "openstack_compute_instance_v2" "test-server" { + ... +} +``` + +## Configuration Reference + +The following arguments are supported: + +* `auth_url` - (Required) + +* `user_name` - (Optional; Required for Identity V2) + +* `user_id` - (Optional) + +* `password` - (Optional; Required if not using `api_key`) + +* `api_key` - (Optional; Required if not using `password`) + +* `domain_id` - (Optional) + +* `domain_name` - (Optional) + +* `tenant_id` - (Optional) + +* `tenant_name` - (Optional) diff --git a/website/source/docs/providers/openstack/r/blockstorage_volume_v1.html.markdown b/website/source/docs/providers/openstack/r/blockstorage_volume_v1.html.markdown new file mode 100644 index 0000000000..0b91eb11c6 --- /dev/null +++ b/website/source/docs/providers/openstack/r/blockstorage_volume_v1.html.markdown @@ -0,0 +1,68 @@ +--- +layout: "openstack" +page_title: "OpenStack: openstack_blockstorage_volume_v1" +sidebar_current: "docs-openstack-resource-blockstorage-volume-v1" +description: |- +Manages a V1 volume resource within OpenStack. +--- + +# openstack\_blockstorage\_volume_v1 + +Manages a V1 volume resource within OpenStack. + +## Example Usage + +``` +resource "openstack_blockstorage_volume_v1" "volume_1" { + region = "RegionOne" + name = "tf-test-volume" + description = "first test volume" + size = 3 +} +``` + +## Argument Reference + +The following arguments are supported: + +* `region` - (Required) The region in which to create the volume. If + omitted, the `OS_REGION_NAME` environment variable is used. Changing this + creates a new volume. + +* `size` - (Required) The size of the volume to create (in gigabytes). Changing + this creates a new volume. + +* `name` - (Optional) A unique name for the volume. Changing this updates the + volume's name. + +* `description` - (Optional) A description of the volume. Changing this updates + the volume's description. + +* `image_id` - (Optional) The image ID from which to create the volume. + Changing this creates a new volume. + +* `snapshot_id` - (Optional) The snapshot ID from which to create the volume. + Changing this creates a new volume. + +* `source_vol_id` - (Optional) The volume ID from which to create the volume. + Changing this creates a new volume. + +* `metadata` - (Optional) Metadata key/value pairs to associate with the volume. + Changing this updates the existing volume metadata. + +* `volume_type` - (Optional) The type of volume to create (either SATA or SSD). + Changing this creates a new volume. + +## Attributes Reference + +The following attributes are exported: + +* `region` - See Argument Reference above. +* `size` - See Argument Reference above. +* `name` - See Argument Reference above. +* `description` - See Argument Reference above. +* `image_id` - See Argument Reference above. +* `source_vol_id` - See Argument Reference above. +* `snapshot_id` - See Argument Reference above. +* `metadata` - See Argument Reference above. +* `volume_type` - See Argument Reference above. diff --git a/website/source/docs/providers/openstack/r/compute_instance_v2.html.markdown b/website/source/docs/providers/openstack/r/compute_instance_v2.html.markdown new file mode 100644 index 0000000000..36805ed0d2 --- /dev/null +++ b/website/source/docs/providers/openstack/r/compute_instance_v2.html.markdown @@ -0,0 +1,120 @@ +--- +layout: "openstack" +page_title: "OpenStack: openstack_compute_instance_v2" +sidebar_current: "docs-openstack-resource-compute-instance-v2" +description: |- + Manages a V2 VM instance resource within OpenStack. +--- + +# openstack\_compute\_instance_v2 + +Manages a V2 VM instance resource within OpenStack. + +## Example Usage + +``` +resource "openstack_compute_instance_v2" "test-server" { + name = "tf-test" + image_id = "ad091b52-742f-469e-8f3c-fd81cadf0743" + flavor_id = "3" + metadata { + this = "that" + } + key_pair = "my_key_pair_name" + security_groups = ["test-group-1"] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `region` - (Required) The region in which to create the server instance. If + omitted, the `OS_REGION_NAME` environment variable is used. Changing this + creates a new server. + +* `name` - (Required) A unique name for the resource. + +* `image_id` - (Optional; Required if `image_name` is empty) The image ID of + the desired image for the server. Changing this creates a new server. + +* `image_name` - (Optional; Required if `image_id` is empty) The name of the + desired image for the server. Changing this creates a new server. + +* `flavor_id` - (Optional; Required if `flavor_name` is empty) The flavor ID of + the desired flavor for the server. Changing this resizes the existing server. + +* `flavor_name` - (Optional; Required if `flavor_id` is empty) The name of the + desired flavor for the server. Changing this resizes the existing server. + +* `security_groups` - (Optional) An array of one or more security group names + to associate with the server. Changing this results in adding/removing + security groups from the existing server. + +* `availability_zone` - (Optional) The availability zone in which to create + the server. Changing this creates a new server. + +* `network` - (Optional) An array of one or more networks to attach to the + instance. The network object structure is documented below. Changing this + creates a new server. + +* `metadata` - (Optional) Metadata key/value pairs to make available from + within the instance. Changing this updates the existing server metadata. + +* `admin_pass` - (Optional) The administrative password to assign to the server. + Changing this changes the root password on the existing server. + +* `key_pair` - (Optional) The name of a key pair to put on the server. The key + pair must already be created and associated with the tenant's account. + Changing this creates a new server. + +* `block_device` - (Optional) The object for booting by volume. The block_device + object structure is documented below. Changing this creates a new server. + +* `volume` - (Optional) Attach an existing volume to the instance. The volume + structure is described below. + +The `network` block supports: + +* `uuid` - (Required unless `port` is provided) The network UUID to attach to + the server. + +* `port` - (Required unless `uuid` is provided) The port UUID of a network to + attach to the server. + +* `fixed_ip` - (Optional) Specifies a fixed IP address to be used on this + network. + +The `block_device` block supports: + +* `uuid` - (Required) The UUID of the image, volume, or snapshot. + +* `source_type` - (Required) The source type of the device. Must be one of + "image", "volume", or "snapshot". + +* `volume_size` - (Optional) The size of the volume to create (in gigabytes). + +* `boot_index` - (Optional) The boot index of the volume. It defaults to 0. + +* `destination_type` - (Optional) The type that gets created. Possible values + are "volume" and "local". + +The `volume` block supports: + +* `volume_id` - (Required) The UUID of the volume to attach. + +* `device` - (Optional) The device that the volume will be attached as. For + example: `/dev/vdc`. Omit this option to allow the volume to be + auto-assigned a device. + +## Attributes Reference + +The following attributes are exported: + +* `region` - See Argument Reference above. +* `name` - See Argument Reference above. +* `access_ip_v4` - See Argument Reference above. +* `access_ip_v6` - See Argument Reference above. +* `metadata` - See Argument Reference above. +* `security_groups` - See Argument Reference above. +* `flavor_ref` - See Argument Reference above. diff --git a/website/source/docs/providers/openstack/r/compute_keypair_v2.html.markdown b/website/source/docs/providers/openstack/r/compute_keypair_v2.html.markdown new file mode 100644 index 0000000000..0c3beae279 --- /dev/null +++ b/website/source/docs/providers/openstack/r/compute_keypair_v2.html.markdown @@ -0,0 +1,43 @@ +--- +layout: "openstack" +page_title: "OpenStack: openstack_compute_keypair_v2" +sidebar_current: "docs-openstack-resource-compute-keypair-v2" +description: |- + Manages a V2 keypair resource within OpenStack. +--- + +# openstack\_compute\_keypair_v2 + +Manages a V2 keypair resource within OpenStack. + +## Example Usage + +``` +resource "openstack_compute_keypair_v2" "test-keypair" { + name = "my-keypair" + public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDAjpC1hwiOCCmKEWxJ4qzTTsJbKzndLotBCz5PcwtUnflmU+gHJtWMZKpuEGVi29h0A/+ydKek1O18k10Ff+4tyFjiHDQAnOfgWf7+b1yK+qDip3X1C0UPMbwHlTfSGWLGZqd9LvEFx9k3h/M+VtMvwR1lJ9LUyTAImnNjWG7TaIPmui30HvM2UiFEmqkr4ijq45MyX2+fLIePLRIF61p4whjHAQYufqyno3BS48icQb4p6iVEZPo4AE2o9oIyQvj2mx4dk5Y8CgSETOZTYDOR3rU2fZTRDRgPJDH9FWvQjF5tA0p3d9CoWWd2s6GKKbfoUIi8R/Db1BSPJwkqB" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `region` - (Required) The region in which to obtain the V2 Compute client. + Keypairs are associated with accounts, but a Compute client is needed to + create one. If omitted, the `OS_REGION_NAME` environment variable is used. + Changing this creates a new keypair. + +* `name` - (Required) A unique name for the keypair. Changing this creates a new + keypair. + +* `public_key` - (Required) A pregenerated OpenSSH-formatted public key. + Changing this creates a new keypair. + +## Attributes Reference + +The following attributes are exported: + +* `region` - See Argument Reference above. +* `name` - See Argument Reference above. +* `public_key` - See Argument Reference above. diff --git a/website/source/docs/providers/openstack/r/compute_secgroup_v2.html.markdown b/website/source/docs/providers/openstack/r/compute_secgroup_v2.html.markdown new file mode 100644 index 0000000000..5b9538793d --- /dev/null +++ b/website/source/docs/providers/openstack/r/compute_secgroup_v2.html.markdown @@ -0,0 +1,76 @@ +--- +layout: "openstack" +page_title: "OpenStack: openstack_compute_secgroup_v2" +sidebar_current: "docs-openstack-resource-compute-secgroup-2" +description: |- + Manages a V2 security group resource within OpenStack. +--- + +# openstack\_compute\_secgroup_v2 + +Manages a V2 security group resource within OpenStack. + +## Example Usage + +``` +resource "openstack_compute_secgroup_v2" "secgroup_1" { + name = "my_secgroup" + description = "my security group" + rule { + from_port = 22 + to_port = 22 + ip_protocol = "tcp" + cidr = "0.0.0.0/0" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `region` - (Required) The region in which to obtain the V2 Compute client. + A Compute client is needed to create a security group. If omitted, the + `OS_REGION_NAME` environment variable is used. Changing this creates a new + security group. + +* `name` - (Required) A unique name for the security group. Changing this + updates the `name` of an existing security group. + +* `description` - (Required) A description for the security group. Changing this + updates the `description` of an existing security group. + +* `rule` - (Optional) A rule describing how the security group operates. The + rule object structure is documented below. Changing this updates the + security group rules. + +The `rule` block supports: + +* `from_port` - (Required) An integer representing the lower bound of the port +range to open. Changing this creates a new security group rule. + +* `to_port` - (Required) An integer representing the upper bound of the port +range to open. Changing this creates a new security group rule. + +* `ip_protocol` - (Required) The protocol type that will be allowed. Changing +this creates a new security group rule. + +* `cidr` - (Optional) Required if `from_group_id` is empty. The IP range that +will be the source of network traffic to the security group. Use 0.0.0.0./0 +to allow all IP addresses. Changing this creates a new security group rule. + +* `from_group_id` - (Optional) Required if `cidr` is empty. The ID of a group +from which to forward traffic to the parent group. Changing +this creates a new security group rule. + +* `self` - (Optional) Required if `cidr` and `from_group_id` is empty. If true, +the security group itself will be added as a source to this ingress rule. + +## Attributes Reference + +The following attributes are exported: + +* `region` - See Argument Reference above. +* `name` - See Argument Reference above. +* `description` - See Argument Reference above. +* `rule` - See Argument Reference above. diff --git a/website/source/docs/providers/openstack/r/lb_monitor_v1.html.markdown b/website/source/docs/providers/openstack/r/lb_monitor_v1.html.markdown new file mode 100644 index 0000000000..cbf6b2b873 --- /dev/null +++ b/website/source/docs/providers/openstack/r/lb_monitor_v1.html.markdown @@ -0,0 +1,82 @@ +--- +layout: "openstack" +page_title: "OpenStack: openstack_lb_monitor_v1" +sidebar_current: "docs-openstack-resource-lb-monitor-v1" +description: |- + Manages a V1 load balancer monitor resource within OpenStack. +--- + +# openstack\_lb\_monitor_v1 + +Manages a V1 load balancer monitor resource within OpenStack. + +## Example Usage + +``` +resource "openstack_lb_monitor_v1" "monitor_1" { + type = "PING" + delay = 30 + timeout = 5 + max_retries = 3 + 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 an LB monitor. If omitted, the + `OS_REGION_NAME` environment variable is used. Changing this creates a new + LB monitor. + +* `type` - (Required) The type of probe, which is PING, TCP, HTTP, or HTTPS, + that is sent by the monitor to verify the member state. Changing this + creates a new monitor. + +* `delay` - (Required) The time, in seconds, between sending probes to members. + Changing this creates a new monitor. + +* `timeout` - (Required) Maximum number of seconds for a monitor to wait for a + ping reply before it times out. The value must be less than the delay value. + Changing this updates the timeout of the existing monitor. + +* `max_retries` - (Required) Number of permissible ping failures before changing + the member's status to INACTIVE. Must be a number between 1 and 10. Changing + this updates the max_retries of the existing monitor. + +* `url_path` - (Optional) Required for HTTP(S) types. URI path that will be + accessed if monitor type is HTTP or HTTPS. Changing this updates the + url_path of the existing monitor. + +* `http_method` - (Optional) Required for HTTP(S) types. The HTTP method used + for requests by the monitor. If this attribute is not specified, it defaults + to "GET". Changing this updates the http_method of the existing monitor. + +* `expected_codes` - (Optional) equired for HTTP(S) types. Expected HTTP codes + for a passing HTTP(S) monitor. You can either specify a single status like + "200", or a range like "200-202". Changing this updates the expected_codes + of the existing monitor. + +* `admin_state_up` - (Optional) The administrative state of the monitor. + Acceptable values are "true" and "false". Changing this value updates the + state of the existing monitor. + +* `tenant_id` - (Optional) The owner of the monitor. Required if admin wants to + create a monitor for another tenant. Changing this creates a new monitor. + +## Attributes Reference + +The following attributes are exported: + +* `region` - See Argument Reference above. +* `type` - See Argument Reference above. +* `delay` - See Argument Reference above. +* `timeout` - See Argument Reference above. +* `max_retries` - See Argument Reference above. +* `url_path` - See Argument Reference above. +* `http_method` - See Argument Reference above. +* `expected_codes` - See Argument Reference above. +* `admin_state_up` - See Argument Reference above. +* `tenant_id` - See Argument Reference above. diff --git a/website/source/docs/providers/openstack/r/lb_pool_v1.html.markdown b/website/source/docs/providers/openstack/r/lb_pool_v1.html.markdown new file mode 100644 index 0000000000..5ddbdf1af8 --- /dev/null +++ b/website/source/docs/providers/openstack/r/lb_pool_v1.html.markdown @@ -0,0 +1,90 @@ +--- +layout: "openstack" +page_title: "OpenStack: openstack_lb_pool_v1" +sidebar_current: "docs-openstack-resource-lb-pool-v1" +description: |- + Manages a V1 load balancer pool resource within OpenStack. +--- + +# openstack\_lb\_pool_v1 + +Manages a V1 load balancer pool resource within OpenStack. + +## Example Usage + +``` +resource "openstack_lb_pool_v1" "pool_1" { + name = "tf_test_lb_pool" + protocol = "HTTP" + subnet_id = "12345" + lb_method = "ROUND_ROBIN" + monitor_ids = ["67890"] + member { + address = "192.168.0.1" + port = 80 + 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 an LB pool. If omitted, the + `OS_REGION_NAME` environment variable is used. Changing this creates a new + LB pool. + +* `name` - (Required) The name of the pool. Changing this updates the name of + the existing pool. + +* `protocol` - (Required) The protocol used by the pool members, you can use + either 'TCP, 'HTTP', or 'HTTPS'. Changing this creates a new pool. + +* `subnet_id` - (Required) The network on which the members of the pool will be + located. Only members that are on this network can be added to the pool. + Changing this creates a new pool. + +* `lb_method` - (Required) The algorithm used to distribute load between the + members of the pool. The current specification supports 'ROUND_ROBIN' and + 'LEAST_CONNECTIONS' as valid values for this attribute. + +* `tenant_id` - (Optional) The owner of the pool. Required if admin wants to + create a pool member for another tenant. Changing this creates a new pool. + +* `monitor_ids` - (Optional) A list of IDs of monitors to associate with the + pool. + +* `member` - (Optional) An existing node to add to the pool. Changing this + updates the members of the pool. The member object structure is documented + below. + +The `member` block supports: + +* `address` - (Required) The IP address of the member. Changing this creates a +new member. + +* `port` - (Required) An integer representing the port on which the member is +hosted. Changing this creates a new member. + +* `admin_state_up` - (Optional) The administrative state of the member. +Acceptable values are 'true' and 'false'. Changing this value updates the +state of the existing member. + +* `tenant_id` - (Optional) The owner of the member. Required if admin wants to +create a pool member for another tenant. Changing this creates a new member. + + +## Attributes Reference + +The following attributes are exported: + +* `region` - See Argument Reference above. +* `name` - See Argument Reference above. +* `protocol` - See Argument Reference above. +* `subnet_id` - See Argument Reference above. +* `lb_method` - See Argument Reference above. +* `tenant_id` - See Argument Reference above. +* `monitor_id` - See Argument Reference above. +* `member` - See Argument Reference above. diff --git a/website/source/docs/providers/openstack/r/lb_vip_v1.html.markdown b/website/source/docs/providers/openstack/r/lb_vip_v1.html.markdown new file mode 100644 index 0000000000..7a9bc3d4b0 --- /dev/null +++ b/website/source/docs/providers/openstack/r/lb_vip_v1.html.markdown @@ -0,0 +1,95 @@ +--- +layout: "openstack" +page_title: "OpenStack: openstack_lb_vip_v1" +sidebar_current: "docs-openstack-resource-lb-vip-v1" +description: |- + Manages a V1 load balancer vip resource within OpenStack. +--- + +# openstack\_lb\_vip_v1 + +Manages a V1 load balancer vip resource within OpenStack. + +## Example Usage + +``` +resource "openstack_lb_vip_v1" "vip_1" { + name = "tf_test_lb_vip" + subnet_id = "12345" + protocol = "HTTP" + port = 80 + pool_id = "67890" +} +``` + +## 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 VIP. If omitted, the + `OS_REGION_NAME` environment variable is used. Changing this creates a new + VIP. + +* `name` - (Required) The name of the vip. Changing this updates the name of + the existing vip. + +* `subnet_id` - (Required) The network on which to allocate the vip's address. A + tenant can only create vips on networks authorized by policy (e.g. networks + that belong to them or networks that are shared). Changing this creates a + new vip. + +* `protocol` - (Required) The protocol - can be either 'TCP, 'HTTP', or + HTTPS'. Changing this creates a new vip. + +* `port` - (Required) The port on which to listen for client traffic. Changing + this creates a new vip. + +* `pool_id` - (Required) The ID of the pool with which the vip is associated. + Changing this updates the pool_id of the existing vip. + +* `tenant_id` - (Optional) The owner of the vip. Required if admin wants to + create a vip member for another tenant. Changing this creates a new vip. + +* `address` - (Optional) The IP address of the vip. Changing this creates a new + vip. + +* `description` - (Optional) Human-readable description for the vip. Changing + this updates the description of the existing vip. + +* `persistence` - (Optional) Omit this field to prevent session persistence. + The persistence object structure is documented below. Changing this updates + the persistence of the existing vip. + +* `conn_limit` - (Optional) The maximum number of connections allowed for the + vip. Default is -1, meaning no limit. Changing this updates the conn_limit + of the existing vip. + +* `admin_state_up` - (Optional) The administrative state of the vip. + Acceptable values are "true" and "false". Changing this value updates the + state of the existing vip. + +The `persistence` block supports: + +* `type` - (Required) The type of persistence mode. Valid values are "SOURCE_IP", + "HTTP_COOKIE", or "APP_COOKIE". + +* `cookie_name` - (Optional) The name of the cookie if persistence mode is set + appropriately. + +## Attributes Reference + +The following attributes are exported: + +* `region` - See Argument Reference above. +* `name` - See Argument Reference above. +* `subnet_id` - See Argument Reference above. +* `protocol` - See Argument Reference above. +* `port` - See Argument Reference above. +* `pool_id` - See Argument Reference above. +* `tenant_id` - See Argument Reference above. +* `address` - See Argument Reference above. +* `description` - See Argument Reference above. +* `persistence` - See Argument Reference above. +* `conn_limit` - See Argument Reference above. +* `admin_state_up` - See Argument Reference above. 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 new file mode 100644 index 0000000000..699cd1ae9b --- /dev/null +++ b/website/source/docs/providers/openstack/r/networking_network_v2.html.markdown @@ -0,0 +1,53 @@ +--- +layout: "openstack" +page_title: "OpenStack: openstack_networking_network_v2" +sidebar_current: "docs-openstack-resource-networking-network-v2" +description: |- + Manages a V2 Neutron network resource within OpenStack. +--- + +# openstack\_networking\_network_v2 + +Manages a V2 Neutron network resource within OpenStack. + +## Example Usage + +``` +resource "openstack_networking_network_v2" "network_1" { + name = "tf_test_network" + 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 Neutron network. If omitted, the + `OS_REGION_NAME` environment variable is used. Changing this creates a new + network. + +* `name` - (Optional) The name of the network. Changing this updates the name of + the existing network. + +* `shared` - (Optional) Specifies whether the network resource can be accessed + by any tenant or not. Changing this updates the sharing capabalities of the + existing network. + +* `tenant_id` - (Optional) The owner of the newtork. Required if admin wants to + create a network for another tenant. Changing this creates a new network. + +* `admin_state_up` - (Optional) The administrative state of the network. + Acceptable values are "true" and "false". Changing this value updates the + state of the existing network. + +## Attributes Reference + +The following attributes are exported: + +* `region` - See Argument Reference above. +* `name` - See Argument Reference above. +* `shared` - See Argument Reference above. +* `tenant_id` - See Argument Reference above. +* `admin_state_up` - See Argument Reference above. diff --git a/website/source/docs/providers/openstack/r/networking_subnet_v2.html.markdown b/website/source/docs/providers/openstack/r/networking_subnet_v2.html.markdown new file mode 100644 index 0000000000..a8243a8178 --- /dev/null +++ b/website/source/docs/providers/openstack/r/networking_subnet_v2.html.markdown @@ -0,0 +1,98 @@ +--- +layout: "openstack" +page_title: "OpenStack: openstack_networking_subnet_v2" +sidebar_current: "docs-openstack-resource-networking-subnet-v2" +description: |- + Manages a V2 Neutron subnet resource within OpenStack. +--- + +# openstack\_networking\_subnet_v2 + +Manages a V2 Neutron subnet resource within OpenStack. + +## Example Usage + +``` +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 +} +``` + +## 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 Neutron subnet. If omitted, the + `OS_REGION_NAME` environment variable is used. Changing this creates a new + subnet. + +* `network_id` - (Required) The UUID of the parent network. Changing this + creates a new subnet. + +* `cidr` - (Required) CIDR representing IP range for this subnet, based on IP + version. Changing this creates a new subnet. + +* `ip_version` - (Required) IP version, either 4 or 6. Changing this creates a + new subnet. + +* `name` - (Optional) The name of the subnet. Changing this updates the name of + the existing subnet. + +* `tenant_id` - (Optional) The owner of the subnet. Required if admin wants to + create a subnet for another tenant. Changing this creates a new subnet. + +* `allocation_pools` - (Optional) An array of sub-ranges of CIDR available for + dynamic allocation to ports. The allocation_pool object structure is + documented below. Changing this creates a new subnet. + +* `gateway_ip` - (Optional) Default gateway used by devices in this subnet. + Changing this updates the gateway IP of the existing subnet. + +* `enable_dhcp` - (Optional) The administrative state of the network. + Acceptable values are "true" and "false". Changing this value enables or + disables the DHCP capabilities of the existing subnet. + +* `dns_nameservers` - (Optional) An array of DNS name server names used by hosts + in this subnet. Changing this updates the DNS name servers for the existing + subnet. + +* `host_routes` - (Optional) An array of routes that should be used by devices + with IPs from this subnet (not including local subnet route). The host_route + object structure is documented below. Changing this updates the host routes + for the existing subnet. + +The `allocation_pools` block supports: + +* `start` - (Required) The starting address. + +* `end` - (Required) The ending address. + +The `host_routes` block supports: + +* `destination_cidr` - (Required) The destination CIDR. + +* `next_hop` - (Required) The next hop in the route. + +## Attributes Reference + +The following attributes are exported: + +* `region` - See Argument Reference above. +* `network_id` - See Argument Reference above. +* `cidr` - See Argument Reference above. +* `ip_version` - See Argument Reference above. +* `name` - See Argument Reference above. +* `tenant_id` - See Argument Reference above. +* `allocation_pools` - See Argument Reference above. +* `gateway_ip` - See Argument Reference above. +* `enable_dhcp` - See Argument Reference above. +* `dns_nameservers` - See Argument Reference above. +* `host_routes` - See Argument Reference above. diff --git a/website/source/docs/providers/openstack/r/objectstorage_container_v1.html.markdown b/website/source/docs/providers/openstack/r/objectstorage_container_v1.html.markdown new file mode 100644 index 0000000000..8101d1ca21 --- /dev/null +++ b/website/source/docs/providers/openstack/r/objectstorage_container_v1.html.markdown @@ -0,0 +1,68 @@ +--- +layout: "openstack" +page_title: "OpenStack: openstack_objectstorage_container_v1" +sidebar_current: "docs-openstack-resource-objectstorage-container-v1" +description: |- +Manages a V1 container resource within OpenStack. +--- + +# openstack\_objectstorage\_container_v1 + +Manages a V1 container resource within OpenStack. + +## Example Usage + +``` +resource "openstack_objectstorage_container_v1" "container_1" { + region = "RegionOne" + name = "tf-test-container-1" + metadata { + test = "true" + } + content_type = "application/json" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `region` - (Required) The region in which to create the container. If + omitted, the `OS_REGION_NAME` environment variable is used. Changing this + creates a new container. + +* `name` - (Required) A unique name for the container. Changing this creates a + new container. + +* `container_read` - (Optional) Sets an access control list (ACL) that grants + read access. This header can contain a comma-delimited list of users that + can read the container (allows the GET method for all objects in the + container). Changing this updates the access control list read access. + +* `container_sync_to` - (Optional) The destination for container synchronization. + Changing this updates container synchronization. + +* `container_sync_key` - (Optional) The secret key for container synchronization. + Changing this updates container synchronization. + +* `container_write` - (Optional) Sets an ACL that grants write access. + Changing this updates the access control list write access. + +* `metadata` - (Optional) Custom key/value pairs to associate with the container. + Changing this updates the existing container metadata. + +* `content_type` - (Optional) The MIME type for the container. Changing this + updates the MIME type. + +## Attributes Reference + +The following attributes are exported: + +* `region` - See Argument Reference above. +* `name` - See Argument Reference above. +* `container_read` - See Argument Reference above. +* `container_sync_to` - See Argument Reference above. +* `container_sync_key` - See Argument Reference above. +* `container_write` - See Argument Reference above. +* `metadata` - See Argument Reference above. +* `content_type` - See Argument Reference above. diff --git a/website/source/layouts/openstack.erb b/website/source/layouts/openstack.erb new file mode 100644 index 0000000000..22afb4aeb2 --- /dev/null +++ b/website/source/layouts/openstack.erb @@ -0,0 +1,53 @@ +<% wrap_layout :inner do %> + <% content_for :sidebar do %> +
+ <% end %> + + <%= yield %> + <% end %>