Home Powershell Activity

PowerShell Activities: First impressions

Jens_SalzgeberJens_Salzgeber Customer IT Monkey ✭
I love me some PowerShell, and I wanted to offer my initial observations on P$A's. 

The first thing I noticed was that I couldn't do anything because the Licensing App wouldn't even see that there was a PowerShell App to license. Then once I updated to the newest Licensing App, it wouldn't pull the PowerShell App license with a Renew All I had to Renew Selected that license individually. After that it worked. Huzzah!

Script Object
General Tab:
Published Checkbox: This controls whether a script is selectable when adding one manually to a P$A.

Activity Object:
Visual Notes:
Overall: This activity class window size is locked (unable to resize or maximize). This makes it difficult to see the parameter mapping table and generally feels a little clunky.

There is a Task in the Task Pane labeled "Run Activity PowerShell" perhaps this is intended for manual execution. I couldn't get this to work.

General Tab:
  • Area field is mapped to the Activity Area enum list. 
  • Stage field is mapped to Activity Stage enum list.
  • Assigned To uses the Assigned To User relationship class. There is not functionality in the Notify Analyst Settings for this assignment, though I can't imagine why there would need to be as the whole point of these seems to be automation.
PowerShell Script Tab: 
  • You can only select the script before the record is created.
  • PowerShell field: Line numbers! Well done, that's great!
  • Parameter table: This is hard to see!
  • If you create a P$A template, then change the script it's linked to/based on, the template will show and run the script as it looked when the template was created. You can open and re-select the same script and it will update with any changes. In creating a template, it writes the text from the Script Object to the management pac.
# Management Pack XML
<Property Path="$Context/Property[Type='CustomCireson_Powershell_Activity_Library!Cireson.Powershell.Activity.Script']/PowershellScript$">$Environment = Get-ChildItem -Path ENV:\ | % { $_.Name.tostring() +" - "+ $_.Value.ToString() }
$PSVersionTable.PSVersion
$Environment</Property>
PowerShell Output Tab:
  • The output works just like you would expect something to write to a screen. So don't Write-Host (bad practice anyway), just output whatever you have e.g.:
# Some Code
$SomeText = 'This is some text'
$SomeText

# Will output
This is some text

Actual Scripting: 
This should be intuitive, but for those of us who are spoons in the knife drawer of life, you cannot use powershell interactively. Meaning: you cannot ask for input (things like Read-Host). Thankfully, Read-Host will cause the activity to fail and have a log entry like: 
"A command that prompts the user failed because the host program or the command type does not support user interaction. The host was attempting to request confirmation with the following message: 
Update instance"
Things like the following will just hang:
Get-Process | Out-GridView
Get-Help -Name Get-Member -ShowWindow

The installation notes state: 
  • When you have object parameters, do not leave any space between parameters, i.e. Param([Parameter(Mandatory=$True)],[string]$Description)
  • For SCSM 2012, you have to write the parameters on a single line 
So of course when told not to push the red button, it's the first thing I do. I'm running 2016 and it seemed to be able to read the params as formatted below:


Error handling:
You are going to need to handle errors. If your script would (or could) result in red text in a console (raise an error to the error stream), then your activity will show as failed and be handled by SCSM as such (aka failing the parent request and not executing subsequent activities). This seems to work with the -ErrorAction parameter, but I would really like to know how Cireson is detecting errors. Presumably if your console wouldn't be bleeding red text, your activity won't fail. 

Note: When I get the chance, and if I remember (harder) I'll upload my UserInput parser that breaks the Question/Answers into a hash table for ease of use.

Comments

  • Leigh_KildayLeigh_Kilday Member Ninja IT Monkey ✭✭✭✭
    Thanks for the informaiton, @Jens_Salzgeber.

    That there is another license key is a bit frustrating. I have only just performed this activity (we have to use the offline method) and my keys are not in the list.

    Is it possible to invoke another script from the library through a P$A? I'd like to be able to keep my scripts as modular as possible and this would be tricky if we're forced do duplicate code or resort to sequential P$As in a single WI.
  • Leigh_KildayLeigh_Kilday Member Ninja IT Monkey ✭✭✭✭
    One more thing I forgot to ask.

    Can you trigger a script from an SCSM Workflow Event? We used to insert scripts into the workflow and have it invoked by this trigger.
  • Jens_SalzgeberJens_Salzgeber Customer IT Monkey ✭
    edited March 2017
    Disclaimer: This is all from testing in 2 very similarly configured environments. YMMV

    @Leigh_Kilday
    I thought I would be able to update my original post with more information, but I'll put it here instead.
    You can trigger other scripts, but unless you specify credentials it will run as the Local System account (see below). What you'll lose by doing this is the ability in the context of the Work Item to detect error\failure conditions.
    I'm not confident I know what you're referring to regarding workflow events. There is a workflow event that fires when scripts are run manually or automatically, but it isn't specific to a script, it's to the activity. The Activity contains the PowerShellScript property and via the Cireson.Powershell.Activity.ActivityRelatesToScript you can access the source.

    Security Context:
    The scripts run using the Local System account of the workflow server (SERVERNAME$) This has a few implications. First, it means that you won't be able to use a PowerShell profile for that account. This is a highly privileged account in the machine context, and effectively unable to reach out by itself on the domain. It can't initiate PSSessions because (AFAIK) you can't grant that account access on other computers. It's possible to pass credentials to anything, the problem is how to store those credentials. I'm not aware of a secure way to store credentials in PowerShell scripts or files:
    • Cleartext is obviously unacceptable.
    • While possible to ConvertTo-SecureString and leave an encrypted version of the password in a file, it's a simple thing to ConvertFrom-SecureString and you're tied to one machine and one account.
    • When not converted to secure string, the System.Security.SecureString class can be fed into the constructor for System.Management.Automation.PSCredential objects and written back as plain text.
    • I won't go down the security rabbit hole. It's possible to manage this with keys, but now we're talking about potentially adding process and infrastructure. 
    • Pay attention, tread carefully.
    Objects
    Writing objects will not always write the properties you would see in a console. Running:
    Get-ChildItem Variable:\
    Output:
    System.Management.Automation.PSVariable
    System.Management.Automation.QuestionMarkVariable
    System.Management.Automation.PSVariable
    System.Management.Automation.LocalVariable
    System.Management.Automation.PSVariable
    System.Management.Automation.PSVariable
    System.Management.Automation.PSVariable
    System.Management.Automation.PSVariable
    [...] etc

    Modules
    Loading modules seems to work fine. I loaded smlets without issue from both the System Environment Variable and directly using local file path and UNC.

    Protip for those who might load SMLets and the Micro$oft SCSM cmdlets: 
    Add the Module name as a prefix to any cmdlet in the event of name collisions (Get-SCSMClass does this).
    This would look like:
    smlets\Get-SCSMClass -Class System.WorkItem.Incident$

    Since we're running as the local machine account two options come to mind for loading modules automatically. 
    1. Loading them using the PSModulePath system environment variable (You'd set these manually in the system properties dialog pane or use .NET. This means changing the environment variables for the whole machine, not per account.

    # Set system variable
    PS> [Environment]::SetEnvironmentVariable("TestVariable", "Test value.", "User")
    
    # remove system variable (after a reboot the system will remove the null variable)
    PS> [Environment]::SetEnvironmentVariable("TestVariable",$null,"Machine")
    

    2. Farm the execution out to other scripts or services like Task Scheduler. Depending on how you do it, this solves the credentials issue, and allows account level PSProfile use, but incurs the penalty of requiring manual password maintenance on the scheduled tasks. If your environment uses service accounts with passwords that don't change and you've already accepted the risk associated there, that's an easy solution. It's also possible to script changing the password on scheduled tasks. I'm uneasy creating automation to maintain automation. 

    Wish List
    Templates need to reference the source script and not cache the script. The more I play with it, the more annoying it is that activity templates don't update when the script changes. This is a problem.
    Oddly, the system struggles to resolve parameter sets when you don't specify the parameter directly. For example:
    # This will work
    Get-SCSMClass -Name System.WorkItem.ServiceRequest$
    
    # This will not
    Get-SCSMClass System.WorkItem.ServiceRequest$

  • Billy_WilsonBilly_Wilson Member Ninja IT Monkey ✭✭✭✭
    Thanks for the feedback Jens! If you get a chance to upload that input parser, that'd be most helpful.
  • Leigh_KildayLeigh_Kilday Member Ninja IT Monkey ✭✭✭✭

    Security Context:
    The scripts run using the Local System account of the workflow server (SERVERNAME$) This has a few implications. First, it means that you won't be able to use a PowerShell profile for that account. This is a highly privileged account in the machine context, and effectively unable to reach out by itself on the domain. It can't initiate PSSessions because (AFAIK) you can't grant that account access on other computers. It's possible to pass credentials to anything, the problem is how to store those credentials. I'm not aware of a secure way to store credentials in PowerShell scripts or files:
    • Cleartext is obviously unacceptable.
    • While possible to ConvertTo-SecureString and leave an encrypted version of the password in a file, it's a simple thing to ConvertFrom-SecureString and you're tied to one machine and one account.
    • When not converted to secure string, the System.Security.SecureString class can be fed into the constructor for System.Management.Automation.PSCredential objects and written back as plain text.
    • I won't go down the security rabbit hole. It's possible to manage this with keys, but now we're talking about potentially adding process and infrastructure. 
    • Pay attention, tread carefully.
    SCSM has defined 'Run As Accounts' that would be ideal to use here. @Billy_Wilson, is this the plan already, or should I create a Feature Request?
  • Jens_SalzgeberJens_Salzgeber Customer IT Monkey ✭
    edited April 2017
    If there's a better place to upload this, let me know, here's the script I use to parse userinput.

    It takes pipeline input fine, or you can feed it the ID of a workitem. It does need SMLets. I usually keep this function in my utility module so I can just load that in the P$A. This outputs the Question, Answer, Live Objects, and the type. 

    #Requires -Module SMLets
    function Get-SCSMUserInput
    {
        [CmdletBinding(DefaultParameterSetName='GUID')]
        [OutputType([String])]
        Param
        (
            # Takes the Id of a record. E.g. SR123456
            [Parameter(Mandatory=$true, 
                       ValueFromPipeline=$true,
                       ValueFromPipelineByPropertyName=$true, 
                       ValueFromRemainingArguments=$false, 
                       Position=0,
                       ParameterSetName='Id')]
            [ValidateNotNullOrEmpty()]
            [string]
            $Id,
    
            # Takes the __InternalId of the record (GUID)
            [Parameter(ParameterSetName='GUID')]
            [ValidateNotNullOrEmpty()]
            [Alias("__InternalId")] 
            [GUID]
            $GUID
        )
    
        Begin
        {
            $WIClass = smlets\Get-SCSMClass System.workitem$
        }
        Process
        {
            If ($PSCmdlet.ParameterSetName -eq 'Id') {
                $WorkItem = Get-SCSMObject -Class $WIClass -Filter "Id = $Id"
            }
    
            If ($PSCmdlet.ParameterSetName -eq 'GUID') {
                $WorkItem = Get-SCSMObject -Id $GUID
            }
            
    
            [xml]$UserInput = $WorkItem.UserInput
    
            Foreach ($Node in $UserInput.UserInputs.ChildNodes) {
                
                # Return useful objects
                $Object = switch ($Node.Type)
                                {
                                    # return an enum obect
                                    'enum' {
                                        Get-SCSMEnumeration -Id $Node.Answer
                                    }
    
                                    # return config item objects
                                    'System.SupportingItem.PortalControl.InstancePicker' {
                                        # .replace is necessary because SCSM will pass unescaped ampersands -_-
                                        [xml]$ItemList = $Node.Answer.Replace("&", "&amp;")
                                        ForEach ($Item in $ItemList.Values.ChildNodes) {
                                            Get-SCSMObject -Id $Item.id
                                        }
                                    }
                                    # For anything else (bool, string, datetime, int, file attachments, floats) ignore it
                                    Default {}
                                }
                
                $Props = [ordered]@{
                    Question = $Node.Question
                    Answer = $Node.Answer
                    Object = $Object
                    Type = $Node.Type
                }
                
                $Obj = New-Object PSObject -Property $Props
                $Obj.pstypenames.Insert(0,'UserInput')
                $Obj
                            
            }
        }
        End
        {
        }
    }<br><br>
    @Billy_Wilson
  • Billy_WilsonBilly_Wilson Member Ninja IT Monkey ✭✭✭✭
    Nice! - Thanks @Jens_Salzgeber !
  • Adam_DzyackyAdam_Dzyacky Product Owner Contributor Monkey ✭✭✭✭✭
    Am I the only one who'd like to see the ability to schedule PowerShell Activity tasks leveraging this? In the same vein of "If you don't have SMA" it seems as though this would be even more powerful if you could create scheduled P$A jobs. This would open up a lot of possibilities such as easing the creation of custom connectors, not having to wrap powershell scripts into MPs, etc.

    You probably wouldn't get the SCSM Workflow granularity of an individual job, but it feels like a fair trade off having a PowerShell script workflow job that just encapsulates all scheduled jobs made through this. If a feature request for this hasn't been raised, I'll certainly submit.
  • Adam_DzyackyAdam_Dzyacky Product Owner Contributor Monkey ✭✭✭✭✭
    edited May 2017
Sign In or Register to comment.