Running PowerShell Scripts from a Management Pack - Part 1

Considering all my work with OpsMgr management packs and my infatuation with PowerShell, it's surprising that I've never put the two together in a public forum.  I keep meaning to, just don't ever seem to get there.  I finally committed myself to this blog post while sitting at my local It's a Grind (that's a coffee house to all you Seattle Starbucks fans.  It's a Grind was born in Long Beach, and I am fanatically loyal to my home town).

So here's the basic question - can I use a PowerShell script in an OpsMgr management pack?  In other words, can I execute a PowerShell script from a rule, monitor, diagnostic, recovery, or task?  The answer is absolutely you can, but there are some considerations to keep in mind.  First let's cover those considerations, then we'll get to concepts and code.  If you just want the modules to copy and paste into your MP, go ahead and jump to the end.

The biggest issue is that most of the servers in your environment don't have PowerShell installed.  Rules and monitors sent to an agent are executed locally on the agent computer, and any executables must be installed prior to the agent attempting to use them.  In the case of VBScript and JScript, we rely on cscript.exe being present, but that's a safe bet since it is automatically installed on all Windows machines.  Powershell is obviously not installed by default, and even in Windows Server 2008 it's an optional feature.  If you want to use a PowerShell script for a console task, then you're going to need to make sure that it is installed on the client workstation the task is being executed from.  This is less of a concern though since, while PowerShell and Command Shell are not required for the OpsMgr User Interfaces, they are pretty strongly suggested.

There can also be some significant overhead from launching PowerShell.  Go ahead and launch a PowerShell window and then have a look at Task Manager.  Powershell.exe will probably be consuming something like 30 MB of memory which is about 5x the typical cscript.exe instance in my personal testing.  This is not surprising considering all the rich functionality that PowerShell provides.  My only point is to try to stay away from scenarios where you need to launch a PowerShell script every couple of minutes.  I've heard the OpsMgr product team is working on some strategies to reduce this overhead, but until then you want to use PowerShell scripts where they don't have to be launched too frequently.

Concepts

The basic idea of running a PowerShell script is to use of the Command Executer modules to launch powershell.exe with your script.  This is actually the method that quite a few of the scripts in existing management packs use - calling cscript.exe with these modules.  The Command Executer modules will launch the executable of your choice, allow you to specify command line arguments, and allow you to specify the name and contents of one or more text files (which will obviously be your script).  These text files are created on the agent prior to command execution, so you can be guaranteed they will be in place when your specified command is launched.

Have a look at Microsoft.Windows.ScriptProbeAction in Microsoft.Windows.Library for example.  That module uses System.CommandExecuterProbe from System.Library.  It specifies cscript.exe as the command to execute, provides the appropriate command line arguments such as /nologo and the name of the script, and then passes in the body of the script as a file.  In order to execute Powershell, we really just need to figure out the command line required to launch PowerShell, execute a script, and then exit.

PowerShell Command Line

If you run powershell.exe /?, you get the command line arguments for PowerShell.  To launch a command and exit, you use the -command argument, the  invoke operator (&), and a command to execute.  The example syntax given by that help is as follows:

powershell -command "& {get-eventlog -logname security}"

We're going to need that basic syntax but put it in a format that the management pack can understand.  First, we're go to have to have to specify a path for the script.  PowerShell demands a complete path to a script even if it's in the current directory.  The Command Executer will drop the script to a temporary directory and then use that directory as its default when it executes the script, so we can assume the script will be located in the current directory.  Assuming that we are going to use a parameter called ScriptName for the name of the script and Arguments for its command line arguments, then the command line in the management pack would look like the following:

-Command "&{.\$Config/ScriptName$ $Config/Arguments$}"

$Config/ScriptName$ and $Config/Arguments$ are context variables.  OpsMgr will replace the variable inside the dollar signs with its actual value at run time.  Config refers to the parameters of the module, and ScriptName is the name of the parameter.  Finally, the .\ just refers to the current directory. 

Another thing to consider is that the execution policy of the local machine may not allow your script to be run.  Rather than worrying about setting it on every agent, we can just include the Execution Policy as part of the command line which will set it for the session.  Now our command line becomes:

-ExecutionPolicy RemoteSigned -Command "&{.\$Config/ScriptName$ $Config/Arguments$}"

Finally, the management pack is not going to like that ampersand because it’s going to confuse the XML.  We have the option of either replacing it with & which will resolve to & when the management pack is loaded, or we can enclose the entire command line in CDATA tags which tells the XML not to try to interpret anything inside the tags.  That’s the solution I’m going with which brings our final command line to:

<![CDATA[-ExecutionPolicy RemoteSigned -Command "&{.\$Config/ScriptName$ $Config/Arguments$}"]]>

So, if we used MyScript.ps1 for the script name and argument1 as the argument, we would end up running the following command:

powershell -ExecutionPolicy RemoteSigned -Command "& {.\MyScript.ps1 argument1}"

 

Implementing the Modules

Rather than write a specific rule that uses the Command Executer modules and includes the specific PowerShell complexity, it is way more valuable to create a couple of base modules that can be leveraged by rules, monitors, and tasks.  A Data Source module that runs a PowerShell script on a timed basis and returns a property bag could be used for a rule, monitor, or diagnostic.  Another Data Source module returning discovery   A Write Action module that just runs a PowerShell script on demand to perform some defined action would support a task or recovery.

I'll provide the data source below.  Given this it should be pretty straightforward to create a write action (hint - use System!System.CommandExecuter).  You could also create a discovery module based on System!System.CommandExecuterDiscoveryDataSource.

 

<DataSourceModuleType ID="PowerShell.Library.PSScriptPropertyBagSource" Accessibility="Public" Batching="false">
   <Configuration>
     <xsd:element minOccurs="1" name="IntervalSeconds" type="xsd:integer" />
     <xsd:element minOccurs="1" name="TimeoutSeconds" type="xsd:integer" />
     <xsd:element minOccurs="1" name="ScriptName" type="xsd:string" />
     <xsd:element minOccurs="1" name="Arguments" type="xsd:string" />
     <xsd:element minOccurs="1" name="ScriptBody" type="xsd:string" />
   </Configuration>
   <ModuleImplementation>
     <Composite>
       <MemberModules>
         <DataSource ID="DS" TypeID="System!System.CommandExecuterPropertyBagSource">
           <IntervalSeconds>$Config/IntervalSeconds$</IntervalSeconds>
           <ApplicationName>%windir%\system32\windowspowershell\v1.0\powershell.exe</ApplicationName>
           <WorkingDirectory />
           <CommandLine><![CDATA[-ExecutionPolicy RemoteSigned -Command "&{.\$Config/ScriptName$ $Config/Arguments$}"]]></CommandLine>
           <SecureInput />
           <TimeoutSeconds>$Config/TimeoutSeconds$</TimeoutSeconds>
           <RequireOutput>true</RequireOutput>
           <Files>
             <File>
               <Name>$Config/ScriptName$</Name>
               <Contents>$Config/ScriptBody$</Contents>
               <Unicode>true</Unicode>
             </File>
           </Files>
         </DataSource>
       </MemberModules>
       <Composition>
         <Node ID="DS" />
       </Composition>
     </Composite>
   </ModuleImplementation>
   <OutputType>System!System.PropertyBagData</OutputType>
</DataSourceModuleType>

 

 

Writing the Script

For the write action, there's really nothing special about the script.  Any working PowerShell script will be fine.  If you need to return a property bag or discovery data from a data source though, you're going to need to use MOM.ScriptAPI just like in VBScript.  PowerShell works fine with COM objects, so this is not a problem at all.  The following line will create an object variable, and using it is pretty similar to how you did it in VBScript.

$api = new-object -comObject "MOM.ScriptAPI"

For example, below is a PowerShell script to output the name and size of a specified file as a property bag.  This could be called from a rule or monitor that uses a condition detection to map the property bag information to performance data. While this is coded in PowerShell, the basic process is identical to a script.

$file = Get-Item $args[0]
$api = New-Object -comObject "MOM.ScriptAPI"
$bag = $api.CreatePropertyBag()
$bag.AddValue($file.Name,$file.Length)
$api.Return($bag)