diff --git a/cloudstack/resource_cloudstack_instance.go b/cloudstack/resource_cloudstack_instance.go index a4e2caec..09e345d6 100644 --- a/cloudstack/resource_cloudstack_instance.go +++ b/cloudstack/resource_cloudstack_instance.go @@ -139,9 +139,17 @@ func resourceCloudStackInstance() *schema.Resource { ForceNew: true, }, - "keypair": { - Type: schema.TypeString, - Optional: true, + "keypair": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"keypairs"}, + }, + + "keypairs": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + ConflictsWith: []string{"keypair"}, }, "host_id": { @@ -213,6 +221,7 @@ func resourceCloudStackInstance() *schema.Resource { } func resourceCloudStackInstanceCreate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) // Retrieve the service_offering ID @@ -354,6 +363,14 @@ func resourceCloudStackInstanceCreate(d *schema.ResourceData, meta interface{}) p.SetKeypair(keypair.(string)) } + if keypairs, ok := d.GetOk("keypairs"); ok { + var keypairStrings []string + for _, kp := range keypairs.([]interface{}) { + keypairStrings = append(keypairStrings, fmt.Sprintf("%v", kp)) + } + p.SetKeypairs(keypairStrings) + } + // If a host_id is supplied, add it to the parameter struct if hostid, ok := d.GetOk("host_id"); ok { @@ -493,6 +510,7 @@ func resourceCloudStackInstanceRead(d *schema.ResourceData, meta interface{}) er } func resourceCloudStackInstanceUpdate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) name := d.Get("name").(string) @@ -537,7 +555,8 @@ func resourceCloudStackInstanceUpdate(d *schema.ResourceData, meta interface{}) // Attributes that require reboot to update if d.HasChange("name") || d.HasChange("service_offering") || d.HasChange("affinity_group_ids") || - d.HasChange("affinity_group_names") || d.HasChange("keypair") || d.HasChange("user_data") { + d.HasChange("affinity_group_names") || d.HasChange("keypair") || d.HasChange("keypairs") || d.HasChange("user_data") { + // Before we can actually make these changes, the virtual machine must be stopped _, err := cs.VirtualMachine.StopVirtualMachine( cs.VirtualMachine.NewStopVirtualMachineParams(d.Id())) @@ -631,8 +650,8 @@ func resourceCloudStackInstanceUpdate(d *schema.ResourceData, meta interface{}) } // Check if the keypair has changed and if so, update the keypair - if d.HasChange("keypair") { - log.Printf("[DEBUG] SSH keypair changed for %s, starting update", name) + if d.HasChange("keypair") || d.HasChange("keypairs") { + log.Printf("[DEBUG] SSH keypair(s) changed for %s, starting update", name) p := cs.SSH.NewResetSSHKeyForVirtualMachineParams(d.Id()) @@ -640,6 +659,26 @@ func resourceCloudStackInstanceUpdate(d *schema.ResourceData, meta interface{}) p.SetKeypair(keypair.(string)) } + if keypairs, ok := d.GetOk("keypairs"); ok { + + // Convert keypairsInterface to []interface{} + keypairsInterfaces := keypairs.([]interface{}) + + // Now, safely convert []interface{} to []string with error handling + strKeyPairs := make([]string, len(keypairsInterfaces)) + + for i, v := range keypairsInterfaces { + switch v := v.(type) { + case string: + strKeyPairs[i] = v + default: + log.Printf("Value at index %d is not a string: %v", i, v) + continue + } + } + p.SetKeypairs(strKeyPairs) + } + // If there is a project supplied, we retrieve and set the project id if err := setProjectid(p, cs, d); err != nil { return err @@ -648,7 +687,7 @@ func resourceCloudStackInstanceUpdate(d *schema.ResourceData, meta interface{}) _, err = cs.SSH.ResetSSHKeyForVirtualMachine(p) if err != nil { return fmt.Errorf( - "Error changing the SSH keypair for instance %s: %s", name, err) + "Error changing the SSH keypair(s) for instance %s: %s", name, err) } } diff --git a/cloudstack/resource_cloudstack_instance_test.go b/cloudstack/resource_cloudstack_instance_test.go index 38307dc3..d7eeada2 100644 --- a/cloudstack/resource_cloudstack_instance_test.go +++ b/cloudstack/resource_cloudstack_instance_test.go @@ -150,6 +150,68 @@ func TestAccCloudStackInstance_keyPair(t *testing.T) { }) } +func TestAccCloudStackInstance_keyPairs(t *testing.T) { + var instance cloudstack.VirtualMachine + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackInstanceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackInstance_keyPairs, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackInstanceExists("cloudstack_instance.foobar", &instance), + resource.TestCheckResourceAttr("cloudstack_instance.foobar", "keypairs.#", "2"), // Expecting 2 key pairs + resource.TestCheckResourceAttr("cloudstack_instance.foobar", "keypairs.0", "terraform-test-keypair-foo"), + resource.TestCheckResourceAttr("cloudstack_instance.foobar", "keypairs.1", "terraform-test-keypair-bar"), + ), + }, + }, + }) +} + +func TestAccCloudStackInstance_keyPair_update(t *testing.T) { + var instance cloudstack.VirtualMachine + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackInstanceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCloudStackInstance_keyPair, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackInstanceExists( + "cloudstack_instance.foobar", &instance), + resource.TestCheckResourceAttr( + "cloudstack_instance.foobar", "keypair", "terraform-test-keypair"), + ), + }, + + { + Config: testAccCloudStackInstance_keyPairs, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackInstanceExists("cloudstack_instance.foobar", &instance), + resource.TestCheckResourceAttr("cloudstack_instance.foobar", "keypairs.#", "2"), // Expecting 2 key pairs + resource.TestCheckResourceAttr("cloudstack_instance.foobar", "keypairs.0", "terraform-test-keypair-foo"), + resource.TestCheckResourceAttr("cloudstack_instance.foobar", "keypairs.1", "terraform-test-keypair-bar"), + ), + }, + + { + Config: testAccCloudStackInstance_keyPair, + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackInstanceExists( + "cloudstack_instance.foobar", &instance), + resource.TestCheckResourceAttr( + "cloudstack_instance.foobar", "keypair", "terraform-test-keypair"), + ), + }, + }, + }) +} + func TestAccCloudStackInstance_project(t *testing.T) { var instance cloudstack.VirtualMachine @@ -416,6 +478,33 @@ resource "cloudstack_instance" "foobar" { expunge = true }` +const testAccCloudStackInstance_keyPairs = ` +resource "cloudstack_network" "foo" { + name = "terraform-network" + cidr = "10.1.1.0/24" + network_offering = "DefaultIsolatedNetworkOfferingWithSourceNatService" + zone = "Sandbox-simulator" +} + +resource "cloudstack_ssh_keypair" "foo" { + name = "terraform-test-keypair-foo" +} + +resource "cloudstack_ssh_keypair" "bar" { + name = "terraform-test-keypair-bar" +} + +resource "cloudstack_instance" "foobar" { + name = "terraform-test" + display_name = "terraform-test" + service_offering= "Small Instance" + network_id = "${cloudstack_network.foo.id}" + template = "CentOS 5.6 (64-bit) no GUI (Simulator)" + zone = "Sandbox-simulator" + keypairs = ["${cloudstack_ssh_keypair.foo.name}", "${cloudstack_ssh_keypair.bar.name}"] + expunge = true +}` + const testAccCloudStackInstance_project = ` resource "cloudstack_network" "foo" { name = "terraform-network" diff --git a/website/docs/r/instance.html.markdown b/website/docs/r/instance.html.markdown index ee21f692..d4d61872 100644 --- a/website/docs/r/instance.html.markdown +++ b/website/docs/r/instance.html.markdown @@ -34,7 +34,7 @@ The following arguments are supported: * `service_offering` - (Required) The name or ID of the service offering used for this instance. -* `host_id` - (Optional) destination Host ID to deploy the VM to - parameter available +* `host_id` - (Optional) destination Host ID to deploy the VM to - parameter available for root admin only * `pod_id` - (Optional) destination Pod ID to deploy the VM to - parameter available for root admin only @@ -82,7 +82,10 @@ The following arguments are supported: instance. This can be either plain text or base64 encoded text. * `keypair` - (Optional) The name of the SSH key pair that will be used to - access this instance. + access this instance. (Mutual exclusive with keypairs) + +* `keypairs` - (Optional) A list of SSH key pair names that will be used to + access this instance. (Mutual exclusive with keypair) * `expunge` - (Optional) This determines if the instance is expunged when it is destroyed (defaults false)