I wrote an MP last year that I thought might be useful for the community. Of course this is just an example and needs some work (error handling, testing, etc…). The purpose of the MP is to alert when a resource is not on its preferred cluster node.

This MP supports both Windows Server 2003 and 2008.

How it works:

  • A rule is targeted to the Microsoft.Windows.Cluster.VirtualServer Class
  • The rule runs a script every x minutes (currently 5 minutes) and the script detects if a group is on its preferred node
  • If it isn’t on its preferred node after x number of iterations (currently 2) then an alert is fired
  • The alert contains the information about the cluster configuration

***Update***
I made some changes to the MP.
The changes are the following:
1.  I changed the script to cookdown so it only runs once per agent
  a. I create a property bag for each cluster group
  b. I modified the data source so it accepts 0 arguments
  c. I discover the server name within the script and use it to connect to WMI
  d. I added a condition detection for the data source so it can match each property bag to the right instance

A good method to test out the MP:

  • Configure the rule in the MP for the intervals you would like to test with
  • Use the MP Simulator in a lab and test the following scenarios
    • Set an active group in a cluster to a preferred node and fail it over to the non-preferred node – ensure the simulator sees the problem
    • Fail it back, ensure the simulator doesn’t see the problem anymore
    • On 2008, set an active group to several preferred nodes – ensure the simulator doesn’t see a problem
    • Fail the active group over to a non-preferred node – ensure the simulator sees the problem
  • Import the MP into the lab
    • See what alerts you get, validate that they are accurate
    • Set an active group in a cluster to a preferred node and fail it over to the non-preferred node – ensure the simulator sees the problem
    • Fail it back, ensure the simulator doesn’t see the problem anymore
    • On 2008, set an active group to several preferred nodes – ensure the simulator doesn’t see a problem
    • Fail the active group over to a non-preferred node – ensure the simulator sees the problem
  • If any bugs in the script are found fix them (and please let me know :-))
  • Add error checking to the script where I commented it should be added and turn on ‘on error resume next’ where I specified.

Enjoy!

<ManagementPack ContentReadable="true" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <Manifest>
    <Identity>
      <ID>Custom.Cluster.PreferredNodeDetection</ID>
      <Version>1.0.0.6</Version>
    </Identity>
    <Name>Custom.Cluster.PreferredNodeDetection</Name>
    <References>
      <Reference Alias="SC">
        <ID>Microsoft.SystemCenter.Library</ID>
        <Version>6.0.6278.0</Version>
        <PublicKeyToken>31bf3856ad364e35</PublicKeyToken>
      </Reference>
      <Reference Alias="MicrosoftWindowsClusterLibrary">
        <ID>Microsoft.Windows.Cluster.Library</ID>
        <Version>6.0.6278.0</Version>
        <PublicKeyToken>31bf3856ad364e35</PublicKeyToken>
      </Reference>
      <Reference Alias="Windows">
        <ID>Microsoft.Windows.Library</ID>
        <Version>6.0.6278.0</Version>
        <PublicKeyToken>31bf3856ad364e35</PublicKeyToken>
      </Reference>
      <Reference Alias="Health">
        <ID>System.Health.Library</ID>
        <Version>6.0.6278.0</Version>
        <PublicKeyToken>31bf3856ad364e35</PublicKeyToken>
      </Reference>
      <Reference Alias="System">
        <ID>System.Library</ID>
        <Version>6.0.6278.0</Version>
        <PublicKeyToken>31bf3856ad364e35</PublicKeyToken>
      </Reference>
    </References>
  </Manifest>
  <TypeDefinitions>
    <ModuleTypes>
      <DataSourceModuleType ID="Custom.Cluster.PreferredNodeDetection.Script" Accessibility="Public" Batching="false">
        <Configuration>
          <xsd:element minOccurs="1" name="IntervalSeconds" type="xsd:integer" />
          <xsd:element minOccurs="1" name="ActiveGroupName" type="xsd:string" />
        </Configuration>
        <ModuleImplementation Isolation="Any">
          <Composite>
            <MemberModules>
              <DataSource ID="ScriptDS" TypeID="Windows!Microsoft.Windows.TimedScript.PropertyBagProvider">
                <IntervalSeconds>$Config/IntervalSeconds$</IntervalSeconds>
                <SyncTime />
                <ScriptName>NodeStacking.vbs</ScriptName>
                <Arguments></Arguments>
                <ScriptBody><![CDATA[
'Rslaten 11/12/2009

'Input = Pass the name of the virtual cluster into this script
'Output = Property Bag:
  'Stacking = True or False <required>
    'If False, just return this value in the property bag
    'If True, then populate the following (optional):
   'PhysicalNode = <string>
   'ActiveGroupName = <string>
   'ActiveGroupDescription = <string> or NULL
   'PreferredNode = <string>

'Globals
Dim sVirtualClusterName, oWMI, oAPI, oBag, bStacking, bFound, sAlertDescription
Dim oClusterNode, colClusterNodes
Dim oActiveGroup, colActiveGroups
Dim oPreferredNode, colPreferredNodes

'Get arguments passed into script
sVirtualClusterName = GetArgs()
'WScript.Echo "Virtual Name: " & sVirtualClusterName

'Create OpsMgr Script API object
Set oAPI = GetMOMScriptAPI()
'Call oAPI.LogScriptEvent("NodeStacking.vbs",9989,4,"Script Started")

'Connect to WMI on the cluster
Set oWMI = GetMSClusterWMIObject(sVirtualClusterName)

'Query WMI for physical cluster nodes
Set colClusterNodes = GetCollection("Select * from MSCluster_Node")

'Loop through the collection of physical cluster nodes
For Each oClusterNode in colClusterNodes
  Wscript.Echo "  Physical Node: " & oClusterNode.Name
 
  'Get the Active Group for each node
  Set colActiveGroups = GetCollection("ASSOCIATORS OF {MSCluster_Node='" & oClusterNode.Name & "'} WHERE AssocClass=MSCluster_NodeToActiveGroup")
 
  'Loop through each Active Group
  For Each oActiveGroup in colActiveGroups
   'Create a Property Bag for storing the data we find
    Set oBag = GetPropertyBag(oAPI)
 
    bStacking = false
    If oActiveGroup.Description <> "" or oActiveGroup.Description = NULL Then
      WScript.Echo "    Active Group: " & oActiveGroup.Name
      WScript.Echo "      Description: " & oActiveGroup.Description
    Else
      WScript.Echo "    Active Group: " & oActiveGroup.Name
    End If

    WScript.Echo "      Group State: " & GetState(oActiveGroup.State)

    'Now get the preferred node for this Active Group.  If there isn't one we just skip this because we can't detect stacking.
    Set colPreferredNodes = GetCollection("ASSOCIATORS OF {MSCluster_ResourceGroup='" & oActiveGroup.name & "'} WHERE AssocClass=MSCluster_ResourceGroupToPreferredNode")
    bFound = false
 
 'Loop through this first to determine if this Active Group is on a preferred node because there can be multiple preferred nodes in 2008
 For Each oPreferredNode in colPreferredNodes
   sPreferredNode = oPreferredNode.name 'Needs to be an array, but this complicates the property bag
   If UCase(oPreferredNode.name) = UCase(oClusterNode.name) Then  
  WScript.Echo "      Preferred Node: " & oPreferredNode.name & " <- Good"
  bFound = True
   End If
    Next
 
 'If we aren't on a preferred node create a property bag.  Note this will submit only 1 of the preferred nodes if there are multiple.
 If Not bFound AND colPreferredNodes.count <> 0 Then
  WScript.Echo "      Preferred Node: " & sPreferredNode & " <- Stacking Detected"
  bStacking = true
  
  oBag.AddValue "PhysicalNode", oClusterNode.Name
  oBag.AddValue "ActiveGroupName", oActiveGroup.Name
  oBag.AddValue "ActiveGroupDescription", oActiveGroup.Description
  oBag.AddValue "PreferredNode", sPreferredNode
  
  'Populate a property to use for the alert description
  sAlertDescription =    "  Physical Node = " & oClusterNode.Name & vbCrLf & _
       "    Active Group = " & oActiveGroup.Name & vbCrLf & _
       "      Active Group Description = " & oActiveGroup.Description & vbCrLf & _
       "      Preferred Node = " & sPreferredNode
  oBag.AddValue "AlertDescription", sAlertDescription
 End If
 
 'Create the required field in the property bag
 If bStacking = true Then
   oBag.AddValue "Stacking", true
   oAPI.AddItem oBag
 Else
   oBag.AddValue "Stacking", false
   oAPI.AddItem oBag
 End If
  Next
Next

'Return property bags to OpsMgr
oAPI.ReturnItems

'Exit
Bailout

'Functions
Function GetArgs()
  'On Error Resume Next
  Dim m_sVirtualClusterName
  'm_sVirtualClusterName = UCASE(WScript.Arguments(0))
  'If Err <> 0 Then
    'Need to add error handling here
  'End If
  Set pc = CreateObject("Wscript.Network")
  m_sVirtualClusterName = pc.ComputerName
  'If Err <> 0 Then
    'Need to add error handling here
  'End If
  'Changed script to support cookdown
  GetArgs = m_sVirtualClusterName
End Function

'Converts the state of an Active Cluster Group from an integer to a string.
'Pass an int from -1 through 4 and get the associated string returned
Function GetState(i)
  Select Case i
    Case -1
      GetState = "STATE_UNKNOWN"
    Case 0
      GetState = "ONLINE"
    Case 1
      GetState = "OFFLINE"
    Case 2
      GetState = "FAILED"
    Case 3
      GetState = "PARTIAL_ONLINE"
    Case 4
      GetState = "PENDING"
    Case Else
      GetState = "Error Getting State"
  End Select
End Function

'Connects to WMI and returns the WMI object 'Pass the name of the virtual cluster
Function GetMSClusterWMIObject(s)
  'On Error Resume Next
  Dim m_oWMI
  Set m_oWMI = GetObject("winmgmts:{impersonationLevel=impersonate," _
  & "authenticationLevel=pktPrivacy}!\\" & s & "\root\mscluster")
  If Err <> 0 Then
    'Need to add error handling here
  End If
  Set GetMSClusterWMIObject = m_oWMI
  Set m_oWMI = Nothing
End Function

'Queries WMI and returns a collection
'Pass in the WMI query you want
Function GetCollection(s)
  'On Error Resume Next
  Set GetCollection = oWMI.ExecQuery(s)
  If Err <> 0 Then
    'Need to add error handling here
  End If
End Function

'Returns a MOM Script API object
Function GetMOMScriptAPI()
  'On Error Resume Next
  Dim m_oAPI
  Set m_oAPI = CreateObject("MOM.ScriptAPI")
  If Err <> 0 Then
    'Need to add error handling here
  End If
  Set GetMOMScriptAPI = m_oAPI
  Set m_oAPI = Nothing
End Function

'Returns a property bag
'Pass in the MOM Script API object
Function GetPropertyBag(o)
  'On Error Resume Next
  Dim m_oBag
  Set m_oBag = o.CreatePropertyBag()
  If Err <> 0 Then
    'Need to add error handling here
  End If
  Set GetPropertyBag = m_oBag
  Set m_oBag = Nothing
End Function

'Closes out any open objects
Sub Bailout
  Set colPreferredNodes = Nothing
  Set colActiveGroups = Nothing
  Set colClusterNodes = Nothing
  Set oWMI = Nothing
  Set oBag = Nothing
  Set oAPI = Nothing
End Sub
]]></ScriptBody>
                <TimeoutSeconds>60</TimeoutSeconds>
              </DataSource>
              <ConditionDetection ID="CD" TypeID="System!System.ExpressionFilter">
                <Expression>
                  <SimpleExpression>
                    <ValueExpression>
                      <XPathQuery Type="String">Property[@Name='ActiveGroupName']</XPathQuery>
                    </ValueExpression>
                    <Operator>Equal</Operator>
                    <ValueExpression>
                      <Value Type="String">$Config/ActiveGroupName$</Value>
                    </ValueExpression>
                  </SimpleExpression>
                </Expression>
              </ConditionDetection>
            </MemberModules>
            <Composition>
              <Node ID="CD">
                <Node ID="ScriptDS" />
              </Node>
            </Composition>
          </Composite>
        </ModuleImplementation>
        <OutputType>System!System.PropertyBagData</OutputType>
      </DataSourceModuleType>
      <ConditionDetectionModuleType ID="Custom.Cluster.PreferredNodeDetection.ConditionDetection" Accessibility="Public" Batching="true" Stateful="true" PassThrough="false">
        <Configuration>
          <xsd:element minOccurs="1" name="IterationsBeforeAlerting" type="xsd:integer" />
          <xsd:element minOccurs="1" name="WithinTimePeriod" type="xsd:integer" />
        </Configuration>
        <ModuleImplementation Isolation="Any">
          <Composite>
            <MemberModules>
              <ConditionDetection ID="ExpressionFilter" TypeID="System!System.ExpressionFilter">
                <Expression>
                  <SimpleExpression>
                    <ValueExpression>
                      <XPathQuery Type="Boolean">Property[@Name='Stacking']</XPathQuery>
                    </ValueExpression>
                    <Operator>Equal</Operator>
                    <ValueExpression>
                      <Value Type="Boolean">true</Value>
                    </ValueExpression>
                  </SimpleExpression>
                </Expression>
              </ConditionDetection>
              <ConditionDetection ID="Consolidator" TypeID="System!System.ConsolidatorCondition">
                <Consolidator>
                  <ConsolidationProperties />
                  <TimeControl>
                    <WithinTimeSchedule>
                      <Interval>$Config/WithinTimePeriod$</Interval>
                    </WithinTimeSchedule>
                  </TimeControl>
                  <CountingCondition>
                    <Count>$Config/IterationsBeforeAlerting$</Count>
                    <CountMode>OnNewItemTestOutputRestart_OnTimerSlideByOne</CountMode>
                  </CountingCondition>
                </Consolidator>
              </ConditionDetection>
            </MemberModules>
            <Composition>
              <Node ID="Consolidator">
                <Node ID="ExpressionFilter" />
              </Node>
            </Composition>
          </Composite>
        </ModuleImplementation>
        <OutputType>System!System.ConsolidatorData</OutputType>
        <InputTypes>
          <InputType>System!System.PropertyBagData</InputType>
        </InputTypes>
      </ConditionDetectionModuleType>
    </ModuleTypes>
  </TypeDefinitions>
  <Monitoring>
    <Rules>
      <Rule ID="Custom.Cluster.PreferredNodeDetection.Rule" Enabled="true" Target="MicrosoftWindowsClusterLibrary!Microsoft.Windows.Cluster.VirtualServer" ConfirmDelivery="true" Remotable="true" Priority="Normal" DiscardLevel="100">
        <Category>Alert</Category>
        <DataSources>
          <DataSource ID="DS" TypeID="Custom.Cluster.PreferredNodeDetection.Script">
            <IntervalSeconds>300</IntervalSeconds>
            <ActiveGroupName>$Target/Property[Type="MicrosoftWindowsClusterLibrary!Microsoft.Windows.Cluster.VirtualServer"]/GroupName$</ActiveGroupName>
          </DataSource>
        </DataSources>
        <ConditionDetection ID="CD" TypeID="Custom.Cluster.PreferredNodeDetection.ConditionDetection">
          <IterationsBeforeAlerting>2</IterationsBeforeAlerting>
          <WithinTimePeriod>630</WithinTimePeriod>
        </ConditionDetection>
        <WriteActions>
          <WriteAction ID="Alerter" TypeID="Health!System.Health.GenerateAlert">
            <Priority>2</Priority>
            <Severity>2</Severity>
            <AlertMessageId>$MPElement[Name="AlertMessageID37465c8ef079455c871583295f0f45a7"]$</AlertMessageId>
            <AlertParameters>
              <AlertParameter1>$Target/Property[Type="Windows!Microsoft.Windows.Computer"]/DNSName$</AlertParameter1>
              <AlertParameter2>$Data/Context/DataItem/Property[@Name='PhysicalNode']$</AlertParameter2>
              <AlertParameter3>$Data/Context/DataItem/Property[@Name='ActiveGroupName']$</AlertParameter3>
              <AlertParameter4>$Data/Context/DataItem/Property[@Name='ActiveGroupDescription']$</AlertParameter4>
              <AlertParameter5>$Data/Context/DataItem/Property[@Name='PreferredNode']$</AlertParameter5>
            </AlertParameters>
            <Suppression>
              <SuppressionValue>$Target/Property[Type="Windows!Microsoft.Windows.Computer"]/DNSName$</SuppressionValue>
              <SuppressionValue>$Data/Context/DataItem/Property[@Name='ActiveGroupName']$</SuppressionValue>
            </Suppression>
            <Custom1>$Target/Property[Type="Windows!Microsoft.Windows.Computer"]/DNSName$</Custom1>
            <Custom2>$Data/Context/DataItem/Property[@Name='PhysicalNode']$</Custom2>
            <Custom3>$Data/Context/DataItem/Property[@Name='ActiveGroupName']$</Custom3>
            <Custom4>$Data/Context/DataItem/Property[@Name='ActiveGroupDescription']$</Custom4>
            <Custom5>$Data/Context/DataItem/Property[@Name='PreferredNode']$</Custom5>
            <Custom6 />
            <Custom7 />
            <Custom8 />
            <Custom9 />
            <Custom10 />
          </WriteAction>
        </WriteActions>
      </Rule>
    </Rules>
  </Monitoring>
  <Presentation>
    <StringResources>
      <StringResource ID="AlertMessageID37465c8ef079455c871583295f0f45a7" />
    </StringResources>
  </Presentation>
  <LanguagePacks>
    <LanguagePack ID="ENU" IsDefault="true">
      <DisplayStrings>
        <DisplayString ElementID="AlertMessageID37465c8ef079455c871583295f0f45a7">
          <Name>A Cluster Active Group is not on the Preferred Node</Name>
          <Description>A Cluster Active Group is not on one of its configured preferred nodes.  If the cluster resource configuration is currently correct then please modify the preferred nodes so that they correspond to the current node that this Active Group resides on.

Virtual Node: {0}
  Physical Node: {1}
    Active Group: {2}
      Description: {3}
      Preferred Node: {4}</Description>
        </DisplayString>
        <DisplayString ElementID="Custom.Cluster.PreferredNodeDetection">
          <Name>Custom Preferred Node Detection MP</Name>
          <Description>This MP detects whether a cluster group is not on its preferred node.</Description>
        </DisplayString>
        <DisplayString ElementID="Custom.Cluster.PreferredNodeDetection.ConditionDetection">
          <Name>Preferred Node Condition Detection</Name>
        </DisplayString>
        <DisplayString ElementID="Custom.Cluster.PreferredNodeDetection.Rule">
          <Name>Preferred Node Detection Rule</Name>
        </DisplayString>
        <DisplayString ElementID="Custom.Cluster.PreferredNodeDetection.Script">
          <Name>Preferred Node Detection Data Source</Name>
          <Description />
        </DisplayString>
      </DisplayStrings>
    </LanguagePack>
  </LanguagePacks>
</ManagementPack>