Az.PowerManager.psm1
If (Import-RequiredModules -ModuleFilePath $PSCommandPath -AutoInstall ) { Write-Verbose "All PSRequiredModules loaded !" Function Get-DateOrNull ([Parameter(Mandatory = $true)] [string]$Date) { Try { Get-Date $Date } Catch { $null } } Function Confirm-TimeWihtinTimeRange ([Parameter(Mandatory = $true)] [datetime]$Time, [Parameter(Mandatory = $true)] [datetime]$TimeRangeStart, [Parameter(Mandatory = $true)] [datetime]$TimeRangeEnd) { Try { If ($Time -ge $TimeRangeStart -and $Time -le $TimeRangeEnd) { return $true } Else { return $false } } Catch { Write-Output "[ERROR] $($_)`n[ERROR] [$($_.InvocationInfo.ScriptLineNumber)] $($_.InvocationInfo.ScriptName) >> $($_.InvocationInfo.Line.TrimStart())" } } Function Get-AzPowerPolicyRuleType ([Parameter(Mandatory = $true)] [string]$PolicyRule) { Try { switch ($PolicyRule) { { $PolicyRule -like "*:*->*:*" } { $PolicyRuleType = "TimeRange" } { $PolicyRule -like "*:*" -and $_ -notlike "*->*" } { $PolicyRuleType = "Time" } { $PolicyRule -like "*-*" -and $PolicyRule -notlike "*->*" } { $PolicyRuleType = "DayOfMonth" } { [System.DayOfWeek].GetEnumValues() -contains $PolicyRule } { $PolicyRuleType = "DayOfWeek" } Default { $PolicyRuleType = "Unknown" } } [PSCustomObject]@{RuleValue = $PolicyRule ; RuleType = $PolicyRuleType } } Catch { Write-Output "[ERROR] $($_)`n[ERROR] [$($_.InvocationInfo.ScriptLineNumber)] $($_.InvocationInfo.ScriptName) >> $($_.InvocationInfo.Line.TrimStart())" } } If (!($PowerManager_ScheduleFrequency)) { $PowerManager_ScheduleFrequency = 60 } Function Expand-AzPowerPolicy ([Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)] [string]$ResourceId, [Parameter(Mandatory = $true)] [string]$ShutdownPolicy, [Parameter(Mandatory = $true)] [string]$ShutdownAssignment, [switch]$SkipInvalidRules, [datetime]$CurrentTime = $(Get-Date)) { Try { $PolicyObject = @() $rangeStart, $rangeEnd, $parsedDay = $null $Midnight = $CurrentTime.AddDays(1).Date #1 Mono-rule Vs. Multi-rules If ($ShutdownPolicy -like "*,*") { # Multi-Rules policy $PolicyRules = $ShutdownPolicy.Split(',') $PolicyRules | % { If ($_) { $PolicyObject += Get-AzPowerPolicyRuleType $_ } } } Else { # Mono-Rule policy $PolicyObject += Get-AzPowerPolicyRuleType $ShutdownPolicy } #2 Rule Type $ShutdownPolicyRules = @() $PolicyObject | % { $RuleValue = $_.RuleValue $RuleValid = $null $TimeRangeStart = $null $TimeRangeEnd = $null Switch ($_) { # Rule Type : Time { $_.RuleType -eq 'Time' } { $ShutdownMode = "ShutdownOnly" $TimeRangeStart = Get-DateOrNull $RuleValue $TimeRangeEnd = $null If ($TimeRangeStart) { $RuleValid = $true } Else { $RuleValid = $false } } # Rule Type : TimeRange { $_.RuleType -eq 'TimeRange' } { $ShutdownMode = "Shutdown/Startup" $TimeRangeStart = Get-DateOrNull ($RuleValue -split '->')[0] $TimeRangeEnd = Get-DateOrNull ($RuleValue -split '->')[1] If ($TimeRangeStart -and $TimeRangeEnd) { $RuleValid = $true # Mono-Day Vs. Dual-Day TimeRange If ($TimeRangeStart -gt $TimeRangeEnd) { # Can't be the same day # Current time within TimeRange AND before Midnight = TimeRangeEnd = Tomorrow If ($CurrentTime -ge $TimeRangeStart -and $CurrentTime -lt $Midnight) { $TimeRangeEnd = $TimeRangeEnd.AddDays(1) } #Else TimeRangeStart = Yesterday Else { $TimeRangeStart = $TimeRangeStart.AddDays(-1) } } } Else { $RuleValid = $false Write-Warning "Wrong rule value ($RuleValue). Flag as invalid rule ..." } } # Rule Type : DayOfWeek { $_.RuleType -eq 'DayOfWeek' } { $RuleValid = $true $ShutdownMode = "Shutdown/Startup" # Is Today ? #If ($RuleValue -eq (Get-Date).DayOfWeek) { $TimeRangeStart = Get-Date "00:00"; $TimeRangeEnd = Get-Date "23:59:59" } If ($RuleValue -eq $CurrentTime.DayOfWeek) { $TimeRangeStart = $($CurrentTime.AddHours(-$CurrentTime.Hour).AddMinutes(-$CurrentTime.Minute).AddSeconds(-$CurrentTime.Second)); $TimeRangeEnd = $($CurrentTime.AddHours(23 - $CurrentTime.Hour).AddMinutes(59 - $CurrentTime.Minute).AddSeconds(59 - $CurrentTime.Second)) } Else { #$TimeRangeStart = Get-Date "00:00" $TimeRangeStart = $($CurrentTime.AddHours(-$CurrentTime.Hour).AddMinutes(-$CurrentTime.Minute).AddSeconds(-$CurrentTime.Second)) While ($TimeRangeStart.DayOfWeek -ne $RuleValue) { $TimeRangeStart = $TimeRangeStart.AddDays(1) } $TimeRangeEnd = $TimeRangeStart.AddHours(23).AddMinutes(59).AddSeconds(59) } } # Rule Type : DayOfMonth { $_.RuleType -eq 'DayOfMonth' } { $ShutdownMode = "Shutdown/Startup" $TimeRangeStart = Get-DateOrNull $RuleValue If ($TimeRangeStart) { $RuleValid = $true $TimeRangeEnd = $TimeRangeStart.AddHours(23).AddMinutes(59).AddSeconds(59) } Else { $RuleValid = $false Write-Warning "Wrong rule value ($RuleValue). Flag as invalid rule ..." } } # Rule Type : Unknown { $_.RuleType -eq 'Unknown' } { $RuleValid = $false } } # Set Shutdown period $ShutdownPeriod = $null If ($RuleValid) { # Rule type : Time If ( ($_.RuleType -eq 'Time') -and ($CurrentTime -ge $TimeRangeStart) -and ($CurrentTime -lt $TimeRangeStart.AddMinutes($PowerManager_ScheduleFrequency))) { $ShutdownPeriod = $true } ElseIf ($_.RuleType -ne 'Time') { $ShutdownPeriod = Confirm-TimeWihtinTimeRange -Time $CurrentTime -TimeRangeStart $TimeRangeStart -TimeRangeEnd $TimeRangeEnd } Else { $ShutdownPeriod = $false } } $ShutdownPolicyRules += [PSCustomObject]@{ResourceId = $ResourceId ; ShutdownAssignment = $ShutdownAssignment ; ShutdownMode = $ShutdownMode ; ShutdownRule = $RuleValue ; ShutdownRuleType = $_.RuleType ; ShutdownRuleValid = $RuleValid ; ShutdownStart = $TimeRangeStart ; ShutdownEnd = $TimeRangeEnd ; ShutdownPeriod = $ShutdownPeriod } } # Rule Type "Time" (Shutdown only) conflict with another types (Shutdown/Startup) If ($ShutdownPolicyRules.Count -ge 2 -and $ShutdownPolicyRules.RuleType -contains 'Time') { Write-Warning "You shouldn't use Time rule type with other types like TimeRange. Please review your ShutdownPolicy to use one of these. Skipping Time for this process ..." $ShutdownPolicyRules = $ShutdownPolicyRules | ? { $_.RuleType -ne 'Time' } } # Option to skip invalide rules (unkown or wrong time ranges) If ($SkipInvalidRules) { $ShutdownPolicyRules | ? { $_.ShutdownRuleValid -eq $true } } Else { $ShutdownPolicyRules } } Catch { Write-Output "[ERROR] $($_)`n[ERROR] [$($_.InvocationInfo.ScriptLineNumber)] $($_.InvocationInfo.ScriptName) >> $($_.InvocationInfo.Line.TrimStart())" } } Function Get-AzPowerPolicy { #### RGs #### # Get RGs with Shutdown Policy $rgsWithShutdownPolicy = Search-AzGraph -Query 'resourcecontainers | where type == "microsoft.resources/subscriptions/resourcegroups" | where notnull(tags.Shutdown) | extend shutdownPolicy=tags.Shutdown | project tenantId, subscriptionId, resourceGroupId=id, resourceGroupName=resourceGroup, resourceName=resourceGroup, resourceType=type, shutdownPolicy' # Build RG Shutdown Policy object $rgsPolicies = $null $rgsWithShutdownPolicy | % { [array]$rgsPolicies += [PSCustomObject]@{ resourceGroupId = $_.resourceGroupId ; shutdownPolicy = $_.shutdownPolicy } } $rgsWithShutdownPolicy.resourceGroupId | % { [array]$rgIds += $_ } $rgIdsString = "'" + $($rgIds -join "','") + "'" #### VMs #### If (!($PowerManager_ExcludeVM)) { ## Get VMs with RG Shutdown tag AND NO explicit tag (inherited) $vmsWithRgShutownPolicy = Search-AzGraph -Query "resources | where type == `"microsoft.compute/virtualmachines`" and isnull(tags.Shutdown) | extend resourceGroupId=strcat(`"/subscriptions/`",subscriptionId,`"/resourceGroups/`",resourceGroup) | where resourceGroupId in~ ($rgIdsString) | extend resourceCurrentState=strcat_array(split(properties.extended.instanceView.powerState.code,`"/`",1),`"`") | project tenantId, subscriptionId, resourceGroupId, resourceGroupName=resourceGroup, resourceId=id, resourceName=name, resourceType=type, resourceCurrentState" # Append RG shutodwn policy $vmsWithRgShutownPolicy | % { $vmRgId = $_.resourceGroupId $ShutdownPolicy = ($rgsPolicies | ? { $_.resourceGroupId -eq $vmRgId }).shutdownPolicy $_ | Add-Member -MemberType NoteProperty -Name shutdownPolicy -Value $ShutdownPolicy $_ | Add-Member -MemberType NoteProperty -Name shutdownAssignment -Value "inherited" } ## Get VMs with explicit Shutdown tag (explicit) $vmsWithExplicitShutdownPolicy = Search-AzGraph -Query "resources | where type == `"microsoft.compute/virtualmachines`" and notnull(tags.Shutdown) | extend resourceGroupId=strcat(`"/subscriptions/`",subscriptionId,`"/resourceGroups/`",resourceGroup) | extend shutdownPolicy=tags.Shutdown | extend shutdownAssignment=`"explicit`" | extend resourceCurrentState=strcat_array(split(properties.extended.instanceView.powerState.code,`"/`",1),`"`") | project tenantId, subscriptionId, resourceGroupId, resourceGroupName=resourceGroup, resourceId=id, resourceName=name, resourceType=type, resourceCurrentState, shutdownPolicy, shutdownAssignment" $vmsWithRgShutownPolicy $vmsWithExplicitShutdownPolicy } Else { Write-Warning "Virtual Machines excluded" } #### App Services #### If (!($PowerManager_ExcludeAppService)) { ## Get App Services with RG Shutdown tag AND NO explicit tag (inherited) $appServiceWithRgShutdownPolicy = Search-AzGraph -Query "resources | where type == `"microsoft.web/sites`" and isnull(tags.Shutdown) | extend resourceGroupId=strcat(`"/subscriptions/`",subscriptionId,`"/resourceGroups/`",resourceGroup) | where resourceGroupId in~ ($rgIdsString) | extend resourceCurrentState=tolower(properties.state) | project tenantId, subscriptionId, resourceGroupId, resourceGroupName=resourceGroup, resourceId=id, resourceName=name, resourceType=type, resourceCurrentState" # Append RG shutodwn policy $appServiceWithRgShutdownPolicy | % { $rgId = $_.resourceGroupId $ShutdownPolicy = ($rgsPolicies | ? { $_.resourceGroupId -eq $rgId }).shutdownPolicy $_ | Add-Member -MemberType NoteProperty -Name shutdownPolicy -Value $ShutdownPolicy $_ | Add-Member -MemberType NoteProperty -Name shutdownAssignment -Value "inherited" } ## Get App Services with explicit Shutdown tag (explicit) $appServiceWithExplicitShutdownPolicy = Search-AzGraph -Query "resources | where type == `"microsoft.web/sites`" and notnull(tags.Shutdown) | extend resourceGroupId=strcat(`"/subscriptions/`",subscriptionId,`"/resourceGroups/`",resourceGroup) | extend shutdownPolicy=tags.Shutdown | extend shutdownAssignment=`"explicit`" | extend resourceCurrentState=tolower(properties.state) | project tenantId, subscriptionId, resourceGroupId, resourceGroupName=resourceGroup, resourceId=id, resourceName=name, resourceType=type, resourceCurrentState, shutdownPolicy, shutdownAssignment" $appServiceWithRgShutdownPolicy $appServiceWithExplicitShutdownPolicy } Else { Write-Warning "App Services excluded" } #### Azure Kubernetes Service #### If (!($PowerManager_ExcludeAKS)) { ## Get Azure Kubernetes Service with RG Shutdown tag AND NO explicit tag (inherited) $aksWithRgShutdownPolicy = Search-AzGraph -Query "resources | where type == `"microsoft.containerservice/managedclusters`" and isnull(tags.Shutdown) | extend resourceGroupId=strcat(`"/subscriptions/`",subscriptionId,`"/resourceGroups/`",resourceGroup) | where resourceGroupId in~ ($rgIdsString) | extend resourceCurrentState=tolower(properties.powerState.code) | project tenantId, subscriptionId, resourceGroupId, resourceGroupName=resourceGroup, resourceId=id, resourceName=name, resourceType=type, resourceCurrentState" # Append RG shutodwn policy $aksWithRgShutdownPolicy | % { $rgId = $_.resourceGroupId $ShutdownPolicy = ($rgsPolicies | ? { $_.resourceGroupId -eq $rgId }).shutdownPolicy $_ | Add-Member -MemberType NoteProperty -Name shutdownPolicy -Value $ShutdownPolicy $_ | Add-Member -MemberType NoteProperty -Name shutdownAssignment -Value "inherited" } ## Get Azure Kubernetes Service with explicit Shutdown tag (explicit) $aksWithExplicitShutdownPolicy = Search-AzGraph -Query "resources | where type == `"microsoft.containerservice/managedclusters`" and notnull(tags.Shutdown) | extend resourceGroupId=strcat(`"/subscriptions/`",subscriptionId,`"/resourceGroups/`",resourceGroup) | extend shutdownPolicy=tags.Shutdown | extend shutdownAssignment=`"explicit`" | extend resourceCurrentState=tolower(properties.powerState.code) | project tenantId, subscriptionId, resourceGroupId, resourceGroupName=resourceGroup, resourceId=id, resourceName=name, resourceType=type, resourceCurrentState, shutdownPolicy, shutdownAssignment" $aksWithRgShutdownPolicy $aksWithExplicitShutdownPolicy } Else { Write-Warning "Azure Kubernetes Service excluded" } #### ADXs #### If (!($PowerManager_ExcludeADX)) { ## Get ADXs with RG Shutdown tag AND NO explicit tag (inherited) $adxsWithRgShutownPolicy = Search-AzGraph -Query "resources | where type == `"microsoft.kusto/clusters`" and isnull(tags.Shutdown) | extend resourceGroupId=strcat(`"/subscriptions/`",subscriptionId,`"/resourceGroups/`",resourceGroup) | where resourceGroupId in~ ($rgIdsString) | extend resourceCurrentState=tolower(properties.state) | project tenantId, subscriptionId, resourceGroupId, resourceGroupName=resourceGroup, resourceId=id, resourceName=name, resourceType=type, resourceCurrentState" # Append RG shutodwn policy $adxsWithRgShutownPolicy | % { $rgId = $_.resourceGroupId $ShutdownPolicy = ($rgsPolicies | ? { $_.resourceGroupId -eq $rgId }).shutdownPolicy $_ | Add-Member -MemberType NoteProperty -Name shutdownPolicy -Value $ShutdownPolicy $_ | Add-Member -MemberType NoteProperty -Name shutdownAssignment -Value "inherited" } $adxsWithRgShutownPolicy } Else { Write-Warning "Azure Data Explorer excluded" } } Function Get-AzPowerPolicyRules { [CmdletBinding()] param ([Parameter(Mandatory = $true)] [object]$ShutdownPolicy, [datetime]$CurrentTime = $(Get-Date)) Try { $ShutdownPolicy | % { Expand-AzPowerPolicy -ResourceId $_.resourceId -ShutdownPolicy $_.shutdownPolicy -ShutdownAssignment $_.shutdownAssignment -SkipInvalidRules -CurrentTime $CurrentTime } } Catch { Write-Output "[ERROR] $($_)`n[ERROR] [$($_.InvocationInfo.ScriptLineNumber)] $($_.InvocationInfo.ScriptName) >> $($_.InvocationInfo.Line.TrimStart())" } } Function Assert-AzPowerComputeStates { [CmdletBinding()] param ([Parameter(Mandatory = $true)] [object]$ShutdownPolicy, [datetime]$CurrentTime = $(Get-Date)) Try { # Get shutdown policy rules $ShutdownPolicyRules = Get-AzPowerPolicyRules -ShutdownPolicy $ShutdownPolicy -CurrentTime $CurrentTime # Add porperties foreeach itereation of the current object $ShutdownPolicy | % { $resourceId = $_.resourceId $resourcecurrentState = $_.resourceCurrentState $resourcePolicyRules = $null $resourcePolicyRules = $ShutdownPolicyRules | ? { $_.ResourceId -eq $resourceId } If ($resourcePolicyRules) { # Manage deallocated state for VM Vs. stopped for other compute resources $ResourceType = $ResourceId.Split('/')[6] + '/' + $ResourceId.Split('/')[7] If ($ResourceType -eq 'Microsoft.Compute/virtualMachines') { If ($resourcePolicyRules.ShutdownPeriod -contains $True) { $desiredState = "deallocated" } Else { If ($resourcePolicyRules.ShutdownRuleType -eq 'Time') { $desiredState = $null } Else { $desiredState = 'running' } } } Else { If ($resourcePolicyRules.ShutdownPeriod -contains $True) { $desiredState = "stopped" } Else { If ($resourcePolicyRules.ShutdownRuleType -eq 'Time') { $desiredState = $null } Else { $desiredState = 'running' } } } # Start If (($resourcecurrentState -ne 'running') -and ($desiredState -eq 'running')) { $powerManagerAction = 'start' } # Deallocate ElseIf (($resourcecurrentState -eq 'running') -and ($desiredState -eq 'deallocated')) { $powerManagerAction = 'deallocate' } # Stop ElseIf (($resourcecurrentState -eq 'running') -and ($desiredState -eq 'stopped')) { $powerManagerAction = 'stop' } # Do nothing Else { $powerManagerAction = 'none' } $_ | Add-Member -MemberType NoteProperty -Name resourceDesiredState -Value $desiredState $_ | Add-Member -MemberType NoteProperty -Name powerManagerSimulationMode -Value $SimulationMode $_ | Add-Member -MemberType NoteProperty -Name powerManagerAction -Value $powerManagerAction } } $ShutdownPolicy | ? { Get-Member -InputObject $_ -Name "resourceDesiredState" -Membertype NoteProperty } } Catch { Write-Output "[ERROR] $($_)`n[ERROR] [$($_.InvocationInfo.ScriptLineNumber)] $($_.InvocationInfo.ScriptName) >> $($_.InvocationInfo.Line.TrimStart())" } } Function Send-AzPowerComputeAction { [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] [String] $ResourceId, [Parameter(Mandatory = $true)] [String] $Action, [Switch] $Simulation, [String] $BearerToken = (Get-AzAccessToken).Token ) Try { $headers = @{"authorization" = "bearer $bearerToken" } $baseUri = 'https://management.azure.com' $ResourceType = $ResourceId.Split('/')[6] + '/' + $ResourceId.Split('/')[7] $ResourceTypeValid = $true $ResourceName = Split-Path $ResourceId -Leaf Switch ($ResourceType) { 'Microsoft.Compute/virtualMachines' { $uri = "$baseUri$ResourceId/$($Action)?api-version=2020-12-01" } 'Microsoft.Kusto/clusters' { $uri = "$baseUri$ResourceId/$($Action)?api-version=2020-09-18" } 'Microsoft.Web/sites' { $uri = "$baseUri$ResourceId/$($Action)?api-version=2019-08-01" } 'Microsoft.ContainerService/managedClusters' { $uri = "$baseUri$ResourceId/$($Action)?api-version=2020-11-01" } default { $ResourceTypeValid = $false } } If ($ResourceTypeValid) { Write-Verbose "Resource type <$ResourceType> for <$ResourceName> is valid !" Write-Verbose "$($SimulationHeader)Sending action <$Action> to the compute resource <$ResourceId> ..." If (!($Simulation)) { $request = iwr -Uri $uri -Method POST -Headers $headers -UseBasicParsing If ($request.StatusCode -eq 202 -or 200) { Write-Verbose "Action sent successfully!" } Else { Write-Warning "Action NOT sent successfully!"; $request } } } Else { Write-Warning "Resource type $ResourceType for <$ResourceName> is invalid !" } } Catch { Write-Output "[ERROR] $($_)`n[ERROR] [$($_.InvocationInfo.ScriptLineNumber)] $($_.InvocationInfo.ScriptName) >> $($_.InvocationInfo.Line.TrimStart())" } } Function Get-AzPowerComputeState { [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] [String] $ResourceId, [String] $BearerToken = (Get-AzAccessToken).Token ) Try { $headers = @{"authorization" = "bearer $bearerToken" } $baseUri = 'https://management.azure.com' $ResourceType = $ResourceId.Split('/')[6] + '/' + $ResourceId.Split('/')[7] $ResourceTypeValid = $true $ResourceName = Split-Path $ResourceId -Leaf Switch ($ResourceType) { 'Microsoft.Kusto/clusters' { $uri = "$baseUri$ResourceId`?api-version=2020-09-18" } 'Microsoft.ContainerService/managedClusters' { $uri = "$baseUri$ResourceId`?api-version=2020-11-01" } default { $ResourceTypeValid = $false } } If ($ResourceTypeValid) { Write-Verbose "Resource type <$ResourceType> for <$ResourceName> is valid !" Write-Verbose "Getting state for the compute resource <$ResourceId> ..." $request = iwr -Uri $uri -Method GET -Headers $headers -UseBasicParsing If ($request.StatusCode -eq 202 -or 200) { Switch ($ResourceType) { 'Microsoft.Kusto/clusters' { ($request.Content | ConvertFrom-Json).properties.state.ToLower() } 'Microsoft.ContainerService/managedClusters' { ($request.Content | ConvertFrom-Json).properties.powerState.code.ToLower() } } } Else { Write-Error $request } } Else { Write-Warning "Resource type $ResourceType for <$ResourceName> is invalid !" } } Catch { Write-Output "[ERROR] $($_)`n[ERROR] [$($_.InvocationInfo.ScriptLineNumber)] $($_.InvocationInfo.ScriptName) >> $($_.InvocationInfo.Line.TrimStart())" } } } Else { Remove-Module PSRequiredModules ; throw "Some required modules versions aren't imported/installed. Can't load this module. See PSRequiredModules in module manifest for more details." } |