From 5e9d385f960c0e4352505c55f5d1e822cef65143 Mon Sep 17 00:00:00 2001 From: Benjamin vickers Date: Thu, 7 May 2015 15:10:57 +0100 Subject: [PATCH] Add cloudstack ssh key resource and attr for instances --- builtin/providers/cloudstack/provider.go | 1 + builtin/providers/cloudstack/provider_test.go | 2 + .../resource_cloudstack_instance.go | 75 +++++--- .../resource_cloudstack_instance_test.go | 20 ++- .../resource_cloudstack_ssh_keypair.go | 122 +++++++++++++ .../resource_cloudstack_ssh_keypair_test.go | 160 ++++++++++++++++++ 6 files changed, 354 insertions(+), 26 deletions(-) create mode 100644 builtin/providers/cloudstack/resource_cloudstack_ssh_keypair.go create mode 100644 builtin/providers/cloudstack/resource_cloudstack_ssh_keypair_test.go diff --git a/builtin/providers/cloudstack/provider.go b/builtin/providers/cloudstack/provider.go index 8cdafd1ab..73ce38b76 100644 --- a/builtin/providers/cloudstack/provider.go +++ b/builtin/providers/cloudstack/provider.go @@ -45,6 +45,7 @@ func Provider() terraform.ResourceProvider { "cloudstack_network_acl_rule": resourceCloudStackNetworkACLRule(), "cloudstack_nic": resourceCloudStackNIC(), "cloudstack_port_forward": resourceCloudStackPortForward(), + "cloudstack_ssh_keypair": resourceCloudStackSSHKeyPair(), "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 c9da7dc8a..8d684a673 100644 --- a/builtin/providers/cloudstack/provider_test.go +++ b/builtin/providers/cloudstack/provider_test.go @@ -82,6 +82,8 @@ var CLOUDSTACK_VPC_OFFERING = "" var CLOUDSTACK_VPC_NETWORK_CIDR = "" var CLOUDSTACK_VPC_NETWORK_OFFERING = "" var CLOUDSTACK_PUBLIC_IPADDRESS = "" +var CLOUDSTACK_SSH_KEYPAIR = "" +var CLOUDSTACK_SSH_PUBLIC_KEY = "" var CLOUDSTACK_TEMPLATE = "" var CLOUDSTACK_TEMPLATE_FORMAT = "" var CLOUDSTACK_TEMPLATE_URL = "" diff --git a/builtin/providers/cloudstack/resource_cloudstack_instance.go b/builtin/providers/cloudstack/resource_cloudstack_instance.go index c07678f3e..59079b7de 100644 --- a/builtin/providers/cloudstack/resource_cloudstack_instance.go +++ b/builtin/providers/cloudstack/resource_cloudstack_instance.go @@ -62,6 +62,11 @@ func resourceCloudStackInstance() *schema.Resource { ForceNew: true, }, + "keypair": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "user_data": &schema.Schema{ Type: schema.TypeString, Optional: true, @@ -136,6 +141,10 @@ func resourceCloudStackInstanceCreate(d *schema.ResourceData, meta interface{}) p.SetIpaddress(ipaddres.(string)) } + if keypair, ok := d.GetOk("keypair"); ok { + p.SetKeypair(keypair.(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 { @@ -186,6 +195,7 @@ func resourceCloudStackInstanceRead(d *schema.ResourceData, meta interface{}) er d.Set("display_name", vm.Displayname) d.Set("ipaddress", vm.Nic[0].Ipaddress) d.Set("zone", vm.Zonename) + //NB cloudstack sometimes sends back the wrong keypair name, so dont update it setValueOrUUID(d, "network", vm.Nic[0].Networkname, vm.Nic[0].Networkid) setValueOrUUID(d, "service_offering", vm.Serviceofferingname, vm.Serviceofferingid) @@ -220,41 +230,58 @@ func resourceCloudStackInstanceUpdate(d *schema.ResourceData, meta interface{}) 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 + // Attributes that require reboot to update + if d.HasChange("service_offering") || d.HasChange("keypair") { + // Before we can actually make these changes, 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) + "Error stopping instance %s before making changes: %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) + + // 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) + + // 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) + } + d.SetPartial("service_offering") } + + if d.HasChange("keypair") { + log.Printf("[DEBUG] SSH keypair changed for %s, starting update", name) + + p := cs.SSH.NewResetSSHKeyForVirtualMachineParams(d.Id(), d.Get("keypair").(string)) + + // Change the ssh keypair + _, err = cs.SSH.ResetSSHKeyForVirtualMachine(p) + if err != nil { + return fmt.Errorf( + "Error changing the SSH keypair for instance %s: %s", name, err) + } + d.SetPartial("keypair") + } + // 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) + "Error starting instance %s after making changes", name) } - - d.SetPartial("service_offering") } - d.Partial(false) return resourceCloudStackInstanceRead(d, meta) } diff --git a/builtin/providers/cloudstack/resource_cloudstack_instance_test.go b/builtin/providers/cloudstack/resource_cloudstack_instance_test.go index 2d1be9c53..d5ff3fe59 100644 --- a/builtin/providers/cloudstack/resource_cloudstack_instance_test.go +++ b/builtin/providers/cloudstack/resource_cloudstack_instance_test.go @@ -27,6 +27,10 @@ func TestAccCloudStackInstance_basic(t *testing.T) { "cloudstack_instance.foobar", "user_data", "0cf3dcdc356ec8369494cb3991985ecd5296cdd5"), + resource.TestCheckResourceAttr( + "cloudstack_instance.foobar", + "keypair", + CLOUDSTACK_SSH_KEYPAIR), ), }, }, @@ -47,6 +51,14 @@ func TestAccCloudStackInstance_update(t *testing.T) { testAccCheckCloudStackInstanceExists( "cloudstack_instance.foobar", &instance), testAccCheckCloudStackInstanceAttributes(&instance), + resource.TestCheckResourceAttr( + "cloudstack_instance.foobar", + "user_data", + "0cf3dcdc356ec8369494cb3991985ecd5296cdd5"), + resource.TestCheckResourceAttr( + "cloudstack_instance.foobar", + "keypair", + CLOUDSTACK_SSH_KEYPAIR), ), }, @@ -193,13 +205,15 @@ resource "cloudstack_instance" "foobar" { network = "%s" template = "%s" zone = "%s" + keypair = "%s" user_data = "foobar\nfoo\nbar" expunge = true }`, CLOUDSTACK_SERVICE_OFFERING_1, CLOUDSTACK_NETWORK_1, CLOUDSTACK_TEMPLATE, - CLOUDSTACK_ZONE) + CLOUDSTACK_ZONE, + CLOUDSTACK_SSH_KEYPAIR) var testAccCloudStackInstance_renameAndResize = fmt.Sprintf(` resource "cloudstack_instance" "foobar" { @@ -209,13 +223,15 @@ resource "cloudstack_instance" "foobar" { network = "%s" template = "%s" zone = "%s" + keypair = "%s" user_data = "foobar\nfoo\nbar" expunge = true }`, CLOUDSTACK_SERVICE_OFFERING_2, CLOUDSTACK_NETWORK_1, CLOUDSTACK_TEMPLATE, - CLOUDSTACK_ZONE) + CLOUDSTACK_ZONE, + CLOUDSTACK_SSH_KEYPAIR) var testAccCloudStackInstance_fixedIP = fmt.Sprintf(` resource "cloudstack_instance" "foobar" { diff --git a/builtin/providers/cloudstack/resource_cloudstack_ssh_keypair.go b/builtin/providers/cloudstack/resource_cloudstack_ssh_keypair.go new file mode 100644 index 000000000..12a7f1b38 --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_ssh_keypair.go @@ -0,0 +1,122 @@ +package cloudstack + +import ( + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +func resourceCloudStackSSHKeyPair() *schema.Resource { + return &schema.Resource{ + Create: resourceCloudStackSSHKeyPairCreate, + Read: resourceCloudStackSSHKeyPairRead, + Delete: resourceCloudStackSSHKeyPairDelete, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "public_key": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "private_key": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "fingerprint": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceCloudStackSSHKeyPairCreate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + name := d.Get("name").(string) + publicKey := d.Get("public_key").(string) + + if publicKey != "" { + //register key supplied + p := cs.SSH.NewRegisterSSHKeyPairParams(name, publicKey) + r, err := cs.SSH.RegisterSSHKeyPair(p) + if err != nil { + return err + } + log.Printf("[DEBUG] RegisterSSHKeyPair response: %+v\n", r) + log.Printf("[DEBUG] Key pair successfully registered at Cloudstack") + d.SetId(name) + } else { + //no key supplied, must create one and return the private key + p := cs.SSH.NewCreateSSHKeyPairParams(name) + r, err := cs.SSH.CreateSSHKeyPair(p) + if err != nil { + return err + } + log.Printf("[DEBUG] CreateSSHKeyPair response: %+v\n", r) + log.Printf("[DEBUG] Key pair successfully generated at Cloudstack") + log.Printf("[DEBUG] Private key returned: %s", r.Privatekey) + d.Set("private_key", r.Privatekey) + d.SetId(name) + } + + return resourceCloudStackSSHKeyPairRead(d, meta) +} + +func resourceCloudStackSSHKeyPairRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + log.Printf("[DEBUG] looking for ssh key %s with name %s", d.Id(), d.Get("name").(string)) + + p := cs.SSH.NewListSSHKeyPairsParams() + p.SetName(d.Get("name").(string)) + + r, err := cs.SSH.ListSSHKeyPairs(p) + if err != nil { + return err + } + if r.Count == 0 { + log.Printf("[DEBUG] Key pair %s does not exist", d.Get("name").(string)) + d.Set("name", "") + return nil + } + + //SSHKeyPair name is unique in a cloudstack account so dont need to check for multiple + d.Set("name", r.SSHKeyPairs[0].Name) + d.Set("fingerprint", r.SSHKeyPairs[0].Fingerprint) + log.Printf("[DEBUG] Read ssh key pair %+v\n", d) + + return nil +} + +func resourceCloudStackSSHKeyPairDelete(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + // Create a new parameter struct + p := cs.SSH.NewDeleteSSHKeyPairParams(d.Get("name").(string)) + + // Remove the SSH Keypair + _, err := cs.SSH.DeleteSSHKeyPair(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( + "A key pair with name '%s' does not exist for account", d.Get("name").(string))) { + return nil + } + + return fmt.Errorf("Error deleting SSH Keypair: %s", err) + } + + return nil +} diff --git a/builtin/providers/cloudstack/resource_cloudstack_ssh_keypair_test.go b/builtin/providers/cloudstack/resource_cloudstack_ssh_keypair_test.go new file mode 100644 index 000000000..06c34f853 --- /dev/null +++ b/builtin/providers/cloudstack/resource_cloudstack_ssh_keypair_test.go @@ -0,0 +1,160 @@ +package cloudstack + +import ( + "fmt" + "log" + "strings" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/xanzy/go-cloudstack/cloudstack" +) + +func TestAccCloudStackSSHKeyPair_create(t *testing.T) { + var sshkey cloudstack.SSHKeyPair + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackSSHKeyPairDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCloudStackSSHKeyPair_create, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackSSHKeyPairExists("cloudstack_ssh_keypair.foo", &sshkey), + testAccCheckCloudStackSSHKeyPairAttributes(&sshkey), + testAccCheckCloudStackSSHKeyPairCreateAttributes("cloudstack_ssh_keypair.foo"), + ), + }, + }, + }) +} + +func TestAccCloudStackSSHKeyPair_register(t *testing.T) { + var sshkey cloudstack.SSHKeyPair + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackSSHKeyPairDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCloudStackSSHKeyPair_register, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackSSHKeyPairExists("cloudstack_ssh_keypair.foo", &sshkey), + testAccCheckCloudStackSSHKeyPairAttributes(&sshkey), + resource.TestCheckResourceAttr( + "cloudstack_ssh_keypair.foo", + "public_key", + CLOUDSTACK_SSH_PUBLIC_KEY), + ), + }, + }, + }) +} + +func testAccCheckCloudStackSSHKeyPairExists(n string, sshkey *cloudstack.SSHKeyPair) 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.Attributes["name"] == "" { + return fmt.Errorf("No ssh key name is set") + } + + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + p := cs.SSH.NewListSSHKeyPairsParams() + p.SetName(rs.Primary.Attributes["name"]) + list, err := cs.SSH.ListSSHKeyPairs(p) + + if err != nil { + return err + } + + if list.Count == 1 && list.SSHKeyPairs[0].Name == rs.Primary.Attributes["name"] { + //ssh key exists + *sshkey = *list.SSHKeyPairs[0] + return nil + } + + return fmt.Errorf("SSH key not found") + } +} + +func testAccCheckCloudStackSSHKeyPairAttributes( + sshkey *cloudstack.SSHKeyPair) resource.TestCheckFunc { + return func(s *terraform.State) error { + + fingerprintLen := len(sshkey.Fingerprint) + if fingerprintLen != 47 { + return fmt.Errorf( + "SSH key: Attribute private_key expected length 47, got %d", + fingerprintLen) + } + + return nil + } +} + +func testAccCheckCloudStackSSHKeyPairCreateAttributes( + name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + ms := s.RootModule() + rs, ok := ms.Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + is := rs.Primary + if is == nil { + return fmt.Errorf("No primary instance: %s", name) + } + + log.Printf("Private key calculated: %s", is.Attributes["private_key"]) + if !strings.Contains(is.Attributes["private_key"], "PRIVATE KEY") { + return fmt.Errorf( + "SSH key: Attribute private_key expected 'PRIVATE KEY' to be present, got %s", + is.Attributes["private_key"]) + } + return nil + } +} + +func testAccCheckCloudStackSSHKeyPairDestroy(s *terraform.State) error { + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "cloudstack_ssh_keypair" { + continue + } + + if rs.Primary.Attributes["name"] == "" { + return fmt.Errorf("No ssh key name is set") + } + + p := cs.SSH.NewDeleteSSHKeyPairParams(rs.Primary.Attributes["name"]) + _, err := cs.SSH.DeleteSSHKeyPair(p) + + if err != nil { + return fmt.Errorf( + "Error deleting ssh key (%s): %s", + rs.Primary.Attributes["name"], err) + } + } + + return nil +} + +var testAccCloudStackSSHKeyPair_create = fmt.Sprintf(` +resource "cloudstack_ssh_keypair" "foo" { + name = "terraform-testacc" +}`) + +var testAccCloudStackSSHKeyPair_register = fmt.Sprintf(` +resource "cloudstack_ssh_keypair" "foo" { + name = "terraform-testacc" + public_key = "%s" +}`, CLOUDSTACK_SSH_PUBLIC_KEY)