lib/SubjectTasks.ps1
function Invoke-SubjectTaskActionParallel { <# .SYNOPSIS Provides the Actionrequest to the TMConsole PowerShell Session Manager for invocation. .DESCRIPTION This function will collect the ActionRequest object from TransitionManager, and will send it to SessionManager for invocation. .PARAMETER TMSession The name of the TMSession that the Broker and Subject belong to .PARAMETER Subject The TMBrokerSubject object representing a Broker's Subject Task .PARAMETER Cache The Broker's cache if available .EXAMPLE Invoke-SubjectTaskActionParallel -TMSession 'Broker' -Subject $Broker.Subjects[0][1] -Cache $Broker.Cache .OUTPUTS None #> [CmdletBinding()] param ( [Parameter(Mandatory = $false, Position = 0)] [Object]$TMSession = 'Default', [Parameter(Mandatory = $true)] [TMBrokerSubject]$Subject, [Parameter(Mandatory = $false)] [Object]$Cache ) if ($TMSession -is [String]) { $TMSession = $global:TMSessions[$TMSession] if (-not $TMSession) { throw "TMSession '$TMSession' not found. Check name or provide a [TMSession] object." } } if (-not ($TMSession -is [TMSession])) { throw "The value for the TMSession parameter must be of type [String] or [TMSession]" } # Tell TM that the Task's Action is starting and receive an ActionRequest with Params $Subject.ActionRequest = Start-TMTaskAction -TMSession $TMSession.Name -TaskId $Subject.Task.Id -Force -ErrorAction 'SilentlyContinue' ## An Error invoking the task may have been handled, and no SubjectActionRequest was returned. if (-not $Subject.ActionRequest) { $Subject.Action.ExecutionStatus = 'Started' return } ## Add the TM User Session to the Subject Action Request so that the TM session and $TM variable are available during script execution $TMUserSession = $ActionRequest.TMUserSession ?? @{ tmVersion = $TMSession.TMVersion.ToString() userContext = $TMSession.UserContext jsessionid = $TMSession.Authentication.JSessionId csrf = @{ token = $TMSession.Authentication.CsrfToken tokenHeaderName = $TMSession.Authentication.CsrfHeaderName } } $Subject.ActionRequest | Add-Member -NotePropertyName 'TMUserSession' -NotePropertyValue $TMUserSession -Force ## Add the Broker ID to the ActionRequest so its output can be interpered by TMConsole $Subject.ActionRequest | Add-Member -NotePropertyName 'BrokerId' -NotePropertyValue "TMTaskID_$($Broker.Task.Id)" -Force # Check the localsettings file to see if this action needs to be logged try { $LocalSettingsFilePath = Join-Path $Env:APPDATA 'tmconsole' 'localsettings' $LocalSettings = Get-Content -LiteralPath $LocalSettingsFilePath -Raw | ConvertFrom-Json if ($LocalSettings.logging.enabled) { $Subject.ActionRequest | Add-Member -NotePropertyName 'logPath' -NotePropertyValue $LocalSettings.logging.path } } catch { Write-Host "Could not read local settings: $($_.Exception.Message)" -ForegroundColor DarkYellow } ## Notify invocation of the next task Write-Host "[$(Get-Date -Format "HH:mm:ss.ffff")] Queueing Parallel Task: " -NoNewline Write-Host "#$($Subject.Task.TaskNumber)" -NoNewline -ForegroundColor Cyan Write-Host ', Title: ' -NoNewline Write-Host $Subject.Task.Title -ForegroundColor Yellow ## Create a TMC message to be forwarded to SessionManager by the Write-Host handler $Subject.ActionRequest | Add-Member -NotePropertyName 'Type' -NotePropertyValue 'ActionRequest' -Force $QueueActionRequestString = "||TMC:$($Subject.ActionRequest | ConvertTo-Json -Depth 10 -Compress)" # Record the time that this Action was invoked $Subject.Action.InvokedAt = Get-Date ## The following command will execute 'Normally' in debug mode, but when run in a TMC runspace, it has a special handling, ## and will result in the ActionRequest object being sent for invocation Write-Host $QueueActionRequestString } function Invoke-SubjectTaskAction { <# .SYNOPSIS Invokes the Action associated with a Broker's Subject Task .DESCRIPTION This function will invoke the PowerShell Action associated with a Broker's Subject Task .PARAMETER TMSession The name of the TMSession that the Broker and Subject belong to .PARAMETER Subject The TMBrokerSubject object representing a Broker's Subject Task .PARAMETER Cache The Broker's cache if available .EXAMPLE Invoke-SubjectTaskAction -TMSession 'Broker' -Subject $Broker.Subjects[0][1] -Cache $Broker.Cache .OUTPUTS None #> [CmdletBinding()] param ( [Parameter(Mandatory = $false, Position = 0)] [Object]$TMSession = 'Default', [Parameter(Mandatory = $true)] [TMBrokerSubject]$Subject, [Parameter(Mandatory = $false)] [Object]$Cache ) if ($TMSession -is [String]) { $TMSession = $global:TMSessions[$TMSession] if (-not $TMSession) { throw "TMSession '$TMSession' not found. Check name or provide a [TMSession] object." } } if (-not ($TMSession -is [TMSession])) { throw "The value for the TMSession parameter must be of type [String] or [TMSession]" } # Tell TM that the Task's Action is starting and receive an ActionRequest with Params $Subject.ActionRequest = Start-TMTaskAction -TMSession $TMSession.Name -TaskId $Subject.Task.Id -Force -ErrorAction 'SilentlyContinue' ## An Error ivoking the task may have been handled, and no SubjectActionRequest was returned. if (-not $Subject.ActionRequest) { $Subject.Action.ExecutionStatus = 'Started' return } ## Add the Broker ID to the ActionRequest so its output can be interpered by TMConsole $Subject.ActionRequest | Add-Member -NotePropertyName 'BrokerId' -NotePropertyValue "TMTaskID_$($Broker.Task.Id)" -Force ## ## Invoke the Action in this runspace ## $InvocationRunspaceScriptBlock = [scriptblock] { param([PSCustomObject]$ActionRequest, [TMSession]$TMSession) try { ## Complete the root activity to provide a consistent experience for all tasks $StartingProgressActivity = @{ Id = 0 ParentId = -1 Activity = 'Broker is Running Task: ' + $ActionRequest.task.taskNumber + ' - ' + $ActionRequest.task.title PercentComplete = 5 SecondsRemaining = -1 Completed = $False } Write-Progress @StartingProgressActivity ## Preserve the Broker's Parameters $BrokerParams = $Params $BrokerTM = $TM ## Import the ActionRequest to create necessary objects in the pipeline . Import-TMCSubjectActionRequest -ActionRequest $ActionRequest -BrokerTaskId 1 -TMSession $TMSession # Start logging if required if ($ActionRequest.logPath) { ## Trim any quote characters from the Log Path $RootLogPath = $ActionRequest.logPath.trim('"').trim("'") Write-Verbose "Creating Logging Folder at: $RootLogPath" Test-FolderPath -FolderPath $RootLogPath ## Create the Transcript Folder Path $ProjectFolder = Join-Path $RootLogPath ($Global:TM.Server.Url -replace '.transitionmanager.net', '') $Global:TM.Project.Name $Global:TM.Event.Name Test-FolderPath -FolderPath $ProjectFolder Write-Verbose "Transcript Folder: $($ProjectFolder)" ## Create a unique file in the Transcript Folder $TranscriptFileName = ( (Get-Date -Format FileDateTimeUniversal) + '_TaskNumber-' + $Global:TM.Task.TaskNumber + '_TaskId-' + $Global:TM.Task.Id + '.txt' ) Write-Verbose "File Name: $TranscriptFileName" $TranscriptFilePath = Join-Path $ProjectFolder $TranscriptFileName ## Start a transcript for this session $TranscriptSplat = @{ Path = $TranscriptFilePath IncludeInvocationHeader = $True Confirm = $False Force = $True Append = $True } Start-Transcript @TranscriptSplat ## Write a verbose message for transcript Write-Verbose "Invoking TransitionManager ActionRequest at: $(Get-Date)" Write-Verbose ($Global:TM | ConvertTo-Json -Depth 100) ## Write a verbose message for transcript Write-Verbose 'Action Request Parameters' Write-Verbose ($Params | ConvertTo-Json -Depth 100) } ## Invoke the Action's Script block $ActionScriptBlock = [scriptblock]::Create($ActionRequest.options.apiAction.script) Invoke-Command -ScriptBlock $ActionScriptBlock -ErrorAction 'Stop' -NoNewScope ## Create a Data Options parameter for the Complete-TMTask command $CompleteTaskParameters = @{} ## Check the Global Variable for any TMAssetUpdates to send to TransitionManager during the task completion if ($Global:TMAssetUpdates) { $CompleteTaskParameters = @{ Data = @{ assetUpdates = $Global:TMAssetUpdates } } ## Clear the Global variable now that it's assigned into the respsonse belonging ## To the subject task. Otherwise, the next subject gets the same data Remove-Variable -Name 'TMAssetUpdates' -Scope Global } ## Add SSL Exception $CompleteTaskParameters | Add-Member -NotePropertyName 'AllowInsecureSSL' -NotePropertyValue $TMSession.AllowInsecureSSL -Force ## Complete the TM Task, sending Updated Data values for the task Asset if ($ActionRequest.HostPID -ne 0) { Complete-TMTask -ActionRequest $ActionRequest @CompleteTaskParameters } ## Complete the root activity to provide a consistent experience for all tasks $CompleteProgressActivity = @{ Id = 0 ParentId = -1 Activity = 'Task Complete: ' + $ActionRequest.task.taskNumber + ' - ' + $ActionRequest.task.title PercentComplete = 100 SecondsRemaining = -1 Completed = $True } Write-Progress @CompleteProgressActivity $Subject.Action.ExecutionStatus = 'Successful' } catch { ## Get the Exception message $ExceptionMessage = $_.Exception.Message ## Send the Error Message (Only send if TMD started the process) if ($ActionRequest.HostPID -ne 0) { Set-TMTaskOnHold -ActionRequest $ActionRequest -Message ('Action Error: ' + $ExceptionMessage) } ## Throw the full error message Write-Host $_.Exception.Message -ForegroundColor Red $Subject.Action.ExecutionStatus = 'Failed' $Subject.Action.Errors = @($ExceptionMessage) } if ($ActionRequest.logPath) { Stop-Transcript } ## Return the Broker Params and TM scope New-Variable -Name Params -Scope Global -Value $BrokerParams -Force New-Variable -Name TM -Scope Global -Value $BrokerTM -Force } ## Notify invocation of the next task Write-Host "[$(Get-Date -Format "HH:mm:ss.ffff")] Invoking Task: " -NoNewline Write-Host "#$($Subject.Task.TaskNumber)" -NoNewline -ForegroundColor Cyan Write-Host ', Title: ' -NoNewline Write-Host $Subject.Task.Title -ForegroundColor Yellow ## Run the Script Block $RunningTime = Measure-Command { $InvokeSplat = @{ ScriptBlock = $InvocationRunspaceScriptBlock ArgumentList = @($Subject.ActionRequest, $TMSession) NoNewScope = $true } $Subject.Action.InvokedAt = Get-Date Invoke-Command @InvokeSplat } $RuntimeSeconds = [Math]::Ceiling($RunningTime.TotalSeconds) ## Produce Broker output to finish the line of output if ($Subject.Action.ExecutionStatus -eq 'Failed') { Write-Host "[$(Get-Date -Format "HH:mm:ss.ffff")] Action completed with errors after running for " -NoNewline -ForegroundColor Red Write-Host $RuntimeSeconds -NoNewline -ForegroundColor Red Write-Host " seconds. Error: $($Subject.Action.Errors -join ', ')" -ForegroundColor Red } else { Write-Host "[$(Get-Date -Format "HH:mm:ss.ffff")] Action completed successfully in: " -NoNewline Write-Host $RuntimeSeconds -NoNewline -ForegroundColor Cyan Write-Host ' seconds' } } function Import-TMCSubjectActionRequest { param( [Parameter(Mandatory = $true)][PSObject]$ActionRequest, [Parameter(Mandatory = $true)][Int64]$BrokerTaskId, [Parameter(Mandatory = $true)][TMSession]$TMSession ) ## Remove any leftover 'get_' parameter names $ActionRequest.params.PSObject.Properties.Name | Where-Object { $_ -like 'get_*' } | ForEach-Object { $ActionRequest.params.PSObject.Properties.Remove($_) } # Check the localsettings file to see if this action needs to be logged try { $LocalSettingsFilePath = Join-Path $Env:APPDATA 'tmconsole' 'localsettings' $LocalSettings = Get-Content -LiteralPath $LocalSettingsFilePath -Raw | ConvertFrom-Json if ($LocalSettings.logging.enabled) { $Subject.ActionRequest | Add-Member -NotePropertyName 'logPath' -NotePropertyValue $LocalSettings.logging.path } } catch { Write-Host "Could not read local settings: $($_.Exception.Message)" -ForegroundColor DarkYellow } ## Allow Required Modules to be imported automatically $PSModuleAutoloadingPreference = 'All' $Global:PSModuleAutoloadingPreference = 'All' ## Import the remaining Variables New-Variable -Name 'ActionRequest' -Value $ActionRequest -Force -Scope Global New-Variable -Name 'Params' -Value $ActionRequest.params -Force -Scope Global if (-not $global:TMSessions) { New-Variable -Name 'TMSessions' -Value @{} -Force -Scope Global } $global:TMSessions.Default = $TMSession ## Compute a Project root folder from the Server, Project and Event $TMServerUrl = ([uri]$ActionRequest.options.callback.siteUrl).Host $TMProjectName = $ActionRequest.task.project.name $TMEventName = $ActionRequest.task.event.name $ProjectRootFolder = Join-Path $Global:userPaths.root ($TMServerUrl -replace '.transitionmanager.net', '') $TMProjectName $TMEventName ## Create a Convenient TM Object with useful details $TM = [pscustomobject]@{ Server = @{ Url = $TMServerUrl Version = $TMSession.TMVersion } Project = @{ Id = $ActionRequest.task.project.id Name = $TMProjectName } Event = [pscustomobject]@{ Id = $ActionRequest.task.event.id Name = $TMEventName Bundles = $ActionRequest.task.event.bundles } Provider = @{ Id = $ActionRequest.options.apiAction.provider.id Name = $ActionRequest.options.apiAction.provider.name } Action = @{ Id = $ActionRequest.options.apiAction.id Name = $ActionRequest.options.apiAction.name } Task = [pscustomobject]@{ Id = $ActionRequest.task.id Title = $ActionRequest.task.title TaskNumber = $ActionRequest.task.taskNumber Invoker = $ActionRequest.task.assignedTo.name Team = $ActionRequest.task.team } Asset = $ActionRequest.task.asset User = @{ Id = $TMSession.UserAccount.Id Username = $TMSession.UserAccount.UserName Name = $TMSession.UserContext.Person.FullName } Paths = @{ debug = Join-Path $ProjectRootFolder 'Debug' logs = Join-Path $ProjectRootFolder 'Logs' queue = Join-Path $ProjectRootFolder 'Queue' config = Join-Path $ProjectRootFolder 'Config' input = Join-Path $ProjectRootFolder 'Input' output = Join-Path $ProjectRootFolder 'Output' credentials = Join-Path $ProjectRootFolder 'Credentials' git = Join-Path $ProjectRootFolder 'Git' referencedesigns = Join-Path $ProjectRootFolder 'Reference Designs' } } ## Scope the variable as global so the user will have access to it New-Variable -Name 'TM' -Value $TM -Scope Global -Force } |