Public/Actions.ps1
# Actions Function New-TMAction { param( [Parameter(Mandatory = $false)][PSObject]$TMSession = 'Default', [Parameter(Mandatory = $true)][PSObject]$Action, [Parameter(Mandatory = $false)][PSObject]$Project, [Parameter(Mandatory = $false)][Switch]$Update, [Parameter(Mandatory = $false)][Switch]$Passthru ) ## Get Session Configuration $TMSession = Get-TMSession $TMSession #Honor SSL Settings $TMCertSettings = @{SkipCertificateCheck = $TMSession.AllowInsecureSSL } ## Check for existing Action $ActionCheck = Get-TMAction -Name $Action.name -TMSession $TMSession if ($ActionCheck -and -not $Update) { if ($PassThru) { return $ActionCheck } else { return } } ## No Action exists. Create it $Instance = $TMSession.TMServer.Replace('/tdstm', '') $instance = $instance.Replace('https://', '') $instance = $instance.Replace('http://', '') ## Construct the intial part of the URL $uri = 'https://' $uri += $instance $uri += '/tdstm/ws/apiAction' ## Cleans the Action Name of characters we don't want $Action.name = $Action.name -replace '\\', '' -replace '\/', '' -replace '\:', '' -replace '>', '' -replace '<', '' -replace '\(', '' -replace '\)', '' -replace '\*', '' $Action.name = $Action.name -replace ('/\:><()*'.ToCharArray() -join '|\'), '' ## If The Existing action should be updated if ($ActionCheck -and $Update) { ## When the Action is an update, use important details from the current object. $Action.id = $ActionCheck.id $Action.dateCreated = $ActionCheck.dateCreated $Action.lastUpdated = $ActionCheck.lastUpdated $Action.version = $ActionCheck.version $Action.project = $ActionCheck.project ## Set the HTTP call details $uri += '/' + $Action.id $HttpMethod = 'Put' } else { ## Process as a new object. $HttpMethod = 'Post' $Action.PSObject.Properties.Remove('id') } ## Lookup Provider and Credential IDs, Field Specs if ([String]::IsNullOrWhiteSpace($Action.provider.name)) { Write-Error -Message "Provider name is blank for Action: $($Action.name)" return } $ProviderID = (Get-TMProvider -Name $Action.provider.name -TMSession $TMSession).id if (!$ProviderID) { # Create the provider if it doesn't exist $NowFormatted = (Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ' -AsUTC).ToString() $Provider = [PSCustomObject]@{ id = $null name = $Action.provider.name description = '' comment = '' dateCreated = $NowFormatted lastUpdated = $NowFormatted } $ProviderID = (New-TMProvider -Provider $Provider -PassThru -TMSession $TMSession).id } $Action.provider.id = $ProviderID $FieldSettings = Get-TMFieldSpecs -TMSession $TMSession ## Evaluate any Parameters on the Action if (-not $Action.methodParams) { ## Assign an empty array instead of null $Action.methodParams = @() } ## Get the Action.methodParams into an array of $Parameters if ($Action.methodParams -is [String]) { $Action.methodParams = $Action.methodParams | ConvertFrom-Json } ## Create a Local Pameters object by running any methodParams through the constructor $Parameters = $Action.methodParams | ForEach-Object { [TMActionParameter]::new($_) } ## Evaluate/Process each of the parameters for ($j = 0; $j -lt $Parameters.Count; $j++) { ## Update Context Linked fields if ($Parameters[$j].context -in @('ASSET', 'DEVICE', 'APPLICATION', 'STORAGE', 'DATABASE')) { ## Use the 'fieldLabel' property to determine the 'fieldName' if ($Parameters[$j].PSobject.Properties.name -match 'fieldLabel') { $DomainClass = $Parameters[$j].context if ($DomainClass -eq 'ASSET') { $DomainClass = 'APPLICATION' } ## Check if this is a CustomN field if ($Parameters[$j].fieldName -eq 'customN') { ## Update the Project's assigned Field by associating the label to the current field list. $Parameters[$j].fieldName = ($FieldSettings.$DomainClass.fields | Where-Object { $_.label -eq $Parameters[$j].fieldLabel }).field } ## Remove the 'fieldLabel property as it's invalid column definition $Parameters[$j].PSObject.Properties.Remove('fieldLabel') } } ## TM-23136 - Convert 6.3+ Method Param changes, adapting to older/newer combinations if ($TMSession.TMVersion -ge '6.3.0') { ## To be tested, need to import a pre-6.3 with prompting to validate if ($Parameters[$j].paramName -like 'get_*') { ## Split the name parts to get the Prompt Type $ParamNameParts = $Parameters[$j].paramName -split '_' $ParamPromptType = $ParamNameParts[1] -replace '-nosave', '' $CanCache = $ParamNameParts[1] -match '-nosave' ## Update the Parameter name and remove the 'prompt' parameter $Parameters[$j].paramName = $Parameters[$j].paramName -replace "get_$($ParamPromptType.toLower())_", '' $Parameters[$j] | Add-Member -Force -NotePropertyName 'prompt' -NotePropertyValue @{ type = $ParamPromptType canCache = $CanCache minSize = 0 maxSize = 1024 } } ## Make sure 'Blank' prompts are reset to null if($Parameters[$j].prompt) { if($Parameters[$j].prompt.type.toLower() -eq 'blank'){ $Parameters[$j].prompt = $null } } else { $Parameters[$j] | Add-Member -Force -NotePropertyName 'prompt' -NotePropertyValue $null } } else { ## Convert Prompt-bearing action definitions that have a type configured into a get_something_name if ( ($Parameters[$j].PSObject.Properties.Name -contains 'prompt') -and -not ([string]::IsNullOrWhiteSpace($Parameters[$j].prompt.type)) -and ($Parameters[$j].prompt.type.ToLower() -ne 'blank') ) { ## Update the name of the parameter $ParamNameParts = @('get_') $ParamNameParts += $Parameters[$j].prompt.type.toLower() if (-Not $Parameters[$j].prompt.canCache) { $ParamNameParts += '-nosave' } $ParamNameParts += @('_') $ParamNameParts += $Parameters[$j].paramName $Parameters[$j].paramName = ($ParamNameParts -join '') ## Remove the prompt field $Parameters[$j].PSObject.Properties.Remove('prompt') } } } ## Update the methodParams to an array of whatever resulted of a possible conversion switch ($Parameters.count) { 0 { $Action.methodParams = [array]@() } 1 { $Action.methodParams = [array](@($Parameters[0])) } Default { $Action.methodParams = [array]$Parameters } } ## Set the Action Type to a single string, which TM 6.1, 6.2 and 6.3... ## All agree to accep this value whether the data being posted is ... ## New or an update... If you change it, there better be a comment at least this... ## long. $Action.actionType = "POWER_SHELL" ## TM 6.3+ requires an Object for Parameters and Reaction Scripts if ($TMSession.TMVersion -ge '6.3') { if ($Action.methodParams -is [String]) { $Action.methodParams = $Action.methodParams | ConvertFrom-Json } if ($Action.reactionScripts -is [String]) { $Action.reactionScripts = $Action.reactionScripts | ConvertFrom-Json } } else { ## Ensure the MethodParams and ReactionScripts are JSON-Strings if (-not ($Action.methodParams -is [String])) { switch ($Action.MethodParams.GetType().BaseType.toString()) { 'TMActionParameter' { $Action.methodParams = "[$($Action.methodParams | ConvertTo-Json -Depth 100 -Compress)]" } 'System.Array' { $Action.methodParams = , $Action.methodParams | ConvertTo-Json -Depth 100 -Compress break } Default { Write-Host "Type: $_" $Action.methodParams = $Action.methodParams | ConvertTo-Json -Depth 100 -Compress } } } if (-not ($Action.reactionScripts -is [String])) { $Action.reactionScripts = $Action.reactionScripts | ConvertTo-Json -Depth 100 -Compress } # ## schema v4 added TM 6.3 Prompt parameters that are only for 6.3 # if ($Action.PSObject.Properties.Name -contains 'prompt') { # $Action.PSObject.Properties.Remove('prompt') # } } <# NOTING an important checkpoint here. Above this point in the code... * Some [object] was provided. Might have been a file path, a basic object, or event a formed TMAction * These come in MANY varieties which are generally the subject of the TME 'formatting defintion' which are all version numbered. The code used in the process and where things happen are very delicate. Trust the way it's organized, because * The object is converted into an $Action object, which carries whatever form it was delivered in * The Since there is no SCHEMA validation system, the important properties are inspected and updated * Based on the existing format (and the schema-version it has (might be old...)) * and based on the Server the Action is going to be installed in, it then updates * All of those important properties, into the TMServerVersion specific format... * There is a Many-Many relationship that was used as a testing framework on this ticket. * Tested Scenarios * Create Action named "... From61" with Parameter definitions, with prompt settings * Export the Actions from 6.1 * Reimported to 6.1 successfully * Import the 6.1 into TM6.2 * Once all of these conversions have completed, the resulting "$Action" variable contains something that's not quite a class-defined thing, without having to build in support for the legacy formats... It might possibly have started as 'new', been converted to old, and updated again, but that's simply the adaptable way given we have 3 versions of support built into this process... Congrats, now post the data. #> $PostBodyJSON = $Action | ConvertTo-Json -Depth 10 -Compress Write-Verbose "Action JSON Data: $PostBodyJSON" Set-TMHeaderAccept 'JSON' -TMSession $TMSession Set-TMHeaderContentType 'JSON' -TMSession $TMSession try { $response = Invoke-WebRequest -Method $HttpMethod -Uri $uri -WebSession $TMSession.TMWebSession -Body $PostBodyJSON @TMCertSettings if ($response.StatusCode -eq 200) { $responseContent = $response.Content | ConvertFrom-Json if ($responseContent.status -ne 'success') { throw ($responseContent.errors -join ', ') } else { if ($PassThru) { return $responseContent.data } } } elseif ($Response.StatusCode -eq 204) { return } } catch { Write-Host 'Unable to create Action.' return $_ } } Function Get-TMAction { [CmdletBinding(DefaultParameterSetName = 'Default')] param( [Parameter(Mandatory = $false)] [PSObject]$TMSession = 'Default', [Parameter(Mandatory = $false, ParameterSetName = 'ByName')] [String[]]$Name, [Parameter(Mandatory = $false, ParameterSetName = 'ById')] [String[]]$Id, [Parameter(Mandatory = $false)] [String[]]$ProviderName, [Parameter(Mandatory = $false)] [Switch]$ResetIDs, [Parameter(Mandatory = $false)] [String]$SaveCodePath, [Parameter(Mandatory = $false)] [bool]$Api = $true, [Parameter(Mandatory = $false)] [Switch]$Passthru ) ## Get Session Configuration $TMSession = Get-TMSession $TMSession #Honor SSL Settings $TMCertSettings = @{SkipCertificateCheck = $TMSession.AllowInsecureSSL } $Instance = $TMSession.TMServer.Replace('/tdstm', '').Replace('https://', '').Replace('http://', '') if ($Api) { # Format the uri $uri = "https://$instance/tdstm/api/apiAction?project=$($TMSession.UserContext.project.id)" try { $response = Invoke-RestMethod -Method Get -Uri $uri -WebSession $TMSession.TMRestSession @TMCertSettings $Result = $response | Where-Object { $_.actionType.id -EQ 'POWER_SHELL' } } catch { throw $_ } } else { # Format the uri $uri = "https://$instance/tdstm/ws/apiAction" try { $response = Invoke-WebRequest -Method Get -Uri $uri -WebSession $TMSession.TMWebSession @TMCertSettings } catch { throw $_ } if ($Response.StatusCode -in @(200, 204)) { $Result = ($Response.Content | ConvertFrom-Json).data $Result = $Result | Where-Object actionType -EQ 'POWER_SHELL' } else { throw 'Unable to collect Actions.' } } ## Return the details -- Filter based on passed parameters if ($ProviderName) { $Result = $Result | Where-Object { $_.provider.name -in $ProviderName } } elseif ($Name) { $Result = $Result | Where-Object { $_.name -in $Name } } elseif ($Id) { $Result = $Result | Where-Object { $_.id -in $Id } } ## 6.1 and forward, the endpoints do not supply the source code with the object if ($TMSession.TMVersion -ge '6.0.0') { if ($Api) { ## Update each of the objects that were returned with their full version $Result = $Result | ForEach-Object { # Format the uri $uri = "https://$instance/tdstm/api/apiAction/$($_.id)?project=$($TMSession.UserContext.project.id)" Invoke-RestMethod -Method Get -Uri $uri -WebSession $TMSession.TMRestSession @TMCertSettings } } else { ## Update each of the objects that were returned with their full version $Result = $Result | ForEach-Object { # Format the uri $uri = "https://$instance/tdstm/ws/apiAction/$($_.id)?project=$($TMSession.UserContext.project.id)" Invoke-WebRequest -Method Get -Uri $uri -WebSession $TMSession.TMWebSession @TMCertSettings try { $response = Invoke-WebRequest -Method Get -Uri $uri -WebSession $TMSession.TMWebSession @TMCertSettings } catch { throw $_ } if ($Response.StatusCode -in @(200, 204)) { ($Response.Content | ConvertFrom-Json).data } else { throw 'Unable to collect Actions.' } } } } # Get Field / Label maps to translate the parameters $FieldToLabelMap = Get-TMFieldToLabelMap -TMSession $TMSession foreach ($Action in $Result | Where-Object methodParams) { ## Convert JSON data into an object $Parameters = $Action.methodParams if ($Parameters -is [String]) { $Parameters = $Action.methodParams | ConvertFrom-Json } foreach ($Parameter in $Parameters | Where-Object Context -In 'DEVICE', 'APPLICATION', 'STORAGE', 'DATABASE' | Where-Object fieldName) { ## The custom column field identifer is lost when the IDs get reset. Add a fieldLabel node to put the custom field label. This will get replaced/updated by the Import $FieldLabel = $FieldToLabelMap[$Parameter.context][$Parameter.fieldName] $Parameter | Add-Member -NotePropertyName 'fieldLabel' -NotePropertyValue $FieldLabel -Force ## Clear the Custom field number associated to the Parameter if ($ResetIDs -and ($Parameter.fieldName -like 'custom*')) { $Parameter.fieldName = 'customN' } } $Action.methodParams = $Parameters ## Apply Package Format version 2 details if ($TMSession.TMVersion -lt '6.1.0') { Add-Member -InputObject $Action -NotePropertyName 'actionType' -NotePropertyValue ($Action.actionType.id ?? $Action.actionType) -Force } } if ($ResetIDs) { ## Clear pertinent data in each Action foreach ($Action in $Result) { $Action.id = $null $Action.project.id = $null if ($Action.credential) { $Action.credential.id = $null } $Action.provider.id = $null } } ## Save the Code Files to a folder if ($SaveCodePath) { ## Save Each of the Script Source Data foreach ($Action in $Result) { ## Get a FileName safe version of the Provider Name $SafeProviderName = Get-FilenameSafeString $Action.provider.name $SafeActionName = Get-FilenameSafeString $Action.name ## Create the Provider Action Folder path $ProviderPath = Join-Path $SaveCodePath $SafeProviderName if (-not (Test-Path -Path $ProviderPath -PathType Container) ) { Test-FolderPath $ProviderPath } ## Create a File name for the Action $ProviderScriptConfigPath = Join-Path $ProviderPath ($SafeActionName + '.json') $ProviderScriptPath = Join-Path $ProviderPath ($SafeActionName + '.ps1') ## Split the script out of the Item, and save both files. $ScriptBlock = $Action.script Set-Content -Path $ProviderScriptPath -Force -Value $ScriptBlock ## Remove the script from the JSON file $Action.PSObject.Properties.Remove('script') ## Disabling the lass structure computation on the write, because there ARE deliberately- ## offline only properties that are required for using them as an input ## Convert the Parameters to a TMActionParameters Class Object if ($Action.methodParams) { $Parameters = $Action.methodParams | ForEach-Object { [TMActionParameter]::new($_) } $Action.methodParams = $Parameters } ## Convert the ReactionScript object to an Object if ($Action.reactionScripts -is [String]) { $Action.reactionScripts = $Action.reactionScripts | ConvertFrom-Json } Set-Content -Path $ProviderScriptConfigPath -Force -Value ($Action | ConvertTo-Json -Depth 100) } } if ($Passthru -or !$SaveCodePath) { ## Return an array even if it holds a single value - consistency return , $Result } } Function Read-TMActionScriptFile { param( [Parameter(Mandatory = $false)] [PSObject]$TMSession = 'Default', [Parameter(Mandatory = $true)] [String]$Path ) ## First order of business is to determine if this Script file has a counterpart Json file $ActionConfigJsonPath = $Path -Replace '.ps1', '.json' $ActionConfigFile = Get-Item -Path $ActionConfigJsonPath -ErrorAction 'SilentlyContinue' ## Check if there is a config JSON file, if so, get the Action from the 2 files if ($ActionConfigFile) { ## Get the Action Object that doesn't have the source code $TMAction = Get-Content -Path $ActionConfigFile | ConvertFrom-Json $FieldToLabelMap = Get-TMLabelToFieldMap -TMSession $TMSession ## If this Action has parameters, handle sorting and updating them if ($TMAction.methodParams) { ## Convert the methodParams to a String, as it should be switch ($TMAction.methodParams.GetType().Name) { ## Array of objects, each is a PSCustomObject of a TMActionParameter type 'Object[]' { ## Remove Offline properties that do not belong on the object. foreach ($TMActionParameter in $TMAction.methodParams) { ## Recast the object as a Parameter with it's current configuration $TMActionParameter = [TMActionParameter]::new($TMActionParameter) ## Add the correct TM Field Label if ($TMActionParameter.fieldName -eq 'customN') { $TMActionParameter.fieldName = $FieldToLabelMap.($TMActionParameter.context.toUpper()).($TMActionParameter.fieldLabel) } } break } ## There might only be one Param (and was written without being forced to a list) 'PSCustomObject' { Write-Debug 'Updating TMAction.methodParams to a Class Object' $TMAction.methodParams = [TMActionParameter]::new($TMAction.methodParams) ## Add the correct TM Field Label if ($TMAction.methodParams.fieldName -eq 'customN') { Write-Host "Updating a customN field reference: $($TMAction.methodParams.fieldName) to " -NoNewline $TMAction.methodParams.fieldName = $FieldToLabelMap.($TMAction.methodParams.context.toUpper()).($TMAction.methodParams.fieldLabel) Write-Host $($TMAction.methodParams.fieldName) -ForegroundColor Yellow } if ($TMAction.methodParams.paramName -like 'get_*') { Write-Debug 'methodparams.paramName matches a legacy get_* string. updating the Parameter settings with the info.' # $PromptNameParts = $TMAction.methodParams.paramName -split '_' # $CanPrompt = $PromptNameParts[1].toLower() -notlike '*-nosave' # $PromptType = $PromptNameParts[1].toUpper() -replace '-NOSAVE', '' # $ParameterName = $PromptNameParts[2] # $TMAction.methodParams = [TMActionParameter]::new(@{ # paramName = $ParameterName # }) } # $TMAction.methodParams.PSObject.Properties.Remove('order') # $TMAction.methodParams.PSObject.Properties.Remove('fieldLabel') # $TMAction.methodParams = @(($TMAction.methodParams)) | ConvertTo-Json -Depth 5 -Compress ## Ensure that the Method Params is an array break } ## For completeness, some Extensions have the literal JSON string data. If so, 'String' { ## Do nothing Write-Host ($TMAction.methodParams | ConvertTo-Json -depth 5) -ForegroundColor RED throw 'TransitionManager.Public.Actions.String wants to understand a STRING value for methodParams. Check it further' break } Default { ## If there was no value, or something else entirely, default to an empty set of parameters $TMAction.methodParams = @() } } } ## Read the Script Content and add it to the Action Object $ScriptContent = (Get-Content -Path $Path -Raw) ?? '' Add-Member -InputObject $TMAction -NotePropertyName script -NotePropertyValue ($ScriptContent.ToString() ?? '') -Force } ## Perform a read of the PS file to capture the ReferenceDesign formatted settings else { ## Name the Input File $Content = Get-Content -Path $Path -Raw ## Ignore Empty Files if (-Not $Content) { return } $ContentLines = Get-Content -Path $Path ## Create Automation Token Variables Parse the Script File New-Variable astTokens -Force New-Variable astErr -Force $ast = [System.Management.Automation.Language.Parser]::ParseInput($Content, [ref]$astTokens, [ref]$astErr) ## ## Assess the Script Parts to get delineating line numbers ## ## Locate the Delimiting line $ConfigBlockEndLine = $astTokens | ` Where-Object { $_.Text -like '## End of TM Configuration, Begin Script*' } |` Select-Object -First 1 | ` Select-Object -ExpandProperty Extent | ` Select-Object -ExpandProperty StartLineNumber ## Test to see if the Script is formatted output with Metadata if (-not $ConfigBlockEndLine) { ## There is no metadata, create the basic object with just the source code $ActionConfig = [pscustomobject]@{ ActionName = (Get-Item -Path $Path).BaseName Description = '' ProviderName = (Get-Item -Path $Path).Directory.BaseName } ## Create the objects the below constructor expect $ConfigBlockEndLine = -1 $Params = $null $TMActionParams = [System.Collections.ArrayList] @() } else { ## ## Read the Script Header to gather the configurations ## ## Get all of the lines in the header comment $TMConfigHeader = 0..$ConfigBlockEndLine | ForEach-Object { if ($astTokens[$_].kind -eq 'comment') { $astTokens[$_] } } | Select-Object -First 1 | Select-Object -ExpandProperty Text ## Create a Properties object that will store the values listed in the header of the script file $ActionConfig = [PSCustomObject]@{ } ## Process each line of the Header string $TMConfigHeader -split "`n" | ForEach-Object { ## Process each line of the comment if ($_ -like '*=*') { $k, $v = $_ -split '=' $k = $k.Trim() -replace "'", '' -replace '"', '' $v = $v.Trim() -replace "'", '' -replace '"', '' $ActionConfig | Add-Member -NotePropertyName $k -NotePropertyValue $v } } ## ## Read the Script Block ## ## Create a Text StrinBuilder to collect the Script into $ActionConfigStringBuilder = New-Object System.Text.StringBuilder ## For each line in the Code Block, add it to the Action Script Code StringBuilder 0..$ConfigBlockEndLine | ForEach-Object { $ActionConfigStringBuilder.AppendLine($ContentLines[$_]) | Out-Null } $ActionConfigScriptString = $ActionConfigStringBuilder.ToString() $ActionConfigScriptBlock = [scriptblock]::Create($ActionConfigScriptString) ## Invoke the Script Block to create the $Params Object in this scope ## this line populates the $Params object from the Action Script Invoke-Command -ScriptBlock $ActionConfigScriptBlock -NoNewScope ## Collect the Parameters $TMActionParams = [System.Collections.ArrayList] @() ## Action Parameter Class Definition # { # "desc": "", # "type": "string", # "value": "", # "context": "DEVICE", # "encoded": false, # "readonly": false, # "required": false, # "fieldName": null, # "paramName": "IPAddress", # "fieldLabel": "IP Address" # } ## Process the Parameters into Action Params foreach ($ParamLabel in $Params.Keys) { ## Create a new Params Object to load to the Action $NewParamConfig = [PSCustomObject]@{ type = 'string' value = '' description = '' context = '' fieldLabel = '' fieldName = '' required = $false encoded = $false readonly = $false } ## Read the existing Params configuration, assembling each additional Paramater option $ScriptParamConfig = $Params.$ParamLabel $ScriptParamConfig.Keys | ForEach-Object { switch ($_.toLower()) { 'value' { $NewParamConfig.value = $ScriptParamConfig[$_] break } 'type' { $NewParamConfig.type = $ScriptParamConfig[$_] break } 'description' { $NewParamConfig.description = $ScriptParamConfig[$_] break } 'desc' { $NewParamConfig.description = $ScriptParamConfig[$_] break } 'context' { $NewParamConfig.context = $ScriptParamConfig[$_] break } 'fieldlabel' { $NewParamConfig.fieldLabel = $ScriptParamConfig[$_] break } 'fieldName' { $NewParamConfig.fieldName = $ScriptParamConfig[$_] break } 'required' { $NewParamConfig.required = (ConvertTo-Boolean $ScriptParamConfig[$_]) break } } } ## Add the Parameter Name from the Configuration Object Add-Member -InputObject $NewParamConfig -NotePropertyName 'paramName' -NotePropertyValue $ParamLabel $TMActionParams.Add($NewParamConfig) | Out-Null } } ## Note where the Action Code is located $StartCodeBlockLine = $ConfigBlockEndLine + 1 $EndCodeBlockLine = $ast[-1].Extent.EndLineNumber ## Create a Text StrinBuilder to collect the Script into $ActionScriptStringBuilder = New-Object System.Text.StringBuilder ## For each line in the Code Block, add it to the Action Script Code StringBuilder $StartCodeBlockLine..$EndCodeBlockLine | ForEach-Object { $ActionScriptStringBuilder.AppendLine($ContentLines[$_]) | Out-Null } ## Convert the StringBuilder to a Multi-Line String $ActionScriptCode = $ActionScriptStringBuilder.ToString() ## If no Parameters were assembled, provide an empty array if (-not $TMActionParams) { $TMActionParams = [System.Collections.ArrayList] @() } # } ## Assemble the Action Object $TMAction = [pscustomobject]@{ id = $null name = $ActionConfig.ActionName description = $ActionConfig.Description debugEnabled = $false methodParams = ($TMActionParams | ConvertTo-Json -Compress) script = $ActionScriptCode reactionScripts = '{"PRE":"","ERROR":"// Put the task on hold and add a comment with the cause of the error\n task.error( response.stderr )","FINAL":"","FAILED":"","LAPSED":"","STATUS":"// Check the HTTP response code for a 200 OK \n if (response.status == SC.OK) { \n \t return SUCCESS \n } else { \n \t return ERROR \n}","DEFAULT":"// Put the task on hold and add a comment with the cause of the error\n task.error( response.stderr )\n","STALLED":"","SUCCESS":"// Update Asset Fields\nif(response?.data?.assetUpdates){\n\tfor (field in response.data.assetUpdates) {\n \t\tasset.\"${field.key}\" = field.value;\n\t}\n}\ntask.done()"}' provider = @{ id = $null name = $ActionConfig.ProviderName } project = @{ id = $null name = $null } remoteCredentialMethod = 'USER_PRIV' credential = $null asyncQueue = $null version = 1 dateCreated = Get-Date lastUpdated = Get-Date timeout = 0 commandLine = $null dictionaryMethodName = 'Select...' callbackMethod = $null connectorMethod = $null pollingStalledAfter = 0 pollingInterval = 0 pollingLapsedAfter = 0 defaultDataScript = $null useWithTask = 0 reactionScriptsValid = 1 docUrl = '' isRemote = $true actionType = 'POWER_SHELL' useWithAsset = 0 isPolling = 0 endpointUrl = '' apiCatalog = $null } } ## Ensure the methodParams is a class object if ($TMAction.methodParams) { $UpdatedMethodParams = @() switch ($TMAction.methodParams.GetType().Name) { 'String' { $ConvertedParameters = $TMAction.methodParams | ConvertFrom-Json foreach ($ConvertedParameter in $ConvertedParameters) { $UpdatedMethodParams += [TMActionParameter]::new($ConvertedParameter) } } 'Object[]' { foreach ($MethodParameters in $TMAction.methodParams) { $UpdatedMethodParams += [TMActionParameter]::new($MethodParameters) } break } 'TMActionParameter' { $UpdatedMethodParams = @($TMAction.methodParams) break } Default { throw "Action Method parameter propety was an unexpected Type [$($_)] and could not be converted." } } $TMAction.methodParams = $UpdatedMethodParams } ## Return the Action Object return $TMAction } function Invoke-TMActionScript { <# .SYNOPSIS Runs the Script Code in the specified TransitionManager Action .DESCRIPTION Runs the Script Code in the specified TransitionManager Action .PARAMETER TMSession Name of an open TMSession. Use Get-TMSession to get open sessions .PARAMETER Name Name of the action in TM that will be invoked. .EXAMPLE Invoke-TMActionScript -Name 'Ping Remote Machine' .OUTPUTS This command does not output any content #> [CmdletBinding()] param( [Parameter(Mandatory = $false)][PSObject]$TMSession = 'Default', [Parameter(Mandatory = $true)][String]$Name, [Parameter(Mandatory = $false)][PSObject]$Project = $global:TMSessions[$TMSession].UserContext.Project, [Parameter(Mandatory = $false)][bool]$Api = $false ) ## Get Session Configuration $TMSession = Get-TMSession -Name $TMSession ## Get the TM Action $TMAction = Get-TMAction @PSBoundParameters -Api $Api if (-Not $TMAction) { throw 'Unable to get the Action' } ## Try converting and running the script block from the Action try { ## Create a Temporary Filename $TempPsFile = New-TemporaryFile | Rename-Item -NewName { $_.FullName -replace '\.tmp', '.ps1' } -PassThru | Select-Object -ExpandProperty FullName $ActionScriptBlock = [scriptblock]::Create($TMAction.Script) Set-Content -Value $ActionScriptBlock.ToString() -Path $TempPsFile -Force -Confirm:$false ## Invoke the script block, No New Scope to use the available variables, ## and with Debug enabled to allow stepping into the file via debugging mode try { $Stopwatch = [System.Diagnostics.Stopwatch]::StartNew() & ($TempPsFile) $Stopwatch.Stop() Write-Host -Message "Task $($Name) completed in $(Get-TimeSpanString -Timespan $Stopwatch.Elapsed)" } catch { $Stopwatch.Stop() Write-Warning -Message "Task $($Name) completed (with errors) in $(Get-TimeSpanString -Timespan $Stopwatch.Elapsed)" -WarningAction Continue throw $_ } finally { Remove-Item $TempPsFile -Force } } catch { throw $_ } } |