Skip to content

Instantly share code, notes, and snippets.

@AlexSen
Last active October 16, 2018 17:46
Show Gist options
  • Save AlexSen/39c966cc2576c65e6373721fd345657a to your computer and use it in GitHub Desktop.
Save AlexSen/39c966cc2576c65e6373721fd345657a to your computer and use it in GitHub Desktop.
Azure PowerShell Runbook script which iterates through Virtual Machines in all Automation Accounts and performs Stop-VM based in case if VM is not active for predefined amount of time
<#
.SYNOPSIS
TBD
.DESCRIPTION
TBD
.URL https://gist.github.com/Forket/39c966cc2576c65e6373721fd345657a
#>
param(
[Parameter (Mandatory= $false, HelpMessage="Name of Azure RunAs Connection of the account where runbook works.")]
[String]
$AzureRunAsConnectionName = "AzureRunAsConnection",
[Parameter (Mandatory= $false, HelpMessage="Name of Azure Automation Account where runbook works.")]
[String]
$AutomationAccountName = "DefaultAutomationAccount",
[Parameter (Mandatory= $false, HelpMessage="Name of Azure Resource Group where runbook works.")]
[String]
$ResourceGroupName = "defaultresourcegroup",
[Parameter (Mandatory= $false, HelpMessage="Number of minutes which will consider DISCONNECTED RDP session as non active and will grant script right to shutdown VM.")]
[Int32]
$RDPsessionsIDLEtimeoutMin = 120,
[Parameter (Mandatory= $false, HelpMessage="OWNER tag of VM should contain email address which will be used for notification.")]
[boolean]
$NotifyVMOwner = $true,
[Parameter (Mandatory= $false, HelpMessage="CC email address which will be used for notification.")]
[String]
$CC = $null,
[Parameter (Mandatory= $false, HelpMessage="Name of Azure automation connection which contains SMTP settings. EMAIL module (in modules gallery) could be used.")]
[string]
$SMTPAutomationConnectionName = "Gmail"
)
# By default, STOP script in case of any error.
$ErrorActionPreference = 'Stop'
# Global Variables
filter timestamp {"$(Get-Date -Format G): $_"} # filter used to add timestamp for console output
$CassiaDllLocation = "C:\Windows\System32\Cassia.dll" # location of Cassia dll library on remote computer
# Get service principal account based on Runbook provided name of Azure RunAs connection name
# and login to Azure RM Account
$ServicePrincipalConnection = Get-AutomationConnection -Name $AzureRunAsConnectionName
$LoginAzureRmAccountOptions = @{
TenantId = $ServicePrincipalConnection.TenantId
ApplicationId = $ServicePrincipalConnection.ApplicationId
ServicePrincipal = $true
CertificateThumbprint = $ServicePrincipalConnection.CertificateThumbprint
}
Login-AzureRmAccount @LoginAzureRmAccountOptions | Out-Null
# Get All Azure automation Connections.
# Here you can include connections which have access to all other Azure Accounts/Subscriptions
$GetAzureRmAutomationConnectionOptions = @{
ResourceGroupName = $ResourceGroupName
AutomationAccountName = $AutomationAccountName
ConnectionTypeName = "AzureServicePrincipal"
}
$AzureAccounts = Get-AzureRmAutomationConnection @GetAzureRmAutomationConnectionOptions
ForEach ( $Account in $AzureAccounts ) { # Iterate through all Azure Automation connections in order to Log In into them and get all VMs
$AutomationConnection = Get-AutomationConnection -Name $Account.Name
# Login to Azure Account
Write-Output "" ; $("#" * 100); "Logging in to Azure Account: $($Account.Name)"
$LoginAzureRmAccountOptions = @{
TenantId = $AutomationConnection.TenantId
ApplicationId = $AutomationConnection.ApplicationId
ServicePrincipal = $true
CertificateThumbprint = $AutomationConnection.CertificateThumbprint
}
try {Login-AzureRmAccount @LoginAzureRmAccountOptions | Out-Null} catch { continue }
#Get Azure VMs and iterate through
$AzureVMs = Get-AzureRmVm -WarningAction Ignore
"Found $(( $AzureVMs | measure ).Count) VMs." | timestamp
ForEach ( $VM in $AzureVMs ) {
""; "===== Processing $($VM.Name) VM"
$VMName = $VM.Name
$VMfqdn = $null
$VMowner = $VM.Tags['owner']
$VMStatus = ( ( Get-AzureRmVm -ResourceGroupName $VM.ResourceGroupName -Name $VM.Name -Status -WarningAction Ignore ).Statuses )[-1].Code
$VMStatusesToMonitor = @( "PowerState/running",
"PowerState/stopped")
# Define SMTP settings
$SendEmailOptions = @{
SMTPSettings = Get-AutomationConnection -Name "Gmail"
To = $VMowner
HTMLBody = $true
CC = $CC
ErrorAction = "Ignore"
}
if ( $VMStatusesToMonitor -contains $VMStatus )
{
if ( $VMStatus -eq "PowerState/stopped")
{
Write-Output "$VMName is stopped, but not deallocated. Deallocating..." | timestamp
$shutdownStatus = ($VM | Stop-AzureRmVm -Force -ErrorAction SilentlyContinue).Status
Write-Output "$VMName deallocated with status $shutdownStatus" | timestamp
if ( $VMowner -and $NotifyVMOwner )
{
Send-Email @SendEmailOptions -Subject "$VMName deallocated" -Body "<b>Reason</b>: VM was stopped, but not deallocated."
}
continue # There is nothing to do with current VM, go to next one
}
Write-Output "$VMName has status $VMStatus. Going to get DNS name for remote connection to VM." | timestamp
# Get VM FQDN
$nics = Get-AzureRmNetworkInterface -WarningAction Ignore | where VirtualMachine -NE $null #skip Nics with no VM
foreach( $nic in $nics )
{
if ( $nic.VirtualMachine.Id -eq $VM.Id )
{
$PublicAddress = Get-AzureRmPublicIpAddress | where Id -EQ $nic.IpConfigurations.publicIPaddress.Id
$VMfqdn = $PublicAddress.DnsSettings.Fqdn
}
}
# If we don't have VM FQDN than inform and go to next VM
if ( $VMfqdn -eq $null )
{
Write-Output "Couldn't find DNS name for $VMName. Skip." | timestamp
continue
}
# Get VM credentials from Azure Automation
$VMcredentials = Get-AutomationPSCredential -Name $VMName
Write-Output "Connecting to <$VMName> using <$VMfqdn>" | timestamp
Write-Output "Got credentials ""$($VMcredentials.UserName)"" for $VMName." | timestamp
# Create Session object and connect to VM
# Enter-PSSession -ComputerName "epm-kasia.northeurope.cloudapp.azure.com" -Credential (Get-Credential -UserName "Kasia") -UseSSL -SessionOption (New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck); Exit-PSSession
$session = New-PSSession -ComputerName $VMfqdn -Credential $VMcredentials -UseSSL -SessionOption (New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck)
Write-Output "Opened session for $($VM.Name). Going to run main script in order to know if we can shutdown VM" | timestamp
# Perform main script
Invoke-Command -Session $session -ScriptBlock {
$result = [PSCustomObject]@{
status = $false
message = ''
sessions = $null
}
# Get VM uptime
$VMuptime = (get-date) - (gcim Win32_OperatingSystem).LastBootUpTime
if ( $VMuptime -le ($RDPsessionsIDLEtimeoutMin / 2) )
{
$result.status = $false
$result.message = "VM uptime is less than $($RDPsessionsIDLEtimeoutMin / 2) mins. Skipping."
} else {
# Get all sessions to the variable
[reflection.assembly]::loadfile("$CassiaDllLocation") | Out-Null
$manager = new-object Cassia.TerminalServicesManager
$RDsessions = $manager.GetSessions() | Select-Object -Property ClientIPAddress,UserAccount,ClientName,ConnectionState,CurrentTime,ConnectTime,DisconnectTime,LastInputTime,LoginTime,IdleTime,UserName
# Filter sessions which have usernames
$RDsessions = $RDsessions | Where-Object -Property UserName -ne -Value ""
# Get active sessions
$RDactiveSessions = $RDsessions | Where-Object -Property ConnectionState -eq -Value "Active"
# Get disconnected sessions
$RDdisconnectedSessions = $RDsessions | Where-Object -Property ConnectionState -eq -Value "Disconnected"
# If we have active sessions than set shutdown to TRUE but also check for disconnected sessions
if ( ( $RDactiveSessions | measure ).Count -eq 0 )
{
$result.status = $true; $result.message = "We have no active sessions"
ForEach ($disconnectedSession in $RDdisconnectedSessions)
{
# If we found disconnected session but IDLE time less then 120 mins - than don't run shutdown
if ( $disconnectedSession.IdleTime.TotalMinutes -le $RDPsessionsIDLEtimeoutMin ) {
$result.status = $false
$result.message = "There are no active sessions, but we have disconnected session with IDLE time $($disconnectedSession.IdleTime.TotalMinutes) minutes, wich is less than 120 mins."
$result.sessions = $disconnectedSession
break
}
}
} else {
$result.message = "We have $(( $RDactiveSessions | measure ).Count) active sessions"
$result.sessions = $RDactiveSessions
}
}
}
# return result of our check
$shutdown = Invoke-Command -Session $session -ScriptBlock { return $result }
Write-Output "Can we shutdown $($VM.Name)?: $(If ($shutdown.status -eq $true) {"Yes"} Else {"No. Reason: $($shutdown.message)"})" | timestamp
if ( $shutdown.status -eq $true )
{
Write-Output "Stopping $($VM.Name)..." | timestamp
$shutdownStatus = $VM | Stop-AzureRmVm -Force
Write-Output "$($VM.Name) stopped with status $($shutdownStatus.Status)" | timestamp
if ($VMowner) {Send-Email -SMTPSettings (Get-AutomationConnection -Name "Gmail") -To $VMowner -Subject ("$($VM.Name) Shuttedown") -Body "via PowerShell" -HTMLBody $true -ErrorAction Ignore}
} else {
Write-Output $shutdown.sessions
}
} else {
Write-Output "$($VM.Name) is in status $($vmStatus[-1].Code). Skipping." | timestamp
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment