Skip to content

Commit

Permalink
New resource: azurerm_dev_test_global_shutdown_schedule (hashicorp#5536)
Browse files Browse the repository at this point in the history
This resource enables the "Auto-shutdown" functionality of regular ARM-based VMs. It's basically a hack of the Dev Test Labs schedules resource known as a "Global Schedule" in the API docs. The API looks like it would support multiple types of schedules and timings, but in reality based on testing only "ComputeVMShutdownTask" schedules are supported and only with a daily recurrence. The API also seems to require a specific naming standard of the resource of "shutdown-computevm-{{VM Name}}". Anything else results in a 400 Bad Request.

Because of these restrictions, for ease of use, I opted to create a resource specifically for shutdown schedules that essentially maps to the "Auto-shutdown" blade in the Portal. If in the future, if additional types of schedules like Auto-starts are supported, my thought is new resources could be created or a more general resource could be created to support multiple types.

I'm very open to feedback on resource naming and overall approach.

References:

Feature Announcement
azure.microsoft.com/en-us/blog/announcing-auto-shutdown-for-vms-using-azure-resource-manager

API Docs
docs.microsoft.com/en-us/rest/api/dtl/globalschedules

Example Usage Forum Post
social.msdn.microsoft.com/Forums/en-US/25a02403-dba9-4bcb-bdcc-1f4afcba5b65/powershell-script-to-autoshutdown-azure-virtual-machine?forum=WAVirtualMachinesforWindows
  • Loading branch information
sean-nixon authored and pbrit committed May 31, 2020
1 parent ada6651 commit 18603c9
Show file tree
Hide file tree
Showing 25 changed files with 1,221 additions and 180 deletions.
1 change: 1 addition & 0 deletions .teamcity/components/generated/services.kt
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ var services = mapOf(
"storage" to "Storage",
"streamanalytics" to "Stream Analytics",
"subscription" to "Subscription",
"iottimeseriesinsights" to "Time Series Insights",
"trafficmanager" to "Traffic Manager",
"web" to "Web"
)
25 changes: 15 additions & 10 deletions azurerm/internal/services/devtestlabs/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import (
)

type Client struct {
LabsClient *dtl.LabsClient
LabSchedulesClient *dtl.SchedulesClient
PoliciesClient *dtl.PoliciesClient
VirtualMachinesClient *dtl.VirtualMachinesClient
VirtualNetworksClient *dtl.VirtualNetworksClient
GlobalLabSchedulesClient *dtl.GlobalSchedulesClient
LabsClient *dtl.LabsClient
LabSchedulesClient *dtl.SchedulesClient
PoliciesClient *dtl.PoliciesClient
VirtualMachinesClient *dtl.VirtualMachinesClient
VirtualNetworksClient *dtl.VirtualNetworksClient
}

func NewClient(o *common.ClientOptions) *Client {
Expand All @@ -29,11 +30,15 @@ func NewClient(o *common.ClientOptions) *Client {
LabSchedulesClient := dtl.NewSchedulesClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId)
o.ConfigureClient(&LabSchedulesClient.Client, o.ResourceManagerAuthorizer)

GlobalLabSchedulesClient := dtl.NewGlobalSchedulesClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId)
o.ConfigureClient(&GlobalLabSchedulesClient.Client, o.ResourceManagerAuthorizer)

return &Client{
LabsClient: &LabsClient,
LabSchedulesClient: &LabSchedulesClient,
PoliciesClient: &PoliciesClient,
VirtualMachinesClient: &VirtualMachinesClient,
VirtualNetworksClient: &VirtualNetworksClient,
GlobalLabSchedulesClient: &GlobalLabSchedulesClient,
LabsClient: &LabsClient,
LabSchedulesClient: &LabSchedulesClient,
PoliciesClient: &PoliciesClient,
VirtualMachinesClient: &VirtualMachinesClient,
VirtualNetworksClient: &VirtualNetworksClient,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
package devtestlabs

import (
"fmt"
"regexp"
"time"

"github.com/Azure/azure-sdk-for-go/services/devtestlabs/mgmt/2016-05-15/dtl"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/helper/validation"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/validate"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/features"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/devtestlabs/parse"
devtestValidate "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/devtestlabs/validate"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tags"
azSchema "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tf/schema"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/timeouts"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils"
)

func resourceArmDevTestLabGlobalVMShutdownSchedule() *schema.Resource {
return &schema.Resource{
Create: resourceArmDevTestLabGlobalVMShutdownScheduleCreateUpdate,
Read: resourceArmDevTestLabGlobalVMShutdownScheduleRead,
Update: resourceArmDevTestLabGlobalVMShutdownScheduleCreateUpdate,
Delete: resourceArmDevTestLabGlobalVMShutdownScheduleDelete,
Importer: azSchema.ValidateResourceIDPriorToImport(func(id string) error {
_, err := parse.GlobalScheduleID(id)
return err
}),

Timeouts: &schema.ResourceTimeout{
Create: schema.DefaultTimeout(30 * time.Minute),
Read: schema.DefaultTimeout(5 * time.Minute),
Update: schema.DefaultTimeout(30 * time.Minute),
Delete: schema.DefaultTimeout(30 * time.Minute),
},

Schema: map[string]*schema.Schema{
"location": azure.SchemaLocation(),

"virtual_machine_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: devtestValidate.GlobalScheduleVirtualMachineID,
},

"enabled": {
Type: schema.TypeBool,
Optional: true,
Default: true,
},

"daily_recurrence_time": {
Type: schema.TypeString,
Required: true,
ValidateFunc: validation.StringMatch(
regexp.MustCompile("^(0[0-9]|1[0-9]|2[0-3]|[0-9])[0-5][0-9]$"),
"Time of day must match the format HHmm where HH is 00-23 and mm is 00-59",
),
},

"timezone": {
Type: schema.TypeString,
Required: true,
ValidateFunc: validate.VirtualMachineTimeZoneCaseInsensitive(),
},

"notification_settings": {
Type: schema.TypeList,
Required: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"enabled": {
Type: schema.TypeBool,
Required: true,
},
"time_in_minutes": {
Type: schema.TypeInt,
Optional: true,
Default: 30,
ValidateFunc: validation.IntBetween(15, 120),
},
"webhook_url": {
Type: schema.TypeString,
Optional: true,
},
},
},
},

"tags": tags.Schema(),
},
}
}

func resourceArmDevTestLabGlobalVMShutdownScheduleCreateUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*clients.Client).DevTestLabs.GlobalLabSchedulesClient
ctx, cancel := timeouts.ForCreateUpdate(meta.(*clients.Client).StopContext, d)
defer cancel()

vmID := d.Get("virtual_machine_id").(string)
id, err := parse.GlobalScheduleVirtualMachineID(vmID)
if err != nil {
return err
}

// Can't find any official documentation on this, but the API returns a 400 for any other name.
// The best example I could find is here: https://social.msdn.microsoft.com/Forums/en-US/25a02403-dba9-4bcb-bdcc-1f4afcba5b65/powershell-script-to-autoshutdown-azure-virtual-machine?forum=WAVirtualMachinesforWindows
name := "shutdown-computevm-" + id.Name

if features.ShouldResourcesBeImported() && d.IsNewResource() {
existing, err := client.Get(ctx, id.ResourceGroup, name, "")
if err != nil {
if !utils.ResponseWasNotFound(existing.Response) {
return fmt.Errorf("Error checking for presence of existing Schedule %q (Resource Group %q): %s", name, id.ResourceGroup, err)
}
}

if existing.ID != nil && *existing.ID != "" {
return tf.ImportAsExistsError("azurerm_dev_test_global_vm_shutdown_schedule", *existing.ID)
}
}

location := azure.NormalizeLocation(d.Get("location").(string))
taskType := "ComputeVmShutdownTask"

schedule := dtl.Schedule{
Location: &location,
ScheduleProperties: &dtl.ScheduleProperties{
TargetResourceID: &vmID,
TaskType: &taskType,
},
Tags: tags.Expand(d.Get("tags").(map[string]interface{})),
}

if d.Get("enabled").(bool) {
schedule.ScheduleProperties.Status = dtl.EnableStatusEnabled
} else {
schedule.ScheduleProperties.Status = dtl.EnableStatusDisabled
}

if timeZoneId := d.Get("timezone").(string); timeZoneId != "" {
schedule.ScheduleProperties.TimeZoneID = &timeZoneId
}

if v, ok := d.GetOk("daily_recurrence_time"); ok {
dailyRecurrence := expandArmDevTestLabGlobalVMShutdownScheduleRecurrenceDaily(v)
schedule.DailyRecurrence = dailyRecurrence
}

if _, ok := d.GetOk("notification_settings"); ok {
notificationSettings := expandArmDevTestLabGlobalVMShutdownScheduleNotificationSettings(d)
schedule.NotificationSettings = notificationSettings
}

if _, err := client.CreateOrUpdate(ctx, id.ResourceGroup, name, schedule); err != nil {
return err
}

read, err := client.Get(ctx, id.ResourceGroup, name, "")
if err != nil {
return err
}

if read.ID == nil {
return fmt.Errorf("Cannot read Dev Test Global Schedule %s (resource group %s) ID", name, id.ResourceGroup)
}

d.SetId(*read.ID)

return resourceArmDevTestLabGlobalVMShutdownScheduleRead(d, meta)
}

func resourceArmDevTestLabGlobalVMShutdownScheduleRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*clients.Client).DevTestLabs.GlobalLabSchedulesClient
ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d)
defer cancel()

id, err := parse.GlobalScheduleID(d.Id())
if err != nil {
return err
}

resp, err := client.Get(ctx, id.ResourceGroup, id.Name, "")

if err != nil {
if utils.ResponseWasNotFound(resp.Response) {
d.SetId("")
return nil
}
return fmt.Errorf("Error making Read request on Dev Test Global Schedule %s: %s", id.Name, err)
}

if location := resp.Location; location != nil {
d.Set("location", azure.NormalizeLocation(*location))
}

if props := resp.ScheduleProperties; props != nil {
d.Set("virtual_machine_id", props.TargetResourceID)
d.Set("timezone", props.TimeZoneID)
d.Set("enabled", props.Status == dtl.EnableStatusEnabled)

if err := d.Set("daily_recurrence_time", flattenArmDevTestLabGlobalVMShutdownScheduleRecurrenceDaily(props.DailyRecurrence)); err != nil {
return fmt.Errorf("Error setting `dailyRecurrence`: %#v", err)
}

if err := d.Set("notification_settings", flattenArmDevTestLabGlobalVMShutdownScheduleNotificationSettings(props.NotificationSettings)); err != nil {
return fmt.Errorf("Error setting `notificationSettings`: %#v", err)
}
}

return tags.FlattenAndSet(d, resp.Tags)
}

func resourceArmDevTestLabGlobalVMShutdownScheduleDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*clients.Client).DevTestLabs.GlobalLabSchedulesClient
ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d)
defer cancel()

id, err := parse.GlobalScheduleID(d.Id())
if err != nil {
return err
}

if _, err := client.Delete(ctx, id.ResourceGroup, id.Name); err != nil {
return err
}

return nil
}

func expandArmDevTestLabGlobalVMShutdownScheduleRecurrenceDaily(dailyTime interface{}) *dtl.DayDetails {
time := dailyTime.(string)
return &dtl.DayDetails{
Time: &time,
}
}

func flattenArmDevTestLabGlobalVMShutdownScheduleRecurrenceDaily(dailyRecurrence *dtl.DayDetails) interface{} {
if dailyRecurrence == nil {
return nil
}

var result string
if dailyRecurrence.Time != nil {
result = *dailyRecurrence.Time
}

return result
}

func expandArmDevTestLabGlobalVMShutdownScheduleNotificationSettings(d *schema.ResourceData) *dtl.NotificationSettings {
notificationSettingsConfigs := d.Get("notification_settings").([]interface{})
notificationSettingsConfig := notificationSettingsConfigs[0].(map[string]interface{})
webhookUrl := notificationSettingsConfig["webhook_url"].(string)
timeInMinutes := int32(notificationSettingsConfig["time_in_minutes"].(int))

var notificationStatus dtl.NotificationStatus
if notificationSettingsConfig["enabled"].(bool) {
notificationStatus = dtl.NotificationStatusEnabled
} else {
notificationStatus = dtl.NotificationStatusDisabled
}

return &dtl.NotificationSettings{
WebhookURL: &webhookUrl,
TimeInMinutes: &timeInMinutes,
Status: notificationStatus,
}
}

func flattenArmDevTestLabGlobalVMShutdownScheduleNotificationSettings(notificationSettings *dtl.NotificationSettings) []interface{} {
if notificationSettings == nil {
return []interface{}{}
}

result := make(map[string]interface{})

if notificationSettings.WebhookURL != nil {
result["webhook_url"] = *notificationSettings.WebhookURL
}

if notificationSettings.TimeInMinutes != nil {
result["time_in_minutes"] = *notificationSettings.TimeInMinutes
}

result["enabled"] = notificationSettings.Status == dtl.NotificationStatusEnabled

return []interface{}{result}
}
Loading

0 comments on commit 18603c9

Please sign in to comment.