riweston

Untitled

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