diff --git a/azurerm/config.go b/azurerm/config.go index 8fdb95d19a7a..85bdfc83f866 100644 --- a/azurerm/config.go +++ b/azurerm/config.go @@ -49,6 +49,7 @@ type ArmClient struct { vmScaleSetClient compute.VirtualMachineScaleSetsClient vmImageClient compute.VirtualMachineImagesClient vmClient compute.VirtualMachinesClient + imageClient compute.ImagesClient diskClient disk.DisksClient @@ -260,6 +261,12 @@ func (c *Config) getArmClient() (*ArmClient, error) { dkc.Sender = autorest.CreateSender(withRequestLogging()) client.diskClient = dkc + img := compute.NewImagesClientWithBaseURI(endpoint, c.SubscriptionID) + setUserAgent(&img.Client) + img.Authorizer = auth + img.Sender = autorest.CreateSender(withRequestLogging()) + client.imageClient = img + ehc := eventhub.NewEventHubsClientWithBaseURI(endpoint, c.SubscriptionID) setUserAgent(&ehc.Client) ehc.Authorizer = auth diff --git a/azurerm/import_arm_image_test.go b/azurerm/import_arm_image_test.go new file mode 100644 index 000000000000..371c5660fe58 --- /dev/null +++ b/azurerm/import_arm_image_test.go @@ -0,0 +1,48 @@ +package azurerm + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" +) + +func TestAccAzureRMImage_importStandalone(t *testing.T) { + ri := acctest.RandInt() + userName := "testadmin" + password := "Password1234s!" + hostName := fmt.Sprintf("tftestcustomimagesrc%[1]d", ri) + sshPort := "22" + preConfig := testAccAzureRMImage_standaloneImage_setup(ri, userName, password, hostName) + postConfig := testAccAzureRMImage_standaloneImage_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( + testCheckAzureRMImageExists("azurerm_image.test", true), + ), + }, + resource.TestStep{ + ResourceName: "azurerm_image.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} diff --git a/azurerm/provider.go b/azurerm/provider.go index 05c10505cfd9..020ab1d25479 100644 --- a/azurerm/provider.go +++ b/azurerm/provider.go @@ -90,6 +90,7 @@ func Provider() terraform.ResourceProvider { "azurerm_lb_rule": resourceArmLoadBalancerRule(), "azurerm_managed_disk": resourceArmManagedDisk(), + "azurerm_image": resourceArmImage(), "azurerm_key_vault": resourceArmKeyVault(), "azurerm_local_network_gateway": resourceArmLocalNetworkGateway(), diff --git a/azurerm/resource_arm_image.go b/azurerm/resource_arm_image.go new file mode 100644 index 000000000000..0a1303673afd --- /dev/null +++ b/azurerm/resource_arm_image.go @@ -0,0 +1,416 @@ +package azurerm + +import ( + "fmt" + "log" + "net/http" + + "github.com/Azure/azure-sdk-for-go/arm/compute" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" +) + +func resourceArmImage() *schema.Resource { + return &schema.Resource{ + Create: resourceArmImageCreateUpdate, + Read: resourceArmImageRead, + Update: resourceArmImageCreateUpdate, + Delete: resourceArmImageDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "location": locationSchema(), + + "resource_group_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "source_virtual_machine_id": { + Type: schema.TypeString, + Optional: true, + }, + + "os_disk": { + Type: schema.TypeSet, + Optional: true, + MaxItems: 1, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + + "os_type": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + string(compute.Linux), + string(compute.Windows), + }, true), + DiffSuppressFunc: ignoreCaseDiffSuppressFunc, + }, + + "os_state": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + string(compute.Generalized), + string(compute.Specialized), + }, true), + DiffSuppressFunc: ignoreCaseDiffSuppressFunc, + }, + + "managed_disk_id": { + Type: schema.TypeString, + Computed: true, + Optional: true, + }, + + "blob_uri": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "caching": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringInSlice([]string{ + string(compute.None), + string(compute.ReadOnly), + string(compute.ReadWrite), + }, true), + DiffSuppressFunc: ignoreCaseDiffSuppressFunc, + }, + + "size_gb": { + Type: schema.TypeInt, + Computed: true, + Optional: true, + }, + }, + }, + }, + + "data_disk": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + + "lun": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + }, + + "managed_disk_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "blob_uri": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "caching": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringInSlice([]string{ + string(compute.None), + string(compute.ReadOnly), + string(compute.ReadWrite), + }, true), + DiffSuppressFunc: ignoreCaseDiffSuppressFunc, + }, + + "size_gb": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + }, + }, + }, + + "tags": tagsSchema(), + }, + } +} + +func resourceArmImageCreateUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient) + imageClient := client.imageClient + + log.Printf("[INFO] preparing arguments for AzureRM Image creation.") + + name := d.Get("name").(string) + location := d.Get("location").(string) + resGroup := d.Get("resource_group_name").(string) + tags := d.Get("tags").(map[string]interface{}) + expandedTags := expandTags(tags) + properties := compute.ImageProperties{} + + osDisk, err := expandAzureRmImageOsDisk(d) + if err != nil { + return err + } + + dataDisks, err := expandAzureRmImageDataDisks(d) + if err != nil { + return err + } + + storageProfile := compute.ImageStorageProfile{ + OsDisk: osDisk, + DataDisks: &dataDisks, + } + + sourceVM := compute.SubResource{} + if v, ok := d.GetOk("source_virtual_machine_id"); ok { + vmID := v.(string) + sourceVM = compute.SubResource{ + ID: &vmID, + } + } + + //either source VM or storage profile can be specified, but not both + if sourceVM.ID == nil { + //if both sourceVM and storageProfile are empty, return an error + if storageProfile.OsDisk == nil && len(*storageProfile.DataDisks) == 0 { + return fmt.Errorf("[ERROR] Cannot create image when both source VM and storage profile are empty") + } + + properties = compute.ImageProperties{ + StorageProfile: &storageProfile, + } + } else { + //creating an image from source VM + properties = compute.ImageProperties{ + SourceVirtualMachine: &sourceVM, + } + } + + createImage := compute.Image{ + Name: &name, + Location: &location, + Tags: expandedTags, + ImageProperties: &properties, + } + + _, imageErr := imageClient.CreateOrUpdate(resGroup, name, createImage, make(chan struct{})) + err = <-imageErr + if err != nil { + return err + } + + read, err := imageClient.Get(resGroup, name, "") + if err != nil { + return err + } + if read.ID == nil { + return fmt.Errorf("[ERROR] Cannot read AzureRM Image %s (resource group %s) ID", name, resGroup) + } + + d.SetId(*read.ID) + + return resourceArmImageRead(d, meta) +} + +func resourceArmImageRead(d *schema.ResourceData, meta interface{}) error { + imageClient := meta.(*ArmClient).imageClient + + id, err := parseAzureResourceID(d.Id()) + if err != nil { + return err + } + resGroup := id.ResourceGroup + name := id.Path["images"] + + resp, err := imageClient.Get(resGroup, name, "") + if err != nil { + if resp.StatusCode == http.StatusNotFound { + d.SetId("") + return nil + } + return fmt.Errorf("[ERROR] Error making Read request on AzureRM Image %s (resource group %s): %+v", name, resGroup, err) + } + + d.Set("name", resp.Name) + d.Set("resource_group_name", resGroup) + d.Set("location", resp.Location) + + //either source VM or storage profile can be specified, but not both + if resp.SourceVirtualMachine != nil { + d.Set("source_virtual_machine_id", resp.SourceVirtualMachine.ID) + } else if resp.StorageProfile != nil { + if err := d.Set("os_disk", flattenAzureRmStorageProfileOsDisk(d, resp.StorageProfile)); err != nil { + return fmt.Errorf("[DEBUG] Error setting AzureRM Image OS Disk error: %#v", err) + } + + if resp.StorageProfile.DataDisks != nil { + if err := d.Set("data_disk", flattenAzureRmStorageProfileDataDisks(d, resp.StorageProfile)); err != nil { + return fmt.Errorf("[DEBUG] Error setting AzureRM Image Data Disks error: %#v", err) + } + } + } + + flattenAndSetTags(d, resp.Tags) + + return nil +} + +func resourceArmImageDelete(d *schema.ResourceData, meta interface{}) error { + imageClient := meta.(*ArmClient).imageClient + + id, err := parseAzureResourceID(d.Id()) + if err != nil { + return err + } + resGroup := id.ResourceGroup + name := id.Path["images"] + + _, deleteErr := imageClient.Delete(resGroup, name, make(chan struct{})) + err = <-deleteErr + if err != nil { + return err + } + + return nil +} + +func flattenAzureRmStorageProfileOsDisk(d *schema.ResourceData, storageProfile *compute.ImageStorageProfile) []interface{} { + result := make(map[string]interface{}) + if storageProfile.OsDisk != nil { + osDisk := *storageProfile.OsDisk + result["os_type"] = osDisk.OsType + result["os_state"] = osDisk.OsState + if osDisk.ManagedDisk != nil { + result["managed_disk_id"] = *osDisk.ManagedDisk.ID + } + result["blob_uri"] = *osDisk.BlobURI + result["caching"] = osDisk.Caching + if osDisk.DiskSizeGB != nil { + result["size_gb"] = *osDisk.DiskSizeGB + } + } + + return []interface{}{result} +} + +func flattenAzureRmStorageProfileDataDisks(d *schema.ResourceData, storageProfile *compute.ImageStorageProfile) []interface{} { + disks := storageProfile.DataDisks + result := make([]interface{}, len(*disks)) + for i, disk := range *disks { + l := make(map[string]interface{}) + if disk.ManagedDisk != nil { + l["managed_disk_id"] = *disk.ManagedDisk.ID + } + l["blob_uri"] = disk.BlobURI + l["caching"] = string(disk.Caching) + if disk.DiskSizeGB != nil { + l["size_gb"] = *disk.DiskSizeGB + } + l["lun"] = *disk.Lun + + result[i] = l + } + return result +} + +func expandAzureRmImageOsDisk(d *schema.ResourceData) (*compute.ImageOSDisk, error) { + + osDisk := &compute.ImageOSDisk{} + disks := d.Get("os_disk").(*schema.Set).List() + + if len(disks) > 0 { + config := disks[0].(map[string]interface{}) + + if v := config["os_type"].(string); v != "" { + osType := compute.OperatingSystemTypes(v) + osDisk.OsType = osType + } + + if v := config["os_state"].(string); v != "" { + osState := compute.OperatingSystemStateTypes(v) + osDisk.OsState = osState + } + + managedDiskID := config["managed_disk_id"].(string) + if managedDiskID != "" { + managedDisk := &compute.SubResource{ + ID: &managedDiskID, + } + osDisk.ManagedDisk = managedDisk + } + + blobURI := config["blob_uri"].(string) + osDisk.BlobURI = &blobURI + + if v := config["caching"].(string); v != "" { + caching := compute.CachingTypes(v) + osDisk.Caching = caching + } + + if size := config["size_gb"]; size != 0 { + diskSize := int32(size.(int)) + osDisk.DiskSizeGB = &diskSize + } + } + + return osDisk, nil +} + +func expandAzureRmImageDataDisks(d *schema.ResourceData) ([]compute.ImageDataDisk, error) { + + disks := d.Get("data_disk").([]interface{}) + + dataDisks := make([]compute.ImageDataDisk, 0, len(disks)) + for _, diskConfig := range disks { + config := diskConfig.(map[string]interface{}) + + managedDiskID := d.Get("managed_disk_id").(string) + blobURI := d.Get("blob_uri").(string) + lun := int32(config["lun"].(int)) + + dataDisk := compute.ImageDataDisk{ + Lun: &lun, + BlobURI: &blobURI, + } + + if size := d.Get("size_gb"); size != 0 { + diskSize := int32(size.(int)) + dataDisk.DiskSizeGB = &diskSize + } + + if v := d.Get("caching").(string); v != "" { + caching := compute.CachingTypes(v) + dataDisk.Caching = caching + } + + if managedDiskID != "" { + managedDisk := &compute.SubResource{ + ID: &managedDiskID, + } + dataDisk.ManagedDisk = managedDisk + } + + dataDisks = append(dataDisks, dataDisk) + } + + return dataDisks, nil + +} diff --git a/azurerm/resource_arm_image_test.go b/azurerm/resource_arm_image_test.go new file mode 100644 index 000000000000..36d74a5c9170 --- /dev/null +++ b/azurerm/resource_arm_image_test.go @@ -0,0 +1,967 @@ +package azurerm + +import ( + "bytes" + "fmt" + "log" + "net/http" + "strings" + "testing" + + "golang.org/x/crypto/ssh" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAzureRMImage_standaloneImage(t *testing.T) { + ri := acctest.RandInt() + userName := "testadmin" + password := "Password1234!" + hostName := fmt.Sprintf("tftestcustomimagesrc%[1]d", ri) + sshPort := "22" + preConfig := testAccAzureRMImage_standaloneImage_setup(ri, userName, password, hostName) + postConfig := testAccAzureRMImage_standaloneImage_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-%d", ri), "testsource", + userName, password, hostName, sshPort), + ), + }, + resource.TestStep{ + Config: postConfig, + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMImageExists("azurerm_image.test", true), + ), + }, + }, + }) +} + +func TestAccAzureRMImage_customImageVMFromVHD(t *testing.T) { + ri := acctest.RandInt() + userName := "testadmin" + password := "Password1234!" + hostName := fmt.Sprintf("tftestcustomimagesrc%[1]d", ri) + sshPort := "22" + preConfig := testAccAzureRMImage_customImage_fromVHD_setup(ri, userName, password, hostName) + postConfig := testAccAzureRMImage_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( + testCheckAzureVMExists("azurerm_virtual_machine.testdestination", true), + ), + }, + }, + }) +} + +func TestAccAzureRMImage_customImageVMFromVM(t *testing.T) { + ri := acctest.RandInt() + userName := "testadmin" + password := "Password1234!" + hostName := fmt.Sprintf("tftestcustomimagesrc%[1]d", ri) + sshPort := "22" + preConfig := testAccAzureRMImage_customImage_fromVM_sourceVM(ri, userName, password, hostName) + postConfig := testAccAzureRMImage_customImage_fromVM_destinationVM(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( + testCheckAzureVMExists("azurerm_virtual_machine.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 + dnsName := fmt.Sprintf("%[1]s.westus.cloudapp.azure.com", hostName) + + deprovisionErr := deprovisionVM(userName, password, dnsName, port) + if deprovisionErr != nil { + return fmt.Errorf("Bad: Deprovisioning error %s", deprovisionErr) + } + + _, deallocateErr := vmClient.Deallocate(groupName, vmName, nil) + err := <-deallocateErr + if err != nil { + return fmt.Errorf("Bad: Deallocating error %s", err) + } + + _, generalizeErr := vmClient.Generalize(groupName, vmName) + if generalizeErr != nil { + return fmt.Errorf("Bad: Generalizing error %s", generalizeErr) + } + + return nil + } +} + +func deprovisionVM(userName string, password string, hostName string, port string) error { + //SSH into the machine and execute a waagent deprovisioning command + var b bytes.Buffer + cmd := "sudo waagent -verbose -deprovision+user -force" + + config := &ssh.ClientConfig{ + User: userName, + Auth: []ssh.AuthMethod{ + ssh.Password(password), + }, + } + log.Printf("[INFO] Connecting to %s:%v remote server...", hostName, port) + + hostAddress := strings.Join([]string{hostName, port}, ":") + client, err := ssh.Dial("tcp", hostAddress, config) + if err != nil { + return fmt.Errorf("Bad: deprovisioning error %s", err.Error()) + } + + session, err := client.NewSession() + if err != nil { + return fmt.Errorf("Bad: deprovisioning error, failure creating session %s", err.Error()) + } + defer session.Close() + + session.Stdout = &b + if err := session.Run(cmd); err != nil { + return fmt.Errorf("Bad: deprovisioning error, failure running command %s", err.Error()) + } + + return nil +} + +func testCheckAzureRMImageExists(name string, shouldExist bool) resource.TestCheckFunc { + return func(s *terraform.State) error { + + log.Printf("[INFO] testing MANAGED IMAGE EXISTS - BEGIN.") + + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("Not found: %s", name) + } + + dName := rs.Primary.Attributes["name"] + resourceGroup, hasResourceGroup := rs.Primary.Attributes["resource_group_name"] + if !hasResourceGroup { + return fmt.Errorf("Bad: no resource group found in state for image: %s", dName) + } + + conn := testAccProvider.Meta().(*ArmClient).imageClient + + resp, err := conn.Get(resourceGroup, dName, "") + if err != nil { + return fmt.Errorf("Bad: Get on imageClient: %s", err) + } + + if resp.StatusCode == http.StatusNotFound && shouldExist { + return fmt.Errorf("Bad: Image %q (resource group %q) does not exist", dName, resourceGroup) + } + if resp.StatusCode != http.StatusNotFound && !shouldExist { + return fmt.Errorf("Bad: Image %q (resource group %q) still exists", dName, resourceGroup) + } + + return nil + } +} + +func testCheckAzureVMExists(sourceVM string, shouldExist bool) resource.TestCheckFunc { + return func(s *terraform.State) error { + log.Printf("[INFO] testing MANAGED IMAGE VM EXISTS - BEGIN.") + + vmClient := testAccProvider.Meta().(*ArmClient).vmClient + vmRs, vmOk := s.RootModule().Resources[sourceVM] + if !vmOk { + return fmt.Errorf("VM Not found: %s", sourceVM) + } + vmName := 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 VM: %s", vmName) + } + + resp, err := vmClient.Get(resourceGroup, vmName, "") + if err != nil { + return fmt.Errorf("Bad: Get on vmClient: %s", err) + } + + if resp.StatusCode == http.StatusNotFound && shouldExist { + return fmt.Errorf("Bad: VM %q (resource group %q) does not exist", vmName, resourceGroup) + } + if resp.StatusCode != http.StatusNotFound && !shouldExist { + return fmt.Errorf("Bad: VM %q (resource group %q) still exists", vmName, resourceGroup) + } + + log.Printf("[INFO] testing MANAGED IMAGE VM EXISTS - END.") + + return nil + } +} + +func testCheckAzureRMImageDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*ArmClient).diskClient + + for _, rs := range s.RootModule().Resources { + if rs.Type != "azurerm_image" { + continue + } + + name := rs.Primary.Attributes["name"] + resourceGroup := rs.Primary.Attributes["resource_group_name"] + + resp, err := conn.Get(resourceGroup, name) + + if err != nil { + return nil + } + + if resp.StatusCode != http.StatusNotFound { + return fmt.Errorf("Managed Image still exists: \n%#v", resp.Properties) + } + } + + return nil +} + +func testAccAzureRMImage_standaloneImage_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 testAccAzureRMImage_standaloneImage_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 = "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" + } +} + +resource "azurerm_image" "test" { + 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" + } +} +`, rInt, userName, password, hostName) +} + +func testAccAzureRMImage_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 testAccAzureRMImage_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_network_interface" "testdestination" { + name = "acctnicdest-%[1]d" + location = "West US" + resource_group_name = "${azurerm_resource_group.test.name}" + + ip_configuration { + name = "testconfiguration2" + subnet_id = "${azurerm_subnet.test.id}" + private_ip_address_allocation = "dynamic" + } +} + +resource "azurerm_virtual_machine" "testdestination" { + name = "acctvm" + location = "West US" + resource_group_name = "${azurerm_resource_group.test.name}" + network_interface_ids = ["${azurerm_network_interface.testdestination.id}"] + vm_size = "Standard_D1_v2" + + storage_image_reference { + id = "${azurerm_image.testdestination.id}" + } + + storage_os_disk { + name = "myosdisk1" + caching = "ReadWrite" + create_option = "FromImage" + } + + 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 testAccAzureRMImage_customImage_fromVM_sourceVM(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_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" + caching = "ReadWrite" + create_option = "FromImage" + } + + 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 testAccAzureRMImage_customImage_fromVM_destinationVM(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_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" + caching = "ReadWrite" + create_option = "FromImage" + } + + 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 = "acctestdest-%[1]d" + location = "West US" + resource_group_name = "${azurerm_resource_group.test.name}" + source_virtual_machine_id = "${azurerm_virtual_machine.testsource.id}" + tags { + environment = "acctest" + cost-center = "ops" + } +} + +resource "azurerm_network_interface" "testdestination" { + name = "acctnicdest-%[1]d" + location = "West US" + resource_group_name = "${azurerm_resource_group.test.name}" + + ip_configuration { + name = "testconfiguration2" + subnet_id = "${azurerm_subnet.test.id}" + private_ip_address_allocation = "dynamic" + } +} + +resource "azurerm_virtual_machine" "testdestination" { + name = "testdestination" + location = "West US" + resource_group_name = "${azurerm_resource_group.test.name}" + network_interface_ids = ["${azurerm_network_interface.testdestination.id}"] + vm_size = "Standard_D1_v2" + + storage_image_reference { + id = "${azurerm_image.testdestination.id}" + } + + storage_os_disk { + name = "myosdisk2" + caching = "ReadWrite" + create_option = "FromImage" + } + + os_profile { + computer_name = "mdimagetestdest" + 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) +} diff --git a/azurerm/resource_arm_virtual_machine.go b/azurerm/resource_arm_virtual_machine.go index 8e0f56b24ec2..2ba52c7ed6f3 100644 --- a/azurerm/resource_arm_virtual_machine.go +++ b/azurerm/resource_arm_virtual_machine.go @@ -96,21 +96,27 @@ func resourceArmVirtualMachine() *schema.Resource { MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "publisher": { Type: schema.TypeString, - Required: true, + Optional: true, ForceNew: true, }, "offer": { Type: schema.TypeString, - Required: true, + Optional: true, ForceNew: true, }, "sku": { Type: schema.TypeString, - Required: true, + Optional: true, ForceNew: true, }, @@ -845,10 +851,18 @@ func resourceArmVirtualMachinePlanHash(v interface{}) int { func resourceArmVirtualMachineStorageImageReferenceHash(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))) - + 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["id"] != nil { + buf.WriteString(fmt.Sprintf("%s-", m["id"].(string))) + } return hashcode.String(buf.String()) } @@ -901,13 +915,21 @@ func flattenAzureRmVirtualMachinePlan(plan *compute.Plan) []interface{} { func flattenAzureRmVirtualMachineImageReference(image *compute.ImageReference) []interface{} { result := make(map[string]interface{}) - result["offer"] = *image.Offer - result["publisher"] = *image.Publisher - result["sku"] = *image.Sku - + 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} } @@ -1332,14 +1354,12 @@ func expandAzureRmVirtualMachineDataDisk(d *schema.ResourceData) ([]compute.Data data_disk.ManagedDisk = managedDisk } - //BEGIN: code to be removed after GH-13016 is merged if vhdURI != "" && managedDiskID != "" { return nil, fmt.Errorf("[ERROR] Conflict between `vhd_uri` and `managed_disk_id` (only one or the other can be used)") } if vhdURI != "" && managedDiskType != "" { return nil, fmt.Errorf("[ERROR] Conflict between `vhd_uri` and `managed_disk_type` (only one or the other can be used)") } - //END: code to be removed after GH-13016 is merged if managedDiskID == "" && strings.EqualFold(string(data_disk.CreateOption), string(compute.Attach)) { return nil, fmt.Errorf("[ERROR] Must specify which disk to attach") } @@ -1383,18 +1403,31 @@ func expandAzureRmVirtualMachineImageReference(d *schema.ResourceData) (*compute storageImageRefs := d.Get("storage_image_reference").(*schema.Set).List() 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 = compute.ImageReference{ + Publisher: &publisher, + Offer: &offer, + Sku: &sku, + Version: &version, + } + } + + return &imageReference, nil } func expandAzureRmVirtualMachineNetworkProfile(d *schema.ResourceData) compute.NetworkProfile { diff --git a/website/azurerm.erb b/website/azurerm.erb index 95c5b39791cf..3e4004aa3465 100644 --- a/website/azurerm.erb +++ b/website/azurerm.erb @@ -171,6 +171,9 @@