diff --git a/builtin/providers/azurerm/import_arm_virtual_machine_extension_test.go b/builtin/providers/azurerm/import_arm_virtual_machine_extension_test.go new file mode 100644 index 000000000..839c2c9f8 --- /dev/null +++ b/builtin/providers/azurerm/import_arm_virtual_machine_extension_test.go @@ -0,0 +1,34 @@ +package azurerm + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" +) + +func TestAccAzureRMVirtualMachineExtension_importBasic(t *testing.T) { + resourceName := "azurerm_virtual_machine_extension.test" + + ri := acctest.RandInt() + config := fmt.Sprintf(testAccAzureRMVirtualMachineExtension_basic, ri, ri, ri, ri, ri, ri, ri, ri) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMVirtualMachineExtensionDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: config, + }, + + resource.TestStep{ + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"protected_settings"}, + }, + }, + }) +} diff --git a/builtin/providers/azurerm/provider.go b/builtin/providers/azurerm/provider.go index 0ef882d51..77a36b47f 100644 --- a/builtin/providers/azurerm/provider.go +++ b/builtin/providers/azurerm/provider.go @@ -86,6 +86,7 @@ func Provider() terraform.ResourceProvider { "azurerm_template_deployment": resourceArmTemplateDeployment(), "azurerm_traffic_manager_endpoint": resourceArmTrafficManagerEndpoint(), "azurerm_traffic_manager_profile": resourceArmTrafficManagerProfile(), + "azurerm_virtual_machine_extension": resourceArmVirtualMachineExtensions(), "azurerm_virtual_machine": resourceArmVirtualMachine(), "azurerm_virtual_machine_scale_set": resourceArmVirtualMachineScaleSet(), "azurerm_virtual_network": resourceArmVirtualNetwork(), diff --git a/builtin/providers/azurerm/resource_arm_virtual_machine_extension.go b/builtin/providers/azurerm/resource_arm_virtual_machine_extension.go new file mode 100644 index 000000000..2f8a24763 --- /dev/null +++ b/builtin/providers/azurerm/resource_arm_virtual_machine_extension.go @@ -0,0 +1,236 @@ +package azurerm + +import ( + "encoding/json" + "fmt" + "net/http" + "reflect" + + "github.com/Azure/azure-sdk-for-go/arm/compute" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceArmVirtualMachineExtensions() *schema.Resource { + return &schema.Resource{ + Create: resourceArmVirtualMachineExtensionsCreate, + Read: resourceArmVirtualMachineExtensionsRead, + Update: resourceArmVirtualMachineExtensionsCreate, + Delete: resourceArmVirtualMachineExtensionsDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "location": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + StateFunc: azureRMNormalizeLocation, + }, + + "resource_group_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "virtual_machine_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "publisher": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "type_handler_version": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "auto_upgrade_minor_version": { + Type: schema.TypeBool, + Optional: true, + }, + + "settings": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateJsonString, + DiffSuppressFunc: suppressDiffVirtualMachineExtensionSettings, + }, + + // due to the sensitive nature, these are not returned by the API + "protected_settings": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Sensitive: true, + ValidateFunc: validateJsonString, + DiffSuppressFunc: suppressDiffVirtualMachineExtensionSettings, + }, + + "tags": tagsSchema(), + }, + } +} + +func resourceArmVirtualMachineExtensionsCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).vmExtensionClient + + name := d.Get("name").(string) + location := d.Get("location").(string) + vmName := d.Get("virtual_machine_name").(string) + resGroup := d.Get("resource_group_name").(string) + publisher := d.Get("publisher").(string) + extensionType := d.Get("type").(string) + typeHandlerVersion := d.Get("type_handler_version").(string) + autoUpgradeMinor := d.Get("auto_upgrade_minor_version").(bool) + tags := d.Get("tags").(map[string]interface{}) + + extension := compute.VirtualMachineExtension{ + Location: &location, + Properties: &compute.VirtualMachineExtensionProperties{ + Publisher: &publisher, + Type: &extensionType, + TypeHandlerVersion: &typeHandlerVersion, + AutoUpgradeMinorVersion: &autoUpgradeMinor, + }, + Tags: expandTags(tags), + } + + if settingsString := d.Get("settings").(string); settingsString != "" { + settings, err := expandArmVirtualMachineExtensionSettings(settingsString) + if err != nil { + return fmt.Errorf("unable to parse settings: %s", err) + } + extension.Properties.Settings = &settings + } + + if protectedSettingsString := d.Get("protected_settings").(string); protectedSettingsString != "" { + protectedSettings, err := expandArmVirtualMachineExtensionSettings(protectedSettingsString) + if err != nil { + return fmt.Errorf("unable to parse protected_settings: %s", err) + } + extension.Properties.ProtectedSettings = &protectedSettings + } + + _, err := client.CreateOrUpdate(resGroup, vmName, name, extension, make(chan struct{})) + if err != nil { + return err + } + + read, err := client.Get(resGroup, vmName, name, "") + if err != nil { + return err + } + + if read.ID == nil { + return fmt.Errorf("Cannot read Virtual Machine Extension %s (resource group %s) ID", name, resGroup) + } + + d.SetId(*read.ID) + + return resourceArmVirtualMachineExtensionsRead(d, meta) +} + +func resourceArmVirtualMachineExtensionsRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).vmExtensionClient + + id, err := parseAzureResourceID(d.Id()) + if err != nil { + return err + } + resGroup := id.ResourceGroup + vmName := id.Path["virtualMachines"] + name := id.Path["extensions"] + + resp, err := client.Get(resGroup, vmName, name, "") + if err != nil { + return fmt.Errorf("Error making Read request on Virtual Machine Extension %s: %s", name, err) + } + if resp.StatusCode == http.StatusNotFound { + d.SetId("") + return nil + } + + d.Set("name", resp.Name) + d.Set("location", azureRMNormalizeLocation(*resp.Location)) + d.Set("virtual_machine_name", vmName) + d.Set("resource_group_name", resGroup) + d.Set("publisher", resp.Properties.Publisher) + d.Set("type", resp.Properties.Type) + d.Set("type_handler_version", resp.Properties.TypeHandlerVersion) + d.Set("auto_upgrade_minor_version", resp.Properties.AutoUpgradeMinorVersion) + + if resp.Properties.Settings != nil { + settings, err := flattenArmVirtualMachineExtensionSettings(*resp.Properties.Settings) + if err != nil { + return fmt.Errorf("unable to parse settings from response: %s", err) + } + d.Set("settings", settings) + } + + flattenAndSetTags(d, resp.Tags) + + return nil +} + +func resourceArmVirtualMachineExtensionsDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).vmExtensionClient + + id, err := parseAzureResourceID(d.Id()) + if err != nil { + return err + } + resGroup := id.ResourceGroup + name := id.Path["extensions"] + vmName := id.Path["virtualMachines"] + + _, err = client.Delete(resGroup, vmName, name, make(chan struct{})) + + return nil +} + +func expandArmVirtualMachineExtensionSettings(jsonString string) (map[string]interface{}, error) { + var result map[string]interface{} + + err := json.Unmarshal([]byte(jsonString), &result) + + return result, err +} + +func flattenArmVirtualMachineExtensionSettings(settingsMap map[string]interface{}) (string, error) { + result, err := json.Marshal(settingsMap) + if err != nil { + return "", err + } + + return string(result), nil +} + +func suppressDiffVirtualMachineExtensionSettings(k, old, new string, d *schema.ResourceData) bool { + oldMap, err := expandArmVirtualMachineExtensionSettings(old) + if err != nil { + return false + } + + newMap, err := expandArmVirtualMachineExtensionSettings(new) + if err != nil { + return false + } + + return reflect.DeepEqual(oldMap, newMap) +} diff --git a/builtin/providers/azurerm/resource_arm_virtual_machine_extension_test.go b/builtin/providers/azurerm/resource_arm_virtual_machine_extension_test.go new file mode 100644 index 000000000..fb7ac488a --- /dev/null +++ b/builtin/providers/azurerm/resource_arm_virtual_machine_extension_test.go @@ -0,0 +1,561 @@ +package azurerm + +import ( + "fmt" + "net/http" + "testing" + + "regexp" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAzureRMVirtualMachineExtension_basic(t *testing.T) { + ri := acctest.RandInt() + preConfig := fmt.Sprintf(testAccAzureRMVirtualMachineExtension_basic, ri, ri, ri, ri, ri, ri, ri, ri) + postConfig := fmt.Sprintf(testAccAzureRMVirtualMachineExtension_basicUpdate, ri, ri, ri, ri, ri, ri, ri, ri) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMVirtualMachineExtensionDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: preConfig, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMVirtualMachineExtensionExists("azurerm_virtual_machine_extension.test"), + resource.TestMatchResourceAttr("azurerm_virtual_machine_extension.test", "settings", regexp.MustCompile("hostname")), + ), + }, + resource.TestStep{ + Config: postConfig, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMVirtualMachineExtensionExists("azurerm_virtual_machine_extension.test"), + resource.TestMatchResourceAttr("azurerm_virtual_machine_extension.test", "settings", regexp.MustCompile("whoami")), + ), + }, + }, + }) +} + +func TestAccAzureRMVirtualMachineExtension_concurrent(t *testing.T) { + ri := acctest.RandInt() + config := fmt.Sprintf(testAccAzureRMVirtualMachineExtension_concurrent, ri, ri, ri, ri, ri, ri, ri, ri, ri) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMVirtualMachineExtensionDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: config, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMVirtualMachineExtensionExists("azurerm_virtual_machine_extension.test"), + testCheckAzureRMVirtualMachineExtensionExists("azurerm_virtual_machine_extension.test2"), + resource.TestMatchResourceAttr("azurerm_virtual_machine_extension.test", "settings", regexp.MustCompile("hostname")), + resource.TestMatchResourceAttr("azurerm_virtual_machine_extension.test2", "settings", regexp.MustCompile("whoami")), + ), + }, + }, + }) +} + +func TestAccAzureRMVirtualMachineExtension_linuxDiagnostics(t *testing.T) { + ri := acctest.RandInt() + config := fmt.Sprintf(testAccAzureRMVirtualMachineExtension_linuxDiagnostics, ri, ri, ri, ri, ri, ri, ri, ri) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMVirtualMachineExtensionDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: config, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMVirtualMachineExtensionExists("azurerm_virtual_machine_extension.test"), + ), + }, + }, + }) +} + +func testCheckAzureRMVirtualMachineExtensionExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + // Ensure we have enough information in state to look up in API + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + name := rs.Primary.Attributes["name"] + vmName := rs.Primary.Attributes["virtual_machine_name"] + resourceGroup := rs.Primary.Attributes["resource_group_name"] + + conn := testAccProvider.Meta().(*ArmClient).vmExtensionClient + + resp, err := conn.Get(resourceGroup, vmName, name, "") + if err != nil { + return fmt.Errorf("Bad: Get on vmExtensionClient: %s", err) + } + + if resp.StatusCode == http.StatusNotFound { + return fmt.Errorf("Bad: VirtualMachine Extension %q (resource group: %q) does not exist", name, resourceGroup) + } + + return nil + } +} + +func testCheckAzureRMVirtualMachineExtensionDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*ArmClient).vmExtensionClient + + for _, rs := range s.RootModule().Resources { + if rs.Type != "azurerm_virtual_machine_extension" { + continue + } + + name := rs.Primary.Attributes["name"] + vmName := rs.Primary.Attributes["virtual_machine_name"] + resourceGroup := rs.Primary.Attributes["resource_group_name"] + + resp, err := conn.Get(resourceGroup, vmName, name, "") + + if err != nil { + return nil + } + + if resp.StatusCode != http.StatusNotFound { + return fmt.Errorf("Virtual Machine Extension still exists:\n%#v", resp.Properties) + } + } + + return nil +} + +var testAccAzureRMVirtualMachineExtension_basic = ` +resource "azurerm_resource_group" "test" { + name = "acctestrg-%d" + location = "West US" +} + +resource "azurerm_virtual_network" "test" { + name = "acctvn-%d" + address_space = ["10.0.0.0/16"] + location = "West US" + resource_group_name = "${azurerm_resource_group.test.name}" +} + +resource "azurerm_subnet" "test" { + name = "acctsub-%d" + resource_group_name = "${azurerm_resource_group.test.name}" + virtual_network_name = "${azurerm_virtual_network.test.name}" + address_prefix = "10.0.2.0/24" +} + +resource "azurerm_network_interface" "test" { + name = "acctni-%d" + location = "West US" + resource_group_name = "${azurerm_resource_group.test.name}" + + ip_configuration { + name = "testconfiguration1" + subnet_id = "${azurerm_subnet.test.id}" + private_ip_address_allocation = "dynamic" + } +} + +resource "azurerm_storage_account" "test" { + name = "accsa%d" + resource_group_name = "${azurerm_resource_group.test.name}" + location = "westus" + account_type = "Standard_LRS" + + tags { + environment = "staging" + } +} + +resource "azurerm_storage_container" "test" { + name = "vhds" + resource_group_name = "${azurerm_resource_group.test.name}" + storage_account_name = "${azurerm_storage_account.test.name}" + container_access_type = "private" +} + +resource "azurerm_virtual_machine" "test" { + name = "acctvm-%d" + location = "West US" + resource_group_name = "${azurerm_resource_group.test.name}" + network_interface_ids = ["${azurerm_network_interface.test.id}"] + vm_size = "Standard_A0" + + storage_image_reference { + publisher = "Canonical" + offer = "UbuntuServer" + sku = "14.04.2-LTS" + version = "latest" + } + + storage_os_disk { + name = "myosdisk1" + vhd_uri = "${azurerm_storage_account.test.primary_blob_endpoint}${azurerm_storage_container.test.name}/myosdisk1.vhd" + caching = "ReadWrite" + create_option = "FromImage" + } + + os_profile { + computer_name = "hostname%d" + admin_username = "testadmin" + admin_password = "Password1234!" + } + + os_profile_linux_config { + disable_password_authentication = false + } +} + +resource "azurerm_virtual_machine_extension" "test" { + name = "acctvme-%d" + location = "West US" + resource_group_name = "${azurerm_resource_group.test.name}" + virtual_machine_name = "${azurerm_virtual_machine.test.name}" + publisher = "Microsoft.Azure.Extensions" + type = "CustomScript" + type_handler_version = "2.0" + + settings = <azurerm_virtual_machine + > + azurerm_virtual_machine_extension + + > azurerm_virtual_machine_scale_set