Public/Invoke-ControlCommand.ps1
function Invoke-ControlCommand { <# .SYNOPSIS Will issue a command against a given machine and return the results. .DESCRIPTION Will issue a command against a given machine and return the results. .PARAMETER SessionID The GUID identifier for the machine you wish to connect to. You can retrieve session info with the 'Get-ControlSessions' commandlet SessionIDs can be provided via the pipeline. IE - Get-AutomateComputer -ComputerID 5 | Get-ControlSessions | Invoke-ControlCommand -Powershell -Command "Get-Service" .PARAMETER Command The command you wish to issue to the machine. .PARAMETER CommandID The command ID (Control SessionEventType) to issue to the machine. When using -CommandID the command will be queued but results will not be checked for or returned. The "Wait" OfflineAction is treated like "Queue". For CommandID values see https://docs.connectwise.com/ConnectWise_Control_Documentation/Developers/Session_Manager_API_Reference/Enumerations .PARAMETER CommandBody The command body. Used with the -CommandID parameter. .PARAMETER MaxLength The maximum number of bytes to return from the remote session. The default is 5000 bytes. .PARAMETER PowerShell Issues the command in a powershell session. .PARAMETER TimeOut The amount of time in milliseconds that a command can execute. The default is 10000 milliseconds. .PARAMETER BatchSize Number of control sessions to invoke commands in parallel. .PARAMETER ResultPropertyName String containing the name of the member you would like to add to the input pipeline object that will hold the result of this command .PARAMETER OfflineAction Specifies the action to take if the session is offline. - Wait : Will queue the command and wait up to the timeout specified for a response. - Queue : Will queue the command but not wait for any response. - Skip : Will not queue the command to the session. .OUTPUTS The output of the Command provided. When -CommandID is used, the output will only indicate if the commandid was queued or not. .NOTES Version: 2.3.1 Author: Chris Taylor Modified By: Gavin Stone Modified By: Darren White Creation Date: 2016-01-20 Purpose/Change: Initial script development Update Date: 2019-02-19 Author: Darren White Purpose/Change: Enable Pipeline support. Enable processing using Automate Control Extension. The cached APIKey will be used if present. Update Date: 2019-02-23 Author: Darren White Purpose/Change: Enable command batching against multiple sessions. Added OfflineAction parameter. Update Date: 2019-06-24 Author: Darren White Purpose/Change: Updates to process object returned by Get-ControlSessions Update Date: 2019-08-20 Author: Darren Kattan Purpose/Change: Added ability to retain Computer object passed in from pipeline and append result of script to a named member of the computer object Update Date: 2020-07-04 Author: Darren White Purpose/Change: Removed object processing on the remote host. Added -CommandID support Update Date: 2020-08-01 Author: Darren White Purpose/Change: Use Invoke-ControlAPIMaster .EXAMPLE Invoke-ControlCommand -SessionID $SessionID -Command 'hostname' Will return the hostname of the machine. .EXAMPLE Invoke-ControlCommand -SessionID $SessionID -TimeOut 120000 -Command 'iwr -UseBasicParsing "https://bit.ly/ltposh" | iex; Restart-LTService' -PowerShell Will restart the Automate agent on the target machine. .EXAMPLE Invoke-ControlCommand -SessionID $SessionID -CommandID 40 Will tell the control service to Reinstall (update) #> [CmdletBinding(DefaultParameterSetName = 'ExecuteCommand',SupportsShouldProcess=$True)] param ( [Parameter(Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] [guid[]]$SessionID, [Parameter(ParameterSetName = ('ExecuteCommand','PassthroughObjects'), Mandatory = $True)] [string]$Command, [Parameter(ParameterSetName = 'CommandID', Mandatory = $True)] [int]$CommandID, [Parameter(ParameterSetName = 'CommandID')] $CommandBody='', [Parameter(ParameterSetName = ('ExecuteCommand','PassthroughObjects'))] [int]$TimeOut = 10000, [Parameter(ParameterSetName = ('ExecuteCommand','PassthroughObjects'))] [int]$MaxLength = 5000, [Parameter(ParameterSetName = ('ExecuteCommand','PassthroughObjects'))] [switch]$PowerShell, [Parameter(ParameterSetName = ('ExecuteCommand','PassthroughObjects'))] [ValidateSet('Wait', 'Queue', 'Skip')] $OfflineAction = 'Wait', [ValidateRange(1, 100)] [int]$BatchSize = 20, [switch]$PassthroughObjects, [string]$ResultPropertyName = 'Output' ) Begin { $ProgressPreference='SilentlyContinue' If (('SessionID','IsSuccess','__CommandTimeout') -contains $ResultPropertyName) {Throw "ResultPropertyName value $($ResultPropertyName) is reserved."} If ($PSCmdlet.ParameterSetName -eq 'CommandID') { $SessionEventType = $CommandID } Else { $cmdMarker="AUTOMATEAPICOMMAND:"+[GUID]::NewGuid().ToString().ToUpper().Replace('-','') # Format command $FormattedCommand = @() $FormattedCommand += "#timeout=$TimeOut" $FormattedCommand += "#maxlength=$MaxLength" If ($Powershell) { $FormattedCommand = @('#!ps',$FormattedCommand,"`$ProgressPreference='SilentlyContinue'") } $FormattedCommand += @("ECHO OFF","ECHO $cmdMarker") $FormattedCommand += $Command $CommandBody = ($FormattedCommand |Out-String -Stream) -join "`n" $SessionEventType = 44 } If (${Script:ControlAPIKey}) { If (${Script:CWACredentials}.UserName) { $User = ${Script:CWACredentials}.UserName } Else { $User = 'AutomateAPI' } } ElseIf (${Script:ControlAPICredentials}.UserName) { $User = ${Script:ControlAPICredentials}.UserName } Else { $User = '' } $ResultObjects = @{ } $SessionCollection = {}.Invoke() } Process { $ObjectsIn=$_ If ($PassthroughObjects) { Foreach ($xObject in $ObjectsIn) { If ($xObject -and $xObject.SessionID) { [string]$Session=$xObject.SessionID If (!($ResultObjects.ContainsKey($Session))) { $ResultObjects.Add($Session, [pscustomobject]@{SessionID = $Session }) } Else {Write-Warning "SesssionID $Session has already been added. Skipping"} $Null = $SessionCollection.Add($xObject) } Else {Write-Warning "Input Object is missing SesssionID property"} } } Else { Foreach ($Session in $SessionID) { If ($Session.SessionID) {$Session=$Session.SessionID} [string]$Session="$($Session)" [pscustomobject]@{SessionID = $Session} | ForEach-Object { If (!($ResultObjects.ContainsKey($Session))) { $ResultObjects.Add($Session, $_) } Else {Write-Warning "SesssionID $Session has already been added. Skipping"} $Null = $SessionCollection.Add($_) } } } } End { Function New-ReturnObject { param([object]$InputObject, [object]$Result, [bool]$IsSuccess, [string]$PropertyName) $InputObject | Add-Member -NotePropertyName $PropertyName -NotePropertyValue $Result -Force $InputObject | Add-Member -NotePropertyName 'IsSuccess' -NotePropertyValue $IsSuccess -Force $InputObject } $ProcessSessions=@($ResultObjects.Keys) $RemainingSessions={}.Invoke() $AddSessions={}.Invoke() $EventDateFormatted=$Null $SessionIndex=0 Do { While (($AddSessions.Count+$RemainingSessions.Count) -lt $BatchSize -and $SessionIndex -lt $ProcessSessions.Count) { $AddSessions.Add($ProcessSessions[$SessionIndex]) $SessionIndex++ } If ($AddSessions.Count -gt 0 -and ($OfflineAction -eq 'Skip' -or $SessionEventType -eq 44)) { #Need to check session details before queueing command $AddingSessions=@($AddSessions.GetEnumerator()) $ControlSessions = @{ }; Get-ControlSession -SessionID $AddingSessions | ForEach-Object { $ControlSessions.Add($_.SessionID, $($_ | Select-Object -Property OnlineStatusControl,LastConnected)) } ForEach ($AddGUID in $AddingSessions) { Write-Debug "Checking if session $($AddGUID) is connected: ($($ControlSessions[$AddGUID].OnlineStatusControl))" #Check Online Status. If ($OfflineAction -eq 'Skip' -and !($ControlSessions[$AddGUID].OnlineStatusControl -eq $True)) { #Weed out sessions that have never connected or are not valid. If ($PSCmdlet.ShouldProcess("Disconnected Session $($AddGUID)","Skipping")) { $ResultObjects[$AddGUID] = New-ReturnObject -InputObject $ResultObjects[$AddGUID] -Result 'Skipped. Session was not connected.' -PropertyName $ResultPropertyName -IsSuccess $false } $Null = $AddSessions.Remove($AddGUID) } } } If ($AddSessions.Count -gt 0) { $Body = ConvertTo-Json @($User, $AddSessions, $SessionEventType, $CommandBody) -Compress $RESTRequest = @{ 'URI' = "ReplicaService.ashx/PageAddEventToSessions" 'Body' = $Body } $CWCServerTime=$Null If ($PSCmdlet.ShouldProcess("Session(s) $($Addsessions -join ',')","Queue command id $($SessionEventType)")) { $Null = Invoke-ControlAPIMaster -Arguments $RESTRequest If (!$CWCServerTime) { Write-Error $Error[0] return } $RequestTimer = [diagnostics.stopwatch]::StartNew() If (!($EventDateFormatted)) { $EventDateFormatted = (Get-Date $CWCServerTime.ToUniversalTime() -UFormat "%Y-%m-%d %T") } $TimeOutDateTime = $CWCServerTime.AddMilliseconds($TimeOut+3000) Foreach ($SessionsGUID in $AddSessions) { If ($PSCmdlet.ParameterSetName -ne 'CommandID') { $ResultObjects[$SessionsGUID] = New-ReturnObject -InputObject $ResultObjects[$SessionsGUID] -Result $TimeOutDateTime -PropertyName '__CommandTimeout' -IsSuccess $false $Null = $RemainingSessions.Add($SessionsGUID) } Else { $ResultObjects[$SessionsGUID] = New-ReturnObject -InputObject $ResultObjects[$SessionsGUID] -Result 'Command was queued for the session' -PropertyName $ResultPropertyName -IsSuccess $true } } } $AddSessions.Clear() } If ($RemainingSessions.Count -gt 0) { Start-Sleep -Seconds $(Get-SleepDelay -Seconds $([int]($RequestTimer.Elapsed.TotalSeconds)) -TotalSeconds $([int]($TimeOut / 1000))) #Build GUID Conditional $GuidCondition = $(ForEach ($SessionsGUID in $RemainingSessions) { "sessionid='$SessionsGUID'" }) -join ' OR ' # Look for results of command $Body = ConvertTo-Json @("SessionConnectionEvent", @(), @("SessionID", "Time", "Data"), "($GuidCondition) AND EventType='RanCommand' AND Time>='$EventDateFormatted'", "", 200) -Compress $RESTRequest = @{ 'URI' = "ReportService.ashx/GenerateReportForAutomate" 'Body' = $Body } $CWCServerTime=$Null $Events = Invoke-ControlAPIMaster -Arguments $RESTRequest If (!$CWCServerTime) { Write-Error $Error[0] return } $EventDateFormatted = (Get-Date $CWCServerTime.ToUniversalTime() -UFormat "%Y-%m-%d %T") Foreach ($Event in $Events) { [string]$EventGUID = "$($Event.SessionID)" If ($RemainingSessions.Contains($EventGUID)) { $Output = $Event.Data.Trim() If ($Output -match "$cmdMarker") { $Output = $Output -replace "(?s)^.*?(?<!ECHO )$cmdMarker((?=$)|\s*(?<=[\n]))", '' $ResultObjects[$EventGUID] = New-ReturnObject -InputObject $ResultObjects[$EventGUID] -Result $Output -PropertyName $ResultPropertyName -IsSuccess $true $Null = $RemainingSessions.Remove($EventGUID) } } } $WaitingSessions=@($RemainingSessions.GetEnumerator()) Foreach ($WaitingGUID in $WaitingSessions) { If ($CWCServerTime -gt $ResultObjects[$WaitingGUID].__CommandTimeout) { Write-Debug "Expiring Session $($WaitingGUID)" If ($OfflineAction -eq 'Queue') { $ResultObjects[$WaitingGUID] = New-ReturnObject -InputObject $ResultObjects[$WaitingGUID] -Result 'Command was queued for the session' -PropertyName $ResultPropertyName -IsSuccess $false } Else { $ResultObjects[$WaitingGUID] = New-ReturnObject -InputObject $ResultObjects[$WaitingGUID] -Result 'Command timed out for the session' -PropertyName $ResultPropertyName -IsSuccess $false } $Null = $RemainingSessions.Remove($WaitingGUID) } } } } Until ($SessionIndex -eq $ProcessSessions.Count -and $RemainingSessions.Count -eq 0) If ($SessionCollection.Count -eq 1 -and !($PassthroughObjects)) { $ResultObjects.Values | Select-Object -ExpandProperty $ResultPropertyName -ErrorAction SilentlyContinue } ElseIf (!($PassthroughObjects)) { $SessionCollection | Select-Object -Property @{n=$ResultPropertyName;e={If ($_.SessionID) {[string]$SessionID=$_.SessionID} Else {[string]$SessionID=$_}; Write-Debug "Inserting results for sessionID $($SessionID)"; If ($ResultObjects.ContainsKey($SessionID)) {$ResultObjects[$SessionID]|Select-Object -Property * -ExcludeProperty __CommandTimeout} Else {"Results for SessionID $($SessionID) were not found"} }} | Select-Object -ExpandProperty "$ResultPropertyName" } Else { $SessionCollection | Select-Object -ExcludeProperty $ResultPropertyName -Property *,@{n=$ResultPropertyName;e={If ($_.SessionID) {[string]$SessionID=$_.SessionID} Else {[string]$SessionID=$_}; Write-Debug "Inserting results for sessionID $($SessionID)"; If ($ResultObjects.ContainsKey($SessionID)) {$ResultObjects[$SessionID]|Select-Object -Property * -ExcludeProperty __CommandTimeout} Else {"Results for SessionID $($SessionID) were not found"} }} } } } |