diff --git a/builtin/providers/cloudstack/provider.go b/builtin/providers/cloudstack/provider.go index c5c506fec..3d3d35514 100644 --- a/builtin/providers/cloudstack/provider.go +++ b/builtin/providers/cloudstack/provider.go @@ -53,11 +53,13 @@ func Provider() terraform.ResourceProvider { "cloudstack_network_acl_rule": resourceCloudStackNetworkACLRule(), "cloudstack_nic": resourceCloudStackNIC(), "cloudstack_port_forward": resourceCloudStackPortForward(), + "cloudstack_private_gateway": resourceCloudStackPrivateGateway(), "cloudstack_secondary_ipaddress": resourceCloudStackSecondaryIPAddress(), "cloudstack_security_group": resourceCloudStackSecurityGroup(), "cloudstack_security_group_rule": resourceCloudStackSecurityGroupRule(), "cloudstack_ssh_keypair": resourceCloudStackSSHKeyPair(), "cloudstack_static_nat": resourceCloudStackStaticNAT(), + "cloudstack_static_route": resourceCloudStackStaticRoute(), "cloudstack_template": resourceCloudStackTemplate(), "cloudstack_vpc": resourceCloudStackVPC(), "cloudstack_vpn_connection": resourceCloudStackVPNConnection(), diff --git a/builtin/providers/cloudstack/provider_test.go b/builtin/providers/cloudstack/provider_test.go index 2585c5fa0..2e177d4e8 100644 --- a/builtin/providers/cloudstack/provider_test.go +++ b/builtin/providers/cloudstack/provider_test.go @@ -144,6 +144,21 @@ func testAccPreCheck(t *testing.T) { if v := os.Getenv("CLOUDSTACK_ZONE"); v == "" { t.Fatal("CLOUDSTACK_ZONE must be set for acceptance tests") } + if v := os.Getenv("CLOUDSTACK_PRIVGW_GATEWAY"); v == "" { + t.Fatal("CLOUDSTACK_PRIVGW_GATEWAY must be set for acceptance tests") + } + if v := os.Getenv("CLOUDSTACK_PRIVGW_IPADDRESS"); v == "" { + t.Fatal("CLOUDSTACK_PRIVGW_IPADDRESS must be set for acceptance tests") + } + if v := os.Getenv("CLOUDSTACK_PRIVGW_NETMASK"); v == "" { + t.Fatal("CLOUDSTACK_PRIVGW_NETMASK must be set for acceptance tests") + } + if v := os.Getenv("CLOUDSTACK_PRIVGW_VLAN"); v == "" { + t.Fatal("CLOUDSTACK_PRIVGW_VLAN must be set for acceptance tests") + } + if v := os.Getenv("CLOUDSTACK_STATIC_ROUTE_CIDR"); v == "" { + t.Fatal("CLOUDSTACK_STATIC_ROUTE_CIDR must be set for acceptance tests") + } } // Name of a valid disk offering @@ -223,3 +238,12 @@ var CLOUDSTACK_PROJECT_NETWORK = os.Getenv("CLOUDSTACK_PROJECT_NETWORK") // Name of a zone that exists already var CLOUDSTACK_ZONE = os.Getenv("CLOUDSTACK_ZONE") + +// Details of the private gateway that will be added to VPC testing this, should be done using ROOT keys +var CLOUDSTACK_PRIVGW_GATEWAY = os.Getenv("CLOUDSTACK_PRIVGW_GATEWAY") +var CLOUDSTACK_PRIVGW_IPADDRESS = os.Getenv("CLOUDSTACK_PRIVGW_IPADDRESS") +var CLOUDSTACK_PRIVGW_NETMASK = os.Getenv("CLOUDSTACK_PRIVGW_NETMASK") +var CLOUDSTACK_PRIVGW_VLAN = os.Getenv("CLOUDSTACK_PRIVGW_VLAN") + +// Details of the static route that will be added to private gateway testing this. +var CLOUDSTACK_STATIC_ROUTE_CIDR = os.Getenv("CLOUDSTACK_STATIC_ROUTE_CIDR") diff --git a/builtin/providers/cloudstack/resource_cloudstack_private_gateway.go b/builtin/providers/cloudstack/resource_cloudstack_private_gateway.go new file mode 100644 index 000000000..b4194c0bd --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_private_gateway.go @@ -0,0 +1,173 @@ +package cloudstack + +import ( + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +func resourceCloudStackPrivateGateway() *schema.Resource { + return &schema.Resource{ + Create: resourceCloudStackPrivateGatewayCreate, + Read: resourceCloudStackPrivateGatewayRead, + Update: resourceCloudStackPrivateGatewayUpdate, + Delete: resourceCloudStackPrivateGatewayDelete, + + Schema: map[string]*schema.Schema{ + "gateway": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "ip_address": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "netmask": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "vlan": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "physical_network_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "network_offering": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "acl_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "vpc_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceCloudStackPrivateGatewayCreate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + ipaddress := d.Get("ip_address").(string) + networkofferingid := d.Get("network_offering").(string) + + // Create a new parameter struct + p := cs.VPC.NewCreatePrivateGatewayParams( + d.Get("gateway").(string), + ipaddress, + d.Get("netmask").(string), + d.Get("vlan").(string), + d.Get("vpc_id").(string), + ) + + // Retrieve the network_offering ID + if networkofferingid != "" { + networkofferingid, e := retrieveID(cs, "network_offering", networkofferingid) + if e != nil { + return e.Error() + } + p.SetNetworkofferingid(networkofferingid) + } + + // Check if we want to associate an ACL + if aclid, ok := d.GetOk("acl_id"); ok { + // Set the acl ID + p.SetAclid(aclid.(string)) + } + + // Create the new private gateway + r, err := cs.VPC.CreatePrivateGateway(p) + if err != nil { + return fmt.Errorf("Error creating private gateway for %s: %s", ipaddress, err) + } + + d.SetId(r.Id) + + return resourceCloudStackPrivateGatewayRead(d, meta) +} + +func resourceCloudStackPrivateGatewayRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Get the private gateway details + gw, count, err := cs.VPC.GetPrivateGatewayByID(d.Id()) + if err != nil { + if count == 0 { + log.Printf("[DEBUG] Private gateway %s does no longer exist", d.Id()) + d.SetId("") + return nil + } + + return err + } + + d.Set("gateway", gw.Gateway) + d.Set("ip_address", gw.Ipaddress) + d.Set("netmask", gw.Netmask) + d.Set("vlan", gw.Vlan) + d.Set("acl_id", gw.Aclid) + d.Set("vpc_id", gw.Vpcid) + + return nil +} + +func resourceCloudStackPrivateGatewayUpdate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Replace the ACL if the ID has changed + if d.HasChange("acl_id") { + p := cs.NetworkACL.NewReplaceNetworkACLListParams(d.Get("acl_id").(string)) + p.SetNetworkid(d.Id()) + + _, err := cs.NetworkACL.ReplaceNetworkACLList(p) + if err != nil { + return fmt.Errorf("Error replacing ACL: %s", err) + } + } + + return resourceCloudStackNetworkRead(d, meta) +} + +func resourceCloudStackPrivateGatewayDelete(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Create a new parameter struct + p := cs.VPC.NewDeletePrivateGatewayParams(d.Id()) + + // Delete the private gateway + _, err := cs.VPC.DeletePrivateGateway(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 deleting private gateway %s: %s", d.Id(), err) + } + + return nil +} diff --git a/builtin/providers/cloudstack/resource_cloudstack_private_gateway_test.go b/builtin/providers/cloudstack/resource_cloudstack_private_gateway_test.go new file mode 100644 index 000000000..77ccad870 --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_private_gateway_test.go @@ -0,0 +1,123 @@ +package cloudstack + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +func TestAccCloudStackPrivateGateway_basic(t *testing.T) { + var gateway cloudstack.PrivateGateway + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackPrivateGatewayDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCloudStackPrivateGateway_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackPrivateGatewayExists( + "cloudstack_private_gateway.foo", &gateway), + testAccCheckCloudStackPrivateGatewayAttributes(&gateway), + ), + }, + }, + }) +} + +func testAccCheckCloudStackPrivateGatewayExists( + n string, gateway *cloudstack.PrivateGateway) 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 Private Gateway ID is set") + } + + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + pgw, _, err := cs.VPC.GetPrivateGatewayByID(rs.Primary.ID) + + if err != nil { + return err + } + + if pgw.Id != rs.Primary.ID { + return fmt.Errorf("Private Gateway not found") + } + + *gateway = *pgw + + return nil + } +} + +func testAccCheckCloudStackPrivateGatewayAttributes( + gateway *cloudstack.PrivateGateway) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if gateway.Gateway != CLOUDSTACK_PRIVGW_GATEWAY { + return fmt.Errorf("Bad Gateway: %s", gateway.Gateway) + } + + if gateway.Ipaddress != CLOUDSTACK_PRIVGW_IPADDRESS { + return fmt.Errorf("Bad Gateway: %s", gateway.Ipaddress) + } + + if gateway.Netmask != CLOUDSTACK_PRIVGW_NETMASK { + return fmt.Errorf("Bad Gateway: %s", gateway.Netmask) + } + + return nil + } +} + +func testAccCheckCloudStackPrivateGatewayDestroy(s *terraform.State) error { + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "cloudstack_private_gateway" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No private gateway ID is set") + } + + gateway, _, err := cs.VPC.GetPrivateGatewayByID(rs.Primary.ID) + if err == nil && gateway.Id != "" { + return fmt.Errorf("Private gateway %s still exists", rs.Primary.ID) + } + } + + return nil +} + +var testAccCloudStackPrivateGateway_basic = fmt.Sprintf(` +resource "cloudstack_vpc" "foobar" { + name = "terraform-vpc" + cidr = "%s" + vpc_offering = "%s" + zone = "%s" +} + +resource "cloudstack_private_gateway" "foo" { + gateway = "%s" + ip_address = "%s" + netmask = "%s" + vlan = "%s" + vpc_id = "${cloudstack_vpc.foobar.id}" +}`, + CLOUDSTACK_VPC_CIDR_1, + CLOUDSTACK_VPC_OFFERING, + CLOUDSTACK_ZONE, + CLOUDSTACK_PRIVGW_GATEWAY, + CLOUDSTACK_PRIVGW_IPADDRESS, + CLOUDSTACK_PRIVGW_NETMASK, + CLOUDSTACK_PRIVGW_VLAN) diff --git a/builtin/providers/cloudstack/resource_cloudstack_static_route.go b/builtin/providers/cloudstack/resource_cloudstack_static_route.go new file mode 100644 index 000000000..3eed2878d --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_static_route.go @@ -0,0 +1,94 @@ +package cloudstack + +import ( + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +func resourceCloudStackStaticRoute() *schema.Resource { + return &schema.Resource{ + Create: resourceCloudStackStaticRouteCreate, + Read: resourceCloudStackStaticRouteRead, + Delete: resourceCloudStackStaticRouteDelete, + + Schema: map[string]*schema.Schema{ + "cidr": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "gateway_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceCloudStackStaticRouteCreate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Create a new parameter struct + p := cs.VPC.NewCreateStaticRouteParams( + d.Get("cidr").(string), + d.Get("gateway_id").(string), + ) + + // Create the new private gateway + r, err := cs.VPC.CreateStaticRoute(p) + if err != nil { + return fmt.Errorf("Error creating static route for %s: %s", d.Get("cidr").(string), err) + } + + d.SetId(r.Id) + + return resourceCloudStackStaticRouteRead(d, meta) +} + +func resourceCloudStackStaticRouteRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Get the virtual machine details + staticroute, count, err := cs.VPC.GetStaticRouteByID(d.Id()) + if err != nil { + if count == 0 { + log.Printf("[DEBUG] Static route %s does no longer exist", d.Id()) + d.SetId("") + return nil + } + + return err + } + + d.Set("cidr", staticroute.Cidr) + + return nil +} + +func resourceCloudStackStaticRouteDelete(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Create a new parameter struct + p := cs.VPC.NewDeleteStaticRouteParams(d.Id()) + + // Delete the private gateway + _, err := cs.VPC.DeleteStaticRoute(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 deleting static route for %s: %s", d.Get("cidr").(string), err) + } + + return nil +} diff --git a/builtin/providers/cloudstack/resource_cloudstack_static_route_test.go b/builtin/providers/cloudstack/resource_cloudstack_static_route_test.go new file mode 100644 index 000000000..0aae2ce89 --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_static_route_test.go @@ -0,0 +1,121 @@ +package cloudstack + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +func TestAccCloudStackStaticRoute_basic(t *testing.T) { + var staticroute cloudstack.StaticRoute + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackStaticRouteDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCloudStackStaticRoute_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackStaticRouteExists( + "cloudstack_static_route.bar", &staticroute), + testAccCheckCloudStackStaticRouteAttributes(&staticroute), + ), + }, + }, + }) +} + +func testAccCheckCloudStackStaticRouteExists( + n string, staticroute *cloudstack.StaticRoute) 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 Route ID is set") + } + + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + route, _, err := cs.VPC.GetStaticRouteByID(rs.Primary.ID) + + if err != nil { + return err + } + + if route.Id != rs.Primary.ID { + return fmt.Errorf("Static Route not found") + } + + *staticroute = *route + + return nil + } +} + +func testAccCheckCloudStackStaticRouteAttributes( + staticroute *cloudstack.StaticRoute) resource.TestCheckFunc { + return func(s *terraform.State) error { + + if staticroute.Cidr != CLOUDSTACK_STATIC_ROUTE_CIDR { + return fmt.Errorf("Bad Cidr: %s", staticroute.Cidr) + } + + return nil + } +} + +func testAccCheckCloudStackStaticRouteDestroy(s *terraform.State) error { + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "cloudstack_static_route" { + continue + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No static route ID is set") + } + + staticroute, _, err := cs.VPC.GetStaticRouteByID(rs.Primary.ID) + if err == nil && staticroute.Id != "" { + return fmt.Errorf("Static route %s still exists", rs.Primary.ID) + } + } + + return nil +} + +var testAccCloudStackStaticRoute_basic = fmt.Sprintf(` +resource "cloudstack_vpc" "foobar" { + name = "terraform-vpc" + cidr = "%s" + vpc_offering = "%s" + zone = "%s" +} + +resource "cloudstack_private_gateway" "foo" { + gateway = "%s" + ip_address = "%s" + netmask = "%s" + vlan = "%s" + vpc_id = "${cloudstack_vpc.foobar.id}" +} + +resource "cloudstack_static_route" "bar" { + cidr = "%s" + gateway_id = "${cloudstack_private_gateway.foo.id}" +}`, + CLOUDSTACK_VPC_CIDR_1, + CLOUDSTACK_VPC_OFFERING, + CLOUDSTACK_ZONE, + CLOUDSTACK_PRIVGW_GATEWAY, + CLOUDSTACK_PRIVGW_IPADDRESS, + CLOUDSTACK_PRIVGW_NETMASK, + CLOUDSTACK_PRIVGW_VLAN, + CLOUDSTACK_STATIC_ROUTE_CIDR) diff --git a/website/source/docs/providers/cloudstack/r/private_gateway.html.markdown b/website/source/docs/providers/cloudstack/r/private_gateway.html.markdown new file mode 100644 index 000000000..96e2c517f --- /dev/null +++ b/website/source/docs/providers/cloudstack/r/private_gateway.html.markdown @@ -0,0 +1,58 @@ +--- +layout: "cloudstack" +page_title: "CloudStack: cloudstack_private_gateway" +sidebar_current: "docs-cloudstack-resource-private-gateway" +description: |- + Creates a private gateway. +--- + +# cloudstack\_private\_gateway + +Creates a private gateway for the given VPC. + +*NOTE: private gateway can only be created using a ROOT account!* + +## Example Usage + +``` +resource "cloudstack_private_gateway" "default" { + gateway = 10.0.0.1 + ip_address = "10.0.0.2" + netmask = "255.255.255.252" + vlan = "200" + vpc_id = "76f6e8dc-07e3-4971-b2a2-8831b0cc4cb4" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `gateway` - (Required) the gateway of the Private gateway. Changing this + forces a new resource to be created. + +* `ip_address` - (Required) the IP address of the Private gateway. Changing this forces + a new resource to be created. + +* `netmask` - (Required) The netmask of the Private gateway. Changing + this forces a new resource to be created. + +* `vlan` - (Required) The VLAN number (1-4095) the network will use. + +* `physical_network_id` - (Optional) The ID of the physical network this private + gateway belongs to. + +* `network_offering` - (Optional) The name or ID of the network offering to use for + the private gateways network connection. + +* `acl_id` - (Required) The ACL ID that should be attached to the network. + +* `vpc_id` - (Required) The VPC ID in which to create this Private gateway. Changing + this forces a new resource to be created. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the private gateway. + diff --git a/website/source/docs/providers/cloudstack/r/static_route.html.markdown b/website/source/docs/providers/cloudstack/r/static_route.html.markdown new file mode 100644 index 000000000..0812b4633 --- /dev/null +++ b/website/source/docs/providers/cloudstack/r/static_route.html.markdown @@ -0,0 +1,37 @@ +--- +layout: "cloudstack" +page_title: "CloudStack: cloudstack_static_route" +sidebar_current: "docs-cloudstack-resource-static-route" +description: |- + Creates a static route. +--- + +# cloudstack\_static\_route + +Creates a static route for the given private gateway or VPC. + +## Example Usage + +``` +resource "cloudstack_static_route" "default" { + cidr = "10.0.0.0/16" + gateway_id = "76f607e3-e8dc-4971-8831-b2a2b0cc4cb4" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `cidr` - (Required) The CIDR for the static route. Changing this forces + a new resource to be created. + +* `gateway_id` - (Required) The ID of the Private gateway. Changing this forces + a new resource to be created. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the static route. + diff --git a/website/source/layouts/cloudstack.erb b/website/source/layouts/cloudstack.erb index ab99da78b..bc7ebf216 100644 --- a/website/source/layouts/cloudstack.erb +++ b/website/source/layouts/cloudstack.erb @@ -61,6 +61,10 @@ cloudstack_port_forward + > + cloudstack_private_gateway + + > cloudstack_secondary_ipaddress @@ -81,6 +85,10 @@ cloudstack_static_nat + > + cloudstack_static_route + + > cloudstack_template