Executing Remote Powershell Commands

Dec 24, 2013 powershell

When you begin administering multiple computers, you'll eventually want to automate the tasks you repeat more than once. Instead of logging into each computer, starting powershell and running the same command, you can use one powershell console locally and send commands to multiple remote computers from that single local console.

In addition to running locally, Powershell can run remotely using WinRM (Windows Remote Management) which uses a management protocol called WS-Man. When your local computer talks to another via Powershell remoting (WinRM over WSMan), a remote session is created, the command is executed, then the remote session is normally terminated. Remote sessions can be opened at any time, and a remote session can be persisted if necessary - more about that later.

If you want to execute a command on another machine, you have a couple of options:

  • Start a new Powershell session to the remote machine, execute your code, then close the connection

  • Use Invoke-Command, which runs script in a temporary session

  • Use one of the few Powershell commands that does not run over WinRM and specify the -ComputerName argument

  • According to "Get-Help about_Remote", these commands do not need WinRM to execute:

  • Get-Counter

  • Clear-EventLog

  • Limit-EventLog

  • New-EventLog

  • Get-EventLog

  • Remove-EventLog

  • Get-HotFix

  • Restart-Computer

  • Get-Process

  • Show-EventLog

  • Get-Service

  • Stop-Computer

  • Get-WinEvent

  • Test-Connection

  • Get-WmiObject

  • Write-EventLog

Powershell Sessions

Powershell uses the concept of sessions within which code is executed and acts as the outer scope for variables.

To execute code on a remote machine, you must first start a remote session then enter that remote session. Sessions can be entered and created using Enter-PSSession, and exited using Exit-PSSession. See New-PSSession, Remote-PSSession and Get-PSSession and Get-Command pssession for further commands.

To enter a new session, run Enter-PSSession and specify the name of the remote machine. This immediately puts the current powershell console into the execution context of the remote machine:

PS K:\powershell> Enter-PSSession computername

Note how the [computername] shows we are running in the context of the remote machine.
To leave a session, use Exit-PSSession - "exit" would also work here.

[ltn25039]: PS C:\Users\AlexanderW\Documents> Exit-PSSession
 PS K:\powershell>

Note how we are now back to our local session - there is no [computername] at the last prompt.

PS Sessions can be created and entered as necessary, and can also be stored in variables, letting you switch between sessions too.

PS> $computerName = computername
PS> $session1 = New-PSSession $computerName
PS> $session2 = New-PSSession $computerName
PS> Get-PSSession | Format-List

ComputerName           : computername
ConfigurationName      : Microsoft.PowerShell
InstanceId             : 876b8836-2325-43e5-8ba9-9c1ca25764af
Id                     : 7
Name                   : Session7
Availability           : Available
ApplicationPrivateData : {PSVersionTable}
Runspace               : System.Management.Automation.RemoteRunspace
State                  : Opened
IdleTimeout            : 7200000
OutputBufferingMode    : Block

ComputerName           : computername
ConfigurationName      : Microsoft.PowerShell
InstanceId             : 792d71f8-b4f3-4488-9b35-b2952aa21e90
Id                     : 6
Name                   : Session6
Availability           : Available
ApplicationPrivateData : {PSVersionTable}
Runspace               : System.Management.Automation.RemoteRunspace
State                  : Opened
IdleTimeout            : 7200000
OutputBufferingMode    : Block

PS C:\Users\AlexanderW> (Get-PSSession).length

PS> Get-PSSession | Remove-PSSession

PS> (Get-PSSession).length

Powershell Remoting with Invoke-Command

Check out the documentation for Invoke-Command by using the Get-Help command. Use the -Online switch to open the related MSDN web page.

Get-Help Invoke-Command -Online

To get a list of logged on users locally, we'd use the following command to call WMI and query the list of logged on users. We then pipe this through a Select-Object filter and fetch the Name.

Get-WmiObject Win32_LoggedOnUser | Select-Object { "$($_.Antecedent)".Split(",")[1].Substring(5) }

We can then use the -ScriptBlock argument with Invoke-Command to get a execute the powershell command to get the list of logged on users for that remote machine.

Invoke-Command -ComputerName <computername> { Get-WmiObject Win32_LoggedOnUser | Select-Object { "$($_.Antecedent)".Split(",")[1].Substring(5) } }

Executing local functions and variables on the remote machine

Functions cannot be passed as references so you must redeclare them in the scope of the command you wish to execute. You can pass variables, and I'll show you how to do that below.

The following will not work because the -ScriptBlock is executing in a new context on the remote machine, and has no idea what the SayHello function is.

PS K:\powershell> function SayHello() { Write-Host "hello world" }
PS K:\powershell> Invoke-Command -ComputerName <computer name> -ScriptBlock { sayHello }

The term 'sayHello' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.

 + CategoryInfo : ObjectNotFound: (sayHello:String) [], CommandNot
 + FullyQualifiedErrorId : CommandNotFoundException
 + PSComputerName : ltn25039

To fix this, we re-declare the function inside the ScriptBlock and then we can call it.

PS K:\powershell> Invoke-Command -ComputerName <computername> -ScriptBlock { function SayHello() { Write-Host "hello world" }; sayHello; }
hello world

To prove that was working in the context of the remote machine, we'll add the hostname to the output:

PS K:\powershell> Invoke-Command -ComputerName <computername> -ScriptBlock { function SayHello() { Write-Host "hello world from" (hostname) }; sayHello; }
hello world from <computername>

Variables declared locally can be passed through to the ScriptBlock via the $Using: accessor.

PS K:\powershell> $exampleVariable = "hello world";
PS K:\powershell> Invoke-Command -ComputerName <computername> -ScriptBlock { Write-Host $Using:exampleVariable; }
hello world