diff --git a/azurerm/resource_arm_image_test.go b/azurerm/resource_arm_image_test.go index 36d74a5c9170..e9f598650575 100644 --- a/azurerm/resource_arm_image_test.go +++ b/azurerm/resource_arm_image_test.go @@ -117,6 +117,40 @@ func TestAccAzureRMImage_customImageVMFromVM(t *testing.T) { }) } +func TestAccAzureRMImageVMSS_customImageVMSSFromVHD(t *testing.T) { + ri := acctest.RandInt() + userName := "testadmin" + password := "Password1234!" + hostName := fmt.Sprintf("tftestcustomimagesrc%[1]d", ri) + sshPort := "22" + preConfig := testAccAzureRMImageVMSS_customImage_fromVHD_setup(ri, userName, password, hostName) + postConfig := testAccAzureRMImageVMSS_customImage_fromVHD_provision(ri, userName, password, hostName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMImageDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + //need to create a vm and then reference it in the image creation + Config: preConfig, + Destroy: false, + Check: resource.ComposeTestCheckFunc( + testCheckAzureVMExists("azurerm_virtual_machine.testsource", true), + testGeneralizeVMImage(fmt.Sprintf("acctestRG-%[1]d", ri), "testsource", + userName, password, hostName, sshPort), + ), + }, + resource.TestStep{ + Config: postConfig, + Check: resource.ComposeTestCheckFunc( + testCheckAzureVMSSExists("azurerm_virtual_machine_scale_set.testdestination", true), + ), + }, + }, + }) +} + func testGeneralizeVMImage(groupName string, vmName string, userName string, password string, hostName string, port string) resource.TestCheckFunc { return func(s *terraform.State) error { vmClient := testAccProvider.Meta().(*ArmClient).vmClient @@ -243,6 +277,40 @@ func testCheckAzureVMExists(sourceVM string, shouldExist bool) resource.TestChec } } +func testCheckAzureVMSSExists(sourceVMSS string, shouldExist bool) resource.TestCheckFunc { + return func(s *terraform.State) error { + log.Printf("[INFO] testing MANAGED IMAGE VMSS EXISTS - BEGIN.") + + vmssClient := testAccProvider.Meta().(*ArmClient).vmScaleSetClient + vmRs, vmOk := s.RootModule().Resources[sourceVMSS] + if !vmOk { + return fmt.Errorf("VMSS Not found: %s", sourceVMSS) + } + vmssName := vmRs.Primary.Attributes["name"] + + resourceGroup, hasResourceGroup := vmRs.Primary.Attributes["resource_group_name"] + if !hasResourceGroup { + return fmt.Errorf("Bad: no resource group found in state for VMSS: %s", vmssName) + } + + resp, err := vmssClient.Get(resourceGroup, vmssName) + if err != nil { + return fmt.Errorf("Bad: Get on vmssClient: %+v", err) + } + + if resp.StatusCode == http.StatusNotFound && shouldExist { + return fmt.Errorf("Bad: VMSS %q (resource group %q) does not exist", vmssName, resourceGroup) + } + if resp.StatusCode != http.StatusNotFound && !shouldExist { + return fmt.Errorf("Bad: VMSS %q (resource group %q) still exists", vmssName, resourceGroup) + } + + log.Printf("[INFO] testing MANAGED IMAGE VMSS EXISTS - END.") + + return nil + } +} + func testCheckAzureRMImageDestroy(s *terraform.State) error { conn := testAccProvider.Meta().(*ArmClient).diskClient @@ -965,3 +1033,259 @@ resource "azurerm_virtual_machine" "testdestination" { } `, rInt, userName, password, hostName) } + +func testAccAzureRMImageVMSS_customImage_fromVHD_setup(rInt int, userName string, password string, hostName string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestRG-%[1]d" + location = "West US" +} + +resource "azurerm_virtual_network" "test" { + name = "acctvn-%[1]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-%[1]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_public_ip" "test" { + name = "acctpip-%[1]d" + location = "West US" + resource_group_name = "${azurerm_resource_group.test.name}" + public_ip_address_allocation = "Dynamic" + domain_name_label = "%[4]s" +} + +resource "azurerm_network_interface" "testsource" { + name = "acctnicsource-%[1]d" + location = "West US" + resource_group_name = "${azurerm_resource_group.test.name}" + + ip_configuration { + name = "testconfigurationsource" + subnet_id = "${azurerm_subnet.test.id}" + private_ip_address_allocation = "dynamic" + public_ip_address_id = "${azurerm_public_ip.test.id}" + } +} + +resource "azurerm_storage_account" "test" { + name = "accsa%[1]d" + resource_group_name = "${azurerm_resource_group.test.name}" + location = "West US" + account_type = "Standard_LRS" + + tags { + environment = "Dev" + } +} + +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 = "blob" +} + +resource "azurerm_virtual_machine" "testsource" { + name = "testsource" + location = "West US" + resource_group_name = "${azurerm_resource_group.test.name}" + network_interface_ids = ["${azurerm_network_interface.testsource.id}"] + vm_size = "Standard_D1_v2" + + storage_image_reference { + publisher = "Canonical" + offer = "UbuntuServer" + sku = "16.04-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" + disk_size_gb = "30" + } + + os_profile { + computer_name = "mdimagetestsource" + admin_username = "%[2]s" + admin_password = "%[3]s" + } + + os_profile_linux_config { + disable_password_authentication = false + } + + tags { + environment = "Dev" + cost-center = "Ops" + } +} +`, rInt, userName, password, hostName) +} + +func testAccAzureRMImageVMSS_customImage_fromVHD_provision(rInt int, userName string, password string, hostName string) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestRG-%[1]d" + location = "West US" +} + +resource "azurerm_virtual_network" "test" { + name = "acctvn-%[1]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-%[1]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_public_ip" "test" { + name = "acctpip-%[1]d" + location = "West US" + resource_group_name = "${azurerm_resource_group.test.name}" + public_ip_address_allocation = "Dynamic" + domain_name_label = "%[4]s" +} + +resource "azurerm_network_interface" "testsource" { + name = "acctnicsource-%[1]d" + location = "West US" + resource_group_name = "${azurerm_resource_group.test.name}" + + ip_configuration { + name = "testconfigurationsource" + subnet_id = "${azurerm_subnet.test.id}" + private_ip_address_allocation = "dynamic" + public_ip_address_id = "${azurerm_public_ip.test.id}" + } +} + +resource "azurerm_storage_account" "test" { + name = "accsa%[1]d" + resource_group_name = "${azurerm_resource_group.test.name}" + location = "West US" + account_type = "Standard_LRS" + + tags { + environment = "Dev" + } +} + +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 = "blob" +} + +resource "azurerm_virtual_machine" "testsource" { + name = "testsource" + location = "West US" + resource_group_name = "${azurerm_resource_group.test.name}" + network_interface_ids = ["${azurerm_network_interface.testsource.id}"] + vm_size = "Standard_D1_v2" + + storage_image_reference { + publisher = "Canonical" + offer = "UbuntuServer" + sku = "16.04-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" + disk_size_gb = "45" + } + + os_profile { + computer_name = "mdimagetestsource" + admin_username = "%[2]s" + admin_password = "%[3]s" + } + + os_profile_linux_config { + disable_password_authentication = false + } + + tags { + environment = "Dev" + cost-center = "Ops" + } +} + +resource "azurerm_image" "testdestination" { + name = "accteste" + location = "West US" + resource_group_name = "${azurerm_resource_group.test.name}" + os_disk { + os_type = "Linux" + os_state = "Generalized" + blob_uri = "${azurerm_storage_account.test.primary_blob_endpoint}${azurerm_storage_container.test.name}/myosdisk1.vhd" + size_gb = 30 + caching = "None" + } + + tags { + environment = "Dev" + cost-center = "Ops" + } +} + +resource "azurerm_virtual_machine_scale_set" "testdestination" { + name = "testdestination" + location = "West US" + resource_group_name = "${azurerm_resource_group.test.name}" + upgrade_policy_mode = "Manual" + + sku { + name = "Standard_D1_v2" + tier = "Standard" + capacity = 2 + } + + os_profile { + computer_name_prefix = "testvm%[1]d" + admin_username = "%[2]s" + admin_password = "%[3]s" + } + + network_profile { + name = "TestNetworkProfile%[1]d" + primary = true + ip_configuration { + name = "TestIPConfiguration" + subnet_id = "${azurerm_subnet.test.id}" + } + } + + storage_profile_os_disk { + caching = "ReadWrite" + create_option = "FromImage" + managed_disk_type = "Standard_LRS" + } + + storage_profile_image_reference { + id = "${azurerm_image.testdestination.id}" + } +} +`, rInt, userName, password, hostName) +} diff --git a/azurerm/resource_arm_virtual_machine_scale_set.go b/azurerm/resource_arm_virtual_machine_scale_set.go index f65b4d11e378..5af18a2fbc85 100644 --- a/azurerm/resource_arm_virtual_machine_scale_set.go +++ b/azurerm/resource_arm_virtual_machine_scale_set.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/structure" "github.com/hashicorp/terraform/helper/validation" + riviera "github.com/jen20/riviera/azure" ) func resourceArmVirtualMachineScaleSet() *schema.Resource { @@ -393,24 +394,29 @@ func resourceArmVirtualMachineScaleSet() *schema.Resource { MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Optional: true, + }, + "publisher": { Type: schema.TypeString, - Required: true, + Optional: true, }, "offer": { Type: schema.TypeString, - Required: true, + Optional: true, }, "sku": { Type: schema.TypeString, - Required: true, + Optional: true, }, "version": { Type: schema.TypeString, - Required: true, + Optional: true, }, }, }, @@ -922,12 +928,23 @@ func flattenAzureRmVirtualMachineScaleSetStorageProfileDataDisk(disks *[]compute return result } -func flattenAzureRmVirtualMachineScaleSetStorageProfileImageReference(profile *compute.ImageReference) []interface{} { +func flattenAzureRmVirtualMachineScaleSetStorageProfileImageReference(image *compute.ImageReference) []interface{} { result := make(map[string]interface{}) - result["publisher"] = *profile.Publisher - result["offer"] = *profile.Offer - result["sku"] = *profile.Sku - result["version"] = *profile.Version + if image.Publisher != nil { + result["publisher"] = *image.Publisher + } + if image.Offer != nil { + result["offer"] = *image.Offer + } + if image.Sku != nil { + result["sku"] = *image.Sku + } + if image.Version != nil { + result["version"] = *image.Version + } + if image.ID != nil { + result["id"] = *image.ID + } return []interface{}{result} } @@ -980,11 +997,21 @@ func flattenAzureRmVirtualMachineScaleSetExtensionProfile(profile *compute.Virtu func resourceArmVirtualMachineScaleSetStorageProfileImageReferenceHash(v interface{}) int { var buf bytes.Buffer m := v.(map[string]interface{}) - buf.WriteString(fmt.Sprintf("%s-", m["publisher"].(string))) - buf.WriteString(fmt.Sprintf("%s-", m["offer"].(string))) - buf.WriteString(fmt.Sprintf("%s-", m["sku"].(string))) - buf.WriteString(fmt.Sprintf("%s-", m["version"].(string))) - + if m["publisher"] != nil { + buf.WriteString(fmt.Sprintf("%s-", m["publisher"].(string))) + } + if m["offer"] != nil { + buf.WriteString(fmt.Sprintf("%s-", m["offer"].(string))) + } + if m["sku"] != nil { + buf.WriteString(fmt.Sprintf("%s-", m["sku"].(string))) + } + if m["version"] != nil { + buf.WriteString(fmt.Sprintf("%s-", m["version"].(string))) + } + if m["id"] != nil { + buf.WriteString(fmt.Sprintf("%s-", m["id"].(string))) + } return hashcode.String(buf.String()) } @@ -1318,17 +1345,29 @@ func expandAzureRmVirtualMachineScaleSetStorageProfileImageReference(d *schema.R storageImageRef := storageImageRefs[0].(map[string]interface{}) + imageID := storageImageRef["id"].(string) publisher := storageImageRef["publisher"].(string) - offer := storageImageRef["offer"].(string) - sku := storageImageRef["sku"].(string) - version := storageImageRef["version"].(string) - return &compute.ImageReference{ - Publisher: &publisher, - Offer: &offer, - Sku: &sku, - Version: &version, - }, nil + imageReference := compute.ImageReference{} + + if imageID != "" && publisher != "" { + return nil, fmt.Errorf("[ERROR] Conflict between `id` and `publisher` (only one or the other can be used)") + } + + if imageID != "" { + imageReference.ID = riviera.String(storageImageRef["id"].(string)) + } else { + offer := storageImageRef["offer"].(string) + sku := storageImageRef["sku"].(string) + version := storageImageRef["version"].(string) + + imageReference.Publisher = &publisher + imageReference.Offer = &offer + imageReference.Sku = &sku + imageReference.Version = &version + } + + return &imageReference, nil } func expandAzureRmVirtualMachineScaleSetOsProfileLinuxConfig(d *schema.ResourceData) (*compute.LinuxConfiguration, error) { diff --git a/website/docs/r/virtual_machine.html.markdown b/website/docs/r/virtual_machine.html.markdown index ab51d69214b8..40126acecc90 100644 --- a/website/docs/r/virtual_machine.html.markdown +++ b/website/docs/r/virtual_machine.html.markdown @@ -260,7 +260,7 @@ For more information on the different example configurations, please check out t `storage_image_reference` supports the following: -* `image_id` - (Optional) Specifies the ID of the (custom) image to use to create the virtual +* `id` - (Optional) Specifies the ID of the (custom) image to use to create the virtual machine, for example: ```hcl @@ -275,7 +275,7 @@ resource "azurerm_virtual_machine" "test" { ... storage_image_reference { - image_id = "${azurerm_image.test.id}" + id = "${azurerm_image.test.id}" } ... diff --git a/website/docs/r/virtual_machine_scale_set.html.markdown b/website/docs/r/virtual_machine_scale_set.html.markdown index b3c1204dd24b..ef224f498f60 100644 --- a/website/docs/r/virtual_machine_scale_set.html.markdown +++ b/website/docs/r/virtual_machine_scale_set.html.markdown @@ -346,9 +346,11 @@ The following arguments are supported: `storage_profile_image_reference` supports the following: -* `publisher` - (Required) Specifies the publisher of the image used to create the virtual machines -* `offer` - (Required) Specifies the offer of the image used to create the virtual machines. -* `sku` - (Required) Specifies the SKU of the image used to create the virtual machines. +* `id` - (Optional) Specifies the ID of the (custom) image to use to create the virtual +machine scale set, as in the [example below](#example-of-storage_profile_image_reference-with-id). +* `publisher` - (Optional) Specifies the publisher of the image used to create the virtual machines. +* `offer` - (Optional) Specifies the offer of the image used to create the virtual machines. +* `sku` - (Optional) Specifies the SKU of the image used to create the virtual machines. * `version` - (Optional) Specifies the version of the image used to create the virtual machines. `extension` supports the following: @@ -367,6 +369,26 @@ The following arguments are supported: * `publisher` - (Required) Specifies the publisher of the image. * `product` - (Required) Specifies the product of the image from the marketplace. +## Example of storage_profile_image_reference with id + +```hcl + +resource "azurerm_image" "test" { + name = "test" + ... +} + +resource "azurerm_virtual_machine_scale_set" "test" { + name = "test" + ... + + storage_image_reference { + id = "${azurerm_image.test.id}" + } + +... +``` + ## Attributes Reference The following attributes are exported: