From 4910423d8323eef89d60e9d3f5434b68bc1f57b6 Mon Sep 17 00:00:00 2001 From: Sander van Harmelen Date: Wed, 10 Dec 2014 22:20:52 +0100 Subject: [PATCH] First release of a provider for CloudStack Of course not all resources are covered by this first release, but there should be enough resources available to handle most common operations. Tests and docs are included. --- builtin/bins/provider-cloudstack/main.go | 12 + builtin/bins/provider-cloudstack/main_test.go | 1 + builtin/providers/cloudstack/config.go | 18 + builtin/providers/cloudstack/provider.go | 68 +++ builtin/providers/cloudstack/provider_test.go | 182 +++++++ .../cloudstack/resource_cloudstack_disk.go | 496 ++++++++++++++++++ .../resource_cloudstack_disk_test.go | 292 +++++++++++ .../resource_cloudstack_firewall.go | 442 ++++++++++++++++ .../resource_cloudstack_firewall_test.go | 191 +++++++ .../resource_cloudstack_instance.go | 278 ++++++++++ .../resource_cloudstack_instance_test.go | 235 +++++++++ .../resource_cloudstack_ipaddress.go | 154 ++++++ .../resource_cloudstack_ipaddress_test.go | 137 +++++ .../cloudstack/resource_cloudstack_network.go | 241 +++++++++ .../resource_cloudstack_network_acl.go | 123 +++++ .../resource_cloudstack_network_acl_rule.go | 476 +++++++++++++++++ ...source_cloudstack_network_acl_rule_test.go | 241 +++++++++ .../resource_cloudstack_network_acl_test.go | 117 +++++ .../resource_cloudstack_network_test.go | 193 +++++++ .../cloudstack/resource_cloudstack_nic.go | 147 ++++++ .../resource_cloudstack_nic_test.go | 198 +++++++ .../resource_cloudstack_port_forward.go | 299 +++++++++++ .../resource_cloudstack_port_forward_test.go | 219 ++++++++ .../cloudstack/resource_cloudstack_vpc.go | 168 ++++++ .../resource_cloudstack_vpc_test.go | 118 +++++ builtin/providers/cloudstack/resources.go | 77 +++ .../providers/cloudstack/index.html.markdown | 45 ++ .../providers/cloudstack/r/disk.html.markdown | 58 ++ .../cloudstack/r/firewall.html.markdown | 57 ++ .../cloudstack/r/instance.html.markdown | 59 +++ .../cloudstack/r/ipaddress.html.markdown | 38 ++ .../cloudstack/r/network.html.markdown | 54 ++ .../cloudstack/r/network_acl.html.markdown | 62 +++ .../r/network_acl_rule.html.markdown | 65 +++ .../providers/cloudstack/r/nic.html.markdown | 43 ++ .../cloudstack/r/port_forward.html.markdown | 53 ++ .../providers/cloudstack/r/vpc.html.markdown | 48 ++ website/source/layouts/cloudstack.erb | 62 +++ 38 files changed, 5767 insertions(+) create mode 100644 builtin/bins/provider-cloudstack/main.go create mode 100644 builtin/bins/provider-cloudstack/main_test.go create mode 100644 builtin/providers/cloudstack/config.go create mode 100644 builtin/providers/cloudstack/provider.go create mode 100644 builtin/providers/cloudstack/provider_test.go create mode 100644 builtin/providers/cloudstack/resource_cloudstack_disk.go create mode 100644 builtin/providers/cloudstack/resource_cloudstack_disk_test.go create mode 100644 builtin/providers/cloudstack/resource_cloudstack_firewall.go create mode 100644 builtin/providers/cloudstack/resource_cloudstack_firewall_test.go create mode 100644 builtin/providers/cloudstack/resource_cloudstack_instance.go create mode 100644 builtin/providers/cloudstack/resource_cloudstack_instance_test.go create mode 100644 builtin/providers/cloudstack/resource_cloudstack_ipaddress.go create mode 100644 builtin/providers/cloudstack/resource_cloudstack_ipaddress_test.go create mode 100644 builtin/providers/cloudstack/resource_cloudstack_network.go create mode 100644 builtin/providers/cloudstack/resource_cloudstack_network_acl.go create mode 100644 builtin/providers/cloudstack/resource_cloudstack_network_acl_rule.go create mode 100644 builtin/providers/cloudstack/resource_cloudstack_network_acl_rule_test.go create mode 100644 builtin/providers/cloudstack/resource_cloudstack_network_acl_test.go create mode 100644 builtin/providers/cloudstack/resource_cloudstack_network_test.go create mode 100644 builtin/providers/cloudstack/resource_cloudstack_nic.go create mode 100644 builtin/providers/cloudstack/resource_cloudstack_nic_test.go create mode 100644 builtin/providers/cloudstack/resource_cloudstack_port_forward.go create mode 100644 builtin/providers/cloudstack/resource_cloudstack_port_forward_test.go create mode 100644 builtin/providers/cloudstack/resource_cloudstack_vpc.go create mode 100644 builtin/providers/cloudstack/resource_cloudstack_vpc_test.go create mode 100644 builtin/providers/cloudstack/resources.go create mode 100644 website/source/docs/providers/cloudstack/index.html.markdown create mode 100644 website/source/docs/providers/cloudstack/r/disk.html.markdown create mode 100644 website/source/docs/providers/cloudstack/r/firewall.html.markdown create mode 100644 website/source/docs/providers/cloudstack/r/instance.html.markdown create mode 100644 website/source/docs/providers/cloudstack/r/ipaddress.html.markdown create mode 100644 website/source/docs/providers/cloudstack/r/network.html.markdown create mode 100644 website/source/docs/providers/cloudstack/r/network_acl.html.markdown create mode 100644 website/source/docs/providers/cloudstack/r/network_acl_rule.html.markdown create mode 100644 website/source/docs/providers/cloudstack/r/nic.html.markdown create mode 100644 website/source/docs/providers/cloudstack/r/port_forward.html.markdown create mode 100644 website/source/docs/providers/cloudstack/r/vpc.html.markdown create mode 100644 website/source/layouts/cloudstack.erb diff --git a/builtin/bins/provider-cloudstack/main.go b/builtin/bins/provider-cloudstack/main.go new file mode 100644 index 000000000..2d7434598 --- /dev/null +++ b/builtin/bins/provider-cloudstack/main.go @@ -0,0 +1,12 @@ +package main + +import ( + "github.com/hashicorp/terraform/builtin/providers/cloudstack" + "github.com/hashicorp/terraform/plugin" +) + +func main() { + plugin.Serve(&plugin.ServeOpts{ + ProviderFunc: cloudstack.Provider, + }) +} diff --git a/builtin/bins/provider-cloudstack/main_test.go b/builtin/bins/provider-cloudstack/main_test.go new file mode 100644 index 000000000..06ab7d0f9 --- /dev/null +++ b/builtin/bins/provider-cloudstack/main_test.go @@ -0,0 +1 @@ +package main diff --git a/builtin/providers/cloudstack/config.go b/builtin/providers/cloudstack/config.go new file mode 100644 index 000000000..b7c4c7ce1 --- /dev/null +++ b/builtin/providers/cloudstack/config.go @@ -0,0 +1,18 @@ +package cloudstack + +import "github.com/xanzy/go-cloudstack/cloudstack" + +// Config is the configuration structure used to instantiate a +// new CloudStack client. +type Config struct { + ApiURL string + ApiKey string + SecretKey string +} + +// Client() returns a new CloudStack client. +func (c *Config) NewClient() (*cloudstack.CloudStackClient, error) { + cs := cloudstack.NewAsyncClient(c.ApiURL, c.ApiKey, c.SecretKey, false) + cs.AsyncTimeout(180) + return cs, nil +} diff --git a/builtin/providers/cloudstack/provider.go b/builtin/providers/cloudstack/provider.go new file mode 100644 index 000000000..6440ff2c0 --- /dev/null +++ b/builtin/providers/cloudstack/provider.go @@ -0,0 +1,68 @@ +package cloudstack + +import ( + "os" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +// Provider returns a terraform.ResourceProvider. +func Provider() terraform.ResourceProvider { + return &schema.Provider{ + Schema: map[string]*schema.Schema{ + "api_url": &schema.Schema{ + Type: schema.TypeString, + Required: true, + DefaultFunc: envDefaultFunc("CLOUDSTACK_API_URL"), + }, + + "api_key": &schema.Schema{ + Type: schema.TypeString, + Required: true, + DefaultFunc: envDefaultFunc("CLOUDSTACK_API_KEY"), + }, + + "secret_key": &schema.Schema{ + Type: schema.TypeString, + Required: true, + DefaultFunc: envDefaultFunc("CLOUDSTACK_SECRET_KEY"), + }, + }, + + ResourcesMap: map[string]*schema.Resource{ + "cloudstack_disk": resourceCloudStackDisk(), + "cloudstack_firewall": resourceCloudStackFirewall(), + "cloudstack_instance": resourceCloudStackInstance(), + "cloudstack_ipaddress": resourceCloudStackIPAddress(), + "cloudstack_network": resourceCloudStackNetwork(), + "cloudstack_network_acl": resourceCloudStackNetworkACL(), + "cloudstack_network_acl_rule": resourceCloudStackNetworkACLRule(), + "cloudstack_nic": resourceCloudStackNIC(), + "cloudstack_port_forward": resourceCloudStackPortForward(), + "cloudstack_vpc": resourceCloudStackVPC(), + }, + + ConfigureFunc: providerConfigure, + } +} + +func providerConfigure(d *schema.ResourceData) (interface{}, error) { + config := Config{ + ApiURL: d.Get("api_url").(string), + ApiKey: d.Get("api_key").(string), + SecretKey: d.Get("secret_key").(string), + } + + return config.NewClient() +} + +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/cloudstack/provider_test.go b/builtin/providers/cloudstack/provider_test.go new file mode 100644 index 000000000..91761c344 --- /dev/null +++ b/builtin/providers/cloudstack/provider_test.go @@ -0,0 +1,182 @@ +package cloudstack + +import ( + "os" + "testing" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/terraform" +) + +var testAccProviders map[string]terraform.ResourceProvider +var testAccProvider *schema.Provider + +func init() { + testAccProvider = Provider().(*schema.Provider) + testAccProviders = map[string]terraform.ResourceProvider{ + "cloudstack": 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) { + if v := os.Getenv("CLOUDSTACK_API_URL"); v == "" { + t.Fatal("CLOUDSTACK_API_URL must be set for acceptance tests") + } + if v := os.Getenv("CLOUDSTACK_API_KEY"); v == "" { + t.Fatal("CLOUDSTACK_API_KEY must be set for acceptance tests") + } + if v := os.Getenv("CLOUDSTACK_SECRET_KEY"); v == "" { + t.Fatal("CLOUDSTACK_SECRET_KEY must be set for acceptance tests") + } + + // Testing all environment/installation specific variables which are needed + // to run all the acceptance tests + if CLOUDSTACK_DISK_OFFERING_1 == "" { + if v := os.Getenv("CLOUDSTACK_DISK_OFFERING_1"); v == "" { + t.Fatal("CLOUDSTACK_DISK_OFFERING_1 must be set for acceptance tests") + } else { + CLOUDSTACK_DISK_OFFERING_1 = v + } + } + if CLOUDSTACK_DISK_OFFERING_2 == "" { + if v := os.Getenv("CLOUDSTACK_DISK_OFFERING_2"); v == "" { + t.Fatal("CLOUDSTACK_DISK_OFFERING_2 must be set for acceptance tests") + } else { + CLOUDSTACK_DISK_OFFERING_2 = v + } + } + if CLOUDSTACK_SERVICE_OFFERING_1 == "" { + if v := os.Getenv("CLOUDSTACK_SERVICE_OFFERING_1"); v == "" { + t.Fatal("CLOUDSTACK_SERVICE_OFFERING_1 must be set for acceptance tests") + } else { + CLOUDSTACK_SERVICE_OFFERING_1 = v + } + } + if CLOUDSTACK_SERVICE_OFFERING_2 == "" { + if v := os.Getenv("CLOUDSTACK_SERVICE_OFFERING_2"); v == "" { + t.Fatal("CLOUDSTACK_SERVICE_OFFERING_2 must be set for acceptance tests") + } else { + CLOUDSTACK_SERVICE_OFFERING_2 = v + } + } + if CLOUDSTACK_NETWORK_1 == "" { + if v := os.Getenv("CLOUDSTACK_NETWORK_1"); v == "" { + t.Fatal("CLOUDSTACK_NETWORK_1 must be set for acceptance tests") + } else { + CLOUDSTACK_NETWORK_1 = v + } + } + if CLOUDSTACK_NETWORK_1_CIDR == "" { + if v := os.Getenv("CLOUDSTACK_NETWORK_1_CIDR"); v == "" { + t.Fatal("CLOUDSTACK_NETWORK_1_CIDR must be set for acceptance tests") + } else { + CLOUDSTACK_NETWORK_1_CIDR = v + } + } + if CLOUDSTACK_NETWORK_1_OFFERING == "" { + if v := os.Getenv("CLOUDSTACK_NETWORK_1_OFFERING"); v == "" { + t.Fatal("CLOUDSTACK_NETWORK_1_OFFERING must be set for acceptance tests") + } else { + CLOUDSTACK_NETWORK_1_OFFERING = v + } + } + if CLOUDSTACK_NETWORK_1_IPADDRESS == "" { + if v := os.Getenv("CLOUDSTACK_NETWORK_1_IPADDRESS"); v == "" { + t.Fatal("CLOUDSTACK_NETWORK_1_IPADDRESS must be set for acceptance tests") + } else { + CLOUDSTACK_NETWORK_1_IPADDRESS = v + } + } + if CLOUDSTACK_NETWORK_2 == "" { + if v := os.Getenv("CLOUDSTACK_NETWORK_2"); v == "" { + t.Fatal("CLOUDSTACK_NETWORK_2 must be set for acceptance tests") + } else { + CLOUDSTACK_NETWORK_2 = v + } + } + if CLOUDSTACK_NETWORK_2_IPADDRESS == "" { + if v := os.Getenv("CLOUDSTACK_NETWORK_2_IPADDRESS"); v == "" { + t.Fatal("CLOUDSTACK_NETWORK_2_IPADDRESS must be set for acceptance tests") + } else { + CLOUDSTACK_NETWORK_2_IPADDRESS = v + } + } + if CLOUDSTACK_VPC_CIDR == "" { + if v := os.Getenv("CLOUDSTACK_VPC_CIDR"); v == "" { + t.Fatal("CLOUDSTACK_VPC_CIDR must be set for acceptance tests") + } else { + CLOUDSTACK_VPC_CIDR = v + } + } + if CLOUDSTACK_VPC_OFFERING == "" { + if v := os.Getenv("CLOUDSTACK_VPC_OFFERING"); v == "" { + t.Fatal("CLOUDSTACK_VPC_OFFERING must be set for acceptance tests") + } else { + CLOUDSTACK_VPC_OFFERING = v + } + } + if CLOUDSTACK_VPC_NETWORK_CIDR == "" { + if v := os.Getenv("CLOUDSTACK_VPC_NETWORK_CIDR"); v == "" { + t.Fatal("CLOUDSTACK_VPC_NETWORK_CIDR must be set for acceptance tests") + } else { + CLOUDSTACK_VPC_NETWORK_CIDR = v + } + } + if CLOUDSTACK_VPC_NETWORK_OFFERING == "" { + if v := os.Getenv("CLOUDSTACK_VPC_NETWORK_OFFERING"); v == "" { + t.Fatal("CLOUDSTACK_VPC_NETWORK_OFFERING must be set for acceptance tests") + } else { + CLOUDSTACK_VPC_NETWORK_OFFERING = v + } + } + if CLOUDSTACK_PUBLIC_IPADDRESS == "" { + if v := os.Getenv("CLOUDSTACK_PUBLIC_IPADDRESS"); v == "" { + t.Fatal("CLOUDSTACK_PUBLIC_IPADDRESS must be set for acceptance tests") + } else { + CLOUDSTACK_PUBLIC_IPADDRESS = v + } + } + if CLOUDSTACK_TEMPLATE == "" { + if v := os.Getenv("CLOUDSTACK_TEMPLATE"); v == "" { + t.Fatal("CLOUDSTACK_TEMPLATE must be set for acceptance tests") + } else { + CLOUDSTACK_TEMPLATE = v + } + } + if CLOUDSTACK_ZONE == "" { + if v := os.Getenv("CLOUDSTACK_ZONE"); v == "" { + t.Fatal("CLOUDSTACK_ZONE must be set for acceptance tests") + } else { + CLOUDSTACK_ZONE = v + } + } +} + +// EITHER SET THESE, OR ADD THE VALUES TO YOUR ENV!! +var CLOUDSTACK_DISK_OFFERING_1 = "" +var CLOUDSTACK_DISK_OFFERING_2 = "" +var CLOUDSTACK_SERVICE_OFFERING_1 = "" +var CLOUDSTACK_SERVICE_OFFERING_2 = "" +var CLOUDSTACK_NETWORK_1 = "" +var CLOUDSTACK_NETWORK_1_CIDR = "" +var CLOUDSTACK_NETWORK_1_OFFERING = "" +var CLOUDSTACK_NETWORK_1_IPADDRESS = "" +var CLOUDSTACK_NETWORK_2 = "" +var CLOUDSTACK_NETWORK_2_IPADDRESS = "" +var CLOUDSTACK_VPC_CIDR = "" +var CLOUDSTACK_VPC_OFFERING = "" +var CLOUDSTACK_VPC_NETWORK_CIDR = "" +var CLOUDSTACK_VPC_NETWORK_OFFERING = "" +var CLOUDSTACK_PUBLIC_IPADDRESS = "" +var CLOUDSTACK_TEMPLATE = "" +var CLOUDSTACK_ZONE = "" diff --git a/builtin/providers/cloudstack/resource_cloudstack_disk.go b/builtin/providers/cloudstack/resource_cloudstack_disk.go new file mode 100644 index 000000000..b648daf81 --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_disk.go @@ -0,0 +1,496 @@ +package cloudstack + +import ( + "fmt" + "strings" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +func resourceCloudStackDisk() *schema.Resource { + return &schema.Resource{ + Create: resourceCloudStackDiskCreate, + Read: resourceCloudStackDiskRead, + Update: resourceCloudStackDiskUpdate, + Delete: resourceCloudStackDiskDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "attach": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + + "device": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "disk_offering": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "size": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + }, + + "shrink_ok": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + + "virtual_machine": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "zone": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceCloudStackDiskCreate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + d.Partial(true) + + name := d.Get("name").(string) + + // Create a new parameter struct + p := cs.Volume.NewCreateVolumeParams(name) + + // Retrieve the disk_offering UUID + diskofferingid, e := retrieveUUID(cs, "disk_offering", d.Get("disk_offering").(string)) + if e != nil { + return e.Error() + } + // Set the disk_offering UUID + p.SetDiskofferingid(diskofferingid) + + if d.Get("size").(int) != 0 { + // Set the volume size + p.SetSize(d.Get("size").(int)) + } + + // Retrieve the zone UUID + zoneid, e := retrieveUUID(cs, "zone", d.Get("zone").(string)) + if e != nil { + return e.Error() + } + // Set the zone ID + p.SetZoneid(zoneid) + + // Create the new volume + r, err := cs.Volume.CreateVolume(p) + if err != nil { + return fmt.Errorf("Error creating the new disk %s: %s", name, err) + } + + // Set the volume UUID and partials + d.SetId(r.Id) + d.SetPartial("name") + d.SetPartial("device") + d.SetPartial("disk_offering") + d.SetPartial("size") + d.SetPartial("virtual_machine") + d.SetPartial("zone") + + if d.Get("attach").(bool) { + err := resourceCloudStackDiskAttach(d, meta) + if err != nil { + return fmt.Errorf("Error attaching the new disk %s to virtual machine: %s", name, err) + } + + // Set the additional partial + d.SetPartial("attach") + } + + d.Partial(false) + return resourceCloudStackDiskRead(d, meta) +} + +func resourceCloudStackDiskRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Get the volume details + v, count, err := cs.Volume.GetVolumeByID(d.Id()) + if err != nil { + if count == 0 { + d.SetId("") + return nil + } + + return err + } + + d.Set("name", v.Name) + d.Set("attach", v.Attached != "") // If attached this will contain a timestamp when attached + d.Set("disk_offering", v.Diskofferingname) + d.Set("size", v.Size/(1024*1024*1024)) // Needed to get GB's again + d.Set("zone", v.Zonename) + + if v.Attached != "" { + // Get the virtual machine details + vm, _, err := cs.VirtualMachine.GetVirtualMachineByID(v.Virtualmachineid) + if err != nil { + return err + } + + // Get the guest OS type details + os, _, err := cs.GuestOS.GetOsTypeByID(vm.Guestosid) + if err != nil { + return err + } + + // Get the guest OS category details + c, _, err := cs.GuestOS.GetOsCategoryByID(os.Oscategoryid) + if err != nil { + return err + } + + d.Set("device", retrieveDeviceName(v.Deviceid, c.Name)) + d.Set("virtual_machine", v.Vmname) + } + + return nil +} + +func resourceCloudStackDiskUpdate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + d.Partial(true) + + name := d.Get("name").(string) + + if d.HasChange("disk_offering") || d.HasChange("size") { + // Detach the volume (re-attach is done at the end of this function) + if err := resourceCloudStackDiskDetach(d, meta); err != nil { + return fmt.Errorf("Error detaching disk %s from virtual machine: %s", name, err) + } + + // Create a new parameter struct + p := cs.Volume.NewResizeVolumeParams() + + // Set the volume UUID + p.SetId(d.Id()) + + // Retrieve the disk_offering UUID + diskofferingid, e := retrieveUUID(cs, "disk_offering", d.Get("disk_offering").(string)) + if e != nil { + return e.Error() + } + + // Set the disk_offering UUID + p.SetDiskofferingid(diskofferingid) + + if d.Get("size").(int) != 0 { + // Set the size + p.SetSize(d.Get("size").(int)) + } + + // Set the shrink bit + p.SetShrinkok(d.Get("shrink_ok").(bool)) + + // Change the disk_offering + r, err := cs.Volume.ResizeVolume(p) + if err != nil { + return fmt.Errorf("Error changing disk offering/size for disk %s: %s", name, err) + } + + // Update the volume UUID and set partials + d.SetId(r.Id) + d.SetPartial("disk_offering") + d.SetPartial("size") + } + + // If the device changed, just detach here so we can re-attach the + // volume at the end of this function + if d.HasChange("device") || d.HasChange("virtual_machine") { + // Detach the volume + if err := resourceCloudStackDiskDetach(d, meta); err != nil { + return fmt.Errorf("Error detaching disk %s from virtual machine: %s", name, err) + } + } + + if d.Get("attach").(bool) { + // Attach the volume + err := resourceCloudStackDiskAttach(d, meta) + if err != nil { + return fmt.Errorf("Error attaching disk %s to virtual machine: %s", name, err) + } + + // Set the additional partials + d.SetPartial("attach") + d.SetPartial("device") + d.SetPartial("virtual_machine") + } else { + // Detach the volume + if err := resourceCloudStackDiskDetach(d, meta); err != nil { + return fmt.Errorf("Error detaching disk %s from virtual machine: %s", name, err) + } + } + + d.Partial(false) + return resourceCloudStackDiskRead(d, meta) +} + +func resourceCloudStackDiskDelete(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Detach the volume + if err := resourceCloudStackDiskDetach(d, meta); err != nil { + return err + } + + // Create a new parameter struct + p := cs.Volume.NewDeleteVolumeParams(d.Id()) + + // Delete the voluem + if _, err := cs.Volume.DeleteVolume(p); err != nil { + // This is a very poor way to be told the UUID does no longer exist :( + if strings.Contains(err.Error(), fmt.Sprintf( + "Invalid parameter id value=%s due to incorrect long value format, "+ + "or entity does not exist", d.Id())) { + return nil + } + + return err + } + + return nil +} + +func resourceCloudStackDiskAttach(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // First check if the disk isn't already attached + if attached, err := isAttached(cs, d.Id()); err != nil || attached { + return err + } + + // Retrieve the virtual_machine UUID + virtualmachineid, e := retrieveUUID(cs, "virtual_machine", d.Get("virtual_machine").(string)) + if e != nil { + return e.Error() + } + + // Create a new parameter struct + p := cs.Volume.NewAttachVolumeParams(d.Id(), virtualmachineid) + + if device, ok := d.GetOk("device"); ok { + // Retrieve the device ID + deviceid := retrieveDeviceID(device.(string)) + if deviceid == -1 { + return fmt.Errorf("Device %s is not a valid device", device.(string)) + } + + // Set the device ID + p.SetDeviceid(deviceid) + } + + // Attach the new volume + r, err := cs.Volume.AttachVolume(p) + if err != nil { + return err + } + + d.SetId(r.Id) + + return nil +} + +func resourceCloudStackDiskDetach(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Check if the volume is actually attached, before detaching + if attached, err := isAttached(cs, d.Id()); err != nil || !attached { + return err + } + + // Create a new parameter struct + p := cs.Volume.NewDetachVolumeParams() + + // Set the volume UUID + p.SetId(d.Id()) + + // Detach the currently attached volume + if _, err := cs.Volume.DetachVolume(p); err != nil { + // Retrieve the virtual_machine UUID + virtualmachineid, e := retrieveUUID(cs, "virtual_machine", d.Get("virtual_machine").(string)) + if e != nil { + return e.Error() + } + + // Create a new parameter struct + pd := cs.VirtualMachine.NewStopVirtualMachineParams(virtualmachineid) + + // Stop the virtual machine in order to be able to detach the disk + if _, err := cs.VirtualMachine.StopVirtualMachine(pd); err != nil { + return err + } + + // Try again to detach the currently attached volume + if _, err := cs.Volume.DetachVolume(p); err != nil { + return err + } + + // Create a new parameter struct + pu := cs.VirtualMachine.NewStartVirtualMachineParams(virtualmachineid) + + // Start the virtual machine again + if _, err := cs.VirtualMachine.StartVirtualMachine(pu); err != nil { + return err + } + } + + return nil +} + +func isAttached(cs *cloudstack.CloudStackClient, id string) (bool, error) { + // Get the volume details + v, _, err := cs.Volume.GetVolumeByID(id) + if err != nil { + return false, err + } + + return v.Attached != "", nil +} + +func retrieveDeviceID(device string) int { + switch device { + case "/dev/xvdb", "D:": + return 1 + case "/dev/xvdc", "E:": + return 2 + case "/dev/xvde", "F:": + return 4 + case "/dev/xvdf", "G:": + return 5 + case "/dev/xvdg", "H:": + return 6 + case "/dev/xvdh", "I:": + return 7 + case "/dev/xvdi", "J:": + return 8 + case "/dev/xvdj", "K:": + return 9 + case "/dev/xvdk", "L:": + return 10 + case "/dev/xvdl", "M:": + return 11 + case "/dev/xvdm", "N:": + return 12 + case "/dev/xvdn", "O:": + return 13 + case "/dev/xvdo", "P:": + return 14 + case "/dev/xvdp", "Q:": + return 15 + default: + return -1 + } +} + +func retrieveDeviceName(device int, os string) string { + switch device { + case 1: + if os == "Windows" { + return "D:" + } else { + return "/dev/xvdb" + } + case 2: + if os == "Windows" { + return "E:" + } else { + return "/dev/xvdc" + } + case 4: + if os == "Windows" { + return "F:" + } else { + return "/dev/xvde" + } + case 5: + if os == "Windows" { + return "G:" + } else { + return "/dev/xvdf" + } + case 6: + if os == "Windows" { + return "H:" + } else { + return "/dev/xvdg" + } + case 7: + if os == "Windows" { + return "I:" + } else { + return "/dev/xvdh" + } + case 8: + if os == "Windows" { + return "J:" + } else { + return "/dev/xvdi" + } + case 9: + if os == "Windows" { + return "K:" + } else { + return "/dev/xvdj" + } + case 10: + if os == "Windows" { + return "L:" + } else { + return "/dev/xvdk" + } + case 11: + if os == "Windows" { + return "M:" + } else { + return "/dev/xvdl" + } + case 12: + if os == "Windows" { + return "N:" + } else { + return "/dev/xvdm" + } + case 13: + if os == "Windows" { + return "O:" + } else { + return "/dev/xvdn" + } + case 14: + if os == "Windows" { + return "P:" + } else { + return "/dev/xvdo" + } + case 15: + if os == "Windows" { + return "Q:" + } else { + return "/dev/xvdp" + } + default: + return "unknown" + } +} diff --git a/builtin/providers/cloudstack/resource_cloudstack_disk_test.go b/builtin/providers/cloudstack/resource_cloudstack_disk_test.go new file mode 100644 index 000000000..aa17eeeb3 --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_disk_test.go @@ -0,0 +1,292 @@ +package cloudstack + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +func TestAccCloudStackDisk_basic(t *testing.T) { + var disk cloudstack.Volume + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackDiskDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCloudStackDisk_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackDiskExists( + "cloudstack_disk.foo", &disk), + testAccCheckCloudStackDiskAttributes(&disk), + ), + }, + }, + }) +} + +func TestAccCloudStackDisk_device(t *testing.T) { + var disk cloudstack.Volume + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackDiskDestroyAdvanced, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCloudStackDisk_device, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackDiskExists( + "cloudstack_disk.foo", &disk), + testAccCheckCloudStackDiskAttributes(&disk), + resource.TestCheckResourceAttr( + "cloudstack_disk.foo", "device", "/dev/xvde"), + ), + }, + }, + }) +} + +func TestAccCloudStackDisk_update(t *testing.T) { + var disk cloudstack.Volume + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackDiskDestroyAdvanced, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCloudStackDisk_update, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackDiskExists( + "cloudstack_disk.foo", &disk), + testAccCheckCloudStackDiskAttributes(&disk), + ), + }, + + resource.TestStep{ + Config: testAccCloudStackDisk_resize, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackDiskExists( + "cloudstack_disk.foo", &disk), + testAccCheckCloudStackDiskResized(&disk), + resource.TestCheckResourceAttr( + "cloudstack_disk.foo", "disk_offering", CLOUDSTACK_DISK_OFFERING_2), + ), + }, + }, + }) +} + +func testAccCheckCloudStackDiskExists( + n string, disk *cloudstack.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 disk ID is set") + } + + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + volume, _, err := cs.Volume.GetVolumeByID(rs.Primary.ID) + + if err != nil { + return err + } + + if volume.Id != rs.Primary.ID { + return fmt.Errorf("Disk not found") + } + + *disk = *volume + + return nil + } +} + +func testAccCheckCloudStackDiskAttributes( + disk *cloudstack.Volume) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if disk.Name != "terraform-disk" { + return fmt.Errorf("Bad name: %s", disk.Name) + } + + if disk.Diskofferingname != CLOUDSTACK_DISK_OFFERING_1 { + return fmt.Errorf("Bad disk offering: %s", disk.Diskofferingname) + } + + return nil + } +} + +func testAccCheckCloudStackDiskResized( + disk *cloudstack.Volume) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if disk.Diskofferingname != CLOUDSTACK_DISK_OFFERING_2 { + return fmt.Errorf("Bad disk offering: %s", disk.Diskofferingname) + } + + return nil + } +} + +func testAccCheckCloudStackDiskDestroy(s *terraform.State) error { + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "cloudstack_disk" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No disk ID is set") + } + + p := cs.Volume.NewDeleteVolumeParams(rs.Primary.ID) + err, _ := cs.Volume.DeleteVolume(p) + + if err != nil { + return fmt.Errorf( + "Error deleting disk (%s): %s", + rs.Primary.ID, err) + } + } + + return nil +} + +func testAccCheckCloudStackDiskDestroyAdvanced(s *terraform.State) error { + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "cloudstack_disk" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No disk ID is set") + } + + p := cs.Volume.NewDeleteVolumeParams(rs.Primary.ID) + err, _ := cs.Volume.DeleteVolume(p) + + if err != nil { + return fmt.Errorf( + "Error deleting disk (%s): %s", + rs.Primary.ID, err) + } + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != "cloudstack_instance" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No instance ID is set") + } + + p := cs.VirtualMachine.NewDestroyVirtualMachineParams(rs.Primary.ID) + err, _ := cs.VirtualMachine.DestroyVirtualMachine(p) + + if err != nil { + return fmt.Errorf( + "Error deleting instance (%s): %s", + rs.Primary.ID, err) + } + } + + return nil +} + +var testAccCloudStackDisk_basic = fmt.Sprintf(` +resource "cloudstack_disk" "foo" { + name = "terraform-disk" + attach = false + disk_offering = "%s" + zone = "%s" +}`, + CLOUDSTACK_DISK_OFFERING_1, + CLOUDSTACK_ZONE) + +var testAccCloudStackDisk_device = fmt.Sprintf(` +resource "cloudstack_instance" "foobar" { + name = "terraform-test" + display_name = "terraform" + service_offering= "%s" + network = "%s" + template = "%s" + zone = "%s" + expunge = true +} + +resource "cloudstack_disk" "foo" { + name = "terraform-disk" + attach = true + device = "/dev/xvde" + disk_offering = "%s" + virtual_machine = "${cloudstack_instance.foobar.name}" + zone = "${cloudstack_instance.foobar.zone}" +}`, + CLOUDSTACK_SERVICE_OFFERING_1, + CLOUDSTACK_NETWORK_1, + CLOUDSTACK_TEMPLATE, + CLOUDSTACK_ZONE, + CLOUDSTACK_DISK_OFFERING_1) + +var testAccCloudStackDisk_update = fmt.Sprintf(` +resource "cloudstack_instance" "foobar" { + name = "terraform-test" + display_name = "terraform" + service_offering= "%s" + network = "%s" + template = "%s" + zone = "%s" + expunge = true +} + +resource "cloudstack_disk" "foo" { + name = "terraform-disk" + attach = true + disk_offering = "%s" + virtual_machine = "${cloudstack_instance.foobar.name}" + zone = "${cloudstack_instance.foobar.zone}" +}`, + CLOUDSTACK_SERVICE_OFFERING_1, + CLOUDSTACK_NETWORK_1, + CLOUDSTACK_TEMPLATE, + CLOUDSTACK_ZONE, + CLOUDSTACK_DISK_OFFERING_1) + +var testAccCloudStackDisk_resize = fmt.Sprintf(` +resource "cloudstack_instance" "foobar" { + name = "terraform-test" + display_name = "terraform" + service_offering= "%s" + network = "%s" + template = "%s" + zone = "%s" + expunge = true +} + +resource "cloudstack_disk" "foo" { + name = "terraform-disk" + attach = true + disk_offering = "%s" + virtual_machine = "${cloudstack_instance.foobar.name}" + zone = "${cloudstack_instance.foobar.zone}" +}`, + CLOUDSTACK_SERVICE_OFFERING_1, + CLOUDSTACK_NETWORK_1, + CLOUDSTACK_TEMPLATE, + CLOUDSTACK_ZONE, + CLOUDSTACK_DISK_OFFERING_2) diff --git a/builtin/providers/cloudstack/resource_cloudstack_firewall.go b/builtin/providers/cloudstack/resource_cloudstack_firewall.go new file mode 100644 index 000000000..50083999f --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_firewall.go @@ -0,0 +1,442 @@ +package cloudstack + +import ( + "bytes" + "fmt" + "regexp" + "sort" + "strconv" + "strings" + + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/schema" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +func resourceCloudStackFirewall() *schema.Resource { + return &schema.Resource{ + Create: resourceCloudStackFirewallCreate, + Read: resourceCloudStackFirewallRead, + Update: resourceCloudStackFirewallUpdate, + Delete: resourceCloudStackFirewallDelete, + + Schema: map[string]*schema.Schema{ + "ipaddress": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "rule": &schema.Schema{ + Type: schema.TypeSet, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "source_cidr": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "protocol": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "icmp_type": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + + "icmp_code": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + + "ports": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: func(v interface{}) int { + return hashcode.String(v.(string)) + }, + }, + + "uuids": &schema.Schema{ + Type: schema.TypeMap, + Computed: true, + }, + }, + }, + Set: resourceCloudStackFirewallRuleHash, + }, + }, + } +} + +func resourceCloudStackFirewallCreate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Retrieve the ipaddress UUID + ipaddressid, e := retrieveUUID(cs, "ipaddress", d.Get("ipaddress").(string)) + if e != nil { + return e.Error() + } + + // We need to set this upfront in order to be able to save a partial state + d.SetId(d.Get("ipaddress").(string)) + + // Create all rules that are configured + if rs := d.Get("rule").(*schema.Set); rs.Len() > 0 { + + // Create an empty schema.Set to hold all rules + rules := &schema.Set{ + F: resourceCloudStackFirewallRuleHash, + } + + for _, rule := range rs.List() { + // Create a single rule + err := resourceCloudStackFirewallCreateRule(d, meta, ipaddressid, rule.(map[string]interface{})) + + // We need to update this first to preserve the correct state + rules.Add(rule) + d.Set("rule", rules) + + if err != nil { + return err + } + } + } + + return resourceCloudStackFirewallRead(d, meta) +} + +func resourceCloudStackFirewallCreateRule( + d *schema.ResourceData, meta interface{}, ipaddressid string, rule map[string]interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + uuids := rule["uuids"].(map[string]interface{}) + + // Make sure all required parameters are there + if err := verifyFirewallParams(d, rule); err != nil { + return err + } + + // Create a new parameter struct + p := cs.Firewall.NewCreateFirewallRuleParams(ipaddressid, rule["protocol"].(string)) + + // Set the CIDR list + p.SetCidrlist([]string{rule["source_cidr"].(string)}) + + // If the protocol is ICMP set the needed ICMP parameters + if rule["protocol"].(string) == "icmp" { + p.SetIcmptype(rule["icmp_type"].(int)) + p.SetIcmpcode(rule["icmp_code"].(int)) + + r, err := cs.Firewall.CreateFirewallRule(p) + if err != nil { + return err + } + uuids["icmp"] = r.Id + rule["uuids"] = uuids + } + + // If protocol is not ICMP, loop through all ports + if rule["protocol"].(string) != "icmp" { + if ps := rule["ports"].(*schema.Set); ps.Len() > 0 { + + // Create an empty schema.Set to hold all processed ports + ports := &schema.Set{ + F: func(v interface{}) int { + return hashcode.String(v.(string)) + }, + } + + for _, port := range ps.List() { + re := regexp.MustCompile(`^(\d+)(?:-(\d+))?$`) + m := re.FindStringSubmatch(port.(string)) + + startPort, err := strconv.Atoi(m[1]) + if err != nil { + return err + } + + endPort := startPort + if m[2] != "" { + endPort, err = strconv.Atoi(m[2]) + if err != nil { + return err + } + } + + p.SetStartport(startPort) + p.SetEndport(endPort) + + r, err := cs.Firewall.CreateFirewallRule(p) + if err != nil { + return err + } + + ports.Add(port) + rule["ports"] = ports + + uuids[port.(string)] = r.Id + rule["uuids"] = uuids + } + } + } + + return nil +} + +func resourceCloudStackFirewallRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Create an empty schema.Set to hold all rules + rules := &schema.Set{ + F: resourceCloudStackFirewallRuleHash, + } + + // Read all rules that are configured + if rs := d.Get("rule").(*schema.Set); rs.Len() > 0 { + for _, rule := range rs.List() { + rule := rule.(map[string]interface{}) + uuids := rule["uuids"].(map[string]interface{}) + + if rule["protocol"].(string) == "icmp" { + id, ok := uuids["icmp"] + if !ok { + continue + } + + // Get the rule + r, count, err := cs.Firewall.GetFirewallRuleByID(id.(string)) + // If the count == 0, there is no object found for this UUID + if err != nil { + if count == 0 { + delete(uuids, "icmp") + continue + } + + return err + } + + // Update the values + rule["source_cidr"] = r.Cidrlist + rule["protocol"] = r.Protocol + rule["icmp_type"] = r.Icmptype + rule["icmp_code"] = r.Icmpcode + rules.Add(rule) + } + + // If protocol is not ICMP, loop through all ports + if rule["protocol"].(string) != "icmp" { + if ps := rule["ports"].(*schema.Set); ps.Len() > 0 { + + // Create an empty schema.Set to hold all ports + ports := &schema.Set{ + F: func(v interface{}) int { + return hashcode.String(v.(string)) + }, + } + + // Loop through all ports and retrieve their info + for _, port := range ps.List() { + id, ok := uuids[port.(string)] + if !ok { + continue + } + + // Get the rule + r, count, err := cs.Firewall.GetFirewallRuleByID(id.(string)) + if err != nil { + if count == 0 { + delete(uuids, port.(string)) + continue + } + + return err + } + + // Update the values + rule["source_cidr"] = r.Cidrlist + rule["protocol"] = r.Protocol + ports.Add(port) + } + + // If there is at least one port found, add this rule to the rules set + if ports.Len() > 0 { + rule["ports"] = ports + rules.Add(rule) + } + } + } + } + } + + if rules.Len() > 0 { + d.Set("rule", rules) + } else { + d.SetId("") + } + + return nil +} + +func resourceCloudStackFirewallUpdate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Retrieve the ipaddress UUID + ipaddressid, e := retrieveUUID(cs, "ipaddress", d.Get("ipaddress").(string)) + if e != nil { + return e.Error() + } + + // Check if the rule set as a whole has changed + if d.HasChange("rule") { + o, n := d.GetChange("rule") + ors := o.(*schema.Set).Difference(n.(*schema.Set)) + nrs := n.(*schema.Set).Difference(o.(*schema.Set)) + + // Now first loop through all the old rules and delete any obsolete ones + for _, rule := range ors.List() { + // Delete the rule as it no longer exists in the config + err := resourceCloudStackFirewallDeleteRule(d, meta, rule.(map[string]interface{})) + if err != nil { + return err + } + } + + // Make sure we save the state of the currently configured rules + rules := o.(*schema.Set).Intersection(n.(*schema.Set)) + d.Set("rule", rules) + + // Then loop through al the currently configured rules and create the new ones + for _, rule := range nrs.List() { + // When succesfully deleted, re-create it again if it still exists + err := resourceCloudStackFirewallCreateRule( + d, meta, ipaddressid, rule.(map[string]interface{})) + + // We need to update this first to preserve the correct state + rules.Add(rule) + d.Set("rule", rules) + + if err != nil { + return err + } + } + } + + return resourceCloudStackFirewallRead(d, meta) +} + +func resourceCloudStackFirewallDelete(d *schema.ResourceData, meta interface{}) error { + // Delete all rules + if rs := d.Get("rule").(*schema.Set); rs.Len() > 0 { + for _, rule := range rs.List() { + // Delete a single rule + err := resourceCloudStackFirewallDeleteRule(d, meta, rule.(map[string]interface{})) + + // We need to update this first to preserve the correct state + d.Set("rule", rs) + + if err != nil { + return err + } + } + } + + return nil +} + +func resourceCloudStackFirewallDeleteRule( + d *schema.ResourceData, meta interface{}, rule map[string]interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + uuids := rule["uuids"].(map[string]interface{}) + + for k, id := range uuids { + // Create the parameter struct + p := cs.Firewall.NewDeleteFirewallRuleParams(id.(string)) + + // Delete the rule + if _, err := cs.Firewall.DeleteFirewallRule(p); err != nil { + + // This is a very poor way to be told the UUID does no longer exist :( + if strings.Contains(err.Error(), fmt.Sprintf( + "Invalid parameter id value=%s due to incorrect long value format, "+ + "or entity does not exist", id.(string))) { + delete(uuids, k) + continue + } + + return err + } + + // Delete the UUID of this rule + delete(uuids, k) + } + + // Update the UUIDs + rule["uuids"] = uuids + + return nil +} + +func resourceCloudStackFirewallRuleHash(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf( + "%s-%s-", m["source_cidr"].(string), m["protocol"].(string))) + + if v, ok := m["icmp_type"]; ok { + buf.WriteString(fmt.Sprintf("%d-", v.(int))) + } + + if v, ok := m["icmp_code"]; ok { + buf.WriteString(fmt.Sprintf("%d-", v.(int))) + } + + // We need to make sure to sort the strings below so that we always + // generate the same hash code no matter what is in the set. + if v, ok := m["ports"]; ok { + vs := v.(*schema.Set).List() + s := make([]string, len(vs)) + + for i, raw := range vs { + s[i] = raw.(string) + } + sort.Strings(s) + + for _, v := range s { + buf.WriteString(fmt.Sprintf("%s-", v)) + } + } + + return hashcode.String(buf.String()) +} + +func verifyFirewallParams(d *schema.ResourceData, rule map[string]interface{}) error { + protocol := rule["protocol"].(string) + if protocol != "tcp" && protocol != "udp" && protocol != "icmp" { + return fmt.Errorf( + "%s is not a valid protocol. Valid options are 'tcp', 'udp' and 'icmp'", protocol) + } + + if protocol == "icmp" { + if _, ok := rule["icmp_type"]; !ok { + return fmt.Errorf( + "Parameter icmp_type is a required parameter when using protocol 'icmp'") + } + if _, ok := rule["icmp_code"]; !ok { + return fmt.Errorf( + "Parameter icmp_code is a required parameter when using protocol 'icmp'") + } + } else { + if _, ok := rule["ports"]; !ok { + return fmt.Errorf( + "Parameter port is a required parameter when using protocol 'tcp' or 'udp'") + } + } + + return nil +} diff --git a/builtin/providers/cloudstack/resource_cloudstack_firewall_test.go b/builtin/providers/cloudstack/resource_cloudstack_firewall_test.go new file mode 100644 index 000000000..865f49507 --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_firewall_test.go @@ -0,0 +1,191 @@ +package cloudstack + +import ( + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +func TestAccCloudStackFirewall_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackFirewallDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCloudStackFirewall_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackFirewallRulesExist("cloudstack_firewall.foo"), + resource.TestCheckResourceAttr( + "cloudstack_firewall.foo", "ipaddress", CLOUDSTACK_PUBLIC_IPADDRESS), + resource.TestCheckResourceAttr( + "cloudstack_firewall.foo", "rule.0.source_cidr", "10.0.0.0/24"), + resource.TestCheckResourceAttr( + "cloudstack_firewall.foo", "rule.0.protocol", "tcp"), + resource.TestCheckResourceAttr( + "cloudstack_firewall.foo", "rule.0.ports.#", "2"), + resource.TestCheckResourceAttr( + "cloudstack_firewall.foo", "rule.0.ports.0", "1000-2000"), + resource.TestCheckResourceAttr( + "cloudstack_firewall.foo", "rule.0.ports.1", "80"), + ), + }, + }, + }) +} + +/* +func TestAccCloudStackFirewall_update(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackFirewallDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCloudStackFirewall_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackFirewallRulesExist("cloudstack_firewall.foo"), + resource.TestCheckResourceAttr( + "cloudstack_firewall.foo", "ipaddress", CLOUDSTACK_PUBLIC_IPADDRESS), + resource.TestCheckResourceAttr( + "cloudstack_firewall.foo", "rule.#", "1"), + resource.TestCheckResourceAttr( + "cloudstack_firewall.foo", "rule.0.source_cidr", "10.0.0.0/24"), + resource.TestCheckResourceAttr( + "cloudstack_firewall.foo", "rule.0.protocol", "tcp"), + resource.TestCheckResourceAttr( + "cloudstack_firewall.foo", "rule.0.ports.#", "2"), + resource.TestCheckResourceAttr( + "cloudstack_firewall.foo", "rule.0.ports.0", "1000-2000"), + resource.TestCheckResourceAttr( + "cloudstack_firewall.foo", "rule.0.ports.1", "80"), + ), + }, + + resource.TestStep{ + Config: testAccCloudStackFirewall_update, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackFirewallRulesExist("cloudstack_firewall.foo"), + resource.TestCheckResourceAttr( + "cloudstack_firewall.foo", "ipaddress", CLOUDSTACK_PUBLIC_IPADDRESS), + resource.TestCheckResourceAttr( + "cloudstack_firewall.foo", "rule.#", "2"), + resource.TestCheckResourceAttr( + "cloudstack_firewall.foo", "rule.0.source_cidr", "10.0.0.0/24"), + resource.TestCheckResourceAttr( + "cloudstack_firewall.foo", "rule.0.protocol", "tcp"), + resource.TestCheckResourceAttr( + "cloudstack_firewall.foo", "rule.0.ports.#", "2"), + resource.TestCheckResourceAttr( + "cloudstack_firewall.foo", "rule.0.ports.0", "1000-2000"), + resource.TestCheckResourceAttr( + "cloudstack_firewall.foo", "rule.0.ports.1", "80"), + resource.TestCheckResourceAttr( + "cloudstack_firewall.foo", "rule.1.source_cidr", "172.16.100.0/24"), + resource.TestCheckResourceAttr( + "cloudstack_firewall.foo", "rule.1.protocol", "tcp"), + resource.TestCheckResourceAttr( + "cloudstack_firewall.foo", "rule.1.ports.#", "2"), + resource.TestCheckResourceAttr( + "cloudstack_firewall.foo", "rule.1.ports.0", "80"), + resource.TestCheckResourceAttr( + "cloudstack_firewall.foo", "rule.1.ports.1", "443"), + ), + }, + }, + }) +} +*/ + +func testAccCheckCloudStackFirewallRulesExist(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 firewall ID is set") + } + + for k, uuid := range rs.Primary.Attributes { + if !strings.Contains(k, "uuids") { + continue + } + + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + _, count, err := cs.Firewall.GetFirewallRuleByID(uuid) + + if err != nil { + return err + } + + if count == 0 { + return fmt.Errorf("Firewall rule for %s not found", k) + } + } + + return nil + } +} + +func testAccCheckCloudStackFirewallDestroy(s *terraform.State) error { + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "cloudstack_firewall" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No instance ID is set") + } + + for k, uuid := range rs.Primary.Attributes { + if !strings.Contains(k, "uuids") { + continue + } + + p := cs.Firewall.NewDeleteFirewallRuleParams(uuid) + _, err := cs.Firewall.DeleteFirewallRule(p) + + if err != nil { + return err + } + } + } + + return nil +} + +var testAccCloudStackFirewall_basic = fmt.Sprintf(` +resource "cloudstack_firewall" "foo" { + ipaddress = "%s" + + rule { + source_cidr = "10.0.0.0/24" + protocol = "tcp" + ports = ["80", "1000-2000"] + } +}`, CLOUDSTACK_PUBLIC_IPADDRESS) + +var testAccCloudStackFirewall_update = fmt.Sprintf(` +resource "cloudstack_firewall" "foo" { + ipaddress = "%s" + + rule { + source_cidr = "10.0.0.0/24" + protocol = "tcp" + ports = ["80", "1000-2000"] + } + + rule { + source_cidr = "172.16.100.0/24" + protocol = "tcp" + ports = ["80", "443"] + } +}`, CLOUDSTACK_PUBLIC_IPADDRESS) diff --git a/builtin/providers/cloudstack/resource_cloudstack_instance.go b/builtin/providers/cloudstack/resource_cloudstack_instance.go new file mode 100644 index 000000000..600001a27 --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_instance.go @@ -0,0 +1,278 @@ +package cloudstack + +import ( + "crypto/sha1" + "encoding/base64" + "encoding/hex" + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +func resourceCloudStackInstance() *schema.Resource { + return &schema.Resource{ + Create: resourceCloudStackInstanceCreate, + Read: resourceCloudStackInstanceRead, + Update: resourceCloudStackInstanceUpdate, + Delete: resourceCloudStackInstanceDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "display_name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "service_offering": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "network": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "ipaddress": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "template": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "zone": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "user_data": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + StateFunc: func(v interface{}) string { + switch v.(type) { + case string: + hash := sha1.Sum([]byte(v.(string))) + return hex.EncodeToString(hash[:]) + default: + return "" + } + }, + }, + + "expunge": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + }, + } +} + +func resourceCloudStackInstanceCreate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Retrieve the service_offering UUID + serviceofferingid, e := retrieveUUID(cs, "service_offering", d.Get("service_offering").(string)) + if e != nil { + return e.Error() + } + + // Retrieve the template UUID + templateid, e := retrieveUUID(cs, "template", d.Get("template").(string)) + if e != nil { + return e.Error() + } + + // Retrieve the zone object + zone, _, err := cs.Zone.GetZoneByName(d.Get("zone").(string)) + if err != nil { + return err + } + + // Create a new parameter struct + p := cs.VirtualMachine.NewDeployVirtualMachineParams(serviceofferingid, templateid, zone.Id) + + // Set the name + name := d.Get("name").(string) + p.SetName(name) + + // Set the display name + if displayname, ok := d.GetOk("display_name"); ok { + p.SetDisplayname(displayname.(string)) + } else { + p.SetDisplayname(name) + } + + if zone.Networktype == "Advanced" { + // Retrieve the network UUID + networkid, e := retrieveUUID(cs, "network", d.Get("network").(string)) + if e != nil { + return e.Error() + } + // Set the default network ID + p.SetNetworkids([]string{networkid}) + } + + // If there is a ipaddres supplied, add it to the parameter struct + if ipaddres, ok := d.GetOk("ipaddress"); ok { + p.SetIpaddress(ipaddres.(string)) + } + + // If the user data contains any info, it needs to be base64 encoded and + // added to the parameter struct + if userData, ok := d.GetOk("user_data"); ok { + ud := base64.StdEncoding.EncodeToString([]byte(userData.(string))) + if len(ud) > 2048 { + return fmt.Errorf( + "The supplied user_data contains %d bytes after encoding, "+ + "this exeeds the limit of 2048 bytes", len(ud)) + } + p.SetUserdata(ud) + } + + // Create the new instance + r, err := cs.VirtualMachine.DeployVirtualMachine(p) + if err != nil { + return fmt.Errorf("Error creating the new instance %s: %s", name, err) + } + + d.SetId(r.Id) + + return resourceCloudStackInstanceRead(d, meta) +} + +func resourceCloudStackInstanceRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Get the virtual machine details + vm, count, err := cs.VirtualMachine.GetVirtualMachineByID(d.Id()) + if err != nil { + if count == 0 { + log.Printf("[DEBUG] Instance %s does no longer exist", d.Get("name").(string)) + // Clear out all details so it's obvious the instance is gone + d.SetId("") + return nil + } + + return err + } + + // Update the config + d.Set("name", vm.Name) + d.Set("display_name", vm.Displayname) + d.Set("service_offering", vm.Serviceofferingname) + d.Set("network", vm.Nic[0].Networkname) + d.Set("ipaddress", vm.Nic[0].Ipaddress) + d.Set("template", vm.Templatename) + d.Set("zone", vm.Zonename) + + return nil +} + +func resourceCloudStackInstanceUpdate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + d.Partial(true) + + name := d.Get("name").(string) + + // Check if the display name is changed and if so, update the virtual machine + if d.HasChange("display_name") { + log.Printf("[DEBUG] Display name changed for %s, starting update", name) + + // Create a new parameter struct + p := cs.VirtualMachine.NewUpdateVirtualMachineParams(d.Id()) + + // Set the new display name + p.SetDisplayname(d.Get("display_name").(string)) + + // Update the display name + _, err := cs.VirtualMachine.UpdateVirtualMachine(p) + if err != nil { + return fmt.Errorf( + "Error updating the display name for instance %s: %s", name, err) + } + + d.SetPartial("display_name") + } + + // Check if the service offering is changed and if so, update the offering + if d.HasChange("service_offering") { + log.Printf("[DEBUG] Service offering changed for %s, starting update", name) + + // Retrieve the service_offering UUID + serviceofferingid, e := retrieveUUID(cs, "service_offering", d.Get("service_offering").(string)) + if e != nil { + return e.Error() + } + + // Create a new parameter struct + p := cs.VirtualMachine.NewChangeServiceForVirtualMachineParams(d.Id(), serviceofferingid) + + // Before we can actually change the service offering, the virtual machine must be stopped + _, err := cs.VirtualMachine.StopVirtualMachine(cs.VirtualMachine.NewStopVirtualMachineParams(d.Id())) + if err != nil { + return fmt.Errorf( + "Error stopping instance %s before changing service offering: %s", name, err) + } + // Change the service offering + _, err = cs.VirtualMachine.ChangeServiceForVirtualMachine(p) + if err != nil { + return fmt.Errorf( + "Error changing the service offering for instance %s: %s", name, err) + } + // Start the virtual machine again + _, err = cs.VirtualMachine.StartVirtualMachine(cs.VirtualMachine.NewStartVirtualMachineParams(d.Id())) + if err != nil { + return fmt.Errorf( + "Error starting instance %s after changing service offering: %s", name, err) + } + + d.SetPartial("service_offering") + } + + d.Partial(false) + return resourceCloudStackInstanceRead(d, meta) +} + +func resourceCloudStackInstanceDelete(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Create a new parameter struct + p := cs.VirtualMachine.NewDestroyVirtualMachineParams(d.Id()) + + if d.Get("expunge").(bool) { + p.SetExpunge(true) + } + + log.Printf("[INFO] Destroying instance: %s", d.Get("name").(string)) + if _, err := cs.VirtualMachine.DestroyVirtualMachine(p); err != nil { + // This is a very poor way to be told the UUID does no longer exist :( + if strings.Contains(err.Error(), fmt.Sprintf( + "Invalid parameter id value=%s due to incorrect long value format, "+ + "or entity does not exist", d.Id())) { + return nil + } + + return fmt.Errorf("Error destroying instance: %s", err) + } + + return nil +} diff --git a/builtin/providers/cloudstack/resource_cloudstack_instance_test.go b/builtin/providers/cloudstack/resource_cloudstack_instance_test.go new file mode 100644 index 000000000..47a47253c --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_instance_test.go @@ -0,0 +1,235 @@ +package cloudstack + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +func TestAccCloudStackInstance_basic(t *testing.T) { + var instance cloudstack.VirtualMachine + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackInstanceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCloudStackInstance_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackInstanceExists( + "cloudstack_instance.foobar", &instance), + testAccCheckCloudStackInstanceAttributes(&instance), + resource.TestCheckResourceAttr( + "cloudstack_instance.foobar", + "user_data", + "0cf3dcdc356ec8369494cb3991985ecd5296cdd5"), + ), + }, + }, + }) +} + +func TestAccCloudStackInstance_update(t *testing.T) { + var instance cloudstack.VirtualMachine + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackInstanceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCloudStackInstance_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackInstanceExists( + "cloudstack_instance.foobar", &instance), + testAccCheckCloudStackInstanceAttributes(&instance), + ), + }, + + resource.TestStep{ + Config: testAccCloudStackInstance_renameAndResize, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackInstanceExists( + "cloudstack_instance.foobar", &instance), + testAccCheckCloudStackInstanceRenamedAndResized(&instance), + resource.TestCheckResourceAttr( + "cloudstack_instance.foobar", "display_name", "terraform-updated"), + resource.TestCheckResourceAttr( + "cloudstack_instance.foobar", "service_offering", CLOUDSTACK_SERVICE_OFFERING_2), + ), + }, + }, + }) +} + +func TestAccCloudStackInstance_fixedIP(t *testing.T) { + var instance cloudstack.VirtualMachine + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackInstanceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCloudStackInstance_fixedIP, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackInstanceExists( + "cloudstack_instance.foobar", &instance), + resource.TestCheckResourceAttr( + "cloudstack_instance.foobar", "ipaddress", CLOUDSTACK_NETWORK_1_IPADDRESS), + ), + }, + }, + }) +} + +func testAccCheckCloudStackInstanceExists( + n string, instance *cloudstack.VirtualMachine) 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 instance ID is set") + } + + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + vm, _, err := cs.VirtualMachine.GetVirtualMachineByID(rs.Primary.ID) + + if err != nil { + return err + } + + if vm.Id != rs.Primary.ID { + return fmt.Errorf("Instance not found") + } + + *instance = *vm + + return nil + } +} + +func testAccCheckCloudStackInstanceAttributes( + instance *cloudstack.VirtualMachine) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if instance.Name != "terraform-test" { + return fmt.Errorf("Bad name: %s", instance.Name) + } + + if instance.Displayname != "terraform" { + return fmt.Errorf("Bad display name: %s", instance.Displayname) + } + + if instance.Serviceofferingname != CLOUDSTACK_SERVICE_OFFERING_1 { + return fmt.Errorf("Bad service offering: %s", instance.Serviceofferingname) + } + + if instance.Templatename != CLOUDSTACK_TEMPLATE { + return fmt.Errorf("Bad template: %s", instance.Templatename) + } + + if instance.Nic[0].Networkname != CLOUDSTACK_NETWORK_1 { + return fmt.Errorf("Bad network: %s", instance.Nic[0].Networkname) + } + + return nil + } +} + +func testAccCheckCloudStackInstanceRenamedAndResized( + instance *cloudstack.VirtualMachine) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if instance.Displayname != "terraform-updated" { + return fmt.Errorf("Bad display name: %s", instance.Displayname) + } + + if instance.Serviceofferingname != CLOUDSTACK_SERVICE_OFFERING_2 { + return fmt.Errorf("Bad service offering: %s", instance.Serviceofferingname) + } + + return nil + } +} + +func testAccCheckCloudStackInstanceDestroy(s *terraform.State) error { + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "cloudstack_instance" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No instance ID is set") + } + + p := cs.VirtualMachine.NewDestroyVirtualMachineParams(rs.Primary.ID) + err, _ := cs.VirtualMachine.DestroyVirtualMachine(p) + + if err != nil { + return fmt.Errorf( + "Error deleting instance (%s): %s", + rs.Primary.ID, err) + } + } + + return nil +} + +var testAccCloudStackInstance_basic = fmt.Sprintf(` +resource "cloudstack_instance" "foobar" { + name = "terraform-test" + display_name = "terraform" + service_offering= "%s" + network = "%s" + template = "%s" + zone = "%s" + user_data = "foobar\nfoo\nbar" + expunge = true +}`, + CLOUDSTACK_SERVICE_OFFERING_1, + CLOUDSTACK_NETWORK_1, + CLOUDSTACK_TEMPLATE, + CLOUDSTACK_ZONE) + +var testAccCloudStackInstance_renameAndResize = fmt.Sprintf(` +resource "cloudstack_instance" "foobar" { + name = "terraform-test" + display_name = "terraform-updated" + service_offering= "%s" + network = "%s" + template = "%s" + zone = "%s" + user_data = "foobar\nfoo\nbar" + expunge = true +}`, + CLOUDSTACK_SERVICE_OFFERING_2, + CLOUDSTACK_NETWORK_1, + CLOUDSTACK_TEMPLATE, + CLOUDSTACK_ZONE) + +var testAccCloudStackInstance_fixedIP = fmt.Sprintf(` +resource "cloudstack_instance" "foobar" { + name = "terraform-test" + display_name = "terraform" + service_offering= "%s" + network = "%s" + ipaddress = "%s" + template = "%s" + zone = "%s" + expunge = true +}`, + CLOUDSTACK_SERVICE_OFFERING_1, + CLOUDSTACK_NETWORK_1, + CLOUDSTACK_NETWORK_1_IPADDRESS, + CLOUDSTACK_TEMPLATE, + CLOUDSTACK_ZONE) diff --git a/builtin/providers/cloudstack/resource_cloudstack_ipaddress.go b/builtin/providers/cloudstack/resource_cloudstack_ipaddress.go new file mode 100644 index 000000000..ac6ed58a7 --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_ipaddress.go @@ -0,0 +1,154 @@ +package cloudstack + +import ( + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +func resourceCloudStackIPAddress() *schema.Resource { + return &schema.Resource{ + Create: resourceCloudStackIPAddressCreate, + Read: resourceCloudStackIPAddressRead, + Delete: resourceCloudStackIPAddressDelete, + + Schema: map[string]*schema.Schema{ + "network": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "vpc": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "ipaddress": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceCloudStackIPAddressCreate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + if err := verifyIPAddressParams(d); err != nil { + return err + } + + // Create a new parameter struct + p := cs.Address.NewAssociateIpAddressParams() + + if network, ok := d.GetOk("network"); ok { + // Retrieve the network UUID + networkid, e := retrieveUUID(cs, "network", network.(string)) + if e != nil { + return e.Error() + } + + // Set the networkid + p.SetNetworkid(networkid) + } + + if vpc, ok := d.GetOk("vpc"); ok { + // Retrieve the vpc UUID + vpcid, e := retrieveUUID(cs, "vpc", vpc.(string)) + if e != nil { + return e.Error() + } + + // Set the vpcid + p.SetVpcid(vpcid) + } + + // Associate a new IP address + r, err := cs.Address.AssociateIpAddress(p) + if err != nil { + return fmt.Errorf("Error associating a new IP address: %s", err) + } + + d.SetId(r.Id) + + return resourceCloudStackIPAddressRead(d, meta) +} + +func resourceCloudStackIPAddressRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Get the network ACL list details + f, count, err := cs.Address.GetPublicIpAddressByID(d.Id()) + if err != nil { + if count == 0 { + log.Printf( + "[DEBUG] IP address with ID %s is no longer associated", d.Id()) + d.SetId("") + return nil + } + + return err + } + + // Updated the IP address + d.Set("ipaddress", f.Ipaddress) + + if _, ok := d.GetOk("network"); ok { + // Get the network details + n, _, err := cs.Network.GetNetworkByID(f.Associatednetworkid) + if err != nil { + return err + } + + d.Set("network", n.Name) + } + + if _, ok := d.GetOk("vpc"); ok { + // Get the VPC details + v, _, err := cs.VPC.GetVPCByID(f.Vpcid) + if err != nil { + return err + } + + d.Set("vpc", v.Name) + } + + return nil +} + +func resourceCloudStackIPAddressDelete(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Create a new parameter struct + p := cs.Address.NewDisassociateIpAddressParams(d.Id()) + + // Disassociate the IP address + if _, err := cs.Address.DisassociateIpAddress(p); err != nil { + // This is a very poor way to be told the UUID does no longer exist :( + if strings.Contains(err.Error(), fmt.Sprintf( + "Invalid parameter id value=%s due to incorrect long value format, "+ + "or entity does not exist", d.Id())) { + return nil + } + + return fmt.Errorf("Error deleting network ACL list %s: %s", d.Get("name").(string), err) + } + + return nil +} + +func verifyIPAddressParams(d *schema.ResourceData) error { + _, network := d.GetOk("network") + _, vpc := d.GetOk("vpc") + + if (network && vpc) || (!network && !vpc) { + return fmt.Errorf("You must supply a value for either (so not both) the 'network' or 'vpc' argument") + } + + return nil +} diff --git a/builtin/providers/cloudstack/resource_cloudstack_ipaddress_test.go b/builtin/providers/cloudstack/resource_cloudstack_ipaddress_test.go new file mode 100644 index 000000000..88fdaba40 --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_ipaddress_test.go @@ -0,0 +1,137 @@ +package cloudstack + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +func TestAccCloudStackIPAddress_basic(t *testing.T) { + var ipaddr cloudstack.PublicIpAddress + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackIPAddressDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCloudStackIPAddress_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackIPAddressExists( + "cloudstack_ipaddress.foo", &ipaddr), + testAccCheckCloudStackIPAddressAttributes(&ipaddr), + ), + }, + }, + }) +} + +func TestAccCloudStackIPAddress_vpc(t *testing.T) { + var ipaddr cloudstack.PublicIpAddress + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackIPAddressDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCloudStackIPAddress_vpc, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackIPAddressExists( + "cloudstack_ipaddress.foo", &ipaddr), + resource.TestCheckResourceAttr( + "cloudstack_ipaddress.foo", "vpc", "terraform-vpc"), + ), + }, + }, + }) +} + +func testAccCheckCloudStackIPAddressExists( + n string, ipaddr *cloudstack.PublicIpAddress) 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 IP address ID is set") + } + + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + pip, _, err := cs.Address.GetPublicIpAddressByID(rs.Primary.ID) + + if err != nil { + return err + } + + if pip.Id != rs.Primary.ID { + return fmt.Errorf("IP address not found") + } + + *ipaddr = *pip + + return nil + } +} + +func testAccCheckCloudStackIPAddressAttributes( + ipaddr *cloudstack.PublicIpAddress) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if ipaddr.Associatednetworkname != CLOUDSTACK_NETWORK_1 { + return fmt.Errorf("Bad network: %s", ipaddr.Associatednetworkname) + } + + return nil + } +} + +func testAccCheckCloudStackIPAddressDestroy(s *terraform.State) error { + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "cloudstack_ipaddress" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No IP address ID is set") + } + + p := cs.Address.NewDisassociateIpAddressParams(rs.Primary.ID) + err, _ := cs.Address.DisassociateIpAddress(p) + + if err != nil { + return fmt.Errorf( + "Error disassociating IP address (%s): %s", + rs.Primary.ID, err) + } + } + + return nil +} + +var testAccCloudStackIPAddress_basic = fmt.Sprintf(` +resource "cloudstack_ipaddress" "foo" { + network = "%s" +}`, CLOUDSTACK_NETWORK_1) + +var testAccCloudStackIPAddress_vpc = fmt.Sprintf(` +resource "cloudstack_vpc" "foobar" { + name = "terraform-vpc" + cidr = "%s" + vpc_offering = "%s" + zone = "%s" +} + +resource "cloudstack_ipaddress" "foo" { + vpc = "${cloudstack_vpc.foobar.name}" +}`, + CLOUDSTACK_VPC_CIDR, + CLOUDSTACK_VPC_OFFERING, + CLOUDSTACK_ZONE) diff --git a/builtin/providers/cloudstack/resource_cloudstack_network.go b/builtin/providers/cloudstack/resource_cloudstack_network.go new file mode 100644 index 000000000..161378fe8 --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_network.go @@ -0,0 +1,241 @@ +package cloudstack + +import ( + "fmt" + "log" + "net" + "strings" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +func resourceCloudStackNetwork() *schema.Resource { + return &schema.Resource{ + Create: resourceCloudStackNetworkCreate, + Read: resourceCloudStackNetworkRead, + Update: resourceCloudStackNetworkUpdate, + Delete: resourceCloudStackNetworkDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "display_text": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "cidr": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "network_offering": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "vpc": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "aclid": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "zone": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceCloudStackNetworkCreate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + name := d.Get("name").(string) + + // Retrieve the network_offering UUID + networkofferingid, e := retrieveUUID(cs, "network_offering", d.Get("network_offering").(string)) + if e != nil { + return e.Error() + } + + // Retrieve the zone UUID + zoneid, e := retrieveUUID(cs, "zone", d.Get("zone").(string)) + if e != nil { + return e.Error() + } + + // Compute/set the display text + displaytext := d.Get("display_text").(string) + if displaytext == "" { + displaytext = name + } + + // Create a new parameter struct + p := cs.Network.NewCreateNetworkParams(displaytext, name, networkofferingid, zoneid) + + // Get the network details from the CIDR + m, err := parseCIDR(d.Get("cidr").(string)) + if err != nil { + return err + } + + // Set the needed IP config + p.SetStartip(m["start"]) + p.SetGateway(m["gateway"]) + p.SetEndip(m["end"]) + p.SetNetmask(m["netmask"]) + + // Check is this network needs to be created in a VPC + vpc := d.Get("vpc").(string) + if vpc != "" { + // Retrieve the vpc UUID + vpcid, e := retrieveUUID(cs, "vpc", vpc) + if e != nil { + return e.Error() + } + + // Set the vpc UUID + p.SetVpcid(vpcid) + + // Since we're in a VPC, check if we want to assiciate an ACL list + aclid := d.Get("aclid").(string) + if aclid != "" { + // Set the acl UUID + p.SetAclid(aclid) + } + } + + // Create the new network + r, err := cs.Network.CreateNetwork(p) + if err != nil { + return fmt.Errorf("Error creating network %s: %s", name, err) + } + + d.SetId(r.Id) + + return resourceCloudStackNetworkRead(d, meta) +} + +func resourceCloudStackNetworkRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Get the virtual machine details + n, count, err := cs.Network.GetNetworkByID(d.Id()) + if err != nil { + if count == 0 { + log.Printf( + "[DEBUG] Network %s does no longer exist", d.Get("name").(string)) + d.SetId("") + return nil + } + + return err + } + + d.Set("name", n.Name) + d.Set("display_test", n.Displaytext) + d.Set("cidr", n.Cidr) + d.Set("network_offering", n.Networkofferingname) + d.Set("zone", n.Zonename) + + return nil +} + +func resourceCloudStackNetworkUpdate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + name := d.Get("name").(string) + + // Create a new parameter struct + p := cs.Network.NewUpdateNetworkParams(d.Id()) + + // Check if the name or display text is changed + if d.HasChange("name") || d.HasChange("display_text") { + p.SetName(name) + + // Compute/set the display text + displaytext := d.Get("display_text").(string) + if displaytext == "" { + displaytext = name + } + } + + // Check if the cidr is changed + if d.HasChange("cidr") { + p.SetGuestvmcidr(d.Get("cidr").(string)) + } + + // Check if the network offering is changed + if d.HasChange("network_offering") { + // Retrieve the network_offering UUID + networkofferingid, e := retrieveUUID(cs, "network_offering", d.Get("network_offering").(string)) + if e != nil { + return e.Error() + } + // Set the new network offering + p.SetNetworkofferingid(networkofferingid) + } + + // Update the network + _, err := cs.Network.UpdateNetwork(p) + if err != nil { + return fmt.Errorf( + "Error updating network %s: %s", name, err) + } + + return resourceCloudStackNetworkRead(d, meta) +} + +func resourceCloudStackNetworkDelete(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Create a new parameter struct + p := cs.Network.NewDeleteNetworkParams(d.Id()) + + // Delete the network + _, err := cs.Network.DeleteNetwork(p) + if err != nil { + // This is a very poor way to be told the UUID does no longer exist :( + if strings.Contains(err.Error(), fmt.Sprintf( + "Invalid parameter id value=%s due to incorrect long value format, "+ + "or entity does not exist", d.Id())) { + return nil + } + + return fmt.Errorf("Error deleting network %s: %s", d.Get("name").(string), err) + } + return nil +} + +func parseCIDR(cidr string) (map[string]string, error) { + m := make(map[string]string, 4) + + ip, ipnet, err := net.ParseCIDR(cidr) + if err != nil { + return nil, fmt.Errorf("Unable to parse cidr %s: %s", cidr, err) + } + + msk := ipnet.Mask + sub := ip.Mask(msk) + + m["netmask"] = fmt.Sprintf("%d.%d.%d.%d", msk[0], msk[1], msk[2], msk[3]) + m["gateway"] = fmt.Sprintf("%d.%d.%d.%d", sub[0], sub[1], sub[2], sub[3]+1) + m["start"] = fmt.Sprintf("%d.%d.%d.%d", sub[0], sub[1], sub[2], sub[3]+2) + m["end"] = fmt.Sprintf("%d.%d.%d.%d", + sub[0]+(0xff-msk[0]), sub[1]+(0xff-msk[1]), sub[2]+(0xff-msk[2]), sub[3]+(0xff-msk[3]-1)) + + return m, nil +} diff --git a/builtin/providers/cloudstack/resource_cloudstack_network_acl.go b/builtin/providers/cloudstack/resource_cloudstack_network_acl.go new file mode 100644 index 000000000..3aea0d17e --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_network_acl.go @@ -0,0 +1,123 @@ +package cloudstack + +import ( + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +func resourceCloudStackNetworkACL() *schema.Resource { + return &schema.Resource{ + Create: resourceCloudStackNetworkACLCreate, + Read: resourceCloudStackNetworkACLRead, + Delete: resourceCloudStackNetworkACLDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "vpc": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceCloudStackNetworkACLCreate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + name := d.Get("name").(string) + + // Retrieve the vpc UUID + vpcid, e := retrieveUUID(cs, "vpc", d.Get("vpc").(string)) + if e != nil { + return e.Error() + } + + // Create a new parameter struct + p := cs.NetworkACL.NewCreateNetworkACLListParams(name, vpcid) + + // Set the description + if description, ok := d.GetOk("description"); ok { + p.SetDescription(description.(string)) + } else { + p.SetDescription(name) + } + + // Create the new network ACL list + r, err := cs.NetworkACL.CreateNetworkACLList(p) + if err != nil { + return fmt.Errorf("Error creating network ACL list %s: %s", name, err) + } + + d.SetId(r.Id) + + return resourceCloudStackNetworkACLRead(d, meta) +} + +func resourceCloudStackNetworkACLRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Get the network ACL list details + f, count, err := cs.NetworkACL.GetNetworkACLListByID(d.Id()) + if err != nil { + if count == 0 { + log.Printf( + "[DEBUG] Network ACL list %s does no longer exist", d.Get("name").(string)) + d.SetId("") + return nil + } + + return err + } + + d.Set("name", f.Name) + d.Set("description", f.Description) + + // Get the VPC details + v, _, err := cs.VPC.GetVPCByID(f.Vpcid) + if err != nil { + return err + } + + d.Set("vpc", v.Name) + + return nil +} + +func resourceCloudStackNetworkACLDelete(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Create a new parameter struct + p := cs.NetworkACL.NewDeleteNetworkACLListParams(d.Id()) + + // Delete the network ACL list + _, err := cs.NetworkACL.DeleteNetworkACLList(p) + if err != nil { + // This is a very poor way to be told the UUID does no longer exist :( + if strings.Contains(err.Error(), fmt.Sprintf( + "Invalid parameter id value=%s due to incorrect long value format, "+ + "or entity does not exist", d.Id())) { + return nil + } + + return fmt.Errorf("Error deleting network ACL list %s: %s", d.Get("name").(string), err) + } + + return nil +} diff --git a/builtin/providers/cloudstack/resource_cloudstack_network_acl_rule.go b/builtin/providers/cloudstack/resource_cloudstack_network_acl_rule.go new file mode 100644 index 000000000..95c91682a --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_network_acl_rule.go @@ -0,0 +1,476 @@ +package cloudstack + +import ( + "bytes" + "fmt" + "regexp" + "sort" + "strconv" + "strings" + + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/schema" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +func resourceCloudStackNetworkACLRule() *schema.Resource { + return &schema.Resource{ + Create: resourceCloudStackNetworkACLRuleCreate, + Read: resourceCloudStackNetworkACLRuleRead, + Update: resourceCloudStackNetworkACLRuleUpdate, + Delete: resourceCloudStackNetworkACLRuleDelete, + + Schema: map[string]*schema.Schema{ + "aclid": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "rule": &schema.Schema{ + Type: schema.TypeSet, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "action": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "allow", + }, + + "source_cidr": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "protocol": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "icmp_type": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + + "icmp_code": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + + "ports": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: func(v interface{}) int { + return hashcode.String(v.(string)) + }, + }, + + "traffic_type": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "ingress", + }, + + "uuids": &schema.Schema{ + Type: schema.TypeMap, + Computed: true, + }, + }, + }, + Set: resourceCloudStackNetworkACLRuleHash, + }, + }, + } +} + +func resourceCloudStackNetworkACLRuleCreate(d *schema.ResourceData, meta interface{}) error { + // Get the acl UUID + aclid := d.Get("aclid").(string) + + // We need to set this upfront in order to be able to save a partial state + d.SetId(aclid) + + // Create all rules that are configured + if rs := d.Get("rule").(*schema.Set); rs.Len() > 0 { + + // Create an empty schema.Set to hold all rules + rules := &schema.Set{ + F: resourceCloudStackNetworkACLRuleHash, + } + + for _, rule := range rs.List() { + // Create a single rule + err := resourceCloudStackNetworkACLRuleCreateRule( + d, meta, aclid, rule.(map[string]interface{})) + + // We need to update this first to preserve the correct state + rules.Add(rule) + d.Set("rule", rules) + + if err != nil { + return err + } + } + } + + return resourceCloudStackNetworkACLRuleRead(d, meta) +} + +func resourceCloudStackNetworkACLRuleCreateRule( + d *schema.ResourceData, meta interface{}, aclid string, rule map[string]interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + uuids := rule["uuids"].(map[string]interface{}) + + // Make sure all required parameters are there + if err := verifyNetworkACLRuleParams(d, rule); err != nil { + return err + } + + // Create a new parameter struct + p := cs.NetworkACL.NewCreateNetworkACLParams(rule["protocol"].(string)) + + // Set the acl ID + p.SetAclid(aclid) + + // Set the action + p.SetAction(rule["action"].(string)) + + // Set the CIDR list + p.SetCidrlist([]string{rule["source_cidr"].(string)}) + + // Set the traffic type + p.SetTraffictype(rule["traffic_type"].(string)) + + // If the protocol is ICMP set the needed ICMP parameters + if rule["protocol"].(string) == "icmp" { + p.SetIcmptype(rule["icmp_type"].(int)) + p.SetIcmpcode(rule["icmp_code"].(int)) + + r, err := cs.NetworkACL.CreateNetworkACL(p) + if err != nil { + return err + } + uuids["icmp"] = r.Id + rule["uuids"] = uuids + } + + // If protocol is not ICMP, loop through all ports + if rule["protocol"].(string) != "icmp" { + if ps := rule["ports"].(*schema.Set); ps.Len() > 0 { + + // Create an empty schema.Set to hold all processed ports + ports := &schema.Set{ + F: func(v interface{}) int { + return hashcode.String(v.(string)) + }, + } + + for _, port := range ps.List() { + re := regexp.MustCompile(`^(\d+)(?:-(\d+))?$`) + m := re.FindStringSubmatch(port.(string)) + + startPort, err := strconv.Atoi(m[1]) + if err != nil { + return err + } + + endPort := startPort + if m[2] != "" { + endPort, err = strconv.Atoi(m[2]) + if err != nil { + return err + } + } + + p.SetStartport(startPort) + p.SetEndport(endPort) + + r, err := cs.NetworkACL.CreateNetworkACL(p) + if err != nil { + return err + } + + ports.Add(port) + rule["ports"] = ports + + uuids[port.(string)] = r.Id + rule["uuids"] = uuids + } + } + } + + return nil +} + +func resourceCloudStackNetworkACLRuleRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Create an empty schema.Set to hold all rules + rules := &schema.Set{ + F: resourceCloudStackNetworkACLRuleHash, + } + + // Read all rules that are configured + if rs := d.Get("rule").(*schema.Set); rs.Len() > 0 { + for _, rule := range rs.List() { + rule := rule.(map[string]interface{}) + uuids := rule["uuids"].(map[string]interface{}) + + if rule["protocol"].(string) == "icmp" { + id, ok := uuids["icmp"] + if !ok { + continue + } + + // Get the rule + r, count, err := cs.NetworkACL.GetNetworkACLByID(id.(string)) + // If the count == 0, there is no object found for this UUID + if err != nil { + if count == 0 { + delete(uuids, "icmp") + continue + } + + return err + } + + // Update the values + rule["action"] = r.Action + rule["source_cidr"] = r.Cidrlist + rule["protocol"] = r.Protocol + rule["icmp_type"] = r.Icmptype + rule["icmp_code"] = r.Icmpcode + rule["traffic_type"] = r.Traffictype + rules.Add(rule) + } + + // If protocol is not ICMP, loop through all ports + if rule["protocol"].(string) != "icmp" { + if ps := rule["ports"].(*schema.Set); ps.Len() > 0 { + + // Create an empty schema.Set to hold all ports + ports := &schema.Set{ + F: func(v interface{}) int { + return hashcode.String(v.(string)) + }, + } + + // Loop through all ports and retrieve their info + for _, port := range ps.List() { + id, ok := uuids[port.(string)] + if !ok { + continue + } + + // Get the rule + r, count, err := cs.NetworkACL.GetNetworkACLByID(id.(string)) + if err != nil { + if count == 0 { + delete(uuids, port.(string)) + continue + } + + return err + } + + // Update the values + rule["action"] = strings.ToLower(r.Action) + rule["source_cidr"] = r.Cidrlist + rule["protocol"] = r.Protocol + rule["traffic_type"] = strings.ToLower(r.Traffictype) + ports.Add(port) + } + + // If there is at least one port found, add this rule to the rules set + if ports.Len() > 0 { + rule["ports"] = ports + rules.Add(rule) + } + } + } + } + } + + if rules.Len() > 0 { + d.Set("rule", rules) + } else { + d.SetId("") + } + + return nil +} + +func resourceCloudStackNetworkACLRuleUpdate(d *schema.ResourceData, meta interface{}) error { + // Get the acl UUID + aclid := d.Get("aclid").(string) + + // Check if the rule set as a whole has changed + if d.HasChange("rule") { + o, n := d.GetChange("rule") + ors := o.(*schema.Set).Difference(n.(*schema.Set)) + nrs := n.(*schema.Set).Difference(o.(*schema.Set)) + + // Now first loop through all the old rules and delete any obsolete ones + for _, rule := range ors.List() { + // Delete the rule as it no longer exists in the config + err := resourceCloudStackNetworkACLRuleDeleteRule(d, meta, rule.(map[string]interface{})) + if err != nil { + return err + } + } + + // Make sure we save the state of the currently configured rules + rules := o.(*schema.Set).Intersection(n.(*schema.Set)) + d.Set("rule", rules) + + // Then loop through al the currently configured rules and create the new ones + for _, rule := range nrs.List() { + // When succesfully deleted, re-create it again if it still exists + err := resourceCloudStackNetworkACLRuleCreateRule( + d, meta, aclid, rule.(map[string]interface{})) + + // We need to update this first to preserve the correct state + rules.Add(rule) + d.Set("rule", rules) + + if err != nil { + return err + } + } + } + + return resourceCloudStackNetworkACLRuleRead(d, meta) +} + +func resourceCloudStackNetworkACLRuleDelete(d *schema.ResourceData, meta interface{}) error { + // Delete all rules + if rs := d.Get("rule").(*schema.Set); rs.Len() > 0 { + for _, rule := range rs.List() { + // Delete a single rule + err := resourceCloudStackNetworkACLRuleDeleteRule(d, meta, rule.(map[string]interface{})) + + // We need to update this first to preserve the correct state + d.Set("rule", rs) + + if err != nil { + return err + } + } + } + + return nil +} + +func resourceCloudStackNetworkACLRuleDeleteRule( + d *schema.ResourceData, meta interface{}, rule map[string]interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + uuids := rule["uuids"].(map[string]interface{}) + + for k, id := range uuids { + // Create the parameter struct + p := cs.NetworkACL.NewDeleteNetworkACLParams(id.(string)) + + // Delete the rule + if _, err := cs.NetworkACL.DeleteNetworkACL(p); err != nil { + + // This is a very poor way to be told the UUID does no longer exist :( + if strings.Contains(err.Error(), fmt.Sprintf( + "Invalid parameter id value=%s due to incorrect long value format, "+ + "or entity does not exist", id.(string))) { + delete(uuids, k) + continue + } + + return err + } + + // Delete the UUID of this rule + delete(uuids, k) + } + + // Update the UUIDs + rule["uuids"] = uuids + + return nil +} + +func resourceCloudStackNetworkACLRuleHash(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf( + "%s-%s-%s-%s-", + m["action"].(string), + m["source_cidr"].(string), + m["protocol"].(string), + m["traffic_type"].(string))) + + if v, ok := m["icmp_type"]; ok { + buf.WriteString(fmt.Sprintf("%d-", v.(int))) + } + + if v, ok := m["icmp_code"]; ok { + buf.WriteString(fmt.Sprintf("%d-", v.(int))) + } + + // We need to make sure to sort the strings below so that we always + // generate the same hash code no matter what is in the set. + if v, ok := m["ports"]; ok { + vs := v.(*schema.Set).List() + s := make([]string, len(vs)) + + for i, raw := range vs { + s[i] = raw.(string) + } + sort.Strings(s) + + for _, v := range s { + buf.WriteString(fmt.Sprintf("%s-", v)) + } + } + + return hashcode.String(buf.String()) +} + +func verifyNetworkACLRuleParams(d *schema.ResourceData, rule map[string]interface{}) error { + action := rule["action"].(string) + if action != "allow" && action != "deny" { + return fmt.Errorf("Parameter action only excepts 'allow' or 'deny' as values") + } + + protocol := rule["protocol"].(string) + if protocol == "icmp" { + if _, ok := rule["icmp_type"]; !ok { + return fmt.Errorf( + "Parameter icmp_type is a required parameter when using protocol 'icmp'") + } + if _, ok := rule["icmp_code"]; !ok { + return fmt.Errorf( + "Parameter icmp_code is a required parameter when using protocol 'icmp'") + } + } else { + if protocol != "tcp" && protocol != "udp" && protocol != "all" { + _, err := strconv.ParseInt(protocol, 0, 0) + if err != nil { + return fmt.Errorf( + "%s is not a valid protocol. Valid options are 'tcp', 'udp', "+ + "'icmp', 'all' or a valid protocol number", protocol) + } + } + if _, ok := rule["ports"]; !ok { + return fmt.Errorf( + "Parameter ports is a required parameter when *not* using protocol 'icmp'") + } + } + + traffic := rule["traffic_type"].(string) + if traffic != "ingress" && traffic != "egress" { + return fmt.Errorf( + "Parameter traffic_type only excepts 'ingress' or 'egress' as values") + } + + return nil +} diff --git a/builtin/providers/cloudstack/resource_cloudstack_network_acl_rule_test.go b/builtin/providers/cloudstack/resource_cloudstack_network_acl_rule_test.go new file mode 100644 index 000000000..620065363 --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_network_acl_rule_test.go @@ -0,0 +1,241 @@ +package cloudstack + +import ( + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +func TestAccCloudStackNetworkACLRule_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackNetworkACLRuleDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCloudStackNetworkACLRule_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackNetworkACLRulesExist("cloudstack_network_acl.foo"), + resource.TestCheckResourceAttr( + "cloudstack_network_acl_rule.foo", "rule.#", "1"), + resource.TestCheckResourceAttr( + "cloudstack_network_acl_rule.foo", "rule.0.action", "allow"), + resource.TestCheckResourceAttr( + "cloudstack_network_acl_rule.foo", "rule.0.source_cidr", "172.16.100.0/24"), + resource.TestCheckResourceAttr( + "cloudstack_network_acl_rule.foo", "rule.0.protocol", "tcp"), + resource.TestCheckResourceAttr( + "cloudstack_network_acl_rule.foo", "rule.0.ports.#", "2"), + resource.TestCheckResourceAttr( + "cloudstack_network_acl_rule.foo", "rule.0.ports.0", "80"), + resource.TestCheckResourceAttr( + "cloudstack_network_acl_rule.foo", "rule.0.ports.1", "443"), + resource.TestCheckResourceAttr( + "cloudstack_network_acl_rule.foo", "rule.0.traffic_type", "ingress"), + ), + }, + }, + }) +} + +/* +func TestAccCloudStackNetworkACLRule_update(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackNetworkACLRuleDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCloudStackNetworkACLRule_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackNetworkACLRulesExist("cloudstack_network_acl.foo"), + resource.TestCheckResourceAttr( + "cloudstack_network_acl_rule.foo", "rule.#", "1"), + resource.TestCheckResourceAttr( + "cloudstack_network_acl_rule.foo", "rule.0.action", "allow"), + resource.TestCheckResourceAttr( + "cloudstack_network_acl_rule.foo", "rule.0.source_cidr", "172.16.100.0/24"), + resource.TestCheckResourceAttr( + "cloudstack_network_acl_rule.foo", "rule.0.protocol", "tcp"), + resource.TestCheckResourceAttr( + "cloudstack_network_acl_rule.foo", "rule.0.ports.#", "2"), + resource.TestCheckResourceAttr( + "cloudstack_network_acl_rule.foo", "rule.0.ports.0", "80"), + resource.TestCheckResourceAttr( + "cloudstack_network_acl_rule.foo", "rule.0.ports.1", "443"), + resource.TestCheckResourceAttr( + "cloudstack_network_acl_rule.foo", "rule.0.traffic_type", "ingress"), + ), + }, + + resource.TestStep{ + Config: testAccCloudStackNetworkACLRule_update, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackNetworkACLRulesExist("cloudstack_network_acl.foo"), + resource.TestCheckResourceAttr( + "cloudstack_network_acl_rule.foo", "rule.#", "2"), + resource.TestCheckResourceAttr( + "cloudstack_network_acl_rule.foo", "rule.0.action", "allow"), + resource.TestCheckResourceAttr( + "cloudstack_network_acl_rule.foo", "rule.0.source_cidr", "172.16.100.0/24"), + resource.TestCheckResourceAttr( + "cloudstack_network_acl_rule.foo", "rule.0.protocol", "tcp"), + resource.TestCheckResourceAttr( + "cloudstack_network_acl_rule.foo", "rule.0.ports.#", "2"), + resource.TestCheckResourceAttr( + "cloudstack_network_acl_rule.foo", "rule.0.ports.0", "80"), + resource.TestCheckResourceAttr( + "cloudstack_network_acl_rule.foo", "rule.0.ports.1", "443"), + resource.TestCheckResourceAttr( + "cloudstack_network_acl_rule.foo", "rule.0.traffic_type", "ingress"), + resource.TestCheckResourceAttr( + "cloudstack_network_acl_rule.foo", "rule.1.action", "deny"), + resource.TestCheckResourceAttr( + "cloudstack_network_acl_rule.foo", "rule.1.source_cidr", "10.0.0.0/24"), + resource.TestCheckResourceAttr( + "cloudstack_network_acl_rule.foo", "rule.1.protocol", "tcp"), + resource.TestCheckResourceAttr( + "cloudstack_network_acl_rule.foo", "rule.1.ports.#", "2"), + resource.TestCheckResourceAttr( + "cloudstack_network_acl_rule.foo", "rule.1.ports.0", "1000-2000"), + resource.TestCheckResourceAttr( + "cloudstack_network_acl_rule.foo", "rule.1.ports.1", "80"), + resource.TestCheckResourceAttr( + "cloudstack_network_acl_rule.foo", "rule.1.traffic_type", "engress"), + ), + }, + }, + }) +} +*/ + +func testAccCheckCloudStackNetworkACLRulesExist(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 network ACL rule ID is set") + } + + for k, uuid := range rs.Primary.Attributes { + if !strings.Contains(k, "uuids") { + continue + } + + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + _, count, err := cs.NetworkACL.GetNetworkACLByID(uuid) + + if err != nil { + return err + } + + if count == 0 { + return fmt.Errorf("Network ACL rule %s not found", k) + } + } + + return nil + } +} + +func testAccCheckCloudStackNetworkACLRuleDestroy(s *terraform.State) error { + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "cloudstack_network_acl_rule" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No network ACL rule ID is set") + } + + for k, uuid := range rs.Primary.Attributes { + if !strings.Contains(k, "uuids") { + continue + } + + p := cs.NetworkACL.NewDeleteNetworkACLParams(uuid) + _, err := cs.NetworkACL.DeleteNetworkACL(p) + + if err != nil { + return err + } + } + } + + return nil +} + +var testAccCloudStackNetworkACLRule_basic = fmt.Sprintf(` +resource "cloudstack_vpc" "foobar" { + name = "terraform-vpc" + cidr = "%s" + vpc_offering = "%s" + zone = "%s" +} + +resource "cloudstack_network_acl" "foo" { + name = "terraform-acl" + description = "terraform-acl-text" + vpc = "${cloudstack_vpc.foobar.name}" +} + +resource "cloudstack_network_acl_rule" "foo" { + aclid = "${cloudstack_network_acl.foo.id}" + + rule { + action = "allow" + source_cidr = "172.16.100.0/24" + protocol = "tcp" + ports = ["80", "443"] + traffic_type = "ingress" + } +}`, + CLOUDSTACK_VPC_CIDR, + CLOUDSTACK_VPC_OFFERING, + CLOUDSTACK_ZONE) + +var testAccCloudStackNetworkACLRule_update = fmt.Sprintf(` +resource "cloudstack_vpc" "foobar" { + name = "terraform-vpc" + cidr = "%s" + vpc_offering = "%s" + zone = "%s" +} + +resource "cloudstack_network_acl" "foo" { + name = "terraform-acl" + description = "terraform-acl-text" + vpc = "${cloudstack_vpc.foobar.name}" +} + +resource "cloudstack_network_acl_rule" "foo" { + aclid = "${cloudstack_network_acl.foo.id}" + + rule { + action = "allow" + source_cidr = "172.16.100.0/24" + protocol = "tcp" + ports = ["80", "443"] + traffic_type = "ingress" + } + + rule { + action = "deny" + source_cidr = "10.0.0.0/24" + protocol = "tcp" + ports = ["80", "1000-2000"] + traffic_type = "egress" + } +}`, + CLOUDSTACK_VPC_CIDR, + CLOUDSTACK_VPC_OFFERING, + CLOUDSTACK_ZONE) diff --git a/builtin/providers/cloudstack/resource_cloudstack_network_acl_test.go b/builtin/providers/cloudstack/resource_cloudstack_network_acl_test.go new file mode 100644 index 000000000..7ea42319d --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_network_acl_test.go @@ -0,0 +1,117 @@ +package cloudstack + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +func TestAccCloudStackNetworkACL_basic(t *testing.T) { + var acl cloudstack.NetworkACLList + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackNetworkACLDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCloudStackNetworkACL_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackNetworkACLExists( + "cloudstack_network_acl.foo", &acl), + testAccCheckCloudStackNetworkACLBasicAttributes(&acl), + resource.TestCheckResourceAttr( + "cloudstack_network_acl.foo", "vpc", "terraform-vpc"), + ), + }, + }, + }) +} + +func testAccCheckCloudStackNetworkACLExists( + n string, acl *cloudstack.NetworkACLList) 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 network ACL ID is set") + } + + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + acllist, _, err := cs.NetworkACL.GetNetworkACLListByID(rs.Primary.ID) + if err != nil { + return err + } + + if acllist.Id != rs.Primary.ID { + return fmt.Errorf("Network ACL not found") + } + + *acl = *acllist + + return nil + } +} + +func testAccCheckCloudStackNetworkACLBasicAttributes( + acl *cloudstack.NetworkACLList) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if acl.Name != "terraform-acl" { + return fmt.Errorf("Bad name: %s", acl.Name) + } + + if acl.Description != "terraform-acl-text" { + return fmt.Errorf("Bad description: %s", acl.Description) + } + + return nil + } +} + +func testAccCheckCloudStackNetworkACLDestroy(s *terraform.State) error { + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "cloudstack_network_acl" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No network ACL ID is set") + } + + p := cs.NetworkACL.NewDeleteNetworkACLListParams(rs.Primary.ID) + err, _ := cs.NetworkACL.DeleteNetworkACLList(p) + + if err != nil { + return fmt.Errorf( + "Error deleting network ACL (%s): %s", + rs.Primary.ID, err) + } + } + + return nil +} + +var testAccCloudStackNetworkACL_basic = fmt.Sprintf(` +resource "cloudstack_vpc" "foobar" { + name = "terraform-vpc" + cidr = "%s" + vpc_offering = "%s" + zone = "%s" +} + +resource "cloudstack_network_acl" "foo" { + name = "terraform-acl" + description = "terraform-acl-text" + vpc = "${cloudstack_vpc.foobar.name}" +}`, + CLOUDSTACK_VPC_CIDR, + CLOUDSTACK_VPC_OFFERING, + CLOUDSTACK_ZONE) diff --git a/builtin/providers/cloudstack/resource_cloudstack_network_test.go b/builtin/providers/cloudstack/resource_cloudstack_network_test.go new file mode 100644 index 000000000..6eb3094b8 --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_network_test.go @@ -0,0 +1,193 @@ +package cloudstack + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +func TestAccCloudStackNetwork_basic(t *testing.T) { + var network cloudstack.Network + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackNetworkDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCloudStackNetwork_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackNetworkExists( + "cloudstack_network.foo", &network), + testAccCheckCloudStackNetworkBasicAttributes(&network), + ), + }, + }, + }) +} + +func TestAccCloudStackNetwork_vpcACL(t *testing.T) { + var network cloudstack.Network + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackNetworkDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCloudStackNetwork_vpcACL, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackNetworkExists( + "cloudstack_network.foo", &network), + testAccCheckCloudStackNetworkVPCACLAttributes(&network), + resource.TestCheckResourceAttr( + "cloudstack_network.foo", "vpc", "terraform-vpc"), + ), + }, + }, + }) +} + +func testAccCheckCloudStackNetworkExists( + n string, network *cloudstack.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 network ID is set") + } + + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + ntwrk, _, err := cs.Network.GetNetworkByID(rs.Primary.ID) + + if err != nil { + return err + } + + if ntwrk.Id != rs.Primary.ID { + return fmt.Errorf("Network not found") + } + + *network = *ntwrk + + return nil + } +} + +func testAccCheckCloudStackNetworkBasicAttributes( + network *cloudstack.Network) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if network.Name != "terraform-network" { + return fmt.Errorf("Bad name: %s", network.Name) + } + + if network.Displaytext != "terraform-network" { + return fmt.Errorf("Bad display name: %s", network.Displaytext) + } + + if network.Cidr != CLOUDSTACK_NETWORK_1_CIDR { + return fmt.Errorf("Bad service offering: %s", network.Cidr) + } + + if network.Networkofferingname != CLOUDSTACK_NETWORK_1_OFFERING { + return fmt.Errorf("Bad template: %s", network.Networkofferingname) + } + + return nil + } +} + +func testAccCheckCloudStackNetworkVPCACLAttributes( + network *cloudstack.Network) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if network.Name != "terraform-network" { + return fmt.Errorf("Bad name: %s", network.Name) + } + + if network.Displaytext != "terraform-network" { + return fmt.Errorf("Bad display name: %s", network.Displaytext) + } + + if network.Cidr != CLOUDSTACK_VPC_NETWORK_CIDR { + return fmt.Errorf("Bad service offering: %s", network.Cidr) + } + + if network.Networkofferingname != CLOUDSTACK_VPC_NETWORK_OFFERING { + return fmt.Errorf("Bad template: %s", network.Networkofferingname) + } + + return nil + } +} + +func testAccCheckCloudStackNetworkDestroy(s *terraform.State) error { + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "cloudstack_network" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No network ID is set") + } + + p := cs.Network.NewDeleteNetworkParams(rs.Primary.ID) + err, _ := cs.Network.DeleteNetwork(p) + + if err != nil { + return fmt.Errorf( + "Error deleting network (%s): %s", + rs.Primary.ID, err) + } + } + + return nil +} + +var testAccCloudStackNetwork_basic = fmt.Sprintf(` +resource "cloudstack_network" "foo" { + name = "terraform-network" + cidr = "%s" + network_offering = "%s" + zone = "%s" +}`, + CLOUDSTACK_NETWORK_1_CIDR, + CLOUDSTACK_NETWORK_1_OFFERING, + CLOUDSTACK_ZONE) + +var testAccCloudStackNetwork_vpcACL = fmt.Sprintf(` +resource "cloudstack_vpc" "foobar" { + name = "terraform-vpc" + cidr = "%s" + vpc_offering = "%s" + zone = "%s" +} + +resource "cloudstack_network_acl" "foo" { + name = "terraform-acl" + description = "terraform-acl-text" + vpc = "${cloudstack_vpc.foobar.name}" +} + +resource "cloudstack_network" "foo" { + name = "terraform-network" + cidr = "%s" + network_offering = "%s" + vpc = "${cloudstack_vpc.foobar.name}" + aclid = "${cloudstack_network_acl.foo.id}" + zone = "${cloudstack_vpc.foobar.zone}" +}`, + CLOUDSTACK_VPC_CIDR, + CLOUDSTACK_VPC_OFFERING, + CLOUDSTACK_ZONE, + CLOUDSTACK_VPC_NETWORK_CIDR, + CLOUDSTACK_VPC_NETWORK_OFFERING) diff --git a/builtin/providers/cloudstack/resource_cloudstack_nic.go b/builtin/providers/cloudstack/resource_cloudstack_nic.go new file mode 100644 index 000000000..99f2d3000 --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_nic.go @@ -0,0 +1,147 @@ +package cloudstack + +import ( + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +func resourceCloudStackNIC() *schema.Resource { + return &schema.Resource{ + Create: resourceCloudStackNICCreate, + Read: resourceCloudStackNICRead, + Delete: resourceCloudStackNICDelete, + + Schema: map[string]*schema.Schema{ + "network": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "ipaddress": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "virtual_machine": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceCloudStackNICCreate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Retrieve the network UUID + networkid, e := retrieveUUID(cs, "network", d.Get("network").(string)) + if e != nil { + return e.Error() + } + + // Retrieve the virtual_machine UUID + virtualmachineid, e := retrieveUUID(cs, "virtual_machine", d.Get("virtual_machine").(string)) + if e != nil { + return e.Error() + } + + // Create a new parameter struct + p := cs.VirtualMachine.NewAddNicToVirtualMachineParams(networkid, virtualmachineid) + + // If there is a ipaddres supplied, add it to the parameter struct + if ipaddress, ok := d.GetOk("ipaddress"); ok { + p.SetIpaddress(ipaddress.(string)) + } + + // Create and attach the new NIC + r, err := cs.VirtualMachine.AddNicToVirtualMachine(p) + if err != nil { + return fmt.Errorf("Error creating the new NIC: %s", err) + } + + found := false + for _, n := range r.Nic { + if n.Networkid == networkid { + d.SetId(n.Id) + found = true + break + } + } + + if !found { + return fmt.Errorf("Could not find NIC ID for network: %s", d.Get("network").(string)) + } + + return resourceCloudStackNICRead(d, meta) +} + +func resourceCloudStackNICRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Get the virtual machine details + vm, count, err := cs.VirtualMachine.GetVirtualMachineByName(d.Get("virtual_machine").(string)) + if err != nil { + if count == 0 { + log.Printf("[DEBUG] Instance %s does no longer exist", d.Get("virtual_machine").(string)) + d.SetId("") + return nil + } else { + return err + } + } + + // Read NIC info + found := false + for _, n := range vm.Nic { + if n.Id == d.Id() { + d.Set("network", n.Networkname) + d.Set("ipaddress", n.Ipaddress) + d.Set("virtual_machine", vm.Name) + found = true + break + } + } + + if !found { + log.Printf("[DEBUG] NIC for network %s does no longer exist", d.Get("network").(string)) + d.SetId("") + } + + return nil +} + +func resourceCloudStackNICDelete(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Retrieve the virtual_machine UUID + virtualmachineid, e := retrieveUUID(cs, "virtual_machine", d.Get("virtual_machine").(string)) + if e != nil { + return e.Error() + } + + // Create a new parameter struct + p := cs.VirtualMachine.NewRemoveNicFromVirtualMachineParams(d.Id(), virtualmachineid) + + // Remove the NIC + _, err := cs.VirtualMachine.RemoveNicFromVirtualMachine(p) + if err != nil { + // This is a very poor way to be told the UUID does no longer exist :( + if strings.Contains(err.Error(), fmt.Sprintf( + "Invalid parameter id value=%s due to incorrect long value format, "+ + "or entity does not exist", d.Id())) { + return nil + } + + return fmt.Errorf("Error deleting NIC: %s", err) + } + + return nil +} diff --git a/builtin/providers/cloudstack/resource_cloudstack_nic_test.go b/builtin/providers/cloudstack/resource_cloudstack_nic_test.go new file mode 100644 index 000000000..645b0b01d --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_nic_test.go @@ -0,0 +1,198 @@ +package cloudstack + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +func TestAccCloudStackNIC_basic(t *testing.T) { + var nic cloudstack.Nic + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackNICDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCloudStackNIC_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackNICExists( + "cloudstack_instance.foobar", "cloudstack_nic.foo", &nic), + testAccCheckCloudStackNICAttributes(&nic), + ), + }, + }, + }) +} + +func TestAccCloudStackNIC_update(t *testing.T) { + var nic cloudstack.Nic + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackNICDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCloudStackNIC_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackNICExists( + "cloudstack_instance.foobar", "cloudstack_nic.foo", &nic), + testAccCheckCloudStackNICAttributes(&nic), + ), + }, + + resource.TestStep{ + Config: testAccCloudStackNIC_ipaddress, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackNICExists( + "cloudstack_instance.foobar", "cloudstack_nic.foo", &nic), + testAccCheckCloudStackNICIPAddress(&nic), + resource.TestCheckResourceAttr( + "cloudstack_nic.foo", "ipaddress", CLOUDSTACK_NETWORK_2_IPADDRESS), + ), + }, + }, + }) +} + +func testAccCheckCloudStackNICExists( + v, n string, nic *cloudstack.Nic) resource.TestCheckFunc { + return func(s *terraform.State) error { + rsv, ok := s.RootModule().Resources[v] + if !ok { + return fmt.Errorf("Not found: %s", v) + } + + if rsv.Primary.ID == "" { + return fmt.Errorf("No instance ID is set") + } + + rsn, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rsn.Primary.ID == "" { + return fmt.Errorf("No NIC ID is set") + } + + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + vm, _, err := cs.VirtualMachine.GetVirtualMachineByID(rsv.Primary.ID) + + if err != nil { + return err + } + + for _, n := range vm.Nic { + if n.Id == rsn.Primary.ID { + *nic = n + return nil + } + } + + return fmt.Errorf("NIC not found") + } +} + +func testAccCheckCloudStackNICAttributes( + nic *cloudstack.Nic) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if nic.Networkname != CLOUDSTACK_NETWORK_2 { + return fmt.Errorf("Bad network: %s", nic.Networkname) + } + + return nil + } +} + +func testAccCheckCloudStackNICIPAddress( + nic *cloudstack.Nic) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if nic.Networkname != CLOUDSTACK_NETWORK_2 { + return fmt.Errorf("Bad network: %s", nic.Networkname) + } + + if nic.Ipaddress != CLOUDSTACK_NETWORK_2_IPADDRESS { + return fmt.Errorf("Bad IP address: %s", nic.Ipaddress) + } + + return nil + } +} + +func testAccCheckCloudStackNICDestroy(s *terraform.State) error { + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + + // Deleting the instance automatically deletes any additional NICs + for _, rs := range s.RootModule().Resources { + if rs.Type != "cloudstack_instance" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No instance ID is set") + } + + p := cs.VirtualMachine.NewDestroyVirtualMachineParams(rs.Primary.ID) + err, _ := cs.VirtualMachine.DestroyVirtualMachine(p) + + if err != nil { + return fmt.Errorf( + "Error deleting instance (%s): %s", + rs.Primary.ID, err) + } + } + + return nil +} + +var testAccCloudStackNIC_basic = fmt.Sprintf(` +resource "cloudstack_instance" "foobar" { + name = "terraform-test" + display_name = "terraform" + service_offering= "%s" + network = "%s" + template = "%s" + zone = "%s" + expunge = true +} + +resource "cloudstack_nic" "foo" { + network = "%s" + virtual_machine = "${cloudstack_instance.foobar.name}" +}`, + CLOUDSTACK_SERVICE_OFFERING_1, + CLOUDSTACK_NETWORK_1, + CLOUDSTACK_TEMPLATE, + CLOUDSTACK_ZONE, + CLOUDSTACK_NETWORK_2) + +var testAccCloudStackNIC_ipaddress = fmt.Sprintf(` +resource "cloudstack_instance" "foobar" { + name = "terraform-test" + display_name = "terraform" + service_offering= "%s" + network = "%s" + template = "%s" + zone = "%s" + expunge = true +} + +resource "cloudstack_nic" "foo" { + network = "%s" + ipaddress = "%s" + virtual_machine = "${cloudstack_instance.foobar.name}" +}`, + CLOUDSTACK_SERVICE_OFFERING_1, + CLOUDSTACK_NETWORK_1, + CLOUDSTACK_TEMPLATE, + CLOUDSTACK_ZONE, + CLOUDSTACK_NETWORK_2, + CLOUDSTACK_NETWORK_2_IPADDRESS) diff --git a/builtin/providers/cloudstack/resource_cloudstack_port_forward.go b/builtin/providers/cloudstack/resource_cloudstack_port_forward.go new file mode 100644 index 000000000..ed42a6f51 --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_port_forward.go @@ -0,0 +1,299 @@ +package cloudstack + +import ( + "bytes" + "fmt" + + "strconv" + "strings" + + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/schema" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +func resourceCloudStackPortForward() *schema.Resource { + return &schema.Resource{ + Create: resourceCloudStackPortForwardCreate, + Read: resourceCloudStackPortForwardRead, + Update: resourceCloudStackPortForwardUpdate, + Delete: resourceCloudStackPortForwardDelete, + + Schema: map[string]*schema.Schema{ + "ipaddress": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "forward": &schema.Schema{ + Type: schema.TypeSet, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "protocol": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "private_port": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + }, + + "public_port": &schema.Schema{ + Type: schema.TypeInt, + Required: true, + }, + + "virtual_machine": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "uuid": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + }, + Set: resourceCloudStackPortForwardHash, + }, + }, + } +} + +func resourceCloudStackPortForwardCreate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Retrieve the ipaddress UUID + ipaddressid, e := retrieveUUID(cs, "ipaddress", d.Get("ipaddress").(string)) + if e != nil { + return e.Error() + } + + // We need to set this upfront in order to be able to save a partial state + d.SetId(d.Get("ipaddress").(string)) + + // Create all forwards that are configured + if rs := d.Get("forward").(*schema.Set); rs.Len() > 0 { + + // Create an empty schema.Set to hold all forwards + forwards := &schema.Set{ + F: resourceCloudStackPortForwardHash, + } + + for _, forward := range rs.List() { + // Create a single forward + err := resourceCloudStackPortForwardCreateForward(d, meta, ipaddressid, forward.(map[string]interface{})) + + // We need to update this first to preserve the correct state + forwards.Add(forward) + d.Set("forward", forwards) + + if err != nil { + return err + } + } + } + + return resourceCloudStackPortForwardRead(d, meta) +} + +func resourceCloudStackPortForwardCreateForward( + d *schema.ResourceData, meta interface{}, ipaddressid string, forward map[string]interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Make sure all required parameters are there + if err := verifyPortForwardParams(d, forward); err != nil { + return err + } + + // Retrieve the virtual_machine UUID + virtualmachineid, e := retrieveUUID(cs, "virtual_machine", forward["virtual_machine"].(string)) + if e != nil { + return e.Error() + } + + // Create a new parameter struct + p := cs.Firewall.NewCreatePortForwardingRuleParams(ipaddressid, forward["private_port"].(int), + forward["protocol"].(string), forward["public_port"].(int), virtualmachineid) + + // Do not open the firewall automatically in any case + p.SetOpenfirewall(false) + + r, err := cs.Firewall.CreatePortForwardingRule(p) + if err != nil { + return err + } + + forward["uuid"] = r.Id + + return nil +} + +func resourceCloudStackPortForwardRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Create an empty schema.Set to hold all forwards + forwards := &schema.Set{ + F: resourceCloudStackPortForwardHash, + } + + // Read all forwards that are configured + if rs := d.Get("forward").(*schema.Set); rs.Len() > 0 { + for _, forward := range rs.List() { + forward := forward.(map[string]interface{}) + + id, ok := forward["uuid"] + if !ok || id.(string) == "" { + continue + } + + // Get the forward + r, count, err := cs.Firewall.GetPortForwardingRuleByID(id.(string)) + // If the count == 0, there is no object found for this UUID + if err != nil { + if count != 0 { + continue + } + + return err + } + + privPort, err := strconv.Atoi(r.Privateport) + if err != nil { + return fmt.Errorf("Error converting private_port: %s", err) + } + + pubPort, err := strconv.Atoi(r.Publicport) + if err != nil { + return fmt.Errorf("Error converting public_port: %s", err) + } + + // Update the values + forward["protocol"] = r.Protocol + forward["private_port"] = privPort + forward["public_port"] = pubPort + forward["virtual_machine"] = r.Virtualmachinename + forwards.Add(forward) + } + } + + if forwards.Len() > 0 { + d.Set("forward", forwards) + } else { + d.SetId("") + } + + return nil +} + +func resourceCloudStackPortForwardUpdate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Retrieve the ipaddress UUID + ipaddressid, e := retrieveUUID(cs, "ipaddress", d.Get("ipaddress").(string)) + if e != nil { + return e.Error() + } + + // Check if the forward set as a whole has changed + if d.HasChange("forward") { + o, n := d.GetChange("forward") + ors := o.(*schema.Set).Difference(n.(*schema.Set)) + nrs := n.(*schema.Set).Difference(o.(*schema.Set)) + + // Now first loop through all the old forwards and delete any obsolete ones + for _, forward := range ors.List() { + // Delete the forward as it no longer exists in the config + err := resourceCloudStackPortForwardDeleteForward(d, meta, forward.(map[string]interface{})) + if err != nil { + return err + } + } + + // Make sure we save the state of the currently configured forwards + forwards := o.(*schema.Set).Intersection(n.(*schema.Set)) + d.Set("forward", forwards) + + // Then loop through al the currently configured forwards and create the new ones + for _, forward := range nrs.List() { + err := resourceCloudStackPortForwardCreateForward( + d, meta, ipaddressid, forward.(map[string]interface{})) + + // We need to update this first to preserve the correct state + forwards.Add(forward) + d.Set("forward", forwards) + + if err != nil { + return err + } + } + } + + return resourceCloudStackPortForwardRead(d, meta) +} + +func resourceCloudStackPortForwardDelete(d *schema.ResourceData, meta interface{}) error { + // Delete all forwards + if rs := d.Get("forward").(*schema.Set); rs.Len() > 0 { + for _, forward := range rs.List() { + // Delete a single forward + err := resourceCloudStackPortForwardDeleteForward(d, meta, forward.(map[string]interface{})) + + // We need to update this first to preserve the correct state + d.Set("forward", rs) + + if err != nil { + return err + } + } + } + + return nil +} + +func resourceCloudStackPortForwardDeleteForward( + d *schema.ResourceData, meta interface{}, forward map[string]interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Create the parameter struct + p := cs.Firewall.NewDeletePortForwardingRuleParams(forward["uuid"].(string)) + + // Delete the forward + if _, err := cs.Firewall.DeletePortForwardingRule(p); err != nil { + // This is a very poor way to be told the UUID does no longer exist :( + if !strings.Contains(err.Error(), fmt.Sprintf( + "Invalid parameter id value=%s due to incorrect long value format, "+ + "or entity does not exist", forward["uuid"].(string))) { + return err + } + } + + forward["uuid"] = "" + + return nil +} + +func resourceCloudStackPortForwardHash(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf( + "%s-%d-%d-%s", + m["protocol"].(string), + m["private_port"].(int), + m["public_port"].(int), + m["virtual_machine"].(string))) + + return hashcode.String(buf.String()) +} + +func verifyPortForwardParams(d *schema.ResourceData, forward map[string]interface{}) error { + protocol := forward["protocol"].(string) + if protocol != "tcp" && protocol != "udp" { + return fmt.Errorf( + "%s is not a valid protocol. Valid options are 'tcp' and 'udp'", protocol) + } + return nil +} diff --git a/builtin/providers/cloudstack/resource_cloudstack_port_forward_test.go b/builtin/providers/cloudstack/resource_cloudstack_port_forward_test.go new file mode 100644 index 000000000..4d65df303 --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_port_forward_test.go @@ -0,0 +1,219 @@ +package cloudstack + +import ( + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +func TestAccCloudStackPortForward_basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackPortForwardDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCloudStackPortForward_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackPortForwardsExist("cloudstack_port_forward.foo"), + resource.TestCheckResourceAttr( + "cloudstack_port_forward.foo", "ipaddress", CLOUDSTACK_PUBLIC_IPADDRESS), + resource.TestCheckResourceAttr( + "cloudstack_port_forward.foo", "forward.0.protocol", "tcp"), + resource.TestCheckResourceAttr( + "cloudstack_port_forward.foo", "forward.0.private_port", "443"), + resource.TestCheckResourceAttr( + "cloudstack_port_forward.foo", "forward.0.public_port", "8443"), + resource.TestCheckResourceAttr( + "cloudstack_port_forward.foo", "forward.0.virtual_machine", "terraform-test"), + ), + }, + }, + }) +} + +/* +func TestAccCloudStackPortForward_update(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackPortForwardDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCloudStackPortForward_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackPortForwardsExist("cloudstack_port_forward.foo"), + resource.TestCheckResourceAttr( + "cloudstack_port_forward.foo", "ipaddress", CLOUDSTACK_PUBLIC_IPADDRESS), + resource.TestCheckResourceAttr( + "cloudstack_port_forward.foo", "forward.#", "1"), + resource.TestCheckResourceAttr( + "cloudstack_port_forward.foo", "forward.0.protocol", "tcp"), + resource.TestCheckResourceAttr( + "cloudstack_port_forward.foo", "forward.0.private_port", "443"), + resource.TestCheckResourceAttr( + "cloudstack_port_forward.foo", "forward.0.public_port", "8443"), + resource.TestCheckResourceAttr( + "cloudstack_port_forward.foo", "forward.0.virtual_machine", "terraform-test"), + ), + }, + + resource.TestStep{ + Config: testAccCloudStackPortForward_update, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackPortForwardsExist("cloudstack_port_forward.foo"), + resource.TestCheckResourceAttr( + "cloudstack_port_forward.foo", "ipaddress", CLOUDSTACK_PUBLIC_IPADDRESS), + resource.TestCheckResourceAttr( + "cloudstack_port_forward.foo", "forward.#", "2"), + resource.TestCheckResourceAttr( + "cloudstack_port_forward.foo", "forward.0.protocol", "tcp"), + resource.TestCheckResourceAttr( + "cloudstack_port_forward.foo", "forward.0.private_port", "80"), + resource.TestCheckResourceAttr( + "cloudstack_port_forward.foo", "forward.0.public_port", "8080"), + resource.TestCheckResourceAttr( + "cloudstack_port_forward.foo", "forward.0.virtual_machine", "terraform-test"), + resource.TestCheckResourceAttr( + "cloudstack_port_forward.foo", "forward.1.protocol", "tcp"), + resource.TestCheckResourceAttr( + "cloudstack_port_forward.foo", "forward.1.private_port", "443"), + resource.TestCheckResourceAttr( + "cloudstack_port_forward.foo", "forward.1.public_port", "8443"), + resource.TestCheckResourceAttr( + "cloudstack_port_forward.foo", "forward.1.virtual_machine", "terraform-test"), + ), + }, + }, + }) +} +*/ + +func testAccCheckCloudStackPortForwardsExist(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 port forward ID is set") + } + + for k, uuid := range rs.Primary.Attributes { + if !strings.Contains(k, "uuid") { + continue + } + + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + _, count, err := cs.Firewall.GetPortForwardingRuleByID(uuid) + + if err != nil { + return err + } + + if count == 0 { + return fmt.Errorf("Port forward for %s not found", k) + } + } + + return nil + } +} + +func testAccCheckCloudStackPortForwardDestroy(s *terraform.State) error { + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "cloudstack_port_forward" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No port forward ID is set") + } + + for k, uuid := range rs.Primary.Attributes { + if !strings.Contains(k, "uuid") { + continue + } + + p := cs.Firewall.NewDeletePortForwardingRuleParams(uuid) + _, err := cs.Firewall.DeletePortForwardingRule(p) + + if err != nil { + return err + } + } + } + + return nil +} + +var testAccCloudStackPortForward_basic = fmt.Sprintf(` +resource "cloudstack_instance" "foobar" { + name = "terraform-test" + display_name = "terraform" + service_offering= "%s" + network = "%s" + template = "%s" + zone = "%s" + user_data = "foobar\nfoo\nbar" + expunge = true +} + +resource "cloudstack_port_forward" "foo" { + ipaddress = "%s" + + forward { + protocol = "tcp" + private_port = 443 + public_port = 8443 + virtual_machine = "${cloudstack_instance.foobar.name}" + } +}`, + CLOUDSTACK_SERVICE_OFFERING_1, + CLOUDSTACK_NETWORK_1, + CLOUDSTACK_TEMPLATE, + CLOUDSTACK_ZONE, + CLOUDSTACK_PUBLIC_IPADDRESS) + +var testAccCloudStackPortForward_update = fmt.Sprintf(` +resource "cloudstack_instance" "foobar" { + name = "terraform-test" + display_name = "terraform" + service_offering= "%s" + network = "%s" + template = "%s" + zone = "%s" + user_data = "foobar\nfoo\nbar" + expunge = true +} + +resource "cloudstack_port_forward" "foo" { + ipaddress = "%s" + + forward { + protocol = "tcp" + private_port = 443 + public_port = 8443 + virtual_machine = "${cloudstack_instance.foobar.name}" + } + + forward { + protocol = "tcp" + private_port = 80 + public_port = 8080 + virtual_machine = "${cloudstack_instance.foobar.name}" + } + +}`, + CLOUDSTACK_SERVICE_OFFERING_1, + CLOUDSTACK_NETWORK_1, + CLOUDSTACK_TEMPLATE, + CLOUDSTACK_ZONE, + CLOUDSTACK_PUBLIC_IPADDRESS) diff --git a/builtin/providers/cloudstack/resource_cloudstack_vpc.go b/builtin/providers/cloudstack/resource_cloudstack_vpc.go new file mode 100644 index 000000000..fc76b6071 --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_vpc.go @@ -0,0 +1,168 @@ +package cloudstack + +import ( + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +func resourceCloudStackVPC() *schema.Resource { + return &schema.Resource{ + Create: resourceCloudStackVPCCreate, + Read: resourceCloudStackVPCRead, + Update: resourceCloudStackVPCUpdate, + Delete: resourceCloudStackVPCDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "display_text": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "cidr": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "vpc_offering": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "zone": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceCloudStackVPCCreate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + name := d.Get("name").(string) + + // Retrieve the vpc_offering UUID + vpcofferingid, e := retrieveUUID(cs, "vpc_offering", d.Get("vpc_offering").(string)) + if e != nil { + return e.Error() + } + + // Retrieve the zone UUID + zoneid, e := retrieveUUID(cs, "zone", d.Get("zone").(string)) + if e != nil { + return e.Error() + } + + // Set the display text + displaytext, ok := d.GetOk("display_text") + if !ok { + displaytext = d.Get("name") + } + + // Create a new parameter struct + p := cs.VPC.NewCreateVPCParams(d.Get("cidr").(string), displaytext.(string), name, vpcofferingid, zoneid) + + // Create the new VPC + r, err := cs.VPC.CreateVPC(p) + if err != nil { + return fmt.Errorf("Error creating VPC %s: %s", name, err) + } + + d.SetId(r.Id) + + return resourceCloudStackVPCRead(d, meta) +} + +func resourceCloudStackVPCRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Get the VPC details + v, count, err := cs.VPC.GetVPCByID(d.Id()) + if err != nil { + if count == 0 { + log.Printf( + "[DEBUG] VPC %s does no longer exist", d.Get("name").(string)) + d.SetId("") + return nil + } + + return err + } + + d.Set("name", v.Name) + d.Set("display_test", v.Displaytext) + d.Set("cidr", v.Cidr) + d.Set("zone", v.Zonename) + + // Get the VPC offering details + o, _, err := cs.VPC.GetVPCOfferingByID(v.Vpcofferingid) + if err != nil { + return err + } + + d.Set("vpc_offering", o.Name) + + return nil +} + +func resourceCloudStackVPCUpdate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Check if the name or display text is changed + if d.HasChange("name") || d.HasChange("display_text") { + // Create a new parameter struct + p := cs.VPC.NewUpdateVPCParams(d.Id(), d.Get("name").(string)) + + // Set the display text + displaytext, ok := d.GetOk("display_text") + if !ok { + displaytext = d.Get("name") + } + // Set the (new) display text + p.SetDisplaytext(displaytext.(string)) + + // Update the VPC + _, err := cs.VPC.UpdateVPC(p) + if err != nil { + return fmt.Errorf( + "Error updating VPC %s: %s", d.Get("name").(string), err) + } + } + + return resourceCloudStackVPCRead(d, meta) +} + +func resourceCloudStackVPCDelete(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Create a new parameter struct + p := cs.VPC.NewDeleteVPCParams(d.Id()) + + // Delete the VPC + _, err := cs.VPC.DeleteVPC(p) + if err != nil { + // This is a very poor way to be told the UUID does no longer exist :( + if strings.Contains(err.Error(), fmt.Sprintf( + "Invalid parameter id value=%s due to incorrect long value format, "+ + "or entity does not exist", d.Id())) { + return nil + } + + return fmt.Errorf("Error deleting VPC %s: %s", d.Get("name").(string), err) + } + + return nil +} diff --git a/builtin/providers/cloudstack/resource_cloudstack_vpc_test.go b/builtin/providers/cloudstack/resource_cloudstack_vpc_test.go new file mode 100644 index 000000000..8142a9046 --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_vpc_test.go @@ -0,0 +1,118 @@ +package cloudstack + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +func TestAccCloudStackVPC_basic(t *testing.T) { + var vpc cloudstack.VPC + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackVPCDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCloudStackVPC_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackVPCExists( + "cloudstack_vpc.foo", &vpc), + testAccCheckCloudStackVPCAttributes(&vpc), + resource.TestCheckResourceAttr( + "cloudstack_vpc.foo", "vpc_offering", CLOUDSTACK_VPC_OFFERING), + ), + }, + }, + }) +} + +func testAccCheckCloudStackVPCExists( + n string, vpc *cloudstack.VPC) 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 VPC ID is set") + } + + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + v, _, err := cs.VPC.GetVPCByID(rs.Primary.ID) + + if err != nil { + return err + } + + if v.Id != rs.Primary.ID { + return fmt.Errorf("VPC not found") + } + + *vpc = *v + + return nil + } +} + +func testAccCheckCloudStackVPCAttributes( + vpc *cloudstack.VPC) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if vpc.Name != "terraform-vpc" { + return fmt.Errorf("Bad name: %s", vpc.Name) + } + + if vpc.Displaytext != "terraform-vpc-text" { + return fmt.Errorf("Bad display text: %s", vpc.Displaytext) + } + + if vpc.Cidr != CLOUDSTACK_VPC_CIDR { + return fmt.Errorf("Bad VPC offering: %s", vpc.Cidr) + } + + return nil + } +} + +func testAccCheckCloudStackVPCDestroy(s *terraform.State) error { + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "cloudstack_vpc" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No VPC ID is set") + } + + p := cs.VPC.NewDeleteVPCParams(rs.Primary.ID) + err, _ := cs.VPC.DeleteVPC(p) + + if err != nil { + return fmt.Errorf( + "Error deleting VPC (%s): %s", + rs.Primary.ID, err) + } + } + + return nil +} + +var testAccCloudStackVPC_basic = fmt.Sprintf(` +resource "cloudstack_vpc" "foo" { + name = "terraform-vpc" + display_text = "terraform-vpc-text" + cidr = "%s" + vpc_offering = "%s" + zone = "%s" +}`, + CLOUDSTACK_VPC_CIDR, + CLOUDSTACK_VPC_OFFERING, + CLOUDSTACK_ZONE) diff --git a/builtin/providers/cloudstack/resources.go b/builtin/providers/cloudstack/resources.go new file mode 100644 index 000000000..445260688 --- /dev/null +++ b/builtin/providers/cloudstack/resources.go @@ -0,0 +1,77 @@ +package cloudstack + +import ( + "fmt" + "log" + "regexp" + + "github.com/xanzy/go-cloudstack/cloudstack" +) + +type retrieveError struct { + name string + value string + err error +} + +func (e *retrieveError) Error() error { + return fmt.Errorf("Error retrieving UUID of %s %s: %s", e.name, e.value, e.err) +} + +func retrieveUUID(cs *cloudstack.CloudStackClient, name, value string) (uuid string, e *retrieveError) { + // If the supplied value isn't a UUID, try to retrieve the UUID ourselves + if isUUID(value) { + return value, nil + } + + log.Printf("[DEBUG] Retrieving UUID of %s: %s", name, value) + + var err error + switch name { + case "disk_offering": + uuid, err = cs.DiskOffering.GetDiskOfferingID(value) + case "virtual_machine": + uuid, err = cs.VirtualMachine.GetVirtualMachineID(value) + case "service_offering": + uuid, err = cs.ServiceOffering.GetServiceOfferingID(value) + case "network_offering": + uuid, err = cs.NetworkOffering.GetNetworkOfferingID(value) + case "vpc_offering": + uuid, err = cs.VPC.GetVPCOfferingID(value) + case "vpc": + uuid, err = cs.VPC.GetVPCID(value) + case "template": + uuid, err = cs.Template.GetTemplateID(value, "all") + case "network": + uuid, err = cs.Network.GetNetworkID(value) + case "zone": + uuid, err = cs.Zone.GetZoneID(value) + case "ipaddress": + p := cs.Address.NewListPublicIpAddressesParams() + p.SetIpaddress(value) + l, e := cs.Address.ListPublicIpAddresses(p) + if e != nil { + err = e + break + } + if l.Count == 1 { + uuid = l.PublicIpAddresses[0].Id + break + } + err = fmt.Errorf("Could not find UUID of IP address: %s", value) + default: + return uuid, &retrieveError{name: name, value: value, + err: fmt.Errorf("Unknown request: %s", name)} + } + + if err != nil { + return uuid, &retrieveError{name: name, value: value, err: err} + } + + return uuid, nil +} + +func isUUID(s string) bool { + re := regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`) + return re.MatchString(s) +} diff --git a/website/source/docs/providers/cloudstack/index.html.markdown b/website/source/docs/providers/cloudstack/index.html.markdown new file mode 100644 index 000000000..43d2f815f --- /dev/null +++ b/website/source/docs/providers/cloudstack/index.html.markdown @@ -0,0 +1,45 @@ +--- +layout: "cloudstack" +page_title: "Provider: CloudStack" +sidebar_current: "docs-cloudstack-index" +description: |- + The CloudStack provider is used to interact with the many resources supported by CloudStack. The provider needs to be configured with a URL pointing to a runnning CloudStack API and the proper credentials before it can be used. +--- + +# CloudStack Provider + +The CloudStack provider is used to interact with the many resources +supported by CloudStack. The provider needs to be configured with a +URL pointing to a runnning CloudStack API and the proper credentials +before it can be used. + +Use the navigation to the left to read about the available resources. + +## Example Usage + +``` +# Configure the CloudStack Provider +provider "cloudstack" { + api_url = "${var.cloudstack_api_url}" + api_key = "${var.cloudstack_api_key}" + secret_key = "${var.cloudstack_secret_key}" +} + +# Create a web server +resource "cloudstack_instance" "web" { + ... +} +``` + +## Argument Reference + +The following arguments are supported: + +* `api_url` - (Required) This is the CloudStack API URL. It must be provided, but + it can also be sourced from the `CLOUDSTACK_API_URL` environment variable. + +* `api_key` - (Required) This is the CloudStack API key. It must be provided, but + it can also be sourced from the `CLOUDSTACK_API_KEY` environment variable. + +* `secret_key` - (Required) This is the CloudStack secret key. It must be provided, + but it can also be sourced from the `CLOUDSTACK_SECRET_KEY` environment variable. diff --git a/website/source/docs/providers/cloudstack/r/disk.html.markdown b/website/source/docs/providers/cloudstack/r/disk.html.markdown new file mode 100644 index 000000000..af87ba220 --- /dev/null +++ b/website/source/docs/providers/cloudstack/r/disk.html.markdown @@ -0,0 +1,58 @@ +--- +layout: "cloudstack" +page_title: "CloudStack: cloudstack_disk" +sidebar_current: "docs-cloudstack-resource-disk" +description: |- + Creates a disk volume from a disk offering. This disk volume will be attached to a virtual machine if the optional parameters are configured. +--- + +# cloudstack\_disk + +Creates a disk volume from a disk offering. This disk volume will be attached to +a virtual machine if the optional parameters are configured. + +## Example Usage + +``` +resource "cloudstack_disk" "default" { + name = "test-disk" + attach = "true" + disk_offering = "custom" + size = 50 + virtual-machine = "server-1" + zone = "zone-1" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the disk volume. Changing this forces a new + resource to be created. + +* `attach` - (Optional) Determines whether or not to attach the disk volume to a + virtual machine (defaults false). + +* `device` - (Optional) The device to map the disk volume to within the guest OS. + +* `disk_offering` - (Required) The name of the disk offering to use for this + disk volume. + +* `size` - (Optional) The size of the disk volume in gigabytes. + +* `shrink_ok` - (Optional) Verifies if the disk volume is allowed to shrink when + resizing (defaults false). + +* `virtual_machine` - (Optional) The name of the virtual machine to which you + want to attach the disk volume. + +* `zone` - (Required) The name of the zone where this disk volume will be available. + Changing this forces a new resource to be created. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the disk volume. +* `device` - The device the disk volume is mapped to within the guest OS. diff --git a/website/source/docs/providers/cloudstack/r/firewall.html.markdown b/website/source/docs/providers/cloudstack/r/firewall.html.markdown new file mode 100644 index 000000000..5558a0e6c --- /dev/null +++ b/website/source/docs/providers/cloudstack/r/firewall.html.markdown @@ -0,0 +1,57 @@ +--- +layout: "cloudstack" +page_title: "CloudStack: cloudstack_firewall" +sidebar_current: "docs-cloudstack-resource-firewall" +description: |- + Creates firewall rules for a given ip address. +--- + +# cloudstack\_firewall + +Creates firewall rules for a given ip address. + +## Example Usage + +``` +resource "cloudstack_firewall" "default" { + ipaddress = "192.168.0.1" + + rule { + source_cidr = "10.0.0.0/8" + protocol = "tcp" + ports = ["80", "1000-2000"] + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `ipaddress` - (Required) The ip address for which to create the firewall rules. + Changing this forces a new resource to be created. + +* `rule` - (Required) Can be specified multiple times. Each rule block supports + fields documented below. + +The `rule` block supports: + +* `source_cidr` - (Required) The source cidr to allow access to the given ports. + +* `protocol` - (Required) The name of the protocol to allow. Valid options are: + `tcp`, `udp` and `icmp`. + +* `icmp_type` - (Optional) The ICMP type to allow. This can only be specified if + the protocol is ICMP. + +* `icmp_code` - (Optional) The ICMP code to allow. This can only be specified if + the protocol is ICMP. + +* `ports` - (Optional) List of ports and/or port ranges to allow. This can only + be specified if the protocol is TCP or UDP. + +## Attributes Reference + +The following attributes are exported: + +* `ipaddress` - The ip address for which the firewall rules are created. diff --git a/website/source/docs/providers/cloudstack/r/instance.html.markdown b/website/source/docs/providers/cloudstack/r/instance.html.markdown new file mode 100644 index 000000000..7b550400c --- /dev/null +++ b/website/source/docs/providers/cloudstack/r/instance.html.markdown @@ -0,0 +1,59 @@ +--- +layout: "cloudstack" +page_title: "CloudStack: cloudstack_instance" +sidebar_current: "docs-cloudstack-resource-instance" +description: |- + Creates and automatically starts a virtual machine based on a service offering, disk offering, and template. +--- + +# cloudstack\_instance + +Creates and automatically starts a virtual machine based on a service offering, +disk offering, and template. + +## Example Usage + +``` +resource "cloudstack_instance" "web" { + ami = "ami-1234" + instance_type = "m1.small" + tags { + Name = "HelloWorld" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the instance. Changing this forces a new + resource to be created. + +* `display_name` - (Optional) The display name of the instance. + +* `service_offering` - (Required) The service offering used for this instance. + +* `network` - (Optional) The name of the network to connect this instance to. + Changing this forces a new resource to be created. + +* `ipaddress` - (Optional) The IP address to assign to this instance. Changing + this forces a new resource to be created. + +* `template` - (Required) The name of the template used for this instance. + Changing this forces a new resource to be created. + +* `zone` - (Required) The name of the zone where this instance will be created. + Changing this forces a new resource to be created. + +* `user_data` - (Optional) The user data to provide when launching the instance. + +* `expunge` - (Optional) This determines if the instance is expunged when it is + destroyed (defaults false) + +## Attributes Reference + +The following attributes are exported: + +* `id` - The instance ID. +* `display_name` - The display name of the instance. diff --git a/website/source/docs/providers/cloudstack/r/ipaddress.html.markdown b/website/source/docs/providers/cloudstack/r/ipaddress.html.markdown new file mode 100644 index 000000000..9f6a23681 --- /dev/null +++ b/website/source/docs/providers/cloudstack/r/ipaddress.html.markdown @@ -0,0 +1,38 @@ +--- +layout: "cloudstack" +page_title: "CloudStack: cloudstack_ipaddress" +sidebar_current: "docs-cloudstack-resource-ipaddress" +description: |- + Acquires and associates a public IP. +--- + +# cloudstack\_ipaddress + +Acquires and associates a public IP. + +## Example Usage + +``` +resource "cloudstack_ipaddress" "default" { + network = "test-network" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `network` - (Optional) The name of the network for which an IP address should + be aquired and accociated. Changing this forces a new resource to be created. + +* `vpc` - (Optional) The name of the vpc for which an IP address should + be aquired and accociated. Changing this forces a new resource to be created. + +*NOTE: Either `network` or `vpc` should have a value!* + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the aquired and accociated IP address. +* `ipaddress` - The IP address that was aquired and accociated. diff --git a/website/source/docs/providers/cloudstack/r/network.html.markdown b/website/source/docs/providers/cloudstack/r/network.html.markdown new file mode 100644 index 000000000..47028f611 --- /dev/null +++ b/website/source/docs/providers/cloudstack/r/network.html.markdown @@ -0,0 +1,54 @@ +--- +layout: "cloudstack" +page_title: "CloudStack: cloudstack_network" +sidebar_current: "docs-cloudstack-resource-network" +description: |- + Creates a network. +--- + +# cloudstack\_network + +Creates a network. + +## Example Usage + +Basic usage: + +``` +resource "cloudstack_network" "default" { + name = "test-network" + cidr = "10.0.0.0/16" + network_offering = "Default Network" + zone = "zone-1" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the network. + +* `display_text` - (Optional) The display text of the network. + +* `cidr` - (Required) The CIDR block for the network. Changing this forces a new + resource to be created. + +* `network_offering` - (Required) The name of the network offering to use for + this network. + +* `vpc` - (Optional) The name of the vpc to create this network for. Changing + this forces a new resource to be created. + +* `aclid` - (Optional) The ID of a network ACL that should be attached to the + network. Changing this forces a new resource to be created. + +* `zone` - (Required) The name of the zone where this disk volume will be + available. Changing this forces a new resource to be created. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the network. +* `display_text` - The display text of the network. diff --git a/website/source/docs/providers/cloudstack/r/network_acl.html.markdown b/website/source/docs/providers/cloudstack/r/network_acl.html.markdown new file mode 100644 index 000000000..a96de9bae --- /dev/null +++ b/website/source/docs/providers/cloudstack/r/network_acl.html.markdown @@ -0,0 +1,62 @@ +--- +layout: "aws" +page_title: "AWS: aws_network_acl" +sidebar_current: "docs-aws-resource-network-acl" +description: |- + Provides an network ACL resource. +--- + +# aws\_network\_acl + +Provides an network ACL resource. You might set up network ACLs with rules similar +to your security groups in order to add an additional layer of security to your VPC. + +## Example Usage + +``` +resource "aws_network_acl" "main" { + vpc_id = "${aws_vpc.main.id}" + egress = { + protocol = "tcp" + rule_no = 2 + action = "allow" + cidr_block = "10.3.2.3/18" + from_port = 443 + to_port = 443 + } + + ingress = { + protocol = "tcp" + rule_no = 1 + action = "allow" + cidr_block = "10.3.10.3/18" + from_port = 80 + to_port = 80 + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `vpc_id` - (Required) The ID of the associated VPC. +* `subnet_id` - (Optional) The ID of the associated subnet. +* `ingress` - (Optional) Specifies an ingress rule. Parameters defined below. +* `egress` - (Optional) Speicifes an egress rule. Parameters defined below. + +Both `egress` and `ingress` support the following keys: + +* `from_port` - (Required) The from port to match. +* `to_port` - (Required) The to port to match. +* `rule_no` - (Required) The rule number. Used for ordering. +* `action` - (Required) The action to take. +* `protocol` - (Required) The protocol to match. +* `cidr_block` - (Optional) The CIDR block to match. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the network ACL + diff --git a/website/source/docs/providers/cloudstack/r/network_acl_rule.html.markdown b/website/source/docs/providers/cloudstack/r/network_acl_rule.html.markdown new file mode 100644 index 000000000..3be2f0898 --- /dev/null +++ b/website/source/docs/providers/cloudstack/r/network_acl_rule.html.markdown @@ -0,0 +1,65 @@ +--- +layout: "cloudstack" +page_title: "CloudStack: cloudstack_network_acl_rule" +sidebar_current: "docs-cloudstack-resource-network_acl_rule" +description: |- + Creates network ACL rules for a given network ACL. +--- + +# cloudstack\_network\_acl\_rule + +Creates network ACL rules for a given network ACL. + +## Example Usage + +``` +resource "cloudstack_network_acl_rule" "default" { + aclid = "f3843ce0-334c-4586-bbd3-0c2e2bc946c6" + + rule { + action = "allow" + source_cidr = "10.0.0.0/8" + protocol = "tcp" + ports = ["80", "1000-2000"] + traffic_type = "ingress" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `aclid` - (Required) The network ACL ID for which to create the rules. + Changing this forces a new resource to be created. + +* `rule` - (Required) Can be specified multiple times. Each rule block supports + fields documented below. + +The `rule` block supports: + +* `action` - (Optional) The action for the rule. Valid options are: `allow` and + `deny` (defaults allow). + +* `source_cidr` - (Required) The source cidr to allow access to the given ports. + +* `protocol` - (Required) The name of the protocol to allow. Valid options are: + `tcp`, `udp`, `icmp`, `all` or a valid protocol number. + +* `icmp_type` - (Optional) The ICMP type to allow. This can only be specified if + the protocol is ICMP. + +* `icmp_code` - (Optional) The ICMP code to allow. This can only be specified if + the protocol is ICMP. + +* `ports` - (Optional) List of ports and/or port ranges to allow. This can only + be specified if the protocol is TCP, UDP, ALL or a valid protocol number. + +* `traffic_type` - (Optional) The traffic type for the rule. Valid options are: + `ingress` or `egress` (defaults ingress). + +## Attributes Reference + +The following attributes are exported: + +* `aclid` - The ACL ID for which the rules are created. diff --git a/website/source/docs/providers/cloudstack/r/nic.html.markdown b/website/source/docs/providers/cloudstack/r/nic.html.markdown new file mode 100644 index 000000000..8633034f9 --- /dev/null +++ b/website/source/docs/providers/cloudstack/r/nic.html.markdown @@ -0,0 +1,43 @@ +--- +layout: "cloudstack" +page_title: "CloudStack: cloudstack_nic" +sidebar_current: "docs-cloudstack-resource-nic" +description: |- + Creates an additional NIC to add a VM to the specified network. +--- + +# cloudstack\_nic + +Creates an additional NIC to add a VM to the specified network. + +## Example Usage + +Basic usage: + +``` +resource "cloudstack_nic" "test" { + network = "network-2" + ipaddress = "192.168.1.1" + virtual_machine = "server-1" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `network` - (Required) The name of the network to plug the NIC into. Changing + this forces a new resource to be created. + +* `ipaddress` - (Optional) The IP address to assign to the NIC. Changing this + forces a new resource to be created. + +* `virtual_machine` - (Required) The name of the virtual machine to which to + attach the NIC. Changing this forces a new resource to be created. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the NIC. +* `ipaddress` - The assigned IP address. diff --git a/website/source/docs/providers/cloudstack/r/port_forward.html.markdown b/website/source/docs/providers/cloudstack/r/port_forward.html.markdown new file mode 100644 index 000000000..e2e5adbb9 --- /dev/null +++ b/website/source/docs/providers/cloudstack/r/port_forward.html.markdown @@ -0,0 +1,53 @@ +--- +layout: "cloudstack" +page_title: "CloudStack: cloudstack_port_forward" +sidebar_current: "docs-cloudstack-resource-port-forward" +description: |- + Creates port forwards. +--- + +# cloudstack\_port\_forward + +Creates port forwards. + +## Example Usage + +``` +resource "cloudstack_port_forward" "default" { + ipaddress = "192.168.0.1" + + forward { + protocol = "tcp" + private_port = 80 + public_port = 8080 + virtual_machine = "server-1" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `ipaddress` - (Required) The ip address for which to create the port forwards. + Changing this forces a new resource to be created. + +* `forward` - (Required) Can be specified multiple times. Each forward block supports + fields documented below. + +The `forward` block supports: + +* `protocol` - (Required) The name of the protocol to allow. Valid options are: + `tcp` and `udp`. + +* `private_port` - (Required) The private port to forward to. + +* `public_port` - (Required) The public port to forward from. + +* `virtual_machine` - (Required) The name of the virtual machine to forward to. + +## Attributes Reference + +The following attributes are exported: + +* `ipaddress` - The ip address for which the port forwards are created. diff --git a/website/source/docs/providers/cloudstack/r/vpc.html.markdown b/website/source/docs/providers/cloudstack/r/vpc.html.markdown new file mode 100644 index 000000000..141bc9203 --- /dev/null +++ b/website/source/docs/providers/cloudstack/r/vpc.html.markdown @@ -0,0 +1,48 @@ +--- +layout: "cloudstack" +page_title: "CloudStack: cloudstack_vpc" +sidebar_current: "docs-cloudstack-resource-vpc" +description: |- + Creates a VPC. +--- + +# cloudstack\_vpc + +Creates a VPC. + +## Example Usage + +Basic usage: + +``` +resource "cloudstack_vpc" "default" { + name = "test-vpc" + cidr = "10.0.0.0/16" + vpc_offering = "Default VPC Offering" + zone = "zone-1" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the VPC. + +* `display_text` - (Optional) The display text of the VPC. + +* `cidr` - (Required) The CIDR block for the VPC. Changing this forces a new + resource to be created. + +* `vpc_offering` - (Required) The name of the VPC offering to use for this VPC. + Changing this forces a new resource to be created. + +* `zone` - (Required) The name of the zone where this disk volume will be + available. Changing this forces a new resource to be created. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the VPC. +* `display_text` - The display text of the VPC. diff --git a/website/source/layouts/cloudstack.erb b/website/source/layouts/cloudstack.erb new file mode 100644 index 000000000..a74abdeac --- /dev/null +++ b/website/source/layouts/cloudstack.erb @@ -0,0 +1,62 @@ +<% wrap_layout :inner do %> + <% content_for :sidebar do %> + + <% end %> + + <%= yield %> + <% end %>