Advertisement
mharrison0224

AutoShutdownAzureVMs

May 19th, 2016
1,277
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. <#
  2.     .SYNOPSIS
  3.         This Azure Automation runbook automates the scheduled shutdown and startup of virtual machines in an Azure subscription.
  4.  
  5.     .DESCRIPTION
  6.         The runbook implements a solution for scheduled power management of Azure virtual machines in combination with tags
  7.         on virtual machines or resource groups which define a shutdown schedule. Each time it runs, the runbook looks for all
  8.         virtual machines or resource groups with a tag named "AutoShutdownSchedule" having a value defining the schedule,
  9.         e.g. "10PM -> 6AM". It then checks the current time against each schedule entry, ensuring that VMs with tags or in tagged groups
  10.         are shut down or started to conform to the defined schedule.
  11.  
  12.         This is a PowerShell runbook, as opposed to a PowerShell Workflow runbook.
  13.  
  14.         This runbook requires the "Azure" and "AzureRM.Resources" modules which are present by default in Azure Automation accounts.
  15.         For detailed documentation and instructions, see:
  16.        
  17.         https://automys.com/library/asset/scheduled-virtual-machine-shutdown-startup-microsoft-azure
  18.  
  19.     .PARAMETER AzureCredentialName
  20.         The name of the PowerShell credential asset in the Automation account that contains username and password
  21.         for the account used to connect to target Azure subscription. This user must be configured as co-administrator and owner
  22.         of the subscription for best functionality.
  23.  
  24.         By default, the runbook will use the credential with name "Default Automation Credential"
  25.  
  26.         For for details on credential configuration, see:
  27.         http://azure.microsoft.com/blog/2014/08/27/azure-automation-authenticating-to-azure-using-azure-active-directory/
  28.    
  29.     .PARAMETER AzureSubscriptionName
  30.         The name or ID of Azure subscription in which the resources will be created. By default, the runbook will use
  31.         the value defined in the Variable setting named "Default Azure Subscription"
  32.    
  33.     .PARAMETER Simulate
  34.         If $true, the runbook will not perform any power actions and will only simulate evaluating the tagged schedules. Use this
  35.         to test your runbook to see what it will do when run normally (Simulate = $false).
  36.  
  37.     .EXAMPLE
  38.         For testing examples, see the documentation at:
  39.  
  40.         https://automys.com/library/asset/scheduled-virtual-machine-shutdown-startup-microsoft-azure
  41.    
  42.     .INPUTS
  43.         None.
  44.  
  45.     .OUTPUTS
  46.         Human-readable informational and error messages produced during the job. Not intended to be consumed by another runbook.
  47. #>
  48.  
  49. param(
  50.     [parameter(Mandatory=$false)]
  51.     [String] $AzureCredentialName = #"your user account",
  52.     [parameter(Mandatory=$false)]
  53.     [String] $AzureSubscriptionName = #"your subscription name",
  54.     [parameter(Mandatory=$false)]
  55.     [bool]$Simulate = $false
  56. )
  57.  
  58. $VERSION = "2.0.2"
  59.  
  60. # Define function to check current time against specified range
  61. function CheckScheduleEntry ([string]$TimeRange)
  62. {  
  63.     # Initialize variables
  64.     $rangeStart, $rangeEnd, $parsedDay = $null
  65.     $currentTime = (Get-Date).ToUniversalTime()
  66.     $midnight = $currentTime.AddDays(1).Date           
  67.  
  68.     try
  69.     {
  70.         # Parse as range if contains '->'
  71.         if($TimeRange -like "*->*")
  72.         {
  73.             $timeRangeComponents = $TimeRange -split "->" | foreach {$_.Trim()}
  74.             if($timeRangeComponents.Count -eq 2)
  75.             {
  76.                 $rangeStart = Get-Date $timeRangeComponents[0]
  77.                 $rangeEnd = Get-Date $timeRangeComponents[1]
  78.    
  79.                 # Check for crossing midnight
  80.                 if($rangeStart -gt $rangeEnd)
  81.                 {
  82.                     # If current time is between the start of range and midnight tonight, interpret start time as earlier today and end time as tomorrow
  83.                     if($currentTime -ge $rangeStart -and $currentTime -lt $midnight)
  84.                     {
  85.                         $rangeEnd = $rangeEnd.AddDays(1)
  86.                     }
  87.                     # Otherwise interpret start time as yesterday and end time as today  
  88.                     else
  89.                     {
  90.                         $rangeStart = $rangeStart.AddDays(-1)
  91.                     }
  92.                 }
  93.             }
  94.             else
  95.             {
  96.                 Write-Output "`tWARNING: Invalid time range format. Expects valid .Net DateTime-formatted start time and end time separated by '->'"
  97.             }
  98.         }
  99.         # Otherwise attempt to parse as a full day entry, e.g. 'Monday' or 'December 25'
  100.         else
  101.         {
  102.             # If specified as day of week, check if today
  103.             if([System.DayOfWeek].GetEnumValues() -contains $TimeRange)
  104.             {
  105.                 if($TimeRange -eq (Get-Date).DayOfWeek)
  106.                 {
  107.                     $parsedDay = Get-Date "00:00"
  108.                 }
  109.                 else
  110.                 {
  111.                     # Skip detected day of week that isn't today
  112.                 }
  113.             }
  114.             # Otherwise attempt to parse as a date, e.g. 'December 25'
  115.             else
  116.             {
  117.                 $parsedDay = Get-Date $TimeRange
  118.             }
  119.        
  120.             if($parsedDay -ne $null)
  121.             {
  122.                 $rangeStart = $parsedDay # Defaults to midnight
  123.                 $rangeEnd = $parsedDay.AddHours(23).AddMinutes(59).AddSeconds(59) # End of the same day
  124.             }
  125.         }
  126.     }
  127.     catch
  128.     {
  129.         # Record any errors and return false by default
  130.         Write-Output "`tWARNING: Exception encountered while parsing time range. Details: $($_.Exception.Message). Check the syntax of entry, e.g. '<StartTime> -> <EndTime>', or days/dates like 'Sunday' and 'December 25'"  
  131.         return $false
  132.     }
  133.    
  134.     # Check if current time falls within range
  135.     if($currentTime -ge $rangeStart -and $currentTime -le $rangeEnd)
  136.     {
  137.         return $true
  138.     }
  139.     else
  140.     {
  141.         return $false
  142.     }
  143.    
  144. } # End function CheckScheduleEntry
  145.  
  146. # Function to handle power state assertion for both classic and resource manager VMs
  147. function AssertVirtualMachinePowerState
  148. {
  149.     param(
  150.         [Object]$VirtualMachine,
  151.         [string]$DesiredState,
  152.         [Object[]]$ResourceManagerVMList,
  153.         [Object[]]$ClassicVMList,
  154.         [bool]$Simulate
  155.     )
  156.  
  157.     # Get VM depending on type
  158.     if($VirtualMachine.ResourceType -eq "Microsoft.ClassicCompute/virtualMachines")
  159.     {
  160.         $classicVM = $ClassicVMList | where Name -eq $VirtualMachine.Name
  161.         AssertClassicVirtualMachinePowerState -VirtualMachine $classicVM -DesiredState $DesiredState -Simulate $Simulate
  162.     }
  163.     elseif($VirtualMachine.ResourceType -eq "Microsoft.Compute/virtualMachines")
  164.     {
  165.         $resourceManagerVM = $ResourceManagerVMList | where Name -eq $VirtualMachine.Name
  166.         AssertResourceManagerVirtualMachinePowerState -VirtualMachine $resourceManagerVM -DesiredState $DesiredState -Simulate $Simulate
  167.     }
  168.     else
  169.     {
  170.         Write-Output "VM type not recognized: [$($VirtualMachine.ResourceType)]. Skipping."
  171.     }
  172. }
  173.  
  174. # Function to handle power state assertion for classic VM
  175. function AssertClassicVirtualMachinePowerState
  176. {
  177.     param(
  178.         [Object]$VirtualMachine,
  179.         [string]$DesiredState,
  180.         [bool]$Simulate
  181.     )
  182.  
  183.     # If should be started and isn't, start VM
  184.     if($DesiredState -eq "Started" -and $VirtualMachine.PowerState -notmatch "Started|Starting")
  185.     {
  186.         if($Simulate)
  187.         {
  188.             Write-Output "[$($VirtualMachine.Name)]: SIMULATION -- Would have started VM. (No action taken)"
  189.         }
  190.         else
  191.         {
  192.             Write-Output "[$($VirtualMachine.Name)]: Starting VM"
  193.             $VirtualMachine | Start-AzureVM
  194.         }
  195.     }
  196.        
  197.     # If should be stopped and isn't, stop VM
  198.     elseif($DesiredState -eq "StoppedDeallocated" -and $VirtualMachine.PowerState -ne "Stopped")
  199.     {
  200.         if($Simulate)
  201.         {
  202.             Write-Output "[$($VirtualMachine.Name)]: SIMULATION -- Would have stopped VM. (No action taken)"
  203.         }
  204.         else
  205.         {
  206.             Write-Output "[$($VirtualMachine.Name)]: Stopping VM"
  207.             $VirtualMachine | Stop-AzureVM -Force
  208.         }
  209.     }
  210.  
  211.     # Otherwise, current power state is correct
  212.     else
  213.     {
  214.         Write-Output "[$($VirtualMachine.Name)]: Current power state [$($VirtualMachine.PowerState)] is correct."
  215.     }
  216. }
  217.  
  218. # Function to handle power state assertion for resource manager VM
  219. function AssertResourceManagerVirtualMachinePowerState
  220. {
  221.     param(
  222.         [Object]$VirtualMachine,
  223.         [string]$DesiredState,
  224.         [bool]$Simulate
  225.     )
  226.  
  227.     # Get VM with current status
  228.     $resourceManagerVM = Get-AzureRmVM -ResourceGroupName $VirtualMachine.ResourceGroupName -Name $VirtualMachine.Name -Status
  229.     $currentStatus = $resourceManagerVM.Statuses | where Code -like "PowerState*"
  230.     $currentStatus = $currentStatus.Code -replace "PowerState/",""
  231.  
  232.     # If should be started and isn't, start VM
  233.     if($DesiredState -eq "Started" -and $currentStatus -notmatch "running")
  234.     {
  235.         if($Simulate)
  236.         {
  237.             Write-Output "[$($VirtualMachine.Name)]: SIMULATION -- Would have started VM. (No action taken)"
  238.         }
  239.         else
  240.         {
  241.             Write-Output "[$($VirtualMachine.Name)]: Starting VM"
  242.             $resourceManagerVM | Start-AzureRmVM
  243.         }
  244.     }
  245.        
  246.     # If should be stopped and isn't, stop VM
  247.     elseif($DesiredState -eq "StoppedDeallocated" -and $currentStatus -ne "deallocated")
  248.     {
  249.         if($Simulate)
  250.         {
  251.             Write-Output "[$($VirtualMachine.Name)]: SIMULATION -- Would have stopped VM. (No action taken)"
  252.         }
  253.         else
  254.         {
  255.             Write-Output "[$($VirtualMachine.Name)]: Stopping VM"
  256.             $resourceManagerVM | Stop-AzureRmVM -Force
  257.         }
  258.     }
  259.  
  260.     # Otherwise, current power state is correct
  261.     else
  262.     {
  263.         Write-Output "[$($VirtualMachine.Name)]: Current power state [$currentStatus] is correct."
  264.     }
  265. }
  266.  
  267. # Main runbook content
  268. try
  269. {
  270.     $currentTime = (Get-Date).ToUniversalTime()
  271.     Write-Output "Runbook started. Version: $VERSION"
  272.     if($Simulate)
  273.     {
  274.         Write-Output "*** Running in SIMULATE mode. No power actions will be taken. ***"
  275.     }
  276.     else
  277.     {
  278.         Write-Output "*** Running in LIVE mode. Schedules will be enforced. ***"
  279.     }
  280.     Write-Output "Current UTC/GMT time [$($currentTime.ToString("dddd, yyyy MMM dd HH:mm:ss"))] will be checked against schedules"
  281.    
  282.     # Retrieve subscription name from variable asset if not specified
  283.     if($AzureSubscriptionName -eq "Use *Default Azure Subscription* Variable Value")
  284.     {
  285.         $AzureSubscriptionName = Get-AutomationVariable -Name "Default Azure Subscription"
  286.         if($AzureSubscriptionName.length -gt 0)
  287.         {
  288.             Write-Output "Specified subscription name/ID: [$AzureSubscriptionName]"
  289.         }
  290.         else
  291.         {
  292.             throw "No subscription name was specified, and no variable asset with name 'Default Azure Subscription' was found. Either specify an Azure subscription name or define the default using a variable setting"
  293.         }
  294.     }
  295.  
  296.     # Retrieve credential
  297.     write-output "Specified credential asset name: [$AzureCredentialName]"
  298.     if($AzureCredentialName -eq "Use *Default Automation Credential* asset")
  299.     {
  300.         # By default, look for "Default Automation Credential" asset
  301.         $azureCredential = Get-AutomationPSCredential -Name "Default Automation Credential"
  302.         if($azureCredential -ne $null)
  303.         {
  304.             Write-Output "Attempting to authenticate as: [$($azureCredential.UserName)]"
  305.         }
  306.         else
  307.         {
  308.             throw "No automation credential name was specified, and no credential asset with name 'Default Automation Credential' was found. Either specify a stored credential name or define the default using a credential asset"
  309.         }
  310.     }
  311.     else
  312.     {
  313.         # A different credential name was specified, attempt to load it
  314.         $azureCredential = Get-AutomationPSCredential -Name $AzureCredentialName
  315.         if($azureCredential -eq $null)
  316.         {
  317.             throw "Failed to get credential with name [$AzureCredentialName]"
  318.         }
  319.     }
  320.  
  321.     # Connect to Azure using credential asset (classic API)
  322.     $account = Add-AzureAccount -Credential $azureCredential
  323.    
  324.     # Check for returned userID, indicating successful authentication
  325.     if(Get-AzureAccount -Name $azureCredential.UserName)
  326.     {
  327.         Write-Output "Successfully authenticated as user: [$($azureCredential.UserName)]"
  328.     }
  329.     else
  330.     {
  331.         throw "Authentication failed for credential [$($azureCredential.UserName)]. Ensure a valid Azure Active Directory user account is specified which is configured as a co-administrator (using classic portal) and subscription owner (modern portal) on the target subscription. Verify you can log into the Azure portal using these credentials."
  332.     }
  333.  
  334.     # Validate subscription
  335.     $subscriptions = @(Get-AzureSubscription | where {$_.SubscriptionName -eq $AzureSubscriptionName -or $_.SubscriptionId -eq $AzureSubscriptionName})
  336.     if($subscriptions.Count -eq 1)
  337.     {
  338.         # Set working subscription
  339.         $targetSubscription = $subscriptions | select -First 1
  340.         $targetSubscription | Select-AzureSubscription
  341.  
  342.         # Connect via Azure Resource Manager
  343.         $resourceManagerContext = Add-AzureRmAccount -Credential $azureCredential -SubscriptionId $targetSubscription.SubscriptionId
  344.  
  345.         $currentSubscription = Get-AzureSubscription -Current
  346.         Write-Output "Working against subscription: $($currentSubscription.SubscriptionName) ($($currentSubscription.SubscriptionId))"
  347.     }
  348.     else
  349.     {
  350.         if($subscription.Count -eq 0)
  351.         {
  352.             throw "No accessible subscription found with name or ID [$AzureSubscriptionName]. Check the runbook parameters and ensure user is a co-administrator on the target subscription."
  353.         }
  354.         elseif($subscriptions.Count -gt 1)
  355.         {
  356.             throw "More than one accessible subscription found with name or ID [$AzureSubscriptionName]. Please ensure your subscription names are unique, or specify the ID instead"
  357.         }
  358.     }
  359.  
  360.     # Get a list of all virtual machines in subscription
  361.     $resourceManagerVMList = @(Get-AzureRmResource | where {$_.ResourceType -like "Microsoft.*/virtualMachines"} | sort Name)
  362.     $classicVMList = Get-AzureVM
  363.  
  364.     # Get resource groups that are tagged for automatic shutdown of resources
  365.     $taggedResourceGroups = @(Get-AzureRmResourceGroup | where {$_.Tags.Count -gt 0 -and $_.Tags.Name -contains "AutoShutdownSchedule"})
  366.     $taggedResourceGroupNames = @($taggedResourceGroups | select -ExpandProperty ResourceGroupName)
  367.     Write-Output "Found [$($taggedResourceGroups.Count)] schedule-tagged resource groups in subscription"  
  368.  
  369.     # For each VM, determine
  370.     #  - Is it directly tagged for shutdown or member of a tagged resource group
  371.     #  - Is the current time within the tagged schedule
  372.     # Then assert its correct power state based on the assigned schedule (if present)
  373.     Write-Output "Processing [$($resourceManagerVMList.Count)] virtual machines found in subscription"
  374.     foreach($vm in $resourceManagerVMList)
  375.     {
  376.         $schedule = $null
  377.  
  378.         # Check for direct tag or group-inherited tag
  379.         if($vm.ResourceType -eq "Microsoft.Compute/virtualMachines" -and $vm.Tags -and $vm.Tags.Name -contains "AutoShutdownSchedule")
  380.         {
  381.             # VM has direct tag (possible for resource manager deployment model VMs). Prefer this tag schedule.
  382.             $schedule = ($vm.Tags | where Name -eq "AutoShutdownSchedule")["Value"]
  383.             Write-Output "[$($vm.Name)]: Found direct VM schedule tag with value: $schedule"
  384.         }
  385.         elseif($taggedResourceGroupNames -contains $vm.ResourceGroupName)
  386.         {
  387.             # VM belongs to a tagged resource group. Use the group tag
  388.             $parentGroup = $taggedResourceGroups | where ResourceGroupName -eq $vm.ResourceGroupName
  389.             $schedule = ($parentGroup.Tags | where Name -eq "AutoShutdownSchedule")["Value"]
  390.             Write-Output "[$($vm.Name)]: Found parent resource group schedule tag with value: $schedule"
  391.         }
  392.         else
  393.         {
  394.             # No direct or inherited tag. Skip this VM.
  395.             Write-Output "[$($vm.Name)]: Not tagged for shutdown directly or via membership in a tagged resource group. Skipping this VM."
  396.             continue
  397.         }
  398.  
  399.         # Check that tag value was succesfully obtained
  400.         if($schedule -eq $null)
  401.         {
  402.             Write-Output "[$($vm.Name)]: Failed to get tagged schedule for virtual machine. Skipping this VM."
  403.             continue
  404.         }
  405.  
  406.         # Parse the ranges in the Tag value. Expects a string of comma-separated time ranges, or a single time range
  407.         $timeRangeList = @($schedule -split "," | foreach {$_.Trim()})
  408.        
  409.         # Check each range against the current time to see if any schedule is matched
  410.         $scheduleMatched = $false
  411.         $matchedSchedule = $null
  412.         foreach($entry in $timeRangeList)
  413.         {
  414.             if((CheckScheduleEntry -TimeRange $entry) -eq $true)
  415.             {
  416.                 $scheduleMatched = $true
  417.                 $matchedSchedule = $entry
  418.                 break
  419.             }
  420.         }
  421.  
  422.         # Enforce desired state for group resources based on result.
  423.         if($scheduleMatched)
  424.         {
  425.             # Schedule is matched. Shut down the VM if it is running.
  426.             Write-Output "[$($vm.Name)]: Current time [$currentTime] falls within the scheduled shutdown range [$matchedSchedule]"
  427.             AssertVirtualMachinePowerState -VirtualMachine $vm -DesiredState "StoppedDeallocated" -ResourceManagerVMList $resourceManagerVMList -ClassicVMList $classicVMList -Simulate $Simulate
  428.         }
  429.         else
  430.         {
  431.             # Schedule not matched. Start VM if stopped.
  432.             Write-Output "[$($vm.Name)]: Current time falls outside of all scheduled shutdown ranges."
  433.             AssertVirtualMachinePowerState -VirtualMachine $vm -DesiredState "Started" -ResourceManagerVMList $resourceManagerVMList -ClassicVMList $classicVMList -Simulate $Simulate
  434.         }      
  435.     }
  436.  
  437.     Write-Output "Finished processing virtual machine schedules"
  438. }
  439. catch
  440. {
  441.     $errorMessage = $_.Exception.Message
  442.     throw "Unexpected exception: $errorMessage"
  443. }
  444. finally
  445. {
  446.     Write-Output "Runbook finished (Duration: $(("{0:hh\:mm\:ss}" -f ((Get-Date).ToUniversalTime() - $currentTime))))"
  447. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement