Merge pull request #645 from svanharmelen/f-cloudstack-provider
First release of a provider for CloudStack
This commit is contained in:
commit
17bc60fb68
|
@ -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,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
package main
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 = ""
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
|
@ -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)
|
||||||
|
}
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -0,0 +1,62 @@
|
||||||
|
<% wrap_layout :inner do %>
|
||||||
|
<% content_for :sidebar do %>
|
||||||
|
<div class="docs-sidebar hidden-print affix-top" role="complementary">
|
||||||
|
<ul class="nav docs-sidenav">
|
||||||
|
<li<%= sidebar_current("docs-home") %>>
|
||||||
|
<a href="/docs/index.html">« Documentation Home</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li<%= sidebar_current("docs-cloudstack-index") %>>
|
||||||
|
<a href="/docs/providers/cloudstack/index.html">CloudStack Provider</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li<%= sidebar_current("docs-cloudstack-resource") %>>
|
||||||
|
<a href="#">Resources</a>
|
||||||
|
<ul class="nav nav-visible">
|
||||||
|
<li<%= sidebar_current("docs-cloudstack-resource-disk") %>>
|
||||||
|
<a href="/docs/providers/cloudstack/r/disk.html">cloudstack_disk</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li<%= sidebar_current("docs-cloudstack-resource-firewall") %>>
|
||||||
|
<a href="/docs/providers/cloudstack/r/firewall.html">cloudstack_firewall</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li<%= sidebar_current("docs-cloudstack-resource-instance") %>>
|
||||||
|
<a href="/docs/providers/cloudstack/r/instance.html">cloudstack_instance</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li<%= sidebar_current("docs-cloudstack-resource-ipaddress") %>>
|
||||||
|
<a href="/docs/providers/cloudstack/r/ipaddress.html">cloudstack_ipaddress</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li<%= sidebar_current("docs-cloudstack-resource-network") %>>
|
||||||
|
<a href="/docs/providers/cloudstack/r/network.html">cloudstack_network</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li<%= sidebar_current("docs-cloudstack-resource-network_acl") %>>
|
||||||
|
<a href="/docs/providers/cloudstack/r/network_acl.html">cloudstack_network_acl</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li<%= sidebar_current("docs-cloudstack-resource-network_acl_rule") %>>
|
||||||
|
<a href="/docs/providers/cloudstack/r/network_acl_rule.html">cloudstack_network_acl_rule</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li<%= sidebar_current("docs-cloudstack-resource-nic") %>>
|
||||||
|
<a href="/docs/providers/cloudstack/r/nic.html">cloudstack_nic</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li<%= sidebar_current("docs-cloudstack-resource-port-forward") %>>
|
||||||
|
<a href="/docs/providers/cloudstack/r/port_forward.html">cloudstack_port_forward</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li<%= sidebar_current("docs-cloudstack-resource-vpc") %>>
|
||||||
|
<a href="/docs/providers/cloudstack/r/vpc.html">cloudstack_vpc</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= yield %>
|
||||||
|
<% end %>
|
Loading…
Reference in New Issue