diff --git a/builtin/providers/cloudstack/provider.go b/builtin/providers/cloudstack/provider.go index ac2f0f521..a65cc148f 100644 --- a/builtin/providers/cloudstack/provider.go +++ b/builtin/providers/cloudstack/provider.go @@ -54,6 +54,7 @@ func Provider() terraform.ResourceProvider { "cloudstack_port_forward": resourceCloudStackPortForward(), "cloudstack_secondary_ipaddress": resourceCloudStackSecondaryIPAddress(), "cloudstack_ssh_keypair": resourceCloudStackSSHKeyPair(), + "cloudstack_static_nat": resourceCloudStackStaticNAT(), "cloudstack_template": resourceCloudStackTemplate(), "cloudstack_vpc": resourceCloudStackVPC(), "cloudstack_vpn_connection": resourceCloudStackVPNConnection(), diff --git a/builtin/providers/cloudstack/resource_cloudstack_ipaddress.go b/builtin/providers/cloudstack/resource_cloudstack_ipaddress.go index e2e590f6b..41d9b0851 100644 --- a/builtin/providers/cloudstack/resource_cloudstack_ipaddress.go +++ b/builtin/providers/cloudstack/resource_cloudstack_ipaddress.go @@ -99,7 +99,7 @@ func resourceCloudStackIPAddressCreate(d *schema.ResourceData, meta interface{}) func resourceCloudStackIPAddressRead(d *schema.ResourceData, meta interface{}) error { cs := meta.(*cloudstack.CloudStackClient) - // Get the network ACL list details + // Get the IP address details f, count, err := cs.Address.GetPublicIpAddressByID(d.Id()) if err != nil { if count == 0 { diff --git a/builtin/providers/cloudstack/resource_cloudstack_static_nat.go b/builtin/providers/cloudstack/resource_cloudstack_static_nat.go new file mode 100644 index 000000000..0f7d7a439 --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_static_nat.go @@ -0,0 +1,156 @@ +package cloudstack + +import ( + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +func resourceCloudStackStaticNAT() *schema.Resource { + return &schema.Resource{ + Create: resourceCloudStackStaticNATCreate, + Exists: resourceCloudStackStaticNATExists, + Read: resourceCloudStackStaticNATRead, + Delete: resourceCloudStackStaticNATDelete, + + Schema: map[string]*schema.Schema{ + "ipaddress": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "network": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "virtual_machine": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "vm_guest_ip": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + }, + } +} + +func resourceCloudStackStaticNATCreate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Retrieve the ipaddress ID + ipaddressid, e := retrieveID(cs, "ipaddress", d.Get("ipaddress").(string)) + if e != nil { + return e.Error() + } + + // Retrieve the virtual_machine ID + virtualmachineid, e := retrieveID(cs, "virtual_machine", d.Get("virtual_machine").(string)) + if e != nil { + return e.Error() + } + + // Create a new parameter struct + p := cs.NAT.NewEnableStaticNatParams(ipaddressid, virtualmachineid) + + if network, ok := d.GetOk("network"); ok { + // Retrieve the network ID + networkid, e := retrieveID(cs, "network", network.(string)) + if e != nil { + return e.Error() + } + + p.SetNetworkid(networkid) + } + + if vmGuestIP, ok := d.GetOk("vm_guest_ip"); ok { + p.SetVmguestip(vmGuestIP.(string)) + } + + _, err := cs.NAT.EnableStaticNat(p) + if err != nil { + return fmt.Errorf("Error enabling static NAT: %s", err) + } + + d.SetId(ipaddressid) + + return resourceCloudStackStaticNATRead(d, meta) +} + +func resourceCloudStackStaticNATExists(d *schema.ResourceData, meta interface{}) (bool, error) { + cs := meta.(*cloudstack.CloudStackClient) + + // Get the IP address details + ip, count, err := cs.Address.GetPublicIpAddressByID(d.Id()) + if err != nil { + if count == 0 { + log.Printf("[DEBUG] IP address with ID %s no longer exists", d.Id()) + return false, nil + } + + return false, err + } + + return ip.Isstaticnat, nil +} + +func resourceCloudStackStaticNATRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Get the IP address details + ip, count, err := cs.Address.GetPublicIpAddressByID(d.Id()) + if err != nil { + if count == 0 { + log.Printf("[DEBUG] IP address with ID %s no longer exists", d.Id()) + d.SetId("") + return nil + } + + return err + } + + if !ip.Isstaticnat { + log.Printf("[DEBUG] Static NAT is no longer enabled for IP address with ID %s", d.Id()) + d.SetId("") + return nil + } + + setValueOrID(d, "network", ip.Associatednetworkname, ip.Associatednetworkid) + setValueOrID(d, "virtual_machine", ip.Virtualmachinename, ip.Virtualmachineid) + d.Set("vm_guest_ip", ip.Vmipaddress) + + return nil +} + +func resourceCloudStackStaticNATDelete(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Create a new parameter struct + p := cs.NAT.NewDisableStaticNatParams(d.Id()) + + // Disable static NAT + _, err := cs.NAT.DisableStaticNat(p) + if err != nil { + // This is a very poor way to be told the ID 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 disabling static NAT: %s", err) + } + + return nil +} diff --git a/builtin/providers/cloudstack/resource_cloudstack_static_nat_test.go b/builtin/providers/cloudstack/resource_cloudstack_static_nat_test.go new file mode 100644 index 000000000..f6b86364f --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_static_nat_test.go @@ -0,0 +1,128 @@ +package cloudstack + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +func TestAccCloudStackStaticNAT_basic(t *testing.T) { + var ipaddr cloudstack.PublicIpAddress + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackStaticNATDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCloudStackStaticNAT_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackStaticNATExists( + "cloudstack_static_nat.foo", &ipaddr), + testAccCheckCloudStackStaticNATAttributes(&ipaddr), + ), + }, + }, + }) +} + +func testAccCheckCloudStackStaticNATExists( + 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 static NAT ID is set") + } + + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + ip, _, err := cs.Address.GetPublicIpAddressByID(rs.Primary.ID) + + if err != nil { + return err + } + + if ip.Id != rs.Primary.ID { + return fmt.Errorf("Static NAT not found") + } + + if !ip.Isstaticnat { + return fmt.Errorf("Static NAT not enabled") + } + + *ipaddr = *ip + + return nil + } +} + +func testAccCheckCloudStackStaticNATAttributes( + 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) + } + + if ipaddr.Virtualmachinename != "terraform-test" { + return fmt.Errorf("Bad virtual_machine: %s", ipaddr.Virtualmachinename) + } + + return nil + } +} + +func testAccCheckCloudStackStaticNATDestroy(s *terraform.State) error { + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "cloudstack_static_nat" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No static NAT ID is set") + } + + ip, _, err := cs.Address.GetPublicIpAddressByID(rs.Primary.ID) + if err == nil && ip.Isstaticnat { + return fmt.Errorf("Static NAT %s still enabled", rs.Primary.ID) + } + } + + return nil +} + +var testAccCloudStackStaticNAT_basic = fmt.Sprintf(` +resource "cloudstack_instance" "foobar" { + name = "terraform-test" + display_name = "terraform-test" + service_offering= "%s" + network = "%s" + template = "%s" + zone = "%s" + user_data = "foobar\nfoo\nbar" + expunge = true +} + +resource "cloudstack_ipaddress" "foo" { + network = "%s" +} + +resource "cloudstack_static_nat" "foo" { + ipaddress = "${cloudstack_ipaddress.foo.id}" + network = "${cloudstack_ipaddress.foo.network}" + virtual_machine = "${cloudstack_instance.foobar.id}" +}`, + CLOUDSTACK_SERVICE_OFFERING_1, + CLOUDSTACK_NETWORK_1, + CLOUDSTACK_TEMPLATE, + CLOUDSTACK_ZONE, + CLOUDSTACK_NETWORK_1, +) diff --git a/website/source/docs/providers/cloudstack/r/static_nat.html.markdown b/website/source/docs/providers/cloudstack/r/static_nat.html.markdown new file mode 100644 index 000000000..f899309a0 --- /dev/null +++ b/website/source/docs/providers/cloudstack/r/static_nat.html.markdown @@ -0,0 +1,50 @@ +--- +layout: "cloudstack" +page_title: "CloudStack: cloudstack_static_nat" +sidebar_current: "docs-cloudstack-resource-static-nat" +description: |- + Enables static NAT for a given IP address. +--- + +# cloudstack\_static\_nat + +Enables static NAT for a given IP address + +## Example Usage + +``` +resource "cloudstack_static_nat" "default" { + ipaddress = "192.168.0.1" + virtual_machine = "server-1" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `ipaddress` - (Required) The name or ID of the public IP address for which + static NAT will be enabled. Changing this forces a new resource to be + created. + +* `network` - (Optional) The name or ID of the network of the VM the static + NAT will be enabled for. Required when public IP address is not + associated with any guest network yet (VPC case). Changing this forces + a new resource to be created. + +* `virtual_machine` - (Required) The name or ID of the virtual machine to + enable the static NAT feature for. Changing this forces a new resource + to be created. + +* `vm_guest_ip` - (Optional) The virtual machine IP address for the port + forwarding rule (useful when the virtual machine has a secondairy NIC). + Changing this forces a new resource to be created. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The static nat ID. +* `network` - The network the public IP address is associated with. +* `vm_guest_ip` - The IP address of the virtual machine that is used + for the port forwarding rule.