OpsMgr Scripting: WYWINNWYG
"What You Write Is Not Necessarily What You Get"
OpsMgr 2007 has a fairly substantial library of discovery module types and monitor types that ship with it. Even with that library, it seems that the single most important extensibility point in OpsMgr today is the ability to use custom scripts to perform discovery and monitoring tasks. As I've developed scripts for OpsMgr, I have learned a great deal about how OpsMgr processes scripts. This article describes some important concepts regarding how scripts are processed by OpsMgr.
Besides the Execution Environment...
There is an important distinction between the execution environment of a script and the production of the script itself.
The execution environment is fairly straight-forward: VBScript or JScript that runs through the script processor. In fact, if you trace back the various script modules, you will find that they all derive from a handful of native modules, all of which essentially execute cscript.exe. All of the capabilities of that scripting engine and the facilities it can access are at your disposal (ADSI, WMI, ADO, the remainder of what's in COM, etc.) We also use a COM object in our OpsMgr scripts that is on every system that has the OpsMgr Agent installed, namely "MOM.ScriptAPI". This essentially provides functionality to generate well-formed XML DataItems with the appropriate format, attributes, etc. We'll see that later. While that's a simplification of what "MOM.ScriptAPI" does, the point is that it is not part of some custom script execution environment that runs in-process with OpsMgr. It is just another COM object that the usual cscript.exe environment instantiates at our request.
The production of the script is quite a bit more interesting, so I'll spend the majority of this article addressing it.
Sample 1: Expansion of Embedded Selectors
First, let's define a simple script, which we'll put inside a two-state script unit monitor. This script targets Microsoft.Windows.NTService. I've also created two service classes from the MP templates in OpsMgr, one for Browser and one for SNMP. This virtually guarantees that OpsMgr will discover at least one on any given system. My test system has both.
Figure 1: The script itself. (FYI - The $...$ selector that is not entirely in view is "$Target/Host/Property[Type="System!System.Entity"]/DisplayName$")
Figures 2a and 2b: The unhealthy and health expressions. These are somewhat unrelated to the topic of this article, but I thought I'd add this for general purposes. It is useful to see this and to note that any time you see wizard pages that resemble these, you are using the UI to express the configuration of the System.ExpressionFilter condition detection module. In this case, I've defined a monitor that always returns healthy, regardless of what the script does. You can experiment with this further, but keep in mind that you can specify just about anything for either side of each expression (i.e. row on the wizard page), including the entire gamut of $...$ selectors. ...end aside...
Returning to the main topic, anyone that is a script developer has a particular paradigm in mind. The script is written and runs as written. If you've ever seen constructions that use the $...$ selectors inline (e.g. SomeVar = "$MPElement...$"), you might have assumed that there was something special about the script execution environment that somehow resolves those. The reality is that those resolutions are done before the script is ever executed. Therefore, we have a very tricky paradigm on our hands: the execution environment is completely standard, but the script body itself is "refined" by OpsMgr before it is embodied in an actual file and run.
Let's investigate this. Our example script does nothing of interest except return a static property bag. That will be somewhat useful later, but for now, the really interesting part is not what the script does, but what the script becomes.
To find the actual file that is eventually run, we need to get to the Health Service State directory and find our script. We will actually find two versions of the script, because it is generated once for each target against which the script will run (in this case, Browser and SNMP).
Figure 3a: Finding the script location.
Figure 3b: What's finally in the first instance.
Figure 3c: What's finally in the second instance.
As you can see from the figures, everything that relates to the execution context of the script is not present in the execution environment per se, but baked into the script itself as it is readied to run. This includes information about the target, the MPElement on whose behalf the script is being invoked, any MPElement we may need (which I grant you is usually only required for discovery scripts), and information about the target's host (which is also information from the $Target...$ selector family).
It is useful to understand this mechanism for two reasons:
- You do not need to put everything into the arguments of the script that may seem necessary. The script will be customized per target. This opens up a wide array of information that can be used in the script without worrying about the command line formatting.
- It is contrary to our normal thought process for scripting. Understanding it provides a better picture of how your script will be invoked and it also reveals how these OpsMgr-specialty-laden scripts can be debugged.
In a practice, I always include a comment line at the beginning of my scripts that includes the first four items in our sample script: the basic identification of the Target and MPElement. I find that to be extremely useful in tracking down script errors.
Sample 2: More Embedded Selectors: From <Configuration>, Including Overrides
Continuing our example, let's set some overrides that will make the two instances unique with respect to their configuration as well as their target.
Figure 4a: Overrides targeted to the specific service object Browser (on the test system).
Figure 4b: Overrides targeted to the specific service object SNMP (on the test system).
I also did two other things:
- I added a few additional lines to the script
' Some Config Values
' Arguments: $Config/Arguments$
' Interval Seconds: $Config/IntervalSeconds$
' A service-specific property:
- I created another monitor that is almost identical to the first one. The script itself is actually identical, but this second monitor defines the script in a UnitMonitorType and uses that custom UnitMonitorType in the UnitMonitor. By default, script monitors created by the UI use a built-in UnitMonitorType, which means that the script body itself is defined in the UnitMonitor. There is a subtle difference that we will discuss shortly.
Once the new configuration becomes active, we follow the same pattern to find our scripts and see what's in them.
Figure 5a: Finding the script location. There will be four now: the original and the UnitMonitorType variant (UMT) for both instances.
Figure 5b and 5c: The contents of the two updated but UI-defined scripts.
Note that the additional property of the target came through fine, but the $Config$ selectors were unchanged. This means that using a UI-defined script, the $Config$ selectors are not expanded. This applies to both the "natural" configuration items and any overrides.
What's going on here? Let's check out UMT version.
Figure 5d and 5e: The contents of the two updated, UMT-defined scripts.
Now, that's what we're looking for. I must admit that while I have a reasonable explanation for this, I cannot give you a definitive answer. I'll leave that for a developer that knows the exact inner workings of OpsMgr, but it appears that the $Config$ selectors are only expanded when module and monitor types are assembled for use in a workflow. Think of it this way: when something defined in the <TypeDefinitions> section of an MP is assembled to be used in the <Monitoring> section of an MP, $Config$ selectors are expanded. The UI-defined monitor defines the script body in the <Monitoring> section. It uses a built-in module that itself defines the script body as whatever is in the <Configuration> of the actual monitor $MPElement$, which is defined in the <Monitoring> section. Therefore, our UI-defined script body is never really subjected to assembly in that way. The UMT version, with the script defined in the UMT (under <TypeDefinitions>) is assembled for use in the corresponding monitor, so we see the expansion for it. Both are then assembled for use against the various targets that exist, which is why everything else expands in either case.
Important Notes About Expansion
There are a few additional important notes on this expansion business:
- You must use a script argument for anything that changes between invocations of the script. The script is created only when a new configuration becomes active. That only happens when management packs change. There's a myriad of things that cause a configuration change, such as group membership rules, new monitors, new subscriptions, overrides, etc., but there are an equally large number of changes to the environment that do not cause a configuration change. Some of these include group membership updates that are the result of a dynamic rule, target property changes that are the result of discovery runs, etc. Suffice it to say that a script can run a multitude of times under the same configuration. This is particularly important for:
- $Data$ selectors. There's no way to expand a $Data$ selector when the script instance is created. These are pulled from the contents of the <DataItem> that is passed into the module executing the script and are therefore only available as script arguments. If you've never seen these, they're pretty useful. For any timed script, my favorite is $Data/@time$. You can pass that as an argument to have the trigger time of the monitor at your disposal, in the appropriate OpsMgr <DataItem> attribute format.
- $Target$ selectors. We saw that these work wonderfully as embedded selectors, but you must be careful with them. They are expanded when the script is created, which is when a new configuration becomes active. Some $Target$ properties might change between the time the script is created from the then-active configuration and something happens that causes a new configuration to become active (and hence the script to be re-created). If there's a $Target$ property that can afford to be a little stale, you can embed it. If not, pass it as an argument.
- $MPElement$ selectors are all tied to the configuration, so they're safe. When they change, the configuration is changing, so the script will be re-created anyway.
- I have encountered some quirks, so don't think you're losing your mind if you see strange things in your script. I suspect that some combinations of selectors embedded in scripts are not expanded properly, but I have been thus far unable to reliably reproduce the problem. It definitely happens, so be on the lookout.
- The MP alias used in the selectors must be appropriate to the MP in which the monitor is being created. If you are creating an MP yourself, that's easy to keep straight. If not, you may not be able to predict the alias. System.Library, for instance, is sometimes referenced as System! and sometimes as SystemLibrary<Version>!, where <Version> is whatever version of that MP was installed when the reference was made. An easy way to determine this is to add something as an argument using the UI. It will use the correct alias, and you can just copy it from there.
Debugging the Expanded Script
The final item that is relevant is how this can help debugging. This goes back to the execution environment discussion I started with. When the MOM.ScriptAPI COM object is used to submit property bags, performance data, discovery data, etc., it does so using STDOUT. That's it. Therefore, running your script from the location in which it was created (under Health Service State) works just fine. It just prints the XML representation of the data item that would be picked up by OpsMgr if the script had been run inside its workflow.
Figure 6: A property bag output to STDOUT after an interactive run of the script.
I hope you find this information useful. I believe it is very helpful to understand the machinations that a script undergoes well before it even hits its execution environment. It's also useful to have so many more data from the context of the OpsMgr configuration, target, etc. available to use. It makes scripting that much more robust, which is a major component of OpsMgr's appeal.