lib/TMD.TaskAction.ps1
Function Invoke-TMActionRequest { <# .SYNOPSIS This function accepts a TransitionManager Action Request and queues it for execution .NOTES Name: Invoke-TMActionRequest Author: TransitionManager Version: 1.0 DateCreated: 2021-04-05 .EXAMPLE Invoke-TMActionRequest -ActionRequestB64 $Base64EncodedActionRequest .LINK https://support.transitionmanager.net #> [CmdletBinding()] param( [Parameter( Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 0 )] [string] $ActionRequestB64, [Parameter( Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 1 )] [string] $ScriptBlockString, [Parameter( Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 2 )] [pscredential] $Credential, [Parameter( Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 3 )] [pscustomobject]$Params ) BEGIN {} PROCESS { try { ## Import the ActionRequest from the B64 encoded argument $ActionRequest = Import-TMDActionRequest -FromB64 $ActionRequestB64 -SkipProviderLoad -PassThru ## Create a timestamp for the temp file name $Timestamp = [Math]::Round((Get-Date).ToFileTime() / 10000) ## Write the ActionRequest to the Queue folder $TempFileName = Join-Path $userPaths.queue ($Timestamp.ToString() + '.tmdar') Export-Clixml -InputObject $ActionRequest -Path $TempFileName } catch { throw $_ } } END {} } Function Invoke-TMDebugActionRequest { <# .SYNOPSIS This function accepts a TransitionManager Action Request and queues it for execution .NOTES Name: Invoke-TMActionRequest Author: TransitionManager Version: 1.0 DateCreated: 2021-04-05 .EXAMPLE Invoke-TMActionRequest -ActionRequestB64 $Base64EncodedActionRequest .LINK https://support.transitionmanager.net #> [CmdletBinding()] param( [Parameter( Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 0 )] [string] $ActionRequestFilePath ) BEGIN {} PROCESS { ## Setup an apropriate quiet environment $global:InformationPreference = 'Continue' $global:ProgressPreference = 'SilentlyContinue' $global:VerbosePreference = 'SilentlyContinue' $global:DebugPreference = 'SilentlyContinue' $global:WarningPreference = 'SilentlyContinue' $global:ErrorActionPreference = 'Continue' ## Using 'Stop' will break execution and not throw errors to the next handler. Don't release it that way. $global:ConfirmPreference = 'None' ## Allows functions that would typically require confirmation to be overridden $global:PSModuleAutoloadingPreference = 'All' ## Enable Auto Module Imports if commands match ## Test to see if we already have an ActionRequest object. If so, this session has already been run and the setup is no longer needed. if (-not $actionRequest) { ## ## Setup the Shell and Session ## ## The remote session had previously imported some importent detail, this will do it here in this session. . Import-TMDActionRequest -FromFile $ActionRequestFilePath ## Get the Task File Name if ($IsWindows) { $TaskDebugFolderPath = Join-Path -Path 'C:\Users' $env:USERNAME 'TMD_Files' 'debug' $TaskDebugFilePath = (Join-Path $TaskDebugFolderPath ('Task_' + $actionRequest.task.taskNumber + '.ps1')) } # Print Debug Header Write-Host "Beginning Debug Session for:" Write-Host " Task #: "$actionRequest.task.taskNumber -ForegroundColor Green Write-Host " Action: "$actionRequest.options.apiAction.name -ForegroundColor Green Write-Host "" Write-Host 'Variable [$Params]: ' $Params.PSObject.Properties | Where-Object { $_.Name -notlike 'Dt*' } | Select-Object Name, Value | Out-String New-Variable -Name 'ActionProvider' -Value $actionRequest.task -Force -Scope Global ## Set a breakpoint to give the user control over how the file is run. (Only the first time.) if ((Get-PSBreakpoint).Count -lt 1) { Set-PSBreakpoint -Script $TaskDebugFilePath -Line 1 | Out-Null } } ## Now that the Session has been configured and the ActionRequest imported, Run the User's script . $TaskDebugFilePath ## Mark the task as complete in TM Complete-TMTask -ActionRequest $ActionRequest @DataOption } END {} } Function Import-TMDActionRequest { param( [Parameter(Mandatory = $false)][String]$FromB64, [Parameter(Mandatory = $false)][String]$SerializedActionRequestObject, [Parameter(Mandatory = $false)][PSObject]$PSObjectActionRequest, [Parameter(Mandatory = $false)][String]$FromFile, [Parameter(Mandatory = $false)][Switch]$PassThru, [Parameter(Mandatory = $false)][Switch]$SkipProviderLoad ) if ($FromFile) { $actionRequest = (Get-Content -Path $FromFile | ConvertFrom-Json) } elseif ($SerializedActionRequestObject) { $actionRequest = [System.Management.Automation.PSSerializer]::Deserialize($SerializedActionRequestObject) } elseif ($PSObjectActionRequest) { $actionRequest = $PSObjectActionRequest } elseif ($FromB64) { $actionRequest = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($FromB64)) | ConvertFrom-Json } else { throw 'Unable to create Action Request Object.' } ## Make sure we created a new actionRequest, ## use it as a container to populate the runspace with some variables if ($actionRequest) { ## Create the TM Session Callback options for the Task Update functions $TMDCallbackOptions = @{ SiteURL = $actionRequest.options.callback.siteUrl; BearerToken = $actionRequest.options.callback.token; ProjectID = $actionRequest.options.projectId; TaskID = $actionRequest.task.id; } ## Handle Credential Creation for items passed from Credentials if ($null -ne $actionRequest.options.credentials) { ## Extract the Username and and Encrypt the password $Username = $actionRequest.options.credentials.username $EncryptedPassword = ConvertTo-SecureString $actionRequest.options.credentials.password -AsPlainText -Force ## Remove the credentials object from the $actionRequest $actionRequest.options.credentials = $null ## Create a new Credential and assign it to the $Credential variable in the current scope (session) $PSCredential = New-Object System.Management.Automation.PSCredential ($Username, $EncryptedPassword) $ActionRequest | Add-Member -NotePropertyName 'PSCredential' -NotePropertyValue $PSCredential | Out-Null New-Variable -Name 'Credential' -Value $PSCredential -Force -Scope Global } ## Handle Credential Creation within any Parameters that defined a credential $suppliedCredentials = $actionRequest.params.PSObject.Properties | Where-Object { $_.Name -like 'supplied_credential_*' } ## Convert any 'supplied_credential_' objects into their appropriate PSObject format. if ($suppliedCredentials.count -gt 0) { $suppliedCredentials | ForEach-Object { $suppliedCredential = $_ ## Remove the credentials object from the parameter $actionRequest.params.PSObject.Properties.Remove($suppliedCredential.Name) ## Extract the Username and and Encrypt the password $Username = $suppliedCredential.value.username $EncryptedPassword = ConvertTo-SecureString $suppliedCredential.value.password -AsPlainText -Force $ConvertedCredential = New-Object System.Management.Automation.PSCredential ($Username, $EncryptedPassword) ## Add the converted credential to the Params $actionRequest.params | Add-Member -NotePropertyName ($suppliedCredential.Name -replace 'supplied_credential_', '' ) -NotePropertyValue $ConvertedCredential -Force } } ## Handle Credential Creation within any Parameters that defined a credential $storedCredentials = $actionRequest.params.PSObject.Properties | Where-Object { $_.Name -like 'use_storedcredential_*' } ## Retrieve any 'use_stored_credential_' objects from local storage into their appropriate PSObject format. if ($storedCredentials.count -gt 0) { $storedCredentials | ForEach-Object { $storedCredential = $_ ## Remove the credentials object from the parameter $actionRequest.params.PSObject.Properties.Remove($storedCredential.Name) $RetrievedCredential = Get-StoredCredential -CredentialName $storedCredential.Value ## Add the converted credential to the Params $actionRequest.params | Add-Member -NotePropertyName ($storedCredential.Name -replace 'use_storedcredential_', '' ) -NotePropertyValue $RetrievedCredential -Force } } ## Allow Required Modules to be imported automatically $PSModuleAutoloadingPreference = 'All' $Global:PSModuleAutoloadingPreference = 'All' ## Add the ProviderSetup to the ActionRequest if ($ProviderSetup) { ## Always add the ProviderSetup to the Action Request $actionRequest | Add-Member -NotePropertyName ProviderSetup -NotePropertyValue $ProviderSetup -Force ## Only import the Modules if it's not supposed to skip (This only occurs when a script is being bootstrapped to connect to the Session) if (-not $SkipProviderLoad) { ## Display the Startup/Import Message if ($ProviderSetup.StartupMessage) { Write-Host $ProviderSetup.StartupMessage } ## Import Each Declared Module foreach ($Module in $ProviderSetup.ModulesToImport) { # . Import-PublicGalleryModule $Module . Import-Module $Module -ErrorAction 'SilentlyContinue' -SkipEditionCheck -Scope Global } foreach ($Module in $ProviderSetup.WinPSModulesToImport) { # . Import-PublicGalleryModule $Module # . Import-Module $Module -ErrorAction 'SilentlyContinue' -SkipEditionCheck -Scope Global . Import-Module $Module -UseWindowsPowerShell } ## If the ProviderSetup has a StartupScript Run the ScriptBlock if ($ProviderSetup.StartupScript) { . Invoke-Command -ScriptBlock $ProviderSetup.StartupScript -NoNewScope -ErrorAction 'SilentlyContinue' } } } ## Import the remaining Variables New-Variable -Name 'ActionRequest' -Value $actionRequest -Force -Scope Global New-Variable -Name 'Params' -Value $actionRequest.params -Force -Scope Global New-Variable -Name 'TMDCallbackOptions' -Value $TMDCallbackOptions -Force -Scope Global } else { ## The result of the script didn't produce an actionRequest object throw 'Unable to create an $actionRequest object' } if ($PassThru) { $ActionRequest } } Function Set-TMTaskAction { param( ## TMD Callback Options [Parameter(Mandatory = $false)][PSObject]$TMDCallbackOptions, [Parameter(Mandatory = $false)][PSObject]$ActionRequest, ## The selected Task State drives the APU URI generation [Parameter(Mandatory = $true)] [ValidateSet('started', 'progress', 'success', 'error')] [String]$State, ## Extra Features / Data Values [Parameter(Mandatory = $false)][String]$Message, [Parameter(Mandatory = $false)] [ValidateRange(0, 100)] [int]$Progress, [Parameter(Mandatory = $false)][String]$Stdout, [Parameter(Mandatory = $false)][String]$Stderr, [Parameter(Mandatory = $false)][PSObject]$Data, [Parameter(Mandatory = $false)][String]$DataFile, [Parameter(Mandatory = $false)][Boolean]$AllowInsecureSSL = $false ) ## Create a Callback Options object to know what Server to use if (-not $TMDCallbackOptions -and $ActionRequest) { ## Create the TM Session Callback options for the Task Update functions $TMDCallbackOptions = @{ SiteURL = $ActionRequest.options.callback.siteUrl; BearerToken = $ActionRequest.options.callback.token; ProjectID = $ActionRequest.options.projectId; TaskID = $ActionRequest.task.id; } } ## Don't make a call if Callback options were not present if (-not $TMDCallbackOptions) { return } #Honor SSL Settings if ($TMSessionConfig.AllowInsecureSSL -or $AllowInsecureSSL) { $TMCertSettings = @{SkipCertificateCheck = $true } } else { $TMCertSettings = @{SkipCertificateCheck = $false } } $uriPaths = @{ started = 'actionStarted' progress = 'actionProgress' success = 'actionDone' error = 'actionError' } $MPBoundary = '--------------------------314159265358979323846264' ## Headers $RequestHeaders = @{ "Accept-Version" = "1.0"; "Content-Type" = "multipart/form-data; boundary=" + $MPBoundary; "Accept" = "application/json"; "Authorization" = 'Bearer ' + $TMDCallbackOptions.BearerToken; "Cache-Control" = "no-cache"; } # Body Creation $Body = [System.Text.StringBuilder]::new() ## Add Project [void]$Body.Append( '--' + $MPBoundary ) [void]$Body.Append($CRLF) [void]$Body.Append( 'Content-Disposition: form-data; name="project.id"' ) [void]$Body.Append($CRLF) [void]$Body.Append( '' ) [void]$Body.Append($CRLF) [void]$Body.Append( $TMDCallbackOptions.ProjectID ) [void]$Body.Append($CRLF) ## Add Progress if ($Progress) { [void]$Body.Append( '--' + $MPBoundary ) [void]$Body.Append($CRLF) [void]$Body.Append( 'Content-Disposition: form-data; name="progress"' ) [void]$Body.Append($CRLF) [void]$Body.Append( 'Content-Type: application/x-www-form-urlencoded' ) [void]$Body.Append($CRLF) [void]$Body.Append( '' ) [void]$Body.Append($CRLF) [void]$Body.Append( $Progress ) [void]$Body.Append($CRLF) } ## Add Message if ($Message) { [void]$Body.Append( '--' + $MPBoundary ) [void]$Body.Append($CRLF) [void]$Body.Append( 'Content-Disposition: form-data; name="message"' ) [void]$Body.Append($CRLF) [void]$Body.Append( '' ) [void]$Body.Append($CRLF) [void]$Body.Append( $Message ) [void]$Body.Append($CRLF) } #Standard Out if ($Stdout) { [void]$Body.Append( '--' + $MPBoundary ) [void]$Body.Append($CRLF) [void]$Body.Append( 'Content-Disposition: form-data; name="stdout"' ) [void]$Body.Append($CRLF) [void]$Body.Append( '' ) [void]$Body.Append($CRLF) [void]$Body.Append( $Stdout ) [void]$Body.Append($CRLF) } #Standard Err if ($Stderr) { [void]$Body.Append( '--' + $MPBoundary ) [void]$Body.Append($CRLF) [void]$Body.Append( 'Content-Disposition: form-data; name="stderr"' ) [void]$Body.Append($CRLF) [void]$Body.Append( '' ) [void]$Body.Append($CRLF) [void]$Body.Append( $Stderr ) [void]$Body.Append($CRLF) } #Data if ($Data) { [void]$Body.Append( '--' + $MPBoundary ) [void]$Body.Append($CRLF) [void]$Body.Append( 'Content-Disposition: form-data; name="data"' ) [void]$Body.Append($CRLF) [void]$Body.Append( 'Content-Type: application/json;' ) [void]$Body.Append($CRLF) [void]$Body.Append( '' ) [void]$Body.Append($CRLF) [void]$Body.Append( ($Data | ConvertTo-Json -Depth 100 -Compress ) ) [void]$Body.Append($CRLF) } # #Data File # if($DataFile){ # [void]$Body.Append( '--' + $MPBoundary ) # [void]$Body.Append( 'Content-Disposition: form-data; name="datafile"' ) # [void]$Body.Append( '' ) # [void]$Body.Append( $DataFile ) # } ## End the Body and save it to a string [void]$Body.Append( "--" + $MPBoundary + "--" ) [void]$Body.Append($CRLF) $PostBody = $Body.ToString() # API $uri = $TMDCallbackOptions.SiteURL $uri += "/api/task/" + $TMDCallbackOptions.TaskID + "/" $uri += $uriPaths.$State ## Invoke the HTTP Request and handle the response try { # Perform actual web request $Response = Invoke-WebRequest -Method Post -Uri $uri -Headers $RequestHeaders -Body $PostBody @TMCertSettings if ($Response.StatusCode -eq 200) { $ResponseJson = $response.Content | ConvertFrom-Json -Depth 100 if ($ResponseJson.status -eq 'error') { throw $ResponseJson.errors } } ## TM 5.0+ returns a 204 (OK, but no content) elseif ($Response.StatusCode -eq 204) { return } else { Throw ('Error while setting task to ' + $State) } } catch { throw $_ } } Function Update-TMTaskProgress { param( [Parameter(Mandatory = $true)] [ValidateRange(0, 100)] [int]$Progress, ## Extra Features / Data Values [Parameter(Mandatory = $false)][String]$Message ) ## Allows running scripts that call these functions, so long as there is no Action Request # if (-Not $global:actionRequest) { return } ## If the Script wants to provide a 'Message', Messages are logged as task notes. if ($Message) { $MessageOption = @{ Message = $Message } } ## Post Back Success with Data and Messages as provided by the Script Set-TMTaskAction -TMDCallbackOptions $global:TMDCallbackOptions -State 'progress' -Progress $Progress @MessageOption } Function Complete-TMTask { param( [Parameter(Mandatory = $false)][pscustomobject]$ActionRequest, [Parameter(Mandatory = $false)][pscustomobject]$Data, [Parameter(Mandatory = $false)][Boolean]$AllowInsecureSSL = $false ) if ($Data) { $DataOption = @{ Data = $Data } } ## Post Back Success with Data and Messages as provided by the Script Set-TMTaskAction -ActionRequest $ActionRequest -Progress 100 -State 'success' @DataOption -AllowInsecureSSL $AllowInsecureSSL } Function Set-TMTaskOnHold { param( ## Extra Features / Data Values [Parameter(Mandatory = $false)][pscustomobject]$ActionRequest, [Parameter(Mandatory = $false)][String]$Message, [Parameter(Mandatory = $false)][String]$Data, [Parameter(Mandatory = $false)][Boolean]$AllowInsecureSSL = $false ) ## If the Script wants to provide a 'Message', Messages are logged as task notes. if ($Message) { $MessageOption = @{ Message = $Message } } if ($Data) { $DataOption = @{ Data = $Data } } ## Post Back Success with Data and Messages as provided by the Script Set-TMTaskAction -ActionRequest $ActionRequest -State 'error' @MessageOption @DataOption -AllowInsecureSSL $AllowInsecureSSL } Function Update-TMTaskAsset { param( [Parameter(Mandatory = $false)][PSObject][AllowNull()]$AssetUpdates = $null, [Parameter(Mandatory = $false)][String][AllowNull()]$Field = $null, [Parameter(Mandatory = $false)][String][AllowNull()]$Value = $null ) ## Ensure that there is a TM Asset Updates global Array use # if (-not $global:TMAssetUpdates) { $global:TMAssetUpdates = [System.Collections.ArrayList]@() } if (-not $global:TMAssetUpdates) { $global:TMAssetUpdates = [pscustomobject]@{} } ## Allow for providing in a full Hashtable of updates if ($AssetUpdates) { $AssetUpdates.PSObject.Properties | ForEach-Object { $global:TMAssetUpdates | Add-Member -NotePropertyName $_.Name -NotePropertyValue $_.value -Force Write-Host "Updating TM Asset Field: " -NoNewline Write-Host $_.Name -NoNewline -ForegroundColor Green Write-Host ' with value: ' -NoNewline Write-Host $_.Value -ForegroundColor Yellow } } ## Allow for adding in a single field and value. if ($Field) { $global:TMAssetUpdates | Add-Member -NotePropertyName $Field -NotePropertyValue $Value -Force Write-Host "Updating TM Asset Field: " -NoNewline Write-Host $Field -NoNewline -ForegroundColor Cyan Write-Host ' with value: ' -NoNewline Write-Host $Value -ForegroundColor Yellow } } |