Scheduling Groups of Objects for Maintenance Mode

I was given a challenge recently to come up with a solution for an organization that wanted to suppress any alerts for certain computers and services during a particular time window every night.  The particular set of objects and the times in question had to be manageable by the administrators - preferably from the Operations Console.

This is a great example for a few concepts, so this will be a pretty long post where I'll describe each step along the way.  If you just want the answer, scroll to the link at the end for the sample MP.  Otherwise, you should find some good information on the following concepts:

  • Launching a PowerShell script from a rule
  • Composing a rule made up of a data source and write action
  • Replacing explicit values in a rule with overrides
  • Creating a simple custom class

First of all, the obvious answer to suppressing alerts is to get those objects into maintenance mode.  That way we not only aren't bothered by the alerts, but we can also remove this time window from any availability reporting.  The questions that we're faced with though are:

  1. How can we schedule maintenance mode on a regular basis?
  2. How can we set maintenance mode for a set of objects?
  3. How can we give control to the administrators through the Operations Console?

One answer for our challenge might be to write a Command Shell script to put an object in maintenance mode and then launch it from the Windows Scheduler.  That would handle question 1, but it would leave us hanging on questions 2 and 3.

A very straightforward solution would be to create a group in OpsMgr.  Instead of setting maintenance mode for a single object, the script could put each member of the group in maintenance mode.  Administrators could easily populate the group with any objects that should be included in the maintenance window.  Assuming we had a script that could do that group enumeration, question 2 is covered.  In order to really address question 3, we would need to launch the script from an OpsMgr management pack instead of the Windows Scheduler and have the ability to overwrite critical parameters. 

So that leaves us with two challenges - setting maintenance mode for a group of objects and creating a rule to launch the script from a management pack.  Let's take them one at a time.  First the script.....

Setting Maintenance Mode on a Group of Objects

We're going to need Command Shell in order to enumerate the group and set maintenance mode.  I'm assuming that we're going to end up creating a rule calling powershell.exe as described in my previous blog post, so we're going to need to need the script to include the code to load the Command Shell snap-in and setup the connection to the management server.  When we launch Command Shell from the menu, this is done automatically done for us.

The script should accept the group name as an argument and then enumerate all members of that group.  In specific OpsMgr terms, that means that we need to enumerate the objects that are target of a Containment relationship with the group's class. 

If we use the New-MaintenanceWindow CmdLet, then we would need to write a function to recurs through the group members and follow each group members down through its set of hosted and contained objects.  Rather than use that CmdLet, I'm going to use the ScheduleMaintenanceMode method on the monitoring object class.  That method includes an argument called TraversalDepth.  A value of "OneLevel" means that only the object itself will be put into maintenance while a value of "Recursive" means that we will follow hosting and containment relationships all the way down the object tree setting maintenance mode on each object along the way. A middle ground would be nice where we could just address the second level of objects, which in our case would be the specific group members and not their contained objects, but that's a scenario we're going to have to address on our own.

I thus have two scenarios I want the script to support - setting maintenance mode for just the objects contained in the group and setting maintenance mode for those objects in addition to all of their contained objects.  The second scenario is actually easier since we just execute ScheduleMaintenanceMode against the group with TraversalDepth set the "Recursive".  For the first method, we're going to have the enumerate the group contents ourselves and then set maintenance mode for each one.

With all of that said, my script is below.  Notice that I'm accepting as arguments the name of the group (not to be confused with the Display Name), the number of hours for maintenance, whether to include hosted objects, and the description.  We'll make most of these overrideable on the rule so the administrator has control over them.

 $groupName = $args[0]
$HoursInMaintenance = $args[1]
$IncludeHostedObjects = $args[2]
$Description = $args[3]
$CommandShellPath = $args[4]

Add-PSSnapin "Microsoft.EnterpriseManagement.OperationsManager.Client";
cd "$CommandShellPath"
.\Microsoft.EnterpriseManagement.OperationsManager.ClientShell.Startup.ps1

$api = new-object -comObject "MOM.ScriptAPI"
$api.LogScriptEvent("SetMaintenanceMode",100,4,"Maintenance mode set for all members of group: $groupName")

$startTime = (Get-Date).ToUniversalTime()
$endTime = $startTime.AddHours($HoursInMaintenance)

$group = get-monitoringClass -name $groupName | get-monitoringObject

if ($IncludeHostedObjects -eq $true) 
{
    $group.ScheduleMaintenanceMode($startTime,$endTime,"PlannedOther",$Description,"Recursive")
}
else
{
    $classContain = get-relationshipClass -name 'System.Containment'
    $relations = $group | get-relationshipObject | where {$_.SourceMonitoringObject -eq $group} 
    $groupMembers = $relations | foreach {$_.TargetMonitoringObject}

    foreach ($object in $groupMembers)
    {
        $object.ScheduleMaintenanceMode($startTime,$endTime,"PlannedOther",$Description,"OneLevel")
    }
}
  

Creating the Rule

The next question is how we're going to launch that script and give administrators control over the schedule.  We could do that from the Windows Scheduler, but it would be more straightforward for the administrator if we put it into an OpsMgr rule.  If we configure it correctly, we can allow our administrators to use overrides to control the schedule, length of maintenance mode, and whether to include contained objects. 

Data Source

The rule will need a data source and a write action.  Since we want to execute the script on a nightly schedule, System.Scheduler will be a perfect data source.  There is one little trick with that data source though.  It allows you to define multiple schedules that have a start and a stop time.  In the context we are going to use it though, the stop time is ignored.  It will have to be larger than the start time to prevent errors, but other than that it won't be used. 

You could just use System.Scheduler unchanged in which case you'll get the benefit of the dialog box for setting multiple schedules.  In that case, you would need to modify the rule itself to change the schedule though.  Since I want to control it through overrides I'm going to limit us to a single schedule, and I'm going to have to set the parameters myself.  The Start and End times are pretty obvious.  The DaysOfWeekMask is a little confusing.  This is the integer value of a bit mask with each day representing a single bit.  If you're familiar with bit masks you won't have a problem. If not, just determine the appropriate value by using the following values for each day of the week: Su-1, M-2, T-4, W-8, Th-16, F-32, Sa-64.  Just add up the values for the days you want the script to run.  If you want all days of the week, they'll add up to 127.  If you just want Sunday, the value is 1.  Just Tuesday and Thursday would be 20.

With all of that said, here's a valid data source for running the script every day of the week at 10:00 pm.

    <DataSources>
<DataSource ID="Scheduler" TypeID="System!System.Scheduler">
<Scheduler>
<WeeklySchedule>
<Windows>
<Daily>
<Start>22:00$</Start>
<End>23:59</End>
<DaysOfWeekMask>127</DaysOfWeekMask>
</Daily>
</Windows>
</WeeklySchedule>
<ExcludeDates/>
</Scheduler>
</DataSource>
</DataSources>

 

To define overrides though, we have to create a new module type.  Rather than using this data source directly in the rule, I'll create a data source allowing the StartTime and DaysOfWeekMask to be overriden.  The rule will then use the new data source module.  Notice in the data source below that I've changed the changed the values for <Start> and <DaysOfTheWeeMask> to overrideable values that will be provided by the rule itself.

<DataSourceModuleType ID="bwren.Maintenance.DataSource.Scheduler" Accessibility="Public">
<Configuration>
<xsd:element name="StartTime" type="xsd:string" xmlns:xsd="https://www.w3.org/2001/XMLSchema"/>
<xsd:element name="DaysOfWeekMask" type="xsd:integer" xmlns:xsd="https://www.w3.org/2001/XMLSchema"/>
</Configuration>
<OverrideableParameters>
<OverrideableParameter ID="StartTime" ParameterType="string" Selector="$Config/StartTime$"/>
<OverrideableParameter ID="DaysOfWeekMask" ParameterType="int" Selector="$Config/DaysOfWeekMask$"/>
</OverrideableParameters>
<ModuleImplementation>
<Composite>
<MemberModules>
<DataSource ID="DS" TypeID="System!System.Scheduler">
<Scheduler>
<WeeklySchedule>
<Windows>
<Daily>
<Start>$Config/StartTime$</Start>
<End>23:59</End>
<DaysOfWeekMask>$Config/DaysOfWeekMask$</DaysOfWeekMask>
</Daily>
</Windows>
</WeeklySchedule>
<ExcludeDates/>
</Scheduler>
</DataSource>
</MemberModules>
<Composition>
<Node ID="DS"/>
</Composition>
</Composite>
</ModuleImplementation>
<OutputType>System!System.TriggerData</OutputType>
</DataSourceModuleType>

Write Action

The write action is just a matter of launching our PowerShell script.  We'll again need a new module type to handle overrides.  That strategy has other benefits anyway, since we can put the complexity in the module type and keep the rule simple.  That will make it easy to create other rules for other groups.

The module type using our script is shown below.  I won't bother explaining all the details since you can get that from the PowerShell post.  The formatting gets pretty screwed up here, so you might just want to have a look at it in the sample MP.

            <WriteActionModuleType ID="bwren.Maintenance.WriteAction.SetMaintenance" Accessibility="Public">
<Configuration>
<xsd:element name="GroupName" type="xsd:string" xmlns:xsd="https://www.w3.org/2001/XMLSchema"/>
<xsd:element name="HoursInMaintenance" type="xsd:integer" xmlns:xsd="https://www.w3.org/2001/XMLSchema"/>
<xsd:element name="IncludeHostedObjects" type="xsd:string" xmlns:xsd="https://www.w3.org/2001/XMLSchema"/>
<xsd:element name="Description" type="xsd:string" xmlns:xsd="https://www.w3.org/2001/XMLSchema"/>
</Configuration>
<OverrideableParameters>
<OverrideableParameter ID="HoursInMaintenance" Selector="$Config/HoursInMaintenance$" ParameterType="int"/>
<OverrideableParameter ID="IncludeHostedObjects" Selector="$Config/IncludeHostedObjects$" ParameterType="string"/>
</OverrideableParameters>
<ModuleImplementation Isolation="Any">
<Composite>
<MemberModules>
<WriteAction ID="WA1" TypeID="System!System.CommandExecuter">
<ApplicationName>%windir%\system32\windowspowershell\v1.0\powershell.exe</ApplicationName>
<WorkingDirectory/>
<CommandLine>-Command "&amp; {.\SetMaintenance.ps1 $Config/GroupName$ $Config/HoursInMaintenance$ $Config/IncludeHostedObjects$ '$Config/Description$'}"</CommandLine>
<SecureInput/>
<TimeoutSeconds>30</TimeoutSeconds>
<RequireOutput>true</RequireOutput>
<Files>
<File>
<Name>SetMaintenance.ps1</Name>
<Contents><![CDATA[
$groupName = $args[0]
$HoursInMaintenance = $args[1]
$IncludeHostedObjects = $args[2]
$Description = $args[3]

Add-PSSnapin "Microsoft.EnterpriseManagement.OperationsManager.Client";
cd C:\"Program Files"\"System Center Operations Manager 2007"
.\Microsoft.EnterpriseManagement.OperationsManager.ClientShell.Startup.ps1

$startTime = (Get-Date).ToUniversalTime()
$endTime = $startTime.AddHours($HoursInMaintenance)

$group = get-monitoringClass -name $groupName | get-monitoringObject

if ($IncludeHostedObjects -eq $true)
{
$group.ScheduleMaintenanceMode($startTime,$endTime,"PlannedOther",$Description,"Recursive")
}
else
{
$classContain = get-relationshipClass -name 'System.Containment'
$relations = $group | get-relationshipObject | where {$_.SourceMonitoringObject -eq $group}
$groupMembers = $relations | foreach {$_.TargetMonitoringObject}

    foreach ($object in $groupMembers)
{
$object.ScheduleMaintenanceMode($startTime,$endTime,"PlannedOther",$Description,"OneLevel")
}
}
]]></Contents>
<Unicode>true</Unicode>
</File>
</Files>
</WriteAction>
</MemberModules>
<Composition>
<Node ID="WA1"/>
</Composition>
</Composite>
</ModuleImplementation>
<OutputType>System!System.CommandOutput</OutputType>
<InputType>System!System.BaseData</InputType>
</WriteActionModuleType>

 

Setting the Rule Target

With those modules created, we need a rule that includes each data source and specifies the required arguments.  The biggest question is what to use as its target.  Technically, we can target anything that will end up deploying to an agent with PowerShell and Command Shell installed.  We only want a single agent to get it though.  We also want to make sure that our target is going to be accessible to the server admins.  The root management server typically has Command Shell installed, and this is a perfect candidate for this task.  Chances are pretty good though that we don't want to give permission to our administrators to author rules against our RMS.

I had thought of targeting the group itself since, as described in another previous post the root management server "hosts" groups, so that's where the rule would get deployed.  I had some issues with this working reliably though, and I also thought it might get a bit confusing.

The solution I went with was to create a new class based on ComputerRole called CommandShellProxy. This can serve as the target for any rules I want to create that launch a Command Shell script.  I can give permission to different people to override rules targeted at it without giving them access to the rules and monitors directly targeting the RMS.  As an added benefit, I stored the path to the PowerShell executable and the Command Shell snap-in so I don't have to hardcode those into the rule.

<ClassType ID="bwren.MaintenanceMode.CommandShellProxy" Accessibility="Public" Abstract="false" Base="Windows!Microsoft.Windows.ComputerRole" Hosted="true" Singleton="false">
<Property ID="PowerShellPath" Type="string"/>
<Property ID="CommandShellPath" Type="string"/>
</ClassType>

I did a simple registry discovery for my new class and targeted it at the Root Management Server class.  The only criteria I'm using is determining whether Command Shell is installed.  If you want to use something other than the RMS as your Command Shell proxy, all you would have to do is change that discovery.

I didn't bother pasting in the discovery XML since I've already cluttered up this post with enough code.  You can have a look at it in the sample MP though.

 

End Result

Go to the bottom of this post for a link to the sample MP.  You can go through the following steps to get it up and running:

1.  Load the management pack

image

 

2.  Add an object or two to the group Maintenance Mode Group 1.

image

 

3.  Make sure that you are discovering the Command Shell Proxy by selecting Discovered Inventory.  Then click on Change Target and select Command Shell Proxy from the list.

 image

 

4.  Go to the Authoring tab and look at rules for the target Command Shell Proxy.

image

 

4.  Create an override on the rule Set maintenance mode - group 1, and set appropriate values for StartTime, HoursInMaintenance, IncludeHostedObjects, and Description.  The allowable values for IncludeHostedObjects are $true and $false (those are the PowerShell variables for True and False.  You could do a little script work to allow a boolean in the override and then convert appropriately if you really want).

image

 

I know this is a long and detailed post, but it seemed like a good opportunity to illustrate a few points.  Hope it's helpful.

bwren.MaintenanceMode.xml