From 97b51289bc9e17eb2e1a3b0a472fd067a73be71f Mon Sep 17 00:00:00 2001 From: Yichun Ma Date: Tue, 22 Mar 2022 08:34:00 +0800 Subject: [PATCH] `r\linux_virtual_machine` `r\windows_virual_machine`: Add support for `termination_notification` (#14933) --- .../compute/linux_virtual_machine_resource.go | 19 +++ ...nux_virtual_machine_resource_other_test.go | 137 ++++++++++++++++++ internal/services/compute/virtual_machine.go | 67 +++++++++ .../windows_virtual_machine_resource.go | 19 +++ ...ows_virtual_machine_resource_other_test.go | 129 +++++++++++++++++ .../r/linux_virtual_machine.html.markdown | 12 ++ .../r/windows_virtual_machine.html.markdown | 12 ++ 7 files changed, 395 insertions(+) diff --git a/internal/services/compute/linux_virtual_machine_resource.go b/internal/services/compute/linux_virtual_machine_resource.go index 010bb9b9a1fd..0d9cdcd17f82 100644 --- a/internal/services/compute/linux_virtual_machine_resource.go +++ b/internal/services/compute/linux_virtual_machine_resource.go @@ -293,6 +293,8 @@ func resourceLinuxVirtualMachine() *pluginsdk.Resource { "tags": tags.Schema(), + "termination_notification": virtualMachineTerminationNotificationSchema(), + "user_data": { Type: pluginsdk.TypeString, Optional: true, @@ -497,6 +499,10 @@ func resourceLinuxVirtualMachineCreate(d *pluginsdk.ResourceData, meta interface params.VirtualMachineProperties.SecurityProfile.UefiSettings.SecureBootEnabled = utils.Bool(secureBootEnabled.(bool)) } + if v, ok := d.GetOk("termination_notification"); ok { + params.VirtualMachineProperties.ScheduledEventsProfile = expandVirtualMachineScheduledEventsProfile(v.([]interface{})) + } + if vtpmEnabled, ok := d.GetOk("vtpm_enabled"); ok && vtpmEnabled.(bool) { if params.VirtualMachineProperties.SecurityProfile == nil { params.VirtualMachineProperties.SecurityProfile = &compute.SecurityProfile{} @@ -781,6 +787,12 @@ func resourceLinuxVirtualMachineRead(d *pluginsdk.ResourceData, meta interface{} } } + if scheduleProfile := props.ScheduledEventsProfile; scheduleProfile != nil { + if err := d.Set("termination_notification", flattenVirtualMachineScheduledEventsProfile(scheduleProfile)); err != nil { + return fmt.Errorf("setting `termination_notification`: %+v", err) + } + } + encryptionAtHostEnabled := false vtpmEnabled := false secureBootEnabled := false @@ -1090,6 +1102,13 @@ func resourceLinuxVirtualMachineUpdate(d *pluginsdk.ResourceData, meta interface update.OsProfile.AllowExtensionOperations = utils.Bool(allowExtensionOperations) } + if d.HasChange("termination_notification") { + shouldUpdate = true + + notificationRaw := d.Get("termination_notification").([]interface{}) + update.ScheduledEventsProfile = expandVirtualMachineScheduledEventsProfile(notificationRaw) + } + if d.HasChange("tags") { shouldUpdate = true diff --git a/internal/services/compute/linux_virtual_machine_resource_other_test.go b/internal/services/compute/linux_virtual_machine_resource_other_test.go index 768e433a369d..c87af115c84e 100644 --- a/internal/services/compute/linux_virtual_machine_resource_other_test.go +++ b/internal/services/compute/linux_virtual_machine_resource_other_test.go @@ -452,6 +452,66 @@ func TestAccLinuxVirtualMachine_otherTags(t *testing.T) { }) } +func TestAccLinuxVirtualMachine_otherTerminationNotification(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_linux_virtual_machine", "test") + r := LinuxVirtualMachineResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + // turn termination notification on + { + Config: r.otherTerminationNotification(data, true), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("termination_notification.#").HasValue("1"), + check.That(data.ResourceName).Key("termination_notification.0.enabled").HasValue("true"), + ), + }, + data.ImportStep("admin_password"), + // turn termination notification off + { + Config: r.otherTerminationNotification(data, false), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("termination_notification.#").HasValue("1"), + check.That(data.ResourceName).Key("termination_notification.0.enabled").HasValue("false"), + ), + }, + data.ImportStep("admin_password"), + // turn termination notification on again + { + Config: r.otherTerminationNotification(data, true), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("termination_notification.#").HasValue("1"), + check.That(data.ResourceName).Key("termination_notification.0.enabled").HasValue("true"), + ), + }, + data.ImportStep("admin_password"), + }) +} + +func TestAccLinuxVirtualMachine_otherTerminationNotificationTimeout(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_linux_virtual_machine", "test") + r := LinuxVirtualMachineResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.otherTerminationNotification(data, true), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep("admin_password"), + { + Config: r.otherTerminationNotificationTimeout(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep("admin_password"), + }) +} + func TestAccLinuxVirtualMachine_otherUltraSsdDefault(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_linux_virtual_machine", "test") r := LinuxVirtualMachineResource{} @@ -1773,6 +1833,83 @@ resource "azurerm_linux_virtual_machine" "test" { `, r.template(data), data.RandomInteger) } +func (r LinuxVirtualMachineResource) otherTerminationNotification(data acceptance.TestData, enabled bool) string { + return fmt.Sprintf(` +%s + +resource "azurerm_linux_virtual_machine" "test" { + name = "acctestVM-%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + size = "Standard_F2" + admin_username = "adminuser" + network_interface_ids = [ + azurerm_network_interface.test.id, + ] + + admin_ssh_key { + username = "adminuser" + public_key = local.first_public_key + } + + os_disk { + caching = "ReadWrite" + storage_account_type = "Standard_LRS" + } + + source_image_reference { + publisher = "Canonical" + offer = "UbuntuServer" + sku = "16.04-LTS" + version = "latest" + } + + termination_notification { + enabled = %t + } +} +`, r.template(data), data.RandomInteger, enabled) +} + +func (r LinuxVirtualMachineResource) otherTerminationNotificationTimeout(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_linux_virtual_machine" "test" { + name = "acctestVM-%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + size = "Standard_F2" + admin_username = "adminuser" + network_interface_ids = [ + azurerm_network_interface.test.id, + ] + + admin_ssh_key { + username = "adminuser" + public_key = local.first_public_key + } + + os_disk { + caching = "ReadWrite" + storage_account_type = "Standard_LRS" + } + + source_image_reference { + publisher = "Canonical" + offer = "UbuntuServer" + sku = "16.04-LTS" + version = "latest" + } + + termination_notification { + enabled = true + timeout = "PT15M" + } +} +`, r.template(data), data.RandomInteger) +} + func (r LinuxVirtualMachineResource) otherUltraSsd(data acceptance.TestData, ultraSsdEnabled bool) string { return fmt.Sprintf(` %s diff --git a/internal/services/compute/virtual_machine.go b/internal/services/compute/virtual_machine.go index 35979a44a1db..62e9056ae299 100644 --- a/internal/services/compute/virtual_machine.go +++ b/internal/services/compute/virtual_machine.go @@ -5,6 +5,7 @@ import ( "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2021-11-01/compute" "github.com/hashicorp/go-azure-helpers/resourcemanager/identity" + azValidate "github.com/hashicorp/terraform-provider-azurerm/helpers/validate" "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" @@ -349,3 +350,69 @@ func flattenVirtualMachineOSDisk(ctx context.Context, disksClient *compute.Disks }, }, nil } + +func virtualMachineTerminationNotificationSchema() *pluginsdk.Schema { + return &pluginsdk.Schema{ + Type: pluginsdk.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Elem: &pluginsdk.Resource{ + Schema: map[string]*pluginsdk.Schema{ + "enabled": { + Type: pluginsdk.TypeBool, + Required: true, + }, + "timeout": { + Type: pluginsdk.TypeString, + Optional: true, + ValidateFunc: azValidate.ISO8601DurationBetween("PT5M", "PT15M"), + Default: "PT5M", + }, + }, + }, + } +} + +func expandVirtualMachineScheduledEventsProfile(input []interface{}) *compute.ScheduledEventsProfile { + if len(input) == 0 { + return &compute.ScheduledEventsProfile{ + TerminateNotificationProfile: &compute.TerminateNotificationProfile{ + Enable: utils.Bool(false), + }, + } + } + + raw := input[0].(map[string]interface{}) + enabled := raw["enabled"].(bool) + timeout := raw["timeout"].(string) + + return &compute.ScheduledEventsProfile{ + TerminateNotificationProfile: &compute.TerminateNotificationProfile{ + Enable: &enabled, + NotBeforeTimeout: &timeout, + }, + } +} + +func flattenVirtualMachineScheduledEventsProfile(input *compute.ScheduledEventsProfile) []interface{} { + // if enabled is set to false, there will be no ScheduledEventsProfile in response, to avoid plan non empty when + // a user explicitly set enabled to false, we need to assign a default block to this field + + enabled := false + if input != nil && input.TerminateNotificationProfile != nil && input.TerminateNotificationProfile.Enable != nil { + enabled = *input.TerminateNotificationProfile.Enable + } + + timeout := "PT5M" + if input != nil && input.TerminateNotificationProfile != nil && input.TerminateNotificationProfile.NotBeforeTimeout != nil { + timeout = *input.TerminateNotificationProfile.NotBeforeTimeout + } + + return []interface{}{ + map[string]interface{}{ + "enabled": enabled, + "timeout": timeout, + }, + } +} diff --git a/internal/services/compute/windows_virtual_machine_resource.go b/internal/services/compute/windows_virtual_machine_resource.go index 1ceac782b9ca..82bff9358350 100644 --- a/internal/services/compute/windows_virtual_machine_resource.go +++ b/internal/services/compute/windows_virtual_machine_resource.go @@ -286,6 +286,8 @@ func resourceWindowsVirtualMachine() *pluginsdk.Resource { "tags": tags.Schema(), + "termination_notification": virtualMachineTerminationNotificationSchema(), + "timezone": { Type: pluginsdk.TypeString, Optional: true, @@ -647,6 +649,10 @@ func resourceWindowsVirtualMachineCreate(d *pluginsdk.ResourceData, meta interfa params.PlatformFaultDomain = utils.Int32(int32(platformFaultDomain)) } + if v, ok := d.GetOk("termination_notification"); ok { + params.VirtualMachineProperties.ScheduledEventsProfile = expandVirtualMachineScheduledEventsProfile(v.([]interface{})) + } + if v, ok := d.GetOk("timezone"); ok { params.VirtualMachineProperties.OsProfile.WindowsConfiguration.TimeZone = utils.String(v.(string)) } @@ -847,6 +853,12 @@ func resourceWindowsVirtualMachineRead(d *pluginsdk.ResourceData, meta interface } } + if scheduleProfile := props.ScheduledEventsProfile; scheduleProfile != nil { + if err := d.Set("termination_notification", flattenVirtualMachineScheduledEventsProfile(scheduleProfile)); err != nil { + return fmt.Errorf("setting `termination_notification`: %+v", err) + } + } + encryptionAtHostEnabled := false vtpmEnabled := false secureBootEnabled := false @@ -1170,6 +1182,13 @@ func resourceWindowsVirtualMachineUpdate(d *pluginsdk.ResourceData, meta interfa update.Tags = tags.Expand(tagsRaw) } + if d.HasChange("termination_notification") { + shouldUpdate = true + + notificationRaw := d.Get("termination_notification").([]interface{}) + update.ScheduledEventsProfile = expandVirtualMachineScheduledEventsProfile(notificationRaw) + } + if d.HasChange("additional_capabilities") { shouldUpdate = true diff --git a/internal/services/compute/windows_virtual_machine_resource_other_test.go b/internal/services/compute/windows_virtual_machine_resource_other_test.go index 0d896866f2c2..eb922ecc2a52 100644 --- a/internal/services/compute/windows_virtual_machine_resource_other_test.go +++ b/internal/services/compute/windows_virtual_machine_resource_other_test.go @@ -618,6 +618,66 @@ func TestAccWindowsVirtualMachine_otherTags(t *testing.T) { }) } +func TestAccWindowsVirtualMachine_otherTerminationNotification(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_windows_virtual_machine", "test") + r := WindowsVirtualMachineResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + // turn termination notification on + { + Config: r.otherTerminationNotification(data, true), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("termination_notification.#").HasValue("1"), + check.That(data.ResourceName).Key("termination_notification.0.enabled").HasValue("true"), + ), + }, + data.ImportStep("admin_password"), + // turn termination notification off + { + Config: r.otherTerminationNotification(data, false), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("termination_notification.#").HasValue("1"), + check.That(data.ResourceName).Key("termination_notification.0.enabled").HasValue("false"), + ), + }, + data.ImportStep("admin_password"), + // turn termination notification on again + { + Config: r.otherTerminationNotification(data, true), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("termination_notification.#").HasValue("1"), + check.That(data.ResourceName).Key("termination_notification.0.enabled").HasValue("true"), + ), + }, + data.ImportStep("admin_password"), + }) +} + +func TestAccWindowsVirtualMachine_otherTerminationNotificationTimeout(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_windows_virtual_machine", "test") + r := WindowsVirtualMachineResource{} + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.otherTerminationNotification(data, true), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep("admin_password"), + { + Config: r.otherTerminationNotificationTimeout(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep("admin_password"), + }) +} + func TestAccWindowsVirtualMachine_otherTimeZone(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_windows_virtual_machine", "test") r := WindowsVirtualMachineResource{} @@ -2181,6 +2241,75 @@ resource "azurerm_windows_virtual_machine" "test" { `, r.template(data)) } +func (r WindowsVirtualMachineResource) otherTerminationNotification(data acceptance.TestData, enabled bool) string { + return fmt.Sprintf(` +%s + +resource "azurerm_windows_virtual_machine" "test" { + name = local.vm_name + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + size = "Standard_F2" + admin_username = "adminuser" + admin_password = "P@$$w0rd1234!" + network_interface_ids = [ + azurerm_network_interface.test.id, + ] + + os_disk { + caching = "ReadWrite" + storage_account_type = "Standard_LRS" + } + + source_image_reference { + publisher = "MicrosoftWindowsServer" + offer = "WindowsServer" + sku = "2016-Datacenter" + version = "latest" + } + + termination_notification { + enabled = %t + } +} +`, r.template(data), enabled) +} + +func (r WindowsVirtualMachineResource) otherTerminationNotificationTimeout(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_windows_virtual_machine" "test" { + name = local.vm_name + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + size = "Standard_F2" + admin_username = "adminuser" + admin_password = "P@$$w0rd1234!" + network_interface_ids = [ + azurerm_network_interface.test.id, + ] + + os_disk { + caching = "ReadWrite" + storage_account_type = "Standard_LRS" + } + + source_image_reference { + publisher = "MicrosoftWindowsServer" + offer = "WindowsServer" + sku = "2016-Datacenter" + version = "latest" + } + + termination_notification { + enabled = true + timeout = "PT15M" + } +} +`, r.template(data)) +} + func (r WindowsVirtualMachineResource) otherTimeZone(data acceptance.TestData) string { return fmt.Sprintf(` %s diff --git a/website/docs/r/linux_virtual_machine.html.markdown b/website/docs/r/linux_virtual_machine.html.markdown index 08bfa60e9387..09e8478bdf14 100644 --- a/website/docs/r/linux_virtual_machine.html.markdown +++ b/website/docs/r/linux_virtual_machine.html.markdown @@ -186,6 +186,8 @@ The following arguments are supported: * `tags` - (Optional) A mapping of tags which should be assigned to this Virtual Machine. +* `termination_notification` - (Optional) A `termination_notification` block as defined below. + * `user_data` - (Optional) The Base64-Encoded User Data which should be used for this Virtual Machine. * `vtpm_enabled` - (Optional) Specifies whether vTPM should be enabled on the virtual machine. Changing this forces a new resource to be created. @@ -298,6 +300,16 @@ A `secret` block supports the following: * `version` - (Optional) Specifies the version of the image used to create the virtual machines. +--- + +A `termination_notification` block supports the following: + +* `enabled` - (Required) Should the termination notification be enabled on this Virtual Machine? Defaults to `false`. + +* `timeout` - (Optional) Length of time (in minutes, between 5 and 15) a notification to be sent to the VM on the instance metadata server till the VM gets deleted. The time duration should be specified in ISO 8601 format. + +~> **NOTE:** For more information about the termination notification, please [refer to this doc](https://docs.microsoft.com/azure/virtual-machine-scale-sets/virtual-machine-scale-sets-terminate-notification). + ## Attributes Reference In addition to all arguments above, the following attributes are exported: diff --git a/website/docs/r/windows_virtual_machine.html.markdown b/website/docs/r/windows_virtual_machine.html.markdown index 2f39c9eb0e92..9b059402065f 100644 --- a/website/docs/r/windows_virtual_machine.html.markdown +++ b/website/docs/r/windows_virtual_machine.html.markdown @@ -177,6 +177,8 @@ The following arguments are supported: * `tags` - (Optional) A mapping of tags which should be assigned to this Virtual Machine. +* `termination_notification` - (Optional) A `termination_notification` block as defined below. + * `timezone` - (Optional) Specifies the Time Zone which should be used by the Virtual Machine, [the possible values are defined here](https://jackstromberg.com/2017/01/list-of-time-zones-consumed-by-azure/). * `user_data` - (Optional) The Base64-Encoded User Data which should be used for this Virtual Machine. @@ -295,6 +297,16 @@ A `secret` block supports the following: --- +A `termination_notification` block supports the following: + +* `enabled` - (Required) Should the termination notification be enabled on this Virtual Machine? Defaults to `false`. + +* `timeout` - (Optional) Length of time (in minutes, between 5 and 15) a notification to be sent to the VM on the instance metadata server till the VM gets deleted. The time duration should be specified in ISO 8601 format. + +~> **NOTE:** For more information about the termination notification, please [refer to this doc](https://docs.microsoft.com/azure/virtual-machine-scale-sets/virtual-machine-scale-sets-terminate-notification). + +--- + A `winrm_listener` block supports the following: * `Protocol` - (Required) Specifies Specifies the protocol of listener. Possible values are `Http` or `Https`