diff --git a/internal/features/defaults.go b/internal/features/defaults.go index 4bb9b38a5133..e7f04f1250be 100644 --- a/internal/features/defaults.go +++ b/internal/features/defaults.go @@ -44,9 +44,10 @@ func Default() UserFeatures { DeleteNestedItemsDuringDeletion: true, }, VirtualMachine: VirtualMachineFeatures{ - DeleteOSDiskOnDeletion: true, - GracefulShutdown: false, - SkipShutdownAndForceDelete: false, + DetachImplicitDataDiskOnDeletion: false, + DeleteOSDiskOnDeletion: true, + GracefulShutdown: false, + SkipShutdownAndForceDelete: false, }, VirtualMachineScaleSet: VirtualMachineScaleSetFeatures{ ForceDelete: false, diff --git a/internal/features/user_flags.go b/internal/features/user_flags.go index f696f5cfeb6c..40a8f5194953 100644 --- a/internal/features/user_flags.go +++ b/internal/features/user_flags.go @@ -26,9 +26,10 @@ type CognitiveAccountFeatures struct { } type VirtualMachineFeatures struct { - DeleteOSDiskOnDeletion bool - GracefulShutdown bool - SkipShutdownAndForceDelete bool + DetachImplicitDataDiskOnDeletion bool + DeleteOSDiskOnDeletion bool + GracefulShutdown bool + SkipShutdownAndForceDelete bool } type VirtualMachineScaleSetFeatures struct { diff --git a/internal/provider/features.go b/internal/provider/features.go index 57238f1db26d..8c92c0253526 100644 --- a/internal/provider/features.go +++ b/internal/provider/features.go @@ -196,6 +196,11 @@ func schemaFeatures(supportLegacyTestSuite bool) *pluginsdk.Schema { MaxItems: 1, Elem: &pluginsdk.Resource{ Schema: map[string]*pluginsdk.Schema{ + "detach_implicit_data_disk_on_deletion": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: false, + }, "delete_os_disk_on_deletion": { Type: pluginsdk.TypeBool, Optional: true, @@ -477,6 +482,9 @@ func expandFeatures(input []interface{}) features.UserFeatures { items := raw.([]interface{}) if len(items) > 0 && items[0] != nil { virtualMachinesRaw := items[0].(map[string]interface{}) + if v, ok := virtualMachinesRaw["detach_implicit_data_disk_on_deletion"]; ok { + featuresMap.VirtualMachine.DetachImplicitDataDiskOnDeletion = v.(bool) + } if v, ok := virtualMachinesRaw["delete_os_disk_on_deletion"]; ok { featuresMap.VirtualMachine.DeleteOSDiskOnDeletion = v.(bool) } diff --git a/internal/provider/features_test.go b/internal/provider/features_test.go index 3714c590c6c7..bf6195559272 100644 --- a/internal/provider/features_test.go +++ b/internal/provider/features_test.go @@ -56,9 +56,10 @@ func TestExpandFeatures(t *testing.T) { DeleteNestedItemsDuringDeletion: true, }, VirtualMachine: features.VirtualMachineFeatures{ - DeleteOSDiskOnDeletion: true, - GracefulShutdown: false, - SkipShutdownAndForceDelete: false, + DetachImplicitDataDiskOnDeletion: false, + DeleteOSDiskOnDeletion: true, + GracefulShutdown: false, + SkipShutdownAndForceDelete: false, }, VirtualMachineScaleSet: features.VirtualMachineScaleSetFeatures{ ForceDelete: false, @@ -155,9 +156,10 @@ func TestExpandFeatures(t *testing.T) { }, "virtual_machine": []interface{}{ map[string]interface{}{ - "delete_os_disk_on_deletion": true, - "graceful_shutdown": true, - "skip_shutdown_and_force_delete": true, + "detach_implicit_data_disk_on_deletion": true, + "delete_os_disk_on_deletion": true, + "graceful_shutdown": true, + "skip_shutdown_and_force_delete": true, }, }, "virtual_machine_scale_set": []interface{}{ @@ -223,9 +225,10 @@ func TestExpandFeatures(t *testing.T) { DeleteNestedItemsDuringDeletion: true, }, VirtualMachine: features.VirtualMachineFeatures{ - DeleteOSDiskOnDeletion: true, - GracefulShutdown: true, - SkipShutdownAndForceDelete: true, + DetachImplicitDataDiskOnDeletion: true, + DeleteOSDiskOnDeletion: true, + GracefulShutdown: true, + SkipShutdownAndForceDelete: true, }, VirtualMachineScaleSet: features.VirtualMachineScaleSetFeatures{ ReimageOnManualUpgrade: true, @@ -316,9 +319,10 @@ func TestExpandFeatures(t *testing.T) { }, "virtual_machine": []interface{}{ map[string]interface{}{ - "delete_os_disk_on_deletion": false, - "graceful_shutdown": false, - "skip_shutdown_and_force_delete": false, + "detach_implicit_data_disk_on_deletion": false, + "delete_os_disk_on_deletion": false, + "graceful_shutdown": false, + "skip_shutdown_and_force_delete": false, }, }, "virtual_machine_scale_set": []interface{}{ @@ -384,9 +388,10 @@ func TestExpandFeatures(t *testing.T) { DeleteNestedItemsDuringDeletion: false, }, VirtualMachine: features.VirtualMachineFeatures{ - DeleteOSDiskOnDeletion: false, - GracefulShutdown: false, - SkipShutdownAndForceDelete: false, + DetachImplicitDataDiskOnDeletion: false, + DeleteOSDiskOnDeletion: false, + GracefulShutdown: false, + SkipShutdownAndForceDelete: false, }, VirtualMachineScaleSet: features.VirtualMachineScaleSetFeatures{ ForceDelete: false, @@ -873,9 +878,34 @@ func TestExpandFeaturesVirtualMachine(t *testing.T) { }, Expected: features.UserFeatures{ VirtualMachine: features.VirtualMachineFeatures{ - DeleteOSDiskOnDeletion: true, - GracefulShutdown: false, - SkipShutdownAndForceDelete: false, + DetachImplicitDataDiskOnDeletion: false, + DeleteOSDiskOnDeletion: true, + GracefulShutdown: false, + SkipShutdownAndForceDelete: false, + }, + }, + }, + { + Name: "Detach implicit Data Disk Enabled", + Input: []interface{}{ + map[string]interface{}{ + "virtual_machine": []interface{}{ + map[string]interface{}{ + "detach_implicit_data_disk_on_deletion": true, + "delete_os_disk_on_deletion": false, + "graceful_shutdown": false, + "force_delete": false, + "shutdown_before_deletion": false, + }, + }, + }, + }, + Expected: features.UserFeatures{ + VirtualMachine: features.VirtualMachineFeatures{ + DetachImplicitDataDiskOnDeletion: true, + DeleteOSDiskOnDeletion: false, + GracefulShutdown: false, + SkipShutdownAndForceDelete: false, }, }, }, @@ -885,19 +915,21 @@ func TestExpandFeaturesVirtualMachine(t *testing.T) { map[string]interface{}{ "virtual_machine": []interface{}{ map[string]interface{}{ - "delete_os_disk_on_deletion": true, - "graceful_shutdown": false, - "force_delete": false, - "shutdown_before_deletion": false, + "detach_implicit_data_disk_on_deletion": false, + "delete_os_disk_on_deletion": true, + "graceful_shutdown": false, + "force_delete": false, + "shutdown_before_deletion": false, }, }, }, }, Expected: features.UserFeatures{ VirtualMachine: features.VirtualMachineFeatures{ - DeleteOSDiskOnDeletion: true, - GracefulShutdown: false, - SkipShutdownAndForceDelete: false, + DetachImplicitDataDiskOnDeletion: false, + DeleteOSDiskOnDeletion: true, + GracefulShutdown: false, + SkipShutdownAndForceDelete: false, }, }, }, @@ -907,18 +939,20 @@ func TestExpandFeaturesVirtualMachine(t *testing.T) { map[string]interface{}{ "virtual_machine": []interface{}{ map[string]interface{}{ - "delete_os_disk_on_deletion": false, - "graceful_shutdown": true, - "force_delete": false, + "detach_implicit_data_disk_on_deletion": false, + "delete_os_disk_on_deletion": false, + "graceful_shutdown": true, + "force_delete": false, }, }, }, }, Expected: features.UserFeatures{ VirtualMachine: features.VirtualMachineFeatures{ - DeleteOSDiskOnDeletion: false, - GracefulShutdown: true, - SkipShutdownAndForceDelete: false, + DetachImplicitDataDiskOnDeletion: false, + DeleteOSDiskOnDeletion: false, + GracefulShutdown: true, + SkipShutdownAndForceDelete: false, }, }, }, @@ -928,18 +962,20 @@ func TestExpandFeaturesVirtualMachine(t *testing.T) { map[string]interface{}{ "virtual_machine": []interface{}{ map[string]interface{}{ - "delete_os_disk_on_deletion": false, - "graceful_shutdown": false, - "skip_shutdown_and_force_delete": true, + "detach_implicit_data_disk_on_deletion": false, + "delete_os_disk_on_deletion": false, + "graceful_shutdown": false, + "skip_shutdown_and_force_delete": true, }, }, }, }, Expected: features.UserFeatures{ VirtualMachine: features.VirtualMachineFeatures{ - DeleteOSDiskOnDeletion: false, - GracefulShutdown: false, - SkipShutdownAndForceDelete: true, + DetachImplicitDataDiskOnDeletion: false, + DeleteOSDiskOnDeletion: false, + GracefulShutdown: false, + SkipShutdownAndForceDelete: true, }, }, }, @@ -949,18 +985,20 @@ func TestExpandFeaturesVirtualMachine(t *testing.T) { map[string]interface{}{ "virtual_machine": []interface{}{ map[string]interface{}{ - "delete_os_disk_on_deletion": false, - "graceful_shutdown": false, - "skip_shutdown_and_force_delete": false, + "detach_implicit_data_disk_on_deletion": false, + "delete_os_disk_on_deletion": false, + "graceful_shutdown": false, + "skip_shutdown_and_force_delete": false, }, }, }, }, Expected: features.UserFeatures{ VirtualMachine: features.VirtualMachineFeatures{ - DeleteOSDiskOnDeletion: false, - GracefulShutdown: false, - SkipShutdownAndForceDelete: false, + DetachImplicitDataDiskOnDeletion: false, + DeleteOSDiskOnDeletion: false, + GracefulShutdown: false, + SkipShutdownAndForceDelete: false, }, }, }, diff --git a/internal/services/compute/registration.go b/internal/services/compute/registration.go index 70a043118ee3..03b2c9a70e88 100644 --- a/internal/services/compute/registration.go +++ b/internal/services/compute/registration.go @@ -88,6 +88,7 @@ func (r Registration) DataSources() []sdk.DataSource { func (r Registration) Resources() []sdk.Resource { return []sdk.Resource{ + VirtualMachineImplicitDataDiskFromSourceResource{}, VirtualMachineRunCommandResource{}, GalleryApplicationResource{}, GalleryApplicationVersionResource{}, diff --git a/internal/services/compute/virtual_machine_data_disk_attachment_resource.go b/internal/services/compute/virtual_machine_data_disk_attachment_resource.go index db0a7603410c..6478ffa1d2f2 100644 --- a/internal/services/compute/virtual_machine_data_disk_attachment_resource.go +++ b/internal/services/compute/virtual_machine_data_disk_attachment_resource.go @@ -4,6 +4,7 @@ package compute import ( + "context" "fmt" "log" "time" @@ -29,9 +30,51 @@ func resourceVirtualMachineDataDiskAttachment() *pluginsdk.Resource { Read: resourceVirtualMachineDataDiskAttachmentRead, Update: resourceVirtualMachineDataDiskAttachmentCreateUpdate, Delete: resourceVirtualMachineDataDiskAttachmentDelete, - Importer: pluginsdk.ImporterValidatingResourceId(func(id string) error { + Importer: pluginsdk.ImporterValidatingResourceIdThen(func(id string) error { _, err := parse.DataDiskID(id) return err + }, func(ctx context.Context, d *pluginsdk.ResourceData, meta interface{}) ([]*pluginsdk.ResourceData, error) { + client := meta.(*clients.Client).Compute.VirtualMachinesClient + id, err := parse.DataDiskID(d.Id()) + if err != nil { + return nil, err + } + + virtualMachineId := virtualmachines.NewVirtualMachineID(id.SubscriptionId, id.ResourceGroup, id.VirtualMachineName) + virtualMachine, err := client.Get(ctx, virtualMachineId, virtualmachines.DefaultGetOperationOptions()) + if err != nil { + if response.WasNotFound(virtualMachine.HttpResponse) { + return nil, fmt.Errorf("%s was not found therefore Data Disk Attachment cannot be imported", virtualMachineId) + } + + return nil, fmt.Errorf("retrieving %s: %+v", id, err) + } + + var disk *virtualmachines.DataDisk + if model := virtualMachine.Model; model != nil { + if props := model.Properties; props != nil { + if profile := props.StorageProfile; profile != nil { + if dataDisks := profile.DataDisks; dataDisks != nil { + for _, dataDisk := range *dataDisks { + if *dataDisk.Name == id.Name { + disk = &dataDisk + break + } + } + } + } + } + } + + if disk == nil { + return nil, fmt.Errorf("Data Disk %s was not found", *id) + } + + if disk.CreateOption != virtualmachines.DiskCreateOptionTypesAttach && disk.CreateOption != virtualmachines.DiskCreateOptionTypesEmpty { + return nil, fmt.Errorf("the value of `create_option` for the imported `azurerm_virtual_machine_data_disk_attachment` instance must be `Attach` or `Empty`, whereas now is %s", disk.CreateOption) + } + + return []*pluginsdk.ResourceData{d}, nil }), Timeouts: &pluginsdk.ResourceTimeout{ @@ -154,6 +197,15 @@ func resourceVirtualMachineDataDiskAttachmentCreateUpdate(d *pluginsdk.ResourceD WriteAcceleratorEnabled: pointer.To(writeAcceleratorEnabled), } + // there are ways to provision a VM without a StorageProfile and/or DataDisks + if virtualMachine.Model.Properties.StorageProfile == nil { + virtualMachine.Model.Properties.StorageProfile = &virtualmachines.StorageProfile{} + } + + if virtualMachine.Model.Properties.StorageProfile.DataDisks == nil { + virtualMachine.Model.Properties.StorageProfile.DataDisks = pointer.To(make([]virtualmachines.DataDisk, 0)) + } + disks := *virtualMachine.Model.Properties.StorageProfile.DataDisks existingIndex := -1 diff --git a/internal/services/compute/virtual_machine_data_disk_attachment_resource_test.go b/internal/services/compute/virtual_machine_data_disk_attachment_resource_test.go index d25ef8f3276f..dcc9aa5fba68 100644 --- a/internal/services/compute/virtual_machine_data_disk_attachment_resource_test.go +++ b/internal/services/compute/virtual_machine_data_disk_attachment_resource_test.go @@ -6,6 +6,8 @@ package compute_test import ( "context" "fmt" + "os" + "regexp" "testing" "github.com/hashicorp/go-azure-helpers/lang/pointer" @@ -211,6 +213,23 @@ func TestAccVirtualMachineDataDiskAttachment_virtualMachineApplication(t *testin }) } +func TestAccVirtualMachineDataDiskAttachment_importImplicitDataDisk(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_virtual_machine_data_disk_attachment", "test") + r := VirtualMachineDataDiskAttachmentResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.importImplicitDataDisk(data), + }, + { + ResourceName: data.ResourceName, + ImportState: true, + ImportStateId: fmt.Sprintf("/subscriptions/%s/resourceGroups/acctestRG-%d/providers/Microsoft.Compute/virtualMachines/acctvm-%d/dataDisks/acctestVMIDD-%d", os.Getenv("ARM_SUBSCRIPTION_ID"), data.RandomInteger, data.RandomInteger, data.RandomInteger), + ExpectError: regexp.MustCompile("the value of `create_option` for the imported `azurerm_virtual_machine_data_disk_attachment` instance must be `Attach` or `Empty`"), + }, + }) +} + func (t VirtualMachineDataDiskAttachmentResource) Exists(ctx context.Context, clients *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { id, err := parse.DataDiskID(state.ID) if err != nil { @@ -915,3 +934,121 @@ resource "azurerm_virtual_machine_data_disk_attachment" "test" { } `, r.virtualMachineApplicationPrep(data), data.RandomInteger) } + +func (r VirtualMachineDataDiskAttachmentResource) importImplicitDataDisk(data acceptance.TestData) string { + // currently implicit data disk is only supported in "eastus2" and "westus2". + location := "westus2" + + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_virtual_network" "test" { + name = "acctvn-%d" + address_space = ["10.0.0.0/16"] + location = azurerm_resource_group.test.location + 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_prefixes = ["10.0.2.0/24"] +} + +resource "azurerm_network_interface" "test" { + name = "acctni-%d" + location = azurerm_resource_group.test.location + 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_virtual_machine" "test" { + name = "acctvm-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + network_interface_ids = [azurerm_network_interface.test.id] + vm_size = "Standard_F2" + + delete_os_disk_on_termination = true + + storage_image_reference { + publisher = "Canonical" + offer = "0001-com-ubuntu-server-jammy" + sku = "22_04-lts" + version = "latest" + } + + storage_os_disk { + name = "myosdisk1" + caching = "ReadWrite" + create_option = "FromImage" + managed_disk_type = "Standard_LRS" + } + + os_profile { + computer_name = "hn%d" + admin_username = "testadmin" + admin_password = "Password1234!" + } + + os_profile_linux_config { + disable_password_authentication = false + } +} + +resource "azurerm_managed_disk" "test" { + name = "%d-disk1" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + storage_account_type = "Standard_LRS" + create_option = "Empty" + disk_size_gb = 10 +} + +resource "azurerm_snapshot" "test" { + name = "acctestss-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + create_option = "Copy" + source_uri = azurerm_managed_disk.test.id +} + +resource "azurerm_virtual_machine_implicit_data_disk_from_source" "test" { + name = "acctestVMIDD-%d" + virtual_machine_id = azurerm_virtual_machine.test.id + lun = "0" + create_option = "Copy" + disk_size_gb = 20 + source_resource_id = azurerm_snapshot.test.id +} + +resource "azurerm_managed_disk" "test2" { + name = "%d-disk2" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + storage_account_type = "Standard_LRS" + create_option = "Empty" + disk_size_gb = 10 +} + +resource "azurerm_virtual_machine_data_disk_attachment" "test" { + managed_disk_id = azurerm_managed_disk.test2.id + virtual_machine_id = azurerm_virtual_machine.test.id + lun = "1" + caching = "None" +} +`, data.RandomInteger, location, data.RandomInteger, data.RandomInteger, data.RandomInteger, data.RandomInteger, data.RandomInteger, data.RandomInteger, data.RandomInteger, data.RandomInteger, data.RandomInteger) +} diff --git a/internal/services/compute/virtual_machine_implicit_data_disk_from_source_resource.go b/internal/services/compute/virtual_machine_implicit_data_disk_from_source_resource.go new file mode 100644 index 000000000000..d21a70132b50 --- /dev/null +++ b/internal/services/compute/virtual_machine_implicit_data_disk_from_source_resource.go @@ -0,0 +1,463 @@ +package compute + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonids" + "github.com/hashicorp/go-azure-sdk/resource-manager/compute/2022-03-02/snapshots" + "github.com/hashicorp/go-azure-sdk/resource-manager/compute/2024-03-01/virtualmachines" + "github.com/hashicorp/terraform-provider-azurerm/internal/locks" + "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/compute/parse" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/compute/validate" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" +) + +var ( + _ sdk.Resource = VirtualMachineImplicitDataDiskFromSourceResource{} + _ sdk.ResourceWithUpdate = VirtualMachineImplicitDataDiskFromSourceResource{} + _ sdk.ResourceWithCustomImporter = VirtualMachineImplicitDataDiskFromSourceResource{} +) + +type VirtualMachineImplicitDataDiskFromSourceResource struct{} + +func (r VirtualMachineImplicitDataDiskFromSourceResource) ModelObject() interface{} { + return &VirtualMachineImplicitDataDiskFromSourceResourceModel{} +} + +type VirtualMachineImplicitDataDiskFromSourceResourceModel struct { + Name string `tfschema:"name"` + VirtualMachineId string `tfschema:"virtual_machine_id"` + Lun int64 `tfschema:"lun"` + Caching string `tfschema:"caching"` + CreateOption string `tfschema:"create_option"` + DiskSizeGb int64 `tfschema:"disk_size_gb"` + SourceResourceId string `tfschema:"source_resource_id"` + WriteAcceleratorEnabled bool `tfschema:"write_accelerator_enabled"` +} + +func (r VirtualMachineImplicitDataDiskFromSourceResource) IDValidationFunc() pluginsdk.SchemaValidateFunc { + return validate.DataDiskID +} + +func (r VirtualMachineImplicitDataDiskFromSourceResource) ResourceType() string { + return "azurerm_virtual_machine_implicit_data_disk_from_source" +} + +func (r VirtualMachineImplicitDataDiskFromSourceResource) Arguments() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "virtual_machine_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: commonids.ValidateVirtualMachineID, + }, + + "lun": { + Type: pluginsdk.TypeInt, + Required: true, + ForceNew: true, + ValidateFunc: validation.IntAtLeast(0), + }, + + "create_option": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice([]string{ + string(virtualmachines.DiskCreateOptionTypesCopy), + }, false), + }, + + "disk_size_gb": { + Type: pluginsdk.TypeInt, + Required: true, + ForceNew: true, + ValidateFunc: validation.IntBetween(1, 1023), + }, + + "source_resource_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.Any(snapshots.ValidateSnapshotID, commonids.ValidateManagedDiskID), + }, + + "caching": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + string(virtualmachines.CachingTypesReadOnly), + string(virtualmachines.CachingTypesReadWrite), + }, false), + }, + + "write_accelerator_enabled": { + Type: pluginsdk.TypeBool, + Optional: true, + Default: false, + }, + } +} + +func (r VirtualMachineImplicitDataDiskFromSourceResource) Attributes() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{} +} + +func (r VirtualMachineImplicitDataDiskFromSourceResource) Create() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.Compute.VirtualMachinesClient + + var config VirtualMachineImplicitDataDiskFromSourceResourceModel + if err := metadata.Decode(&config); err != nil { + return fmt.Errorf("decoding: %+v", err) + } + + subscriptionId := metadata.Client.Account.SubscriptionId + virtualMachineId, err := virtualmachines.ParseVirtualMachineID(config.VirtualMachineId) + if err != nil { + return err + } + + locks.ByName(virtualMachineId.VirtualMachineName, VirtualMachineResourceName) + defer locks.UnlockByName(virtualMachineId.VirtualMachineName, VirtualMachineResourceName) + + id := parse.NewDataDiskID(subscriptionId, virtualMachineId.ResourceGroupName, virtualMachineId.VirtualMachineName, config.Name) + + resp, err := client.Get(ctx, *virtualMachineId, virtualmachines.DefaultGetOperationOptions()) + if err != nil { + return fmt.Errorf("checking for the presence of an existing %s: %+v", *virtualMachineId, err) + } + + caching := string(virtualmachines.CachingTypesNone) + if config.Caching != "" { + caching = config.Caching + } + + expandedDisk := virtualmachines.DataDisk{ + Name: pointer.To(config.Name), + Caching: pointer.To(virtualmachines.CachingTypes(caching)), + CreateOption: virtualmachines.DiskCreateOptionTypes(config.CreateOption), + DiskSizeGB: pointer.To(config.DiskSizeGb), + Lun: config.Lun, + SourceResource: &virtualmachines.ApiEntityReference{ + Id: pointer.To(config.SourceResourceId), + }, + WriteAcceleratorEnabled: pointer.To(config.WriteAcceleratorEnabled), + } + + if model := resp.Model; model != nil { + if model.Properties != nil { + // there are ways to provision a VM without a StorageProfile and/or DataDisks + if model.Properties.StorageProfile == nil { + model.Properties.StorageProfile = &virtualmachines.StorageProfile{} + } + + if model.Properties.StorageProfile.DataDisks == nil { + model.Properties.StorageProfile.DataDisks = pointer.To(make([]virtualmachines.DataDisk, 0)) + } + + existingIndex := -1 + disks := *model.Properties.StorageProfile.DataDisks + for i, disk := range disks { + if pointer.From(disk.Name) == config.Name { + existingIndex = i + break + } + } + + if existingIndex != -1 { + return metadata.ResourceRequiresImport(r.ResourceType(), id) + } + + disks = append(disks, expandedDisk) + model.Properties.StorageProfile.DataDisks = &disks + // fixes #24145 + model.Properties.ApplicationProfile = nil + // fixes #2485 + model.Identity = nil + // fixes #1600 + model.Resources = nil + err = client.CreateOrUpdateThenPoll(ctx, *virtualMachineId, *model, virtualmachines.DefaultCreateOrUpdateOperationOptions()) + if err != nil { + return fmt.Errorf("creating %s: %+v", id, err) + } + } + } + + metadata.SetID(id) + return nil + }, + } +} + +func (r VirtualMachineImplicitDataDiskFromSourceResource) Read() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.Compute.VirtualMachinesClient + + id, err := parse.DataDiskID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + virtualMachineId := virtualmachines.NewVirtualMachineID(id.SubscriptionId, id.ResourceGroup, id.VirtualMachineName) + resp, err := client.Get(ctx, virtualMachineId, virtualmachines.DefaultGetOperationOptions()) + if err != nil { + return fmt.Errorf("retrieving %s: %+v", virtualMachineId, err) + } + + schema := VirtualMachineImplicitDataDiskFromSourceResourceModel{ + Name: id.Name, + VirtualMachineId: virtualMachineId.ID(), + } + + var disk *virtualmachines.DataDisk + if model := resp.Model; model != nil { + if props := model.Properties; props != nil { + if profile := props.StorageProfile; profile != nil { + if dataDisks := profile.DataDisks; dataDisks != nil { + for _, dataDisk := range *dataDisks { + if pointer.From(dataDisk.Name) == id.Name { + disk = &dataDisk + break + } + } + } + } + } + } + + if disk == nil { + return metadata.MarkAsGone(*id) + } + + schema.Lun = disk.Lun + if v := pointer.From(disk.Caching); v != virtualmachines.CachingTypesNone { + schema.Caching = string(v) + } + + schema.CreateOption = string(disk.CreateOption) + schema.DiskSizeGb = pointer.From(disk.DiskSizeGB) + if disk.SourceResource != nil { + schema.SourceResourceId = pointer.From(disk.SourceResource.Id) + } + + schema.WriteAcceleratorEnabled = pointer.From(disk.WriteAcceleratorEnabled) + + return metadata.Encode(&schema) + }, + } +} + +func (r VirtualMachineImplicitDataDiskFromSourceResource) Delete() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.Compute.VirtualMachinesClient + + id, err := parse.DataDiskID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + locks.ByName(id.VirtualMachineName, VirtualMachineResourceName) + defer locks.UnlockByName(id.VirtualMachineName, VirtualMachineResourceName) + + virtualMachineId := virtualmachines.NewVirtualMachineID(id.SubscriptionId, id.ResourceGroup, id.VirtualMachineName) + resp, err := client.Get(ctx, virtualMachineId, virtualmachines.DefaultGetOperationOptions()) + if err != nil { + return fmt.Errorf("retrieving %s: %+v", virtualMachineId, err) + } + + if model := resp.Model; model != nil { + if props := model.Properties; props != nil { + if profile := props.StorageProfile; profile != nil { + newDisks := make([]virtualmachines.DataDisk, 0) + var toBeDeletedDisk *virtualmachines.DataDisk + if dataDisks := profile.DataDisks; dataDisks != nil { + for _, dataDisk := range *dataDisks { + if pointer.From(dataDisk.Name) != id.Name { + newDisks = append(newDisks, dataDisk) + } else { + toBeDeletedDisk = pointer.To(dataDisk) + } + } + } + + profile.DataDisks = &newDisks + + // fixes #24145 + model.Properties.ApplicationProfile = nil + + // fixes #2485 + model.Identity = nil + // fixes #1600 + model.Resources = nil + + err = client.CreateOrUpdateThenPoll(ctx, virtualMachineId, *model, virtualmachines.DefaultCreateOrUpdateOperationOptions()) + if err != nil { + return fmt.Errorf("deleting %s: %+v", id, err) + } + + // delete the data disk which was created by Azure Service when creating this resource + detachDataDisk := metadata.Client.Features.VirtualMachine.DetachImplicitDataDiskOnDeletion + if !detachDataDisk && toBeDeletedDisk != nil && toBeDeletedDisk.ManagedDisk != nil && toBeDeletedDisk.ManagedDisk.Id != nil { + diskClient := metadata.Client.Compute.DisksClient + diskId, err := commonids.ParseManagedDiskID(*toBeDeletedDisk.ManagedDisk.Id) + if err != nil { + return err + } + + err = diskClient.DeleteThenPoll(ctx, *diskId) + if err != nil { + return fmt.Errorf("deleting Managed Disk %s: %+v", *diskId, err) + } + } + } + } + } + + return nil + }, + } +} + +func (r VirtualMachineImplicitDataDiskFromSourceResource) Update() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.Compute.VirtualMachinesClient + + var config VirtualMachineImplicitDataDiskFromSourceResourceModel + if err := metadata.Decode(&config); err != nil { + return fmt.Errorf("decoding: %+v", err) + } + + id, err := parse.DataDiskID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + locks.ByName(id.VirtualMachineName, VirtualMachineResourceName) + defer locks.UnlockByName(id.VirtualMachineName, VirtualMachineResourceName) + + virtualMachineId := virtualmachines.NewVirtualMachineID(id.SubscriptionId, id.ResourceGroup, id.VirtualMachineName) + resp, err := client.Get(ctx, virtualMachineId, virtualmachines.DefaultGetOperationOptions()) + if err != nil { + return fmt.Errorf("retrieving %s: %+v", virtualMachineId, err) + } + + if model := resp.Model; model != nil { + if props := model.Properties; props != nil { + if profile := props.StorageProfile; profile != nil { + if dataDisks := profile.DataDisks; dataDisks != nil { + existingIndex := -1 + disks := *dataDisks + for i, disk := range disks { + if pointer.From(disk.Name) == config.Name { + existingIndex = i + break + } + } + + if existingIndex == -1 { + return fmt.Errorf("unable to retrieve the data disk %s ", *id) + } + + expandedDisk := &disks[existingIndex] + if metadata.ResourceData.HasChange("caching") { + caching := string(virtualmachines.CachingTypesNone) + if config.Caching != "" { + caching = config.Caching + } + + expandedDisk.Caching = pointer.To(virtualmachines.CachingTypes(caching)) + } + + if metadata.ResourceData.HasChange("disk_size_gb") { + expandedDisk.DiskSizeGB = pointer.To(config.DiskSizeGb) + } + + if metadata.ResourceData.HasChange("write_accelerator_enabled") { + expandedDisk.WriteAcceleratorEnabled = pointer.To(config.WriteAcceleratorEnabled) + } + + profile.DataDisks = &disks + // fixes #24145 + model.Properties.ApplicationProfile = nil + // fixes #2485 + model.Identity = nil + // fixes #1600 + model.Resources = nil + + err = client.CreateOrUpdateThenPoll(ctx, virtualMachineId, *model, virtualmachines.DefaultCreateOrUpdateOperationOptions()) + if err != nil { + return fmt.Errorf("updating %s: %+v", id, err) + } + } + } + } + } + + return nil + }, + } +} + +func (r VirtualMachineImplicitDataDiskFromSourceResource) CustomImporter() sdk.ResourceRunFunc { + return func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.Compute.VirtualMachinesClient + + id, err := parse.DataDiskID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + virtualMachineId := virtualmachines.NewVirtualMachineID(id.SubscriptionId, id.ResourceGroup, id.VirtualMachineName) + resp, err := client.Get(ctx, virtualMachineId, virtualmachines.DefaultGetOperationOptions()) + if err != nil { + return fmt.Errorf("retrieving %s: %+v", virtualMachineId, err) + } + + if model := resp.Model; model != nil { + if props := model.Properties; props != nil { + if profile := props.StorageProfile; profile != nil { + if dataDisks := profile.DataDisks; dataDisks != nil { + var disk *virtualmachines.DataDisk + for _, dataDisk := range *dataDisks { + if pointer.From(dataDisk.Name) == id.Name { + disk = &dataDisk + break + } + } + + if disk == nil { + return fmt.Errorf("unable to retrieve an existing data disk %s", *id) + } + + if disk.CreateOption != virtualmachines.DiskCreateOptionTypesCopy { + return fmt.Errorf("the value of `create_option` for the imported `azurerm_virtual_machine_implicit_data_disk_from_source` instance must be `Copy`, whereas now is %s", disk.CreateOption) + } + } + } + } + } + + return nil + } +} diff --git a/internal/services/compute/virtual_machine_implicit_data_disk_from_source_resource_test.go b/internal/services/compute/virtual_machine_implicit_data_disk_from_source_resource_test.go new file mode 100644 index 000000000000..72bf87452835 --- /dev/null +++ b/internal/services/compute/virtual_machine_implicit_data_disk_from_source_resource_test.go @@ -0,0 +1,1125 @@ +package compute_test + +import ( + "context" + "fmt" + "os" + "testing" + "time" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonids" + "github.com/hashicorp/go-azure-sdk/resource-manager/compute/2024-03-01/virtualmachines" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance" + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance/check" + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance/testclient" + "github.com/hashicorp/terraform-provider-azurerm/internal/clients" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/compute/parse" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" +) + +type VirtualMachineImplicitDataDiskFromSourceResource struct{} + +func TestAccVirtualMachineImplicitDataDiskFromSource_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_virtual_machine_implicit_data_disk_from_source", "test") + r := VirtualMachineImplicitDataDiskFromSourceResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccVirtualMachineImplicitDataDiskFromSource_requiresImport(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_virtual_machine_implicit_data_disk_from_source", "test") + r := VirtualMachineImplicitDataDiskFromSourceResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + { + Config: r.requiresImport(data), + ExpectError: acceptance.RequiresImportError("azurerm_virtual_machine_implicit_data_disk_from_source"), + }, + }) +} + +func TestAccVirtualMachineImplicitDataDiskFromSource_destroy(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_virtual_machine_implicit_data_disk_from_source", "test") + r := VirtualMachineImplicitDataDiskFromSourceResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + data.DisappearsStep(acceptance.DisappearsStepData{ + Config: r.basic, + TestResource: r, + }), + }) +} + +func TestAccVirtualMachineImplicitDataDiskFromSource_multipleDisks(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_virtual_machine_implicit_data_disk_from_source", "first") + r := VirtualMachineImplicitDataDiskFromSourceResource{} + + secondResourceName := "azurerm_virtual_machine_implicit_data_disk_from_source.second" + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.multipleDisks(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + ResourceName: secondResourceName, + ImportState: true, + ImportStateVerify: true, + }, + }) +} + +func TestAccVirtualMachineImplicitDataDiskFromSource_updatingCaching(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_virtual_machine_implicit_data_disk_from_source", "test") + r := VirtualMachineImplicitDataDiskFromSourceResource{} + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + { + Config: r.readOnly(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + { + Config: r.readWrite(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + }) +} + +func TestAccVirtualMachineImplicitDataDiskFromSource_updatingWriteAccelerator(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_virtual_machine_implicit_data_disk_from_source", "test") + r := VirtualMachineImplicitDataDiskFromSourceResource{} + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.writeAccelerator(data, false), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + { + Config: r.writeAccelerator(data, true), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + { + Config: r.writeAccelerator(data, false), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + }) +} + +func TestAccVirtualMachineImplicitDataDiskFromSource_managedServiceIdentity(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_virtual_machine_implicit_data_disk_from_source", "test") + r := VirtualMachineImplicitDataDiskFromSourceResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.managedServiceIdentity(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccVirtualMachineImplicitDataDiskFromSource_virtualMachineExtension(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_virtual_machine_implicit_data_disk_from_source", "test") + r := VirtualMachineImplicitDataDiskFromSourceResource{} + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.virtualMachineExtensionPrep(data), + }, + { + Config: r.virtualMachineExtensionComplete(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + }) +} + +func TestAccVirtualMachineImplicitDataDiskFromSource_virtualMachineApplication(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_virtual_machine_implicit_data_disk_from_source", "test") + r := VirtualMachineImplicitDataDiskFromSourceResource{} + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.virtualMachineApplicationPrep(data), + }, + { + Config: r.virtualMachineApplicationComplete(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + }) +} + +func TestAccVirtualMachineImplicitDataDiskFromSource_detachImplicitDataDisk(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_virtual_machine_implicit_data_disk_from_source", "test") + r := VirtualMachineImplicitDataDiskFromSourceResource{} + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + }, + { + Config: r.detachImplicitDataDisk(data), + Check: acceptance.ComposeTestCheckFunc( + func(state *terraform.State) error { + client, err := testclient.Build() + if err != nil { + return fmt.Errorf("building client: %+v", err) + } + + ctx, cancel := context.WithDeadline(client.StopContext, time.Now().Add(5*time.Minute)) + defer cancel() + + diskClient := client.Compute.DisksClient + + id, err := commonids.ParseManagedDiskID(fmt.Sprintf("/subscriptions/%s/resourceGroups/acctestRG-%d/providers/Microsoft.Compute/disks/acctestVMIDD-%d", os.Getenv("ARM_SUBSCRIPTION_ID"), data.RandomInteger, data.RandomInteger)) + if err != nil { + return err + } + + _, err = diskClient.Get(ctx, *id) + if err != nil { + return fmt.Errorf("retrieving %s: %+v", id, err) + } + + return nil + }, + ), + }, + }) +} + +func (r VirtualMachineImplicitDataDiskFromSourceResource) detachImplicitDataDisk(data acceptance.TestData) string { + // currently only supported in "eastus2" and "westus2". + location := "westus2" + + return fmt.Sprintf(` +provider "azurerm" { + features { + resource_group { + prevent_deletion_if_contains_resources = false + } + + virtual_machine { + detach_implicit_data_disk_on_deletion = true + } + } +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_virtual_network" "test" { + name = "acctvn-%d" + address_space = ["10.0.0.0/16"] + location = azurerm_resource_group.test.location + 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_prefixes = ["10.0.2.0/24"] +} + +resource "azurerm_network_interface" "test" { + name = "acctni-%d" + location = azurerm_resource_group.test.location + 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_virtual_machine" "test" { + name = "acctvm-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + network_interface_ids = [azurerm_network_interface.test.id] + vm_size = "Standard_F2" + + delete_os_disk_on_termination = true + + storage_image_reference { + publisher = "Canonical" + offer = "0001-com-ubuntu-server-jammy" + sku = "22_04-lts" + version = "latest" + } + + storage_os_disk { + name = "myosdisk1" + caching = "ReadWrite" + create_option = "FromImage" + managed_disk_type = "Standard_LRS" + } + + os_profile { + computer_name = "hn%d" + admin_username = "testadmin" + admin_password = "Password1234!" + } + + os_profile_linux_config { + disable_password_authentication = false + } +} + +resource "azurerm_managed_disk" "test" { + name = "%d-disk1" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + storage_account_type = "Standard_LRS" + create_option = "Empty" + disk_size_gb = 10 +} + +resource "azurerm_snapshot" "test" { + name = "acctestss-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + create_option = "Copy" + source_uri = azurerm_managed_disk.test.id +} +`, data.RandomInteger, location, data.RandomInteger, data.RandomInteger, data.RandomInteger, data.RandomInteger, data.RandomInteger, data.RandomInteger, data.RandomInteger) +} + +func (t VirtualMachineImplicitDataDiskFromSourceResource) Exists(ctx context.Context, clients *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { + id, err := parse.DataDiskID(state.ID) + if err != nil { + return nil, err + } + + virtualMachineId := virtualmachines.NewVirtualMachineID(id.SubscriptionId, id.ResourceGroup, id.VirtualMachineName) + resp, err := clients.Compute.VirtualMachinesClient.Get(ctx, virtualMachineId, virtualmachines.DefaultGetOperationOptions()) + if err != nil { + return nil, fmt.Errorf("retrieving Implicit Data Disk %s: %+v", *id, err) + } + + var disk *virtualmachines.DataDisk + if model := resp.Model; model != nil { + if props := model.Properties; props != nil { + if profile := props.StorageProfile; profile != nil { + if dataDisks := profile.DataDisks; dataDisks != nil { + for _, dataDisk := range *dataDisks { + if pointer.From(dataDisk.Name) == id.Name { + disk = &dataDisk + break + } + } + } + } + } + } + + return pointer.To(disk != nil), nil +} + +func (VirtualMachineImplicitDataDiskFromSourceResource) Destroy(ctx context.Context, client *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { + id, err := parse.DataDiskID(state.ID) + if err != nil { + return nil, err + } + + virtualMachineId := virtualmachines.NewVirtualMachineID(id.SubscriptionId, id.ResourceGroup, id.VirtualMachineName) + resp, err := client.Compute.VirtualMachinesClient.Get(ctx, virtualMachineId, virtualmachines.DefaultGetOperationOptions()) + if err != nil { + return nil, fmt.Errorf("retrieving Implicit Data Disk %s: %+v", *id, err) + } + + outputDisks := make([]virtualmachines.DataDisk, 0) + var toBeDeletedDisk *virtualmachines.DataDisk + if model := resp.Model; model != nil { + if props := model.Properties; props != nil && props.StorageProfile != nil { + for _, disk := range *props.StorageProfile.DataDisks { + if pointer.From(disk.Name) != id.Name { + outputDisks = append(outputDisks, disk) + } else { + toBeDeletedDisk = &disk + } + } + + props.StorageProfile.DataDisks = &outputDisks + + // fixes #2485 + model.Identity = nil + // fixes #1600 + model.Resources = nil + + if err := client.Compute.VirtualMachinesClient.CreateOrUpdateThenPoll(ctx, virtualMachineId, *model, virtualmachines.DefaultCreateOrUpdateOperationOptions()); err != nil { + return nil, fmt.Errorf("deleting implicit data disk %s: %+v", *id, err) + } + + // delete the data disk which was created by Azure Service when creating this resource + detachDataDisk := client.Features.VirtualMachine.DetachImplicitDataDiskOnDeletion + if !detachDataDisk && toBeDeletedDisk != nil && toBeDeletedDisk.ManagedDisk != nil && toBeDeletedDisk.ManagedDisk.Id != nil { + diskClient := client.Compute.DisksClient + diskId, err := commonids.ParseManagedDiskID(*toBeDeletedDisk.ManagedDisk.Id) + if err != nil { + return nil, err + } + + err = diskClient.DeleteThenPoll(ctx, *diskId) + if err != nil { + return nil, fmt.Errorf("deleting Managed Disk %s: %+v", *diskId, err) + } + } + } + } + + return pointer.To(true), nil +} + +func (r VirtualMachineImplicitDataDiskFromSourceResource) basic(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_virtual_machine_implicit_data_disk_from_source" "test" { + name = "acctestVMIDD-%d" + virtual_machine_id = azurerm_virtual_machine.test.id + lun = "0" + create_option = "Copy" + disk_size_gb = 20 + source_resource_id = azurerm_snapshot.test.id +} +`, r.template(data), data.RandomInteger) +} + +func (r VirtualMachineImplicitDataDiskFromSourceResource) requiresImport(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_virtual_machine_implicit_data_disk_from_source" "import" { + name = azurerm_virtual_machine_implicit_data_disk_from_source.test.name + virtual_machine_id = azurerm_virtual_machine_implicit_data_disk_from_source.test.virtual_machine_id + lun = azurerm_virtual_machine_implicit_data_disk_from_source.test.lun + create_option = azurerm_virtual_machine_implicit_data_disk_from_source.test.create_option + disk_size_gb = azurerm_virtual_machine_implicit_data_disk_from_source.test.disk_size_gb + source_resource_id = azurerm_virtual_machine_implicit_data_disk_from_source.test.source_resource_id +} +`, r.basic(data)) +} + +func (VirtualMachineImplicitDataDiskFromSourceResource) managedServiceIdentity(data acceptance.TestData) string { + // currently only supported in "eastus2" and "westus2". + location := "westus2" + + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_virtual_network" "test" { + name = "acctvn-%d" + address_space = ["10.0.0.0/16"] + location = azurerm_resource_group.test.location + 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_prefixes = ["10.0.2.0/24"] +} + +resource "azurerm_network_interface" "test" { + name = "acctni-%d" + location = azurerm_resource_group.test.location + 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_virtual_machine" "test" { + name = "acctvm-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + network_interface_ids = [azurerm_network_interface.test.id] + vm_size = "Standard_F2" + + delete_os_disk_on_termination = true + + storage_image_reference { + publisher = "Canonical" + offer = "0001-com-ubuntu-server-jammy" + sku = "22_04-lts" + version = "latest" + } + + storage_os_disk { + name = "myosdisk1" + caching = "ReadWrite" + create_option = "FromImage" + managed_disk_type = "Standard_LRS" + } + + os_profile { + computer_name = "hn%d" + admin_username = "testadmin" + admin_password = "Password1234!" + } + + os_profile_linux_config { + disable_password_authentication = false + } + + identity { + type = "SystemAssigned" + } +} + +resource "azurerm_managed_disk" "test" { + name = "%d-disk1" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + storage_account_type = "Standard_LRS" + create_option = "Empty" + disk_size_gb = 10 +} + +resource "azurerm_virtual_machine_implicit_data_disk_from_source" "test" { + name = "acctestVMIDD-%d" + virtual_machine_id = azurerm_virtual_machine.test.id + lun = "0" + create_option = "Copy" + disk_size_gb = 20 + source_resource_id = azurerm_managed_disk.test.id +} +`, data.RandomInteger, location, data.RandomInteger, data.RandomInteger, data.RandomInteger, data.RandomInteger, data.RandomInteger, data.RandomInteger, data.RandomInteger) +} + +func (r VirtualMachineImplicitDataDiskFromSourceResource) multipleDisks(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_virtual_machine_implicit_data_disk_from_source" "first" { + name = "acctestVMIDD-%d" + virtual_machine_id = azurerm_virtual_machine.test.id + lun = "0" + create_option = "Copy" + disk_size_gb = 20 + source_resource_id = azurerm_snapshot.test.id +} + +resource "azurerm_managed_disk" "second" { + name = "%d-disk2" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + storage_account_type = "Standard_LRS" + create_option = "Empty" + disk_size_gb = 10 +} + +resource "azurerm_snapshot" "second" { + name = "acctestss2-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + create_option = "Copy" + source_uri = azurerm_managed_disk.second.id +} + +resource "azurerm_virtual_machine_implicit_data_disk_from_source" "second" { + name = "acctestVMIDD2-%d" + virtual_machine_id = azurerm_virtual_machine.test.id + lun = "20" + caching = "ReadOnly" + create_option = "Copy" + disk_size_gb = 20 + source_resource_id = azurerm_snapshot.second.id +} +`, r.template(data), data.RandomInteger, data.RandomInteger, data.RandomInteger, data.RandomInteger) +} + +func (r VirtualMachineImplicitDataDiskFromSourceResource) readOnly(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_virtual_machine_implicit_data_disk_from_source" "test" { + name = "acctestVMIDD-%d" + virtual_machine_id = azurerm_virtual_machine.test.id + lun = "0" + caching = "ReadOnly" + create_option = "Copy" + disk_size_gb = 20 + source_resource_id = azurerm_snapshot.test.id +} +`, r.template(data), data.RandomInteger) +} + +func (r VirtualMachineImplicitDataDiskFromSourceResource) readWrite(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_virtual_machine_implicit_data_disk_from_source" "test" { + name = "acctestVMIDD-%d" + virtual_machine_id = azurerm_virtual_machine.test.id + lun = "0" + caching = "ReadWrite" + create_option = "Copy" + disk_size_gb = 20 + source_resource_id = azurerm_snapshot.test.id +} +`, r.template(data), data.RandomInteger) +} + +func (VirtualMachineImplicitDataDiskFromSourceResource) writeAccelerator(data acceptance.TestData, enabled bool) string { + // currently only supported in "eastus2" and "westus2". + location := "westus2" + + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_virtual_network" "test" { + name = "acctvn-%d" + address_space = ["10.0.0.0/16"] + location = azurerm_resource_group.test.location + 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_prefixes = ["10.0.2.0/24"] +} + +resource "azurerm_network_interface" "test" { + name = "acctni-%d" + location = azurerm_resource_group.test.location + 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_virtual_machine" "test" { + name = "acctvm-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + network_interface_ids = [azurerm_network_interface.test.id] + vm_size = "Standard_M8ms" + + delete_os_disk_on_termination = true + + storage_image_reference { + publisher = "Canonical" + offer = "0001-com-ubuntu-server-jammy" + sku = "22_04-lts" + version = "latest" + } + + storage_os_disk { + name = "myosdisk1" + caching = "ReadWrite" + create_option = "FromImage" + managed_disk_type = "Premium_LRS" + } + + os_profile { + computer_name = "hn%d" + admin_username = "testadmin" + admin_password = "Password1234!" + } + + os_profile_linux_config { + disable_password_authentication = false + } +} + +resource "azurerm_managed_disk" "test" { + name = "%d-disk1" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + storage_account_type = "Premium_LRS" + create_option = "Empty" + disk_size_gb = 10 +} + +resource "azurerm_snapshot" "test" { + name = "acctestss-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + create_option = "Copy" + source_uri = azurerm_managed_disk.test.id +} + +resource "azurerm_virtual_machine_implicit_data_disk_from_source" "test" { + name = "acctestVMIDD-%d" + virtual_machine_id = azurerm_virtual_machine.test.id + lun = "0" + create_option = "Copy" + disk_size_gb = 20 + source_resource_id = azurerm_snapshot.test.id + write_accelerator_enabled = %t +} +`, data.RandomInteger, location, data.RandomInteger, data.RandomInteger, data.RandomInteger, data.RandomInteger, data.RandomInteger, data.RandomInteger, data.RandomInteger, data.RandomInteger, enabled) +} + +func (VirtualMachineImplicitDataDiskFromSourceResource) template(data acceptance.TestData) string { + // currently only supported in "eastus2" and "westus2". + location := "westus2" + + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_virtual_network" "test" { + name = "acctvn-%d" + address_space = ["10.0.0.0/16"] + location = azurerm_resource_group.test.location + 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_prefixes = ["10.0.2.0/24"] +} + +resource "azurerm_network_interface" "test" { + name = "acctni-%d" + location = azurerm_resource_group.test.location + 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_virtual_machine" "test" { + name = "acctvm-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + network_interface_ids = [azurerm_network_interface.test.id] + vm_size = "Standard_F2" + + delete_os_disk_on_termination = true + + storage_image_reference { + publisher = "Canonical" + offer = "0001-com-ubuntu-server-jammy" + sku = "22_04-lts" + version = "latest" + } + + storage_os_disk { + name = "myosdisk1" + caching = "ReadWrite" + create_option = "FromImage" + managed_disk_type = "Standard_LRS" + } + + os_profile { + computer_name = "hn%d" + admin_username = "testadmin" + admin_password = "Password1234!" + } + + os_profile_linux_config { + disable_password_authentication = false + } +} + +resource "azurerm_managed_disk" "test" { + name = "%d-disk1" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + storage_account_type = "Standard_LRS" + create_option = "Empty" + disk_size_gb = 10 +} + +resource "azurerm_snapshot" "test" { + name = "acctestss-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + create_option = "Copy" + source_uri = azurerm_managed_disk.test.id +} +`, data.RandomInteger, location, data.RandomInteger, data.RandomInteger, data.RandomInteger, data.RandomInteger, data.RandomInteger, data.RandomInteger, data.RandomInteger) +} + +func (VirtualMachineImplicitDataDiskFromSourceResource) virtualMachineExtensionPrep(data acceptance.TestData) string { + // currently only supported in "eastus2" and "westus2". + location := "westus2" + + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_virtual_network" "test" { + name = "acctestvn-%d" + address_space = ["10.0.0.0/16"] + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name +} + +resource "azurerm_subnet" "test" { + name = "acctsub" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefixes = ["10.0.2.0/24"] +} + +resource "azurerm_public_ip" "test" { + name = "acctestpip%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + allocation_method = "Static" +} + +resource "azurerm_network_interface" "test" { + name = "acctestni%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + + ip_configuration { + name = "testconfiguration1" + subnet_id = azurerm_subnet.test.id + private_ip_address_allocation = "Dynamic" + public_ip_address_id = azurerm_public_ip.test.id + } +} + +resource "azurerm_virtual_machine" "test" { + name = "acctestvm%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + network_interface_ids = [azurerm_network_interface.test.id] + vm_size = "Standard_F4" + + delete_os_disk_on_termination = true + delete_data_disks_on_termination = true + + storage_image_reference { + publisher = "Canonical" + offer = "0001-com-ubuntu-server-jammy" + sku = "22_04-lts" + version = "latest" + } + + os_profile { + computer_name = "testvm" + admin_username = "tfuser123" + admin_password = "Password1234!" + } + + storage_os_disk { + name = "myosdisk1" + caching = "ReadWrite" + create_option = "FromImage" + managed_disk_type = "Standard_LRS" + } + + os_profile_linux_config { + disable_password_authentication = false + } + + tags = { + environment = "staging" + } +} + +resource "azurerm_virtual_machine_extension" "test" { + name = "random-script" + virtual_machine_id = azurerm_virtual_machine.test.id + publisher = "Microsoft.Azure.Extensions" + type = "CustomScript" + type_handler_version = "2.0" + + settings = < **Note:** This does not affect the older `azurerm_virtual_machine` resource, which has its own flags for managing this within the resource. diff --git a/website/docs/r/virtual_machine_implicit_data_disk_from_source.html.markdown b/website/docs/r/virtual_machine_implicit_data_disk_from_source.html.markdown new file mode 100644 index 000000000000..65969f5699df --- /dev/null +++ b/website/docs/r/virtual_machine_implicit_data_disk_from_source.html.markdown @@ -0,0 +1,160 @@ +--- +subcategory: "Compute" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_virtual_machine_implicit_data_disk_from_source" +description: |- + Manages an implicit Data Disk of a Virtual Machine. +--- + +# azurerm_virtual_machine_implicit_data_disk_from_source + +Manages an implicit Data Disk of a Virtual Machine. + +~> **Note:** The Implicit Data Disk will be deleted instantly after this resource is destroyed. If you want to detach this disk only, you may set `detach_implicit_data_disk_on_deletion` field to `true` within the `virtual_machine` block in the provider `features` block. + +## Example Usage + +```hcl +variable "prefix" { + default = "example" +} + +locals { + vm_name = "${var.prefix}-vm" +} + +resource "azurerm_resource_group" "example" { + name = "${var.prefix}-resources" + location = "West Europe" +} + +resource "azurerm_virtual_network" "main" { + name = "${var.prefix}-network" + address_space = ["10.0.0.0/16"] + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name +} + +resource "azurerm_subnet" "internal" { + name = "internal" + resource_group_name = azurerm_resource_group.example.name + virtual_network_name = azurerm_virtual_network.main.name + address_prefixes = ["10.0.2.0/24"] +} + +resource "azurerm_network_interface" "main" { + name = "${var.prefix}-nic" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + + ip_configuration { + name = "internal" + subnet_id = azurerm_subnet.internal.id + private_ip_address_allocation = "Dynamic" + } +} + +resource "azurerm_virtual_machine" "example" { + name = local.vm_name + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + network_interface_ids = [azurerm_network_interface.main.id] + vm_size = "Standard_F2" + + storage_image_reference { + publisher = "Canonical" + offer = "0001-com-ubuntu-server-jammy" + sku = "22_04-lts" + version = "latest" + } + + storage_os_disk { + name = "myosdisk1" + caching = "ReadWrite" + create_option = "FromImage" + managed_disk_type = "Standard_LRS" + } + + os_profile { + computer_name = local.vm_name + admin_username = "testadmin" + admin_password = "Password1234!" + } + + os_profile_linux_config { + disable_password_authentication = false + } +} + +resource "azurerm_managed_disk" "example" { + name = "${local.vm_name}-disk1" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + storage_account_type = "Standard_LRS" + create_option = "Empty" + disk_size_gb = 10 +} + +resource "azurerm_snapshot" "example" { + name = "${local.vm_name}-snapshot1" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + create_option = "Copy" + source_uri = azurerm_managed_disk.example.id +} + +resource "azurerm_virtual_machine_implicit_data_disk_from_source" "example" { + name = "${local.vm_name}-implicitdisk1" + virtual_machine_id = azurerm_virtual_machine.test.id + lun = "0" + caching = "None" + create_option = "Copy" + disk_size_gb = 20 + source_resource_id = azurerm_snapshot.test.id +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) Specifies the name of this Data Disk. Changing this forces a new resource to be created. + +* `virtual_machine_id` - (Required) The ID of the Virtual Machine to which the Data Disk should be attached. Changing this forces a new resource to be created. + +* `lun` - (Required) The Logical Unit Number of the Data Disk, which needs to be unique within the Virtual Machine. Changing this forces a new resource to be created. + +* `create_option` - (Required) Specifies the Create Option of the Data Disk. The only possible value is `Copy`. Changing this forces a new resource to be created. + +* `disk_size_gb` - (Required) Specifies the size of the Data Disk in gigabytes. Changing this forces a new resource to be created. + +* `source_resource_id` - (Required) The ID of the source resource which this Data Disk was created from. Changing this forces a new resource to be created. + +* `caching` - (Optional) Specifies the caching requirements for this Data Disk. Possible values are `ReadOnly` and `ReadWrite`. + +* `write_accelerator_enabled` - (Optional) Specifies if Write Accelerator is enabled on the disk. This can only be enabled on `Premium_LRS` managed disks with no caching and [M-Series VMs](https://docs.microsoft.com/azure/virtual-machines/workloads/sap/how-to-enable-write-accelerator). Defaults to `false`. + +## Attributes Reference + +In addition to the Arguments listed above - the following Attributes are exported: + +* `id` - The ID of the implicit Data Disk of the Virtual Machine. + +## Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/language/resources/syntax#operation-timeouts) for certain actions: + +* `create` - (Defaults to 30 minutes) Used when creating the implicit Data Disk of the Virtual Machine. +* `update` - (Defaults to 30 minutes) Used when updating the implicit Data Disk of the Virtual Machine. +* `read` - (Defaults to 5 minutes) Used when retrieving the implicit Data Disk of the Virtual Machine. +* `delete` - (Defaults to 30 minutes) Used when deleting the implicit Data Disk of the Virtual Machine. + +## Import + +The implicit Data Disk of the Virtual Machine can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_virtual_machine_implicit_data_disk_from_source.example /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/mygroup1/providers/Microsoft.Compute/virtualMachines/machine1/dataDisks/disk1 +``` + +-> **Please Note:** This is a Terraform Unique ID matching the format: `{virtualMachineID}/dataDisks/{diskName}`