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 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. .OUTPUTS The output of the Command provided. .NOTES Version: 2.2 Author: Chris Taylor Modified By: Gavin Stone Modified By: Darren White Creation Date: 1/20/2016 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 .EXAMPLE Get-AutomateComputer -ComputerID 5 | Get-AutomateControlInfo | Invoke-ControlCommand -Powershell -Command "Get-Service" Will retrieve Computer Information from Automate, Get ControlSession data and merge with the input object, then call Get-Service on the computer. .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. #> [CmdletBinding()] param ( [Parameter(Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] [guid[]]$SessionID, [string]$Command, [int]$TimeOut = 10000, [int]$MaxLength = 5000, [switch]$PowerShell, [ValidateSet('Wait', 'Queue', 'Skip')] $OfflineAction = 'Wait', [ValidateRange(1, 100)] [int]$BatchSize = 20 ) Begin { $Server = $Script:ControlServer -replace '/$', '' # Format command $FormattedCommand = @() if ($Powershell) { $FormattedCommand += '#!ps' } $FormattedCommand += "#timeout=$TimeOut" $FormattedCommand += "#maxlength=$MaxLength" $FormattedCommand += $Command $FormattedCommand = $FormattedCommand | Out-String $SessionEventType = 44 If ($Script:ControlAPIKey) { $User = 'AutomateAPI' } ElseIf ($Script:ControlAPICredentials.UserName) { $User = $Script:ControlAPICredentials.UserName } Else { $User = '' } $SessionIDCollection = @() $ResultSet = @() } Process { If (!($Server -match 'https?://[a-z0-9][a-z0-9\.\-]*(:[1-9][0-9]*)?(\/[a-z0-9\.\-\/]*)?$')) {throw "Control Server address ($Server) is in an invalid format. Use Connect-ControlAPI to assign the server URL."; return} If ($SessionID) { $SessionIDCollection += $SessionID } } End { $SplitGUIDsArray = Split-Every -list $SessionIDCollection -count $BatchSize ForEach ($GUIDs in $SplitGUIDsArray) { If (!$GUIDs) {Continue} #Skip if Null value $RemainingGUIDs = {$GUIDs}.Invoke() If ($OfflineAction -ne 'Wait') { #Check Online Status. Weed out sessions that have never connected or are not valid. $ControlSessions=@{}; Get-ControlSessions -SessionID $RemainingGUIDs | ForEach-Object {$ControlSessions.Add($_.SessionID, $($_|Select-Object -Property OnlineStatusControl,LastConnected))} If ($OfflineAction -eq 'Skip') { ForEach ($GUID in $ControlSessions.Keys) { If (!($ControlSessions[$GUID].OnlineStatusControl -eq $True)) { $ResultSet += [pscustomobject]@{ 'SessionID' = $GUID 'Output' = 'Skipped. Session was not connected.' } $Null = $RemainingGUIDs.Remove($GUID) } } } } If (!$RemainingGUIDs) { Continue; #Nothing to process } $xGUIDS=@(ForEach ($x in $RemainingGUIDs) {$x}) $Body = ConvertTo-Json @($User, $xGUIDS, $SessionEventType, $FormattedCommand) -Compress $RESTRequest = @{ 'URI' = "$Server/App_Extensions/fc234f0e-2e8e-4a1f-b977-ba41b14031f7/ReplicaService.ashx/PageAddEventToSessions" 'Method' = 'POST' 'ContentType' = 'application/json' 'Body' = $Body } If ($Script:ControlAPIKey) { $RESTRequest.Add('Headers', @{'CWAIKToken' = (Get-CWAIKToken)}) } Else { $RESTRequest.Add('Credential', $Script:ControlAPICredentials) } # Issue command Try { $Results = Invoke-WebRequest @RESTRequest } Catch { Write-Error "$(($_.ErrorDetails | ConvertFrom-Json).message)" return } $RequestTimer = [diagnostics.stopwatch]::StartNew() $EventDate = Get-Date $($Results.Headers.Date) $EventDateFormatted = (Get-Date $EventDate.ToUniversalTime() -UFormat "%Y-%m-%d %T") $Looking = $True $TimeOutDateTime = (Get-Date).AddMilliseconds($TimeOut) while ($Looking) { Start-Sleep -Seconds $(Get-SleepDelay -Seconds $([int]($RequestTimer.Elapsed.TotalSeconds)) -TotalSeconds $([int]($TimeOut / 1000))) #Build GUID Conditional $GuidCondition = $(ForEach ($GUID in $RemainingGUIDs) {"sessionid='$GUID'"}) -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' = "$Server/App_Extensions/fc234f0e-2e8e-4a1f-b977-ba41b14031f7/ReportService.ashx/GenerateReportForAutomate" 'Method' = 'POST' 'ContentType' = 'application/json' 'Body' = $Body } If ($Script:ControlAPIKey) { $RESTRequest.Add('Headers', @{'CWAIKToken' = (Get-CWAIKToken)}) } Else { $RESTRequest.Add('Credential', $Script:ControlAPICredentials) } Try { $SessionEvents = Invoke-RestMethod @RESTRequest } Catch { Write-Error $($_.Exception.Message) } $FNames = $SessionEvents.FieldNames $Events = ($SessionEvents.Items | ForEach-Object {$x = $_; $SCEventRecord = [pscustomobject]@{}; for ($i = 0; $i -lt $FNames.Length; $i++) {$Null = $SCEventRecord | Add-Member -NotePropertyName $FNames[$i] -NotePropertyValue $x[$i]}; $SCEventRecord} | Sort-Object -Property Time,SessionID -Descending) foreach ($Event in $Events) { if ($Event.Time -ge $EventDate.ToUniversalTime() -and $RemainingGUIDs.Contains($Event.SessionID)) { $Output = $Event.Data if (!$PowerShell) { $Output = $Output -replace '^[\r\n]*','' } $ResultSet += [pscustomobject]@{ 'SessionID' = $Event.SessionID 'Output' = $Output } $Null = $RemainingGUIDs.Remove($Event.SessionID) } } $WaitingForGUIDs = $RemainingGUIDs If ($OfflineAction -eq 'Queue') { $WaitingForGUIDs=$( ForEach ($GUID in $WaitingForGUIDs) { Write-Debug "Checking if GUID $GUID is online: $($ControlSessions[$GUID.ToString()].OnlineStatusControl)" If ($ControlSessions[$GUID.ToString()].OnlineStatusControl -eq $True) {$GUID} } ) } Write-Debug "$($WaitingForGUIDs.Count) sessions remaining after $($RequestTimer.Elapsed.TotalSeconds) seconds." If (!($WaitingForGUIDs.Count -gt 0)) { $Looking = $False If ($RemainingGUIDs) { ForEach ($GUID in $RemainingGUIDs) { $ResultSet += [pscustomobject]@{ 'SessionID' = $GUID 'Output' = 'Command was queued for the session.' } } return $Output -Join "" } } if ($Looking -and $(Get-Date) -gt $TimeOutDateTime.AddSeconds(1)) { $Looking = $False ForEach ($GUID in $RemainingGUIDs) { If ($OfflineAction -ne 'Wait' -and $ControlSessions[$GUID.ToString()].OnlineStatusControl -eq $False) { $ResultSet += [pscustomobject]@{ 'SessionID' = $GUID 'Output' = 'Command was queued for the session' } } Else { $ResultSet += [pscustomobject]@{ 'SessionID' = $GUID 'Output' = 'Command timed out when sent to Agent' } } } } } } If ($ResultSet.Count -eq 1) { Return $ResultSet | Select-Object -ExpandProperty Output -ErrorAction 0 } Else { Return $ResultSet } } } |