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 %>