PowerShell & System Center Orchestrator - Best Practice Template

This article covers a “best practice” method for executing PowerShell scripts within a System Center Orchestrator runbook using the built-in “Run .Net Script” activity. This method avoids some inherent limitations of Orchestrator’s out-of-the-box PowerShell support:

  • Uses latest installed version of PowerShell: by default, the Run .Net Script activity runs scripts with PowerShell 2.0, which lacks many features of newer versions. This method will use the latest version installed on the runbook server.
  • Uses 64-bit process: by default, the Run .Net Script activity runs scripts in a 32-bit process, because Orchestrator is an x86 application. This technique ensures scripts run in native 64-bit PowerShell, which avoids some potential compatibility problems.
  • Captures detailed logs: keeps a trace log for easier troubleshooting
  • Allows custom published data: provides any script variable values back to the runbook for continued use
  • Robust error handling: all errors are caught and handled by the script, with error messages published to the data bus
  • Semantically-correct result status: you can define your own success criteria and invoke an action when something didn’t work as intended, even if the script ran without throwing any exceptions

https://automys.blob.core.windows.net/images/PS-Orchestrator-10.jpg

**Note: The example runbooks referenced in the article are available for free download here along with a video tutorial:

PowerShell and System Center Orchestrator - Best Practice Template**

When to Use

The short answer is routinely. You can use this example as a template any time you want to run PowerShell scripts in an Orchestrator runbook, unless you are sure you don’t need or want any of the features listed above.

How it Works

The Run .Net Script activity allows running PowerShell code directly by entering it into the activity’s “Script” field. This runs the script in a PowerShell process spawned by the Orchestrator runbook server. Unfortunately, this comes with some limitations as described above.

This example approach uses the same activity but with a particular PowerShell structure as a workaround for these issues. The pattern was originally described in the free eBook Microsoft System Center – Designing Orchestrator Runbooks, in the section “Using Windows PowerShell in Orchestrator” starting on page 61. I highly recommend reading at least this section, if not the rest of the book. The downloadable example script described here is a modified version of this approach.

The next sections discuss a few of the aspects of the script itself and this example runbook to show how it can be used.

Script Input

Any inputs to the script are provided at the top where they are defined as variables and assigned values from the data bus. These can be provided by subscribing to published data or to a global variable. They are then added as arguments passed to the script block executed subsequently by Invoke-Command.

https://automys.blob.core.windows.net/images/PS-Orchestrator-09.jpg
PowerShell Script Data Bus Inputs

Separate “External” Session

The key to the technique is doing all the “real work” of the script in a separate PowerShell session from the one initially created by Orchestrator. This lets us get into a fancy new 64-bit process with the latest PowerShell version where we can work with minimal risk of unexpected legacy behavior or issues. This is done by running the main script content as a script block using Invoke-Command.

https://automys.blob.core.windows.net/images/PS-Orchestrator-03.jpg
Entering PowerShell session with latest 64-bit version

  
Technically, this means we are setting up a “remote” session to the current machine, which means everything that happens in the script block is completely separate from the originating session. Then, we have to explicitly return whatever results we want Orchestrator to know about as we arrive back from this “remote” session.

https://automys.blob.core.windows.net/images/Powershell-process-diagram.jpg
Creating external PowerShell session from Orchestrator script activity

  
Script Block: Work Done Here

Everything you want to accomplish, which is the reason why you’re running the PowerShell script to begin with, is defined in the script block passed to Invoke-Command. Specifically, you’ll put everything inside the try { } block so that any exceptions will be handled and provided back to the runbook.

https://automys.blob.core.windows.net/images/PS-Orchestrator-01.jpg
Main script content goes inside try section within script block

 

Generally speaking, any PowerShell script content can be included here as you would a standalone script. A caveat to this is commands that connect and authenticate to external systems, which require dealing with the “double-hop” authentication scenario discussed under the implementation section below. This still works as long you take the steps to configure the runbook server appropriately.

Script Output / Published Data

The result returned from the “external” session is in the form of an array, which can include as many values as needed, including custom return values. These are then extracted from the array into separate variables to be provided by the script activity as published data. The template includes three basic return elements: ResultStatus, ErrorMessage, and TraceLog, which provide information about the outcome of the script execution.

https://automys.blob.core.windows.net/images/PS-Orchestrator-08.jpg
Defining published data for PowerShell within Run .Net Script activity

 

Any additional custom return variables can be defined by simply adding them to the $resultArray variable that defines what is returned, then extracting them from the returned array.

Any variable extracted from $ReturnArray can then be published to the data bus by defining it within the Published Data section of the script activity properties.

Logging

Within the script block, record any useful action information into the trace log by using the AppendLog function and supplying a string to record. This adds an entry to the trace log that is produced.

If you want the script logs to be saved by Orchestrator, make sure to enable the option “Store activity-specific Published Data” for each runbook where you have a script activity that should record log data.

https://automys.blob.core.windows.net/images/PS-Orchestrator-05.jpg
Logging values published by PowerShell script activity

Error Handling

Any errors that occur in the script block are caught and recorded, then returned as published data (Error Message and Trace Log). You can then use the Result Status to determine whether the script succeeded, and to branch accordingly in your runbook. In the example, any exception will cause the Result Status to be “Failed”. The example runbook shows how to branch on this status in order to record the failure details in the event log and return a negative result for the overall runbook when script errors are encountered. For instance, the example script looks for an input value matching "bad stuff" which causes it to throw an exception. The resulting event log entry is shown below:

https://automys.blob.core.windows.net/images/PS-Orchestrator-04.jpg
Recording detailed script error logs to Windows Event Log

  
If there is an exception running the activity itself (before the script block runs), this should be reported in the normal Error Summary Text published data property.

Implementing In Your Environment

This example serves as a template for creating runbooks with PowerShell script activities. You can use the link at the top of the page to download the example runbook to get the template runbook activity and, for convenience, an associated PowerShell file with the script content. Once downloaded, use the **Import **function of Runbook Designer to bring the example into your Orchestrator environment for testing.

When creating a new script activity in your runbooks, you can copy/paste/modify either the whole template activity or just the template script content in a new Run .Net Script activity.

Prerequisites

This approach has been tested extensively with System Center Orchestrator 2012 SP1 and R2. I also recommend installing the latest official release of PowerShell on your runbook server(s).

Handling Remote Connections

Because the main script logic is performed in a secondary session, any connections from that logic to another system would constitute a second connection. So if the script needs to authenticate to a SharePoint server, for example, you’d be faced with a “multi-hop remoting” scenario. This is a confusing situation that requires a couple additional configuration actions to allow this to happen.

On Orchestrator Runbook Server(s), run each of these commands in an elevated PowerShell console (replacing “*.mydomain.com” with your domain name in the second command):

Enable-PSRemoting

Enable-WSManCredSSP -Role Client -DelegateComputer *.mydomain.com

Enable-WSManCredSSP -Role Server -Force

 

Development and Testing Tip

You can develop scripts in the PowerShell ISE or another script editor before running in Orchestrator. To do that with this template, replace the initial data bus input variables with some mock test values. Then, when the things are working, copy the script content into the script activity and replace the input values with the published data subscription references.

https://automys.blob.core.windows.net/images/PS-Orchestrator-00.jpg
Orchestrator script development in ISE using mock test input values

 

Note: open the editor as administrator to ensure Invoke-Command will be allowed to run. Otherwise, you might receive this error:

New-PSSession : [localhost] Connecting to remote server localhost failed with the following error message : Access is denied.

 

Also, you may receive a warning when trying to run the script after opening from the download, if the file is detected as coming from another computer:

File <path> cannot be loaded. The file <path> is not digitally signed. You cannot run this script on the current system.

 

To fix this, unblock the file from its properties dialog:

https://automys.blob.core.windows.net/images/unblock-psfile.jpg
Unblock PowerShell file to allow execution in script editor

PowerShell Script Template

# Set script parameters from runbook data bus and Orchestrator global variables
# Define any inputs here and then add to the $argsArray and script block parameters below 

$DataBusInput1 = "{Parameter 1 from Initialize Data}"
$DataBusInput2 = "{Global Variable 1}"


#-----------------------------------------------------------------------

## Initialize result and trace variables
# $ResultStatus provides basic success/failed indicator
# $ErrorMessage captures any error text generated by script
# $Trace is used to record a running log of actions
$ResultStatus = ""
$ErrorMessage = ""
$Trace = (Get-Date).ToString() + "`t" + "Runbook activity script started" + " `r`n"
       
# Create argument array for passing data bus inputs to the external script session
$argsArray = @()
$argsArray += $DataBusInput1
$argsArray += $DataBusInput2

# Establish an external session (to localhost) to ensure 64bit PowerShell runtime using the latest version of PowerShell installed on the runbook server
# Use this session to perform all work to ensure latest PowerShell features and behavior available
$Session = New-PSSession -ComputerName localhost

# Invoke-Command used to start the script in the external session. Variables returned by script are then stored in the $ReturnArray variable
$ReturnArray = Invoke-Command -Session $Session -Argumentlist $argsArray -ScriptBlock {
    # Define a parameter to accept each data bus input value. Recommend matching names of parameters and data bus input variables above
    Param(
        [ValidateNotNullOrEmpty()]
        [string]$DataBusInput1,

        [ValidateNotNullOrEmpty()]
        [string]$DataBusInput2
    )

    # Define function to add entry to trace log variable
    function AppendLog ([string]$Message)
    {
        $script:CurrentAction = $Message
        $script:TraceLog += ((Get-Date).ToString() + "`t" + $Message + " `r`n")
    }

    # Set external session trace and status variables to defaults
    $ResultStatus = ""
    $ErrorMessage = ""
    $script:CurrentAction = ""
    $script:TraceLog = ""

    try 
    {
        # Add startup details to trace log
        AppendLog "Script now executing in external PowerShell version [$($PSVersionTable.PSVersion.ToString())] session in a [$([IntPtr]::Size * 8)] bit process"
        AppendLog "Running as user [$([Environment]::UserDomainName)\$([Environment]::UserName)] on host [$($env:COMPUTERNAME)]"
        AppendLog "Parameter values received: DataBusInput1=[$DataBusInput1]; DataBusInput2=[$DataBusInput2]"

        # The actual work the script does goes here
        AppendLog "Doing first action"
        # Do-Stuff -Value $DataBusInput1

        AppendLog "Doing second action"
        # Do-MoreStuff -Value $DataBusInput2

        # Simulate a possible error
        if($DataBusInput1 -ilike "*bad stuff*")
        {
            throw "ERROR: Encountered bad stuff in the parameter input"
        }

        # Example of custom result value
        $myCustomVariable = "Something I want to publish back to the runbook data bus"

        # Validate results and set return status
        AppendLog "Finished work, determining result"
        $EverythingWorked = $true
        if($EverythingWorked -eq $true)
        {
           $ResultStatus = "Success"
        }
        else
        {
            $ResultStatus = "Failed"
        }
    }
    catch
    {
        # Catch any errors thrown above here, setting the result status and recording the error message to return to the activity for data bus publishing
        $ResultStatus = "Failed"
        $ErrorMessage = $error[0].Exception.Message
        AppendLog "Exception caught during action [$script:CurrentAction]: $ErrorMessage"
    }
    finally
    {
        # Always do whatever is in the finally block. In this case, adding some additional detail about the outcome to the trace log for return
        if($ErrorMessage.Length -gt 0)
        {
            AppendLog "Exiting external session with result [$ResultStatus] and error message [$ErrorMessage]"
        }
        else
        {
            AppendLog "Exiting external session with result [$ResultStatus]"
        }
        
    }

    # Return an array of the results. Additional variables like "myCustomVariable" can be returned by adding them onto the array
    $resultArray = @()
    $resultArray += $ResultStatus
    $resultArray += $ErrorMessage
    $resultArray += $script:TraceLog
    $resultArray += $myCustomVariable
    return  $resultArray  
     
}#End Invoke-Command

# Get the values returned from script session for publishing to data bus
$ResultStatus = $ReturnArray[0]
$ErrorMessage = $ReturnArray[1]
$Trace += $ReturnArray[2]
$MyCustomVariable = $ReturnArray[3]

# Record end of activity script process
$Trace += (Get-Date).ToString() + "`t" + "Script finished" + " `r`n"

# Close the external session
Remove-PSSession $Session