Execute-AzureRMVMRemoteScriptRunbook.ps1
<#PSScriptInfo
.VERSION 1.1.0 .GUID 33caea6b-5e18-4ab4-9ff8-9a851000cc95 .AUTHOR Arjun Bahree .COMPANYNAME .COPYRIGHT (c) 2018 Arjun Bahree. All rights reserved. .TAGS Windows PowerShell PSRemoting Azure AzureAutomation Runbooks .LICENSEURI https://github.com/bahreex/Bahree-PowerShell-Library/blob/master/LICENSE .PROJECTURI https://github.com/bahreex/Bahree-PowerShell-Library/tree/master/Azure%20Automation%20Runbooks .ICONURI .EXTERNALMODULEDEPENDENCIES AzureRM .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES #> <# .DESCRIPTION Lets you setup remote connection to all Azure ARM VMs in an Azure Subscription, for remotely executing PowerShell Scripts/Commnads on the target Azure VMs. #> <# .SYNOPSIS Lets you setup remote connection to all Azure ARM VMs in an Azure Subscription for remotely executing PoweShell scripts on the target VMs .DESCRIPTION This Runbook sets up and remotely executes Inline Powershell scripts on one/more/all Azure ARM virtual machines in your Azure Subscription. It enables you to traverse through all resource groups and corresponding VMs in your Azure Subscription, check the current state of VMs (and skip the deallocated/atopped ones), check OS type (Windows or Linux, and skip Linux ones). Thereafter, this script triggers specific Inline functions to enable and configure Windows Remote Management service on each VM, setup a connection to the Azure subscription, get the public IP Address of the VM, and remote into it to for execution of whatever commands/script needs to be executed there. You need to pass the script to be executed on the VMs as an Inline string. You need to execute this Runbook through a 'Azure Run As account (service principal)' Identity from an Azure Automation account. .PARAMETER KeyVaultName Name of the Azure KeyVault, where password for each of the VMs are stored. Assuming Passwords for each VM are stored in Azure Keyvault in the format: "Secret Name = <VM Name>, Secret Value = <Password>" .PARAMETER AzureAutomationAccountName Name of the Azure Automation Account, from where this runbook will be run .PARAMETER AzureAutomationResourceGroupName Name of the Resource Group for the Azure Automation Account, from where this runbook will be run .PARAMETER RemoteScript The string represetation of the Remote PS Script you want to execute on the target VMs .PARAMETER ResourceGroupName Name of the Resource Group containing the VMs you want to remote Into. Specifying just the Resource Group without the "VMName" parameter, will consider all VMs in this specified Resource Group. When passing this paramter from the Start Runbook UI in Azure Automation, you need to pass the value using JSON String format, for e.g. a value of 'rg-01' should be passed as ['rg-01']. Multiple values shoudl be passed as ['rg-01', 'rg-02', 'eg-03'] .PARAMETER VMName Name of the VM you want to remote Into. this parameter cannot be specified without it's Resource group in the "ResourceGroupName" parameter, or else will throw error. When passing this paramter from the Start Runbook UI in Azure Automation, you need to pass the value using JSON String format, for e.g. a value of 'vm-01' should be passed as ['vm-01']. Multiple values shoudl be passed as ['vm-01', 'vm-02', 'vm-03'] .EXAMPLE Execute-AzureVMRemoting -KeyVaultName "CoreKV1" -AzureAutomationAccountName "Automation-AC1" ` -AzureAutomationResourceGroupName "Automation-RG1" -ResourceGroupName RG1 -VMName VM01 ` -RemoteScript "Write-Output 'Hello World!'" .EXAMPLE Execute-AzureVMRemoting -KeyVaultName "CoreKV1" -AzureAutomationAccountName "Automation-AC1" ` -AzureAutomationResourceGroupName "Automation-RG1" -ResourceGroupName RG1 -VMName VM01,VM02,VM11,VM13 ` -RemoteScript "Write-Output 'Hello World!'" .EXAMPLE Execute-AzureVMRemoting -KeyVaultName "CoreKV1" -AzureAutomationAccountName "Automation-AC1" ` -AzureAutomationResourceGroupName "Automation-RG1" -ResourceGroupName RG1 -VMName "VM01" ` -RemoteScript "Write-Output 'Hello World!'" .EXAMPLE Execute-AzureVMRemoting -KeyVaultName "CoreKV1" -AzureAutomationAccountName "Automation-AC1" ` -AzureAutomationResourceGroupName "Automation-RG1" -ResourceGroupName RG1,RG2,RG3 ` -RemoteScript "Write-Output 'Hello World!'" .EXAMPLE Execute-AzureVMRemoting -KeyVaultName "CoreKV1" -AzureAutomationAccountName "Automation-AC1" ` -AzureAutomationResourceGroupName "Automation-RG1" -VMName VM01,VM02,VM11,VM13 ` -RemoteScript "Write-Output 'Hello World!'" .EXAMPLE Execute-AzureVMRemoting -KeyVaultName "CoreKV1" -AzureAutomationAccountName "Automation-AC1" ` -AzureAutomationResourceGroupName "Automation-RG1" ` -RemoteScript "Write-Output 'Hello World!'" .Notes Author: Arjun Bahree E-mail: arjun.bahree@gmail.com Creation Date: 6/Dec/2017 Last Revision Date: 15/Jan/2018 Development Environment: Azure Automation Runbook Editor and VS Code IDE PS Version: 5.1 Platform: Windows #> param( [Parameter(Mandatory = $true)] [String]$KeyVaultName, [Parameter(Mandatory = $true)] [String]$AzureAutomationAccountName, [Parameter(Mandatory = $true)] [String]$AzureAutomationResourceGroupName, [Parameter(Mandatory = $true)] [String]$RemoteScript, [Parameter(Mandatory = $false)] [String[]]$ResourceGroupName, [Parameter(Mandatory = $false)] [String[]]$VMName ) if (!(Get-AzureRmContext).Account) { $connectionName = "AzureRunAsConnection" try { # Get the connection "AzureRunAsConnection " $servicePrincipalConnection = Get-AutomationConnection -Name $connectionName Add-AzureRmAccount ` -ServicePrincipal ` -TenantId $servicePrincipalConnection.TenantId ` -ApplicationId $servicePrincipalConnection.ApplicationId ` -CertificateThumbprint $servicePrincipalConnection.CertificateThumbprint > $null } catch { if (!$servicePrincipalConnection) { $ErrorMessage = "Connection $connectionName not found." throw $ErrorMessage } else { Write-Error -Message $_.Exception throw $_.Exception } } } function Remote-AzureRMVMExecute { Param( [Parameter(Mandatory = $true)] [String]$RemoteVMCredName, [Parameter(Mandatory = $true)] [String]$ResourceGroupName, [Parameter(Mandatory = $true)] [String]$RemoteScript, [Parameter(Mandatory = $true)] [String]$VMName ) [ScriptBlock]$sb = [ScriptBlock]::Create($RemoteScript) try { $IpAddress = Connect-AzureRMVMRemote -VMName $VMName -ResourceGroupName $ResourceGroupName if ($IpAddress -And $IpAddress -match "^(?:(?:0?0?\d|0?[1-9]\d|1\d\d|2[0-5][0-5]|2[0-4]\d)\.){3}(?:0?0?\d|0?[1-9]\d|1\d\d|2[0-5][0-5]|2[0-4]\d)`$") { Write-Output "The IP Address for VM: {$VMName} is $IpAddress. Attempting to remote into the VM.." $VMCredential = Get-AutomationPSCredential -Name $RemoteVMCredName $sessionOptions = New-PSSessionOption -SkipCACheck -SkipCNCheck Invoke-Command -ComputerName $IpAddress -Credential $VMCredential -UseSSL -SessionOption $sessionOptions -ScriptBlock $sb } else { Write-Output "Issue in obtaining IP Address for the VM {$VMName} in Resource Group {$ResourceGroupName}: $IpAddress" } } catch { Write-Output "Could not remote into the VM: {$VMName}..." Write-Output "Ensure that the VM is running and that the correct VM credentials are used to remote" Write-Output "Error in getting the VM Details: $($_.Exception.Message)" } } function Connect-AzureRMVMRemote { Param ( [parameter(Mandatory = $true)] [String]$ResourceGroupName, [parameter(Mandatory = $true)] [String]$VMName ) $ErrorActionPreference = "SilentlyContinue" # Get the VM we need to configure $VM = Get-AzureRmVM -ResourceGroupName $ResourceGroupName -Name $VMName #-------------------------------------------------------------------------------------------------------- $DNSName = $env:COMPUTERNAME $SourceAddressPrefix = "*" # Define a temporary configuration script file in the users TEMP directory $file = $env:TEMP + "\ConfigureWinRM_HTTPS.ps1" $string = "param(`$DNSName)" + "`r`n" + "Enable-PSRemoting -Force" + "`r`n" + "New-NetFirewallRule -Name 'WinRM HTTPS' -DisplayName 'WinRM HTTPS' -Enabled True -Profile 'Any' -Action 'Allow' -Direction 'Inbound' -LocalPort 5986 -Protocol 'TCP'" + "`r`n" + "`$thumbprint = (New-SelfSignedCertificate -DnsName `$DNSName -CertStoreLocation Cert:\LocalMachine\My).Thumbprint" + "`r`n" + "`$cmd = `"winrm create winrm/config/Listener?Address=*+Transport=HTTPS @{Hostname=`"`"`$DNSName`"`"; CertificateThumbprint=`"`"`$thumbprint`"`"}`"" + "`r`n" + "cmd.exe /C `$cmd" $string | Out-File -FilePath $file -force if ($VM) { # Add Azure CustomScript Extension to the VM in context, and download/run a custom PS configuration script located on a remote Uri location (on a Public Github GIST within the repository for this Runbook) Set-AzureRmVMCustomScriptExtension -ResourceGroupName $ResourceGroupName -VMName $VM.Name -Name "EnableWinRM_HTTPS" ` -Location $VM.Location -RunFile "ConfigureWinRM_HTTPS.ps1" -Argument $DNSName ` -FileUri "https://gist.githubusercontent.com/bahreex/526de42953a13ef0e3f3af093cff6a74/raw/b6e42d627d37cd39dc0e31e851a3c1b9230ebc0e/ConfigureWinRM_HTTPS.ps1" > $null # Get the name of the first NIC in the VM $nicName = Get-AzureRmResource -ResourceId $VM.NetworkProfile.NetworkInterfaces[0].Id # Get NIC object for the first NIC in the VM $nic = Get-AzureRmNetworkInterface -ResourceGroupName $ResourceGroupName -Name $nicName.ResourceName # Get the network security group attached to the NIC $nsgRes = Get-AzureRmResource -ResourceId $nic.NetworkSecurityGroup.Id $nsg = Get-AzureRmNetworkSecurityGroup -ResourceGroupName $ResourceGroupName -Name $nsgRes.Name # Get NSG Rule named "WinRM_HTTPS" in the NSG attached to the NIC $CheckNSGRule = $nsg | Get-AzureRmNetworkSecurityRuleConfig -Name "WinRM_HTTPS" -ErrorAction SilentlyContinue # Check if the NSG Rule named "WinRM_HTTPS" already exists or not. If it already exists, skip, else create new rule with same name if (!$CheckNSGRule) { # Add the new NSG rule, and update the NSG $nsg | Add-AzureRmNetworkSecurityRuleConfig -Name "WinRM_HTTPS" -Priority 1100 -Protocol TCP -Access Allow -SourceAddressPrefix $SourceAddressPrefix -SourcePortRange * -DestinationAddressPrefix * -DestinationPortRange 5986 -Direction Inbound -ErrorAction SilentlyContinue | Set-AzureRmNetworkSecurityGroup -ErrorAction SilentlyContinue > $null } $NICs = Get-AzureRmNetworkInterface | Where-Object {$_.VirtualMachine.Id -eq $VM.Id} $IPConfigArray = New-Object System.Collections.ArrayList foreach ($nic in $NICs) { if ($nic.IpConfigurations.LoadBalancerBackendAddressPools) { $arr = $nic.IpConfigurations.LoadBalancerBackendAddressPools.id.Split('/') $LoadBalancerNameIndex = $arr.IndexOf("loadBalancers") + 1 $loadBalancer = Get-AzureRmLoadBalancer | Where-Object {$_.Name -eq $arr[$LoadBalancerNameIndex]} $PublicIpId = $loadBalancer.FrontendIPConfigurations.PublicIpAddress.Id } $publicips = New-Object System.Collections.ArrayList if ($nic.IpConfigurations.PublicIpAddress.Id) { $publicips.Add($nic.IpConfigurations.PublicIpAddress.Id) | Out-Null } if ($PublicIpId) { $publicips.Add($PublicIpId) | Out-Null } foreach ($publicip in $publicips) { $name = $publicip.split('/')[$publicip.Split('/').Count - 1] $ResourceGroup = $publicip.Split('/')[$publicip.Split('/').Indexof("resourceGroups") + 1] $PublicIPAddress = Get-AzureRmPublicIpAddress -Name $name -ResourceGroupName $ResourceGroup | Select-Object -Property Name, ResourceGroupName, Location, PublicIpAllocationMethod, IpAddress $IPConfigArray.Add($PublicIPAddress) | Out-Null } } $Uri = $IPConfigArray | Where-Object {$_.IpAddress -ne $null} | Select-Object -First 1 -Property IpAddress if ($Uri.IpAddress -ne $null) { return $Uri.IpAddress.ToString() } else { Write-Output "Couldnt get the IP Address of the VM" return } } else { Write-Output "VM not found" return } } function Create-AzureAutomationCredentials { Param ( # Parameter help description [Parameter(Mandatory = $true)] [String]$AzureAutomationAccountName, # Parameter help description [Parameter(Mandatory = $true)] [String]$AzureAutomationResourceGroupName, # Parameter help description [Parameter(Mandatory = $true)] [String]$CredentialName, # Parameter help description [Parameter()] [String]$Suffix = "-AACredential", # Parameter help description [Parameter(Mandatory = $true)] [String]$UserName, # Parameter help description [Parameter(Mandatory = $true)] [String]$Password ) # Form standardized name of the Azure Automation PS Credential for the Input credential Info $CredName = $CredentialName + $Suffix # Get all the existing Azure Automation PS Credential for the Input credential Info $CredsCollection = Get-AzureRmAutomationCredential -ResourceGroupName $AzureAutomationResourceGroupName -AutomationAccountName $AzureAutomationAccountName -ErrorAction "SilentlyContinue" if ($CredsCollection) { # Iterate through all existing Automation PS Credential to check for presence of that for the Input credential Info foreach ($credItem in $CredsCollection) { if ($credItem.Name -eq $CredName) { $Cred = $credItem return 0 } } } # If Azure Automation PS Credential for the Input credential Info is null if (!$Cred) { try { # Get Secure version of the Input Password $SecurePassword = ConvertTo-SecureString $Password -AsPlainText -Force # Form PS Credential Object from the Input User Name and Password $Credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $UserName, $SecurePassword # Creat new Azure Automation PS Credential object for the Input credential Info New-AzureRmAutomationCredential -AutomationAccountName $AzureAutomationAccountName -Name $CredName -Value $Credential -ResourceGroupName $AzureAutomationResourceGroupName > $null } catch { Write-Error "Unable to create new Azure Automation Credential for VM {$CredentialName}. Exception: $($_.Exception)" 2> $null return 1 } } } function InitRemoting { Param ( [Parameter(Mandatory = $true)] [String]$AAName, [Parameter(Mandatory = $true)] [String]$AARGName, [Parameter(Mandatory = $false)] [String]$RGName, [Parameter(Mandatory = $false)] [String]$vmName, [Parameter(Mandatory = $true)] [String]$KVName ) $VMBaseName = $vmName $RGBaseName = $RGName $vmRef = Get-AzureRmVM -ResourceGroupName $RGBaseName -Name $VMBaseName # Get OS Type of the VM $OSX = $vmRef.StorageProfile.OsDisk.OsType.ToString() # Check if OS is Windows or Linux if ($OSX -And $OSX -eq "Linux") { Write-Output "The VM {$VMBaseName} in Resource Group {$RGBaseName} is on $OSX OS. Hence, cannot process further since only Windows OS Remoting supported. Skipping forward." continue } # Get current Status of the VM $vmstatus = Get-AzureRmVM -ResourceGroupName $RGBaseName -Name $VMBaseName -Status # Extract current Power State of the VM $VMState = $vmstatus.Statuses[1].Code.Split('/')[1] # Check if PowerState is deallocated/stopped, or in a transient state of deallocating/stopping if ($VMState -And ($VMState -in "deallocated","stopped","deallocating","stopping")) { Write-Output "The VM {$VMBaseName} in Resource Group {$RGBaseName} is currently either deallocated/stopped, or in a transient state. Hence, cannot get IP address, and skipping." continue } else { Write-Output "The VM {$VMBaseName} in Resource Group {$RGBaseName} is currently already Running. Proceeding forward." } # For the VM in context, extract the corresponding username/password from Azure KeyVault $secret = Get-AzureKeyVaultSecret -VaultName $KVName -Name $VMBaseName if ($secret) { # Call the script to check if the Azure Automation Credential for the VM in context alredy exists, and create a new one if absent $SetCredentials = Create-AzureAutomationCredentials -AzureAutomationAccountName $AAName ` -AzureAutomationResourceGroupName $AARGName ` -CredentialName $VMBaseName ` -UserName $vmRef.oSProfile.AdminUsername ` -Password $secret.SecretValueText if ($SetCredentials -eq 0) { # Form standardized name of the Azure Automation PS Credential for the VM in context $RemoteVMCredName = $VMBaseName + "-AACredential" # Call PS Script to Remote Into the VM in context Remote-AzureRMVMExecute -RemoteVMCredName $RemoteVMCredName ` -ResourceGroupName $RGBaseName ` -VMName $VMBaseName ` -RemoteScript $RemoteScript } else { Write-Output "Unable to get or set Azure Automation Credentials for VM {$VMBaseName}. Skipping forward..." continue } } else{ Write-Output "The VM {$VMBaseName} does not have its Credentail Password stored in KeyVault {$KVName}. Skipping..." continue } } # Check if both Resource Groups and VM Name params are not passed If (!$PSBoundParameters.ContainsKey('ResourceGroupName') -And !$PSBoundParameters.ContainsKey('VMName')) { # Get a list of all the VMs in the Azure Subscription $VMs = Get-AzureRmVm -ErrorAction SilentlyContinue # If there are one or more VMs in the Subscription if ($VMs) { # Iterate through all the VMs within the specific Resource Group for this Iteration foreach ($vm in $VMs) { # Call function to proceed further with remoting into the VM InitRemoting -AAName $AzureAutomationAccountName -AARGName $AzureAutomationResourceGroupName ` -RGName $vm.ResourceGroupName -vmName $vm.Name -KVName $KeyVaultName } } else { Write-Output "There are no Azure RM VMs in the Azure Subscription." } } # Check if only Resource Group param is passed, but not the VM Name param Elseif ($PSBoundParameters.ContainsKey('ResourceGroupName') -And !$PSBoundParameters.ContainsKey('VMName')) { # Iterate for one or more Resource Group Names passed in the parameter foreach ($rgName in $ResourceGroupName) { # Get reference to the Resource Group $RG = Get-AzureRmResourceGroup -Name $rgName -ErrorAction SilentlyContinue # If Resource Group exists if ($RG) { # Get a list of all the VMs in the specific Resource Group $VMs = Get-AzureRmVm -ResourceGroupName $rgName -ErrorAction SilentlyContinue if ($VMs) { # Iterate through all the VMs within the specific Resource Group for this Iteration foreach ($vm in $VMs) { # Call function to proceed further with remoting into the VM InitRemoting -AAName $AzureAutomationAccountName -AARGName $AzureAutomationResourceGroupName ` -RGName $vm.ResourceGroupName -vmName $vm.Name -KVName $KeyVaultName } } else { Write-Output "There are no Virtual Machines in the Resource Group {$rgName}. Aborting..." return } } else { Write-Output "There is no Resource Group {$rgName} in the Azure Subscription. Aborting..." return } } } # Check if both Resource Group and VM Name params are passed Elseif ($PSBoundParameters.ContainsKey('ResourceGroupName') -And $PSBoundParameters.ContainsKey('VMName')) { # Check if only One value is passed for the "Resource Group Name" parameter, or else quit if ($ResourceGroupName.Count -gt 1) { Write-Error "You cannot specify more than One Resource Group, when combined with the "VMName" Parameter." return } $RG = Get-AzureRmResourceGroup -Name $rgName -ErrorAction SilentlyContinue # Check if the Resource Group already exists, or else quit if (!$RG) { Write-Output "There is no Resource Group named {$ResourceGroupName} in the Azure Subscription. Aborting..." return } # Iterate for one or more VM Names passed in the parameter foreach ($vmx in $VMName) { # Get the specified VM in the specific Resource Group $vm = Get-AzureRmVm -ResourceGroupName $ResourceGroupName -Name $vmx -ErrorAction "SilentlyContinue" if ($vm) { # Call function to proceed further with remoting into the VM InitRemoting -AAName $AzureAutomationAccountName -AARGName $AzureAutomationResourceGroupName ` -RGName $ResourceGroupName -vmName $VMName -KVName $KeyVaultName } else { Write-Output "There is no Virtual Machine named {$VMName} in Resource Group {$ResourceGroupName}. Aborting..." return } } } # Check if Resource Group param is not passed, but VM Name param is passed Elseif (!$PSBoundParameters.ContainsKey('ResourceGroupName') -And $PSBoundParameters.ContainsKey('VMName')) { # Iterate for one or more VM Names passed in the parameter foreach ($vmx in $VMName) { # Find the specific VM resource $vmFind = Find-AzureRmResource -ResourceNameEquals $vmx # If the VM resource is found in the Subscription if ($vmFind) { # Extract the Resource Group Name of the VM $RGBaseName = $vmFind.ResourceGroupName # Call function to proceed further with remoting into the VM InitRemoting -AAName $AzureAutomationAccountName -AARGName $AzureAutomationResourceGroupName ` -RGName $RGBaseName -vmName $vmx -KVName $KeyVaultName } else { Write-Output "Could not find Virtual Machine {$vmx} in the Azure Subscription." continue } } } |