I previously wrote a simple Cookdown example here:
Demo Cookdown Management Pack Example – Kevin Holman’s Blog
That example was very simple, when a rule and a monitor targeting a single instance of a single class, needed to share a script datasource.
Cookdown is needed when multiple workflows (or copies of the same workflow) need to share a datasource. This is required to keep from running the script multiple times simultaneously and using too many resources on the server.
The are two common scenarios:
1. Multiple rules/monitors share a script based datasource, but target a single instance of a class on an agent.
2. When targeting a multi-instance object (such as logical disks) where multiple instances of the same class exist on the same agent, we want a single datasource script to execute once, and “feed” all the copies of the rule or monitor.
Often times, it can be a combination of both 1 and 2 above.
I have written an advanced demo MP which shows this capability – at https://github.com/thekevinholman/Demo.Cookdown
Developing for cookdown is a little more complex than running a script for each rule or monitor. This might be over your head as a budding MP Author. However, I have always found that having complete and simple management pack with a working example really helps me to understand how to develop what I need. That is the purpose of this posting:
First – on a multi-instance object, we must define a Key property which is unique (Key=”true”). This lets the agent keep each instance discovered with a unique property to identify it.
In my example MP, I will use Network Shares as an example, with a key property of UNC path. We will discover the shares from a text file.
<ClassType ID="Demo.Cookdown.Advanced.Class" Base="Windows!Microsoft.Windows.LocalApplication" Accessibility="Public" Abstract="false" Hosted="true" Singleton="false"> <Property ID="UNCPath" Type="string" AutoIncrement="false" Key="true" CaseSensitive="false" MaxLength="256" MinLength="0" Required="true" Scale="0" /> </ClassType>
The next step is to develop a script datasource. The script needs to get data about ALL the instances, so you cannot just pass the key property into the script as a parameter. Any unique information passed into the script datasource will break cookdown. In my datasource example, I will include a script that queries the same file we used for network share discovery, then loop through each one, gather some performance data, and output that information as propertybags while in a loop.
<DataSourceModuleType ID="Demo.Cookdown.Advanced.Performance.DS" Accessibility="Internal" Batching="false"> <Configuration> <xsd:element minOccurs="1" type="xsd:integer" name="IntervalSeconds" xmlns:xsd="http://www.w3.org/2001/XMLSchema" /> <xsd:element minOccurs="1" type="xsd:integer" name="TimeoutSeconds" xmlns:xsd="http://www.w3.org/2001/XMLSchema" /> </Configuration> <OverrideableParameters> <OverrideableParameter ID="IntervalSeconds" Selector="$Config/IntervalSeconds$" ParameterType="int" /> <OverrideableParameter ID="TimeoutSeconds" Selector="$Config/TimeoutSeconds$" ParameterType="int" /> </OverrideableParameters> <ModuleImplementation Isolation="Any"> <Composite> <MemberModules> <DataSource ID="Scheduler" TypeID="System!System.Scheduler"> <Scheduler> <SimpleReccuringSchedule> <Interval Unit="Seconds">$Config/IntervalSeconds$</Interval> </SimpleReccuringSchedule> <ExcludeDates /> </Scheduler> </DataSource> <ProbeAction ID="PA" TypeID="Windows!Microsoft.Windows.PowerShellPropertyBagTriggerOnlyProbe"> <ScriptName>Demo.Cookdown.Advanced.Performance.DS.ps1</ScriptName> <ScriptBody> #================================================================================= # Script to collect performance data of a UNC path that supports cookdown # # Author: Kevin Holman # v1.0 # # Since this script will support cookdown no script parameters will be passed in #================================================================================= # Manual Testing section - put stuff here for manually testing script - typically parameters: #================================================================================= #================================================================================= # Constants section - modify stuff here: #================================================================================= # Assign script name variable for use in event logging. # ScriptName should be the same as the ID of the module that the script is contained in $ScriptName = "Demo.Cookdown.Advanced.Performance.DS.ps1" $EventID = "3006" #Define the File Path [string]$FilePath = "E:\SCOM\NetworkShares.txt" #================================================================================= # Starting Script section - All scripts get this #================================================================================= # Gather the start time of the script $StartTime = Get-Date #Set variable to be used in logging events $whoami = whoami # Load MOMScript API $momapi = New-Object -comObject MOM.ScriptAPI #Log script event that we are starting task $momapi.LogScriptEvent($ScriptName,$EventID,0,"`nScript is starting. `nRunning as ($whoami).") #================================================================================= # Begin MAIN script section #================================================================================= # Get all the UNC Paths from the same file used in discovery # Loop through all UNC paths and get performance data and output propertybags to support cookdown # Test to see if folder and file exists $FileExists = Test-Path $FilePath IF ($FileExists) { #Get the content from the file $Shares = Get-Content -Path $FilePath #Only continue with performance data if content was found in file IF ($Shares) { #Loop through each line of content found in file FOREACH ($Share in $Shares) { [string]$UNCPath = $Share #Log an event for verbose troubleshooting $momapi.LogScriptEvent($ScriptName,$EventID,0,"`nUNCPath = ($UNCPath)") # Get used drives $usedDrives = Get-PSDrive | Select-Object -Expand Name | Where-Object { $_.Length -eq 1 } # Get a rendomly selected drive letter from list not including used drives $DriveLetter = Get-Random -InputObject @(68..90 | ForEach-Object { [string][char]$_ } | Where-Object { $usedDrives -notcontains $_ }) #Log event for verbose troubleshooting $momapi.LogScriptEvent($ScriptName,$EventID,0,"`nDriveletter was assigned as ($DriveLetter) for path ($UNCPath).") #Check to see if the drive is currently in use (should never happen unless cookdown is broken) IF (Get-PSDrive -Name $DriveLetter -ErrorAction SilentlyContinue) { Start-Sleep -Seconds 5 # Get used drives $usedDrives = (Get-PSDrive | Select-Object -Expand Name | Where-Object { $_.Length -eq 1 }) + $DriveLetter # Get a drive letter from list not including used drives $DriveLetter = Get-Random -InputObject @(68..90 | ForEach-Object { [string][char]$_ } | Where-Object { $usedDrives -notcontains $_ }) $momapi.LogScriptEvent($ScriptName,$EventID,2,"`nOriginal Driveletter was found to be in use, so reassigning new drive letter as ($DriveLetter) for path ($UNCPath).") } # Map the drive $Error.Clear() New-PSDrive -Name $DriveLetter -PSProvider FileSystem -Root $UNCPath -Persist IF ($Error) { $momapi.LogScriptEvent($ScriptName,$EventID,1,"`nFATAL ERROR mapping drive to ($DriveLetter) for path ($UNCPath). `nTerminating. `nError is ($Error).") EXIT } # Get the storage metrics $DriveInfo = Get-PSDrive -Name $DriveLetter [int]$FreeSpaceGB = [math]::Round(($DriveInfo.Free / 1GB), 2) [int]$UsedSpaceGB = [math]::Round(($DriveInfo.Used / 1GB), 2) [int]$TotalSpaceGB = [math]::Round(($DriveInfo.Used + $DriveInfo.Free) / 1GB, 2) [int]$FreeSpacePercent = [math]::Round((($TotalSpaceGB - $UsedSpaceGB) / $TotalSpaceGB) * 100, 1) [int]$UsedSpacePercent = [math]::Round((($TotalSpaceGB - $FreeSpaceGB) / $TotalSpaceGB) * 100, 1) #Log verbose event for troubleshooting $momapi.LogScriptEvent($ScriptName,$EventID,0,"`nValues: `nFreeSpaceGB is ($FreeSpaceGB). `nUsedSpaceGB is ($UsedSpaceGB). `nTotalSpaceGB is ($TotalSpaceGB). `FreeSpacePercent is ($FreeSpacePercent). `UsedSpacePercent is ($UsedSpacePercent). `nDriveletter is ($DriveLetter) for path ($uncPath).") # Disconnect the drive Remove-PSDrive $DriveLetter -Force # Load SCOM PropertyBag function to create a new empty bag for each instance $bag = $momapi.CreatePropertyBag() # Fill propertybags $bag.AddValue("UNCPath",$UNCPath) $bag.AddValue("FreeSpaceGB",$FreeSpaceGB) $bag.AddValue("UsedSpaceGB",$UsedSpaceGB) $bag.AddValue("TotalSpaceGB",$TotalSpaceGB) $bag.AddValue("FreeSpacePercent",$FreeSpacePercent) $bag.AddValue("UsedSpacePercent",$UsedSpacePercent) # Return all bags for path $bag } } ELSE { # Log an event for no objects found in file $momapi.LogScriptEvent($ScriptName,$EventID,0,"`nNo shares found in file.") } } ELSE { #Error finding file $momapi.LogScriptEvent($ScriptName,$EventID,1,"`nError finding discovery file at ($FilePath). `nTerminating.") EXIT } #================================================================================= # End MAIN script section # End of script section #================================================================================= #Log an event for script ending and total execution time. $EndTime = Get-Date $ScriptTime = ($EndTime - $StartTime).TotalSeconds $momapi.LogScriptEvent($ScriptName,$EventID,0,"`nScript Completed. `nRuntime: ($ScriptTime) seconds.") #================================================================================= # End of script </ScriptBody> <Parameters></Parameters> <TimeoutSeconds>$Config/TimeoutSeconds$</TimeoutSeconds> </ProbeAction> </MemberModules> <Composition> <Node ID="PA"> <Node ID="Scheduler" /> </Node> </Composition> </Composite> </ModuleImplementation> <OutputType>System!System.PropertyBagData</OutputType> </DataSourceModuleType>
Now, to help with cookdown, we will add another datasource as a “pre-filter” to help each rule or monitor only pull out the specific performance data from the propertybag dump, that is needed. Since UNCPath is unique, we can use that:
<DataSourceModuleType ID="Demo.Cookdown.Advanced.Performance.Filtered.DS" Accessibility="Public" Batching="false"> <Configuration> <xsd:element minOccurs="1" type="xsd:integer" name="IntervalSeconds" xmlns:xsd="http://www.w3.org/2001/XMLSchema" /> <xsd:element minOccurs="1" type="xsd:integer" name="TimeoutSeconds" xmlns:xsd="http://www.w3.org/2001/XMLSchema" /> <xsd:element minOccurs="1" type="xsd:string" name="UNCPath" xmlns:xsd="http://www.w3.org/2001/XMLSchema" /> </Configuration> <OverrideableParameters> <OverrideableParameter ID="IntervalSeconds" Selector="$Config/IntervalSeconds$" ParameterType="int" /> <OverrideableParameter ID="TimeoutSeconds" Selector="$Config/TimeoutSeconds$" ParameterType="int" /> </OverrideableParameters> <ModuleImplementation Isolation="Any"> <Composite> <MemberModules> <DataSource ID="DS" TypeID="Demo.Cookdown.Advanced.Performance.DS"> <IntervalSeconds>$Config/IntervalSeconds$</IntervalSeconds> <TimeoutSeconds>$Config/TimeoutSeconds$</TimeoutSeconds> </DataSource> <ConditionDetection ID="Filter" TypeID="System!System.ExpressionFilter"> <Expression> <SimpleExpression> <ValueExpression> <XPathQuery Type="String">Property[@Name='UNCPath']</XPathQuery> </ValueExpression> <Operator>Equal</Operator> <ValueExpression> <Value Type="String">$Config/UNCPath$</Value> </ValueExpression> </SimpleExpression> </Expression> </ConditionDetection> </MemberModules> <Composition> <Node ID="Filter"> <Node ID="DS" /> </Node> </Composition> </Composite> </ModuleImplementation> <OutputType>System!System.PropertyBagData</OutputType> </DataSourceModuleType>
Each rule can then consume a specific propertybag output from the script run:
<Rule ID="Demo.Cookdown.Advanced.FreeSpaceGB.PerformanceCollection.Rule" Enabled="true" Target="Demo.Cookdown.Advanced.Class" ConfirmDelivery="false" Remotable="true" Priority="Normal" DiscardLevel="100"> <Category>PerformanceCollection</Category> <DataSources> <DataSource ID="DS" TypeID="Demo.Cookdown.Advanced.Performance.Filtered.DS"> <IntervalSeconds>60</IntervalSeconds> <TimeoutSeconds>300</TimeoutSeconds> <UNCPath>$Target/Property[Type="Demo.Cookdown.Advanced.Class"]/UNCPath$</UNCPath> </DataSource> </DataSources> <ConditionDetection ID="System.Performance.DataGenericMapper" TypeID="Perf!System.Performance.DataGenericMapper"> <ObjectName>Share</ObjectName> <CounterName>FreeSpaceGB</CounterName> <InstanceName>$Data/Property[@Name='UNCPath']$</InstanceName> <Value>$Data/Property[@Name='FreeSpaceGB']$</Value> </ConditionDetection> <WriteActions> <WriteAction ID="WriteToDB" TypeID="SC!Microsoft.SystemCenter.CollectPerformanceData" /> <!-- Can be optional - collect this data to the Operations Database. --> <WriteAction ID="WriteToDW" TypeID="MSDL!Microsoft.SystemCenter.DataWarehouse.PublishPerformanceData" /> <!-- Can be optional - collect this data to the Data Warehouse Database --> </WriteActions> </Rule>
If you have monitors, each monitor type can be coded to use specific property bag outputs:
<UnitMonitorType ID="Demo.Cookdown.Advanced.FreeSpaceGB.MonitorType" Accessibility="Internal"> <MonitorTypeStates> <MonitorTypeState ID="Error" NoDetection="false" /> <MonitorTypeState ID="Warning" NoDetection="false" /> <MonitorTypeState ID="Success" NoDetection="false" /> </MonitorTypeStates> <Configuration> <xsd:element minOccurs="1" type="xsd:integer" name="IntervalSeconds" xmlns:xsd="http://www.w3.org/2001/XMLSchema" /> <xsd:element minOccurs="1" type="xsd:integer" name="TimeoutSeconds" xmlns:xsd="http://www.w3.org/2001/XMLSchema" /> <xsd:element minOccurs="1" type="xsd:string" name="UNCPath" xmlns:xsd="http://www.w3.org/2001/XMLSchema" /> <xsd:element minOccurs="1" type="xsd:integer" name="WarningThreshold" xmlns:xsd="http://www.w3.org/2001/XMLSchema" /> <xsd:element minOccurs="1" type="xsd:integer" name="CriticalThreshold" xmlns:xsd="http://www.w3.org/2001/XMLSchema" /> </Configuration> <OverrideableParameters> <OverrideableParameter ID="IntervalSeconds" Selector="$Config/IntervalSeconds$" ParameterType="int" /> <OverrideableParameter ID="TimeoutSeconds" Selector="$Config/TimeoutSeconds$" ParameterType="int" /> <OverrideableParameter ID="WarningThreshold" Selector="$Config/WarningThreshold$" ParameterType="int" /> <OverrideableParameter ID="CriticalThreshold" Selector="$Config/CriticalThreshold$" ParameterType="int" /> </OverrideableParameters> <MonitorImplementation> <MemberModules> <DataSource ID="DS" TypeID="Demo.Cookdown.Advanced.Performance.Filtered.DS"> <IntervalSeconds>$Config/IntervalSeconds$</IntervalSeconds> <TimeoutSeconds>$Config/TimeoutSeconds$</TimeoutSeconds> <UNCPath>$Config/UNCPath$</UNCPath> </DataSource> <ConditionDetection ID="HealthyCondition" TypeID="System!System.ExpressionFilter"> <Expression> <SimpleExpression> <ValueExpression> <XPathQuery Type="Integer">Property[@Name='FreeSpaceGB']</XPathQuery> </ValueExpression> <Operator>Greater</Operator> <ValueExpression> <Value Type="Integer">$Config/WarningThreshold$</Value> </ValueExpression> </SimpleExpression> </Expression> </ConditionDetection> <ConditionDetection ID="WarningCondition" TypeID="System!System.ExpressionFilter"> <Expression> <And> <Expression> <SimpleExpression> <ValueExpression> <XPathQuery Type="Integer">Property[@Name='FreeSpaceGB']</XPathQuery> </ValueExpression> <Operator>LessEqual</Operator> <ValueExpression> <Value Type="Integer">$Config/WarningThreshold$</Value> </ValueExpression> </SimpleExpression> </Expression> <Expression> <SimpleExpression> <ValueExpression> <XPathQuery Type="Integer">Property[@Name='FreeSpaceGB']</XPathQuery> </ValueExpression> <Operator>Greater</Operator> <ValueExpression> <Value Type="Integer">$Config/CriticalThreshold$</Value> </ValueExpression> </SimpleExpression> </Expression> </And> </Expression> </ConditionDetection> <ConditionDetection ID="CriticalCondition" TypeID="System!System.ExpressionFilter"> <Expression> <SimpleExpression> <ValueExpression> <XPathQuery Type="Integer">Property[@Name='FreeSpaceGB']</XPathQuery> </ValueExpression> <Operator>LessEqual</Operator> <ValueExpression> <Value Type="Integer">$Config/CriticalThreshold$</Value> </ValueExpression> </SimpleExpression> </Expression> </ConditionDetection> </MemberModules> <RegularDetections> <RegularDetection MonitorTypeStateID="Error"> <Node ID="CriticalCondition"> <Node ID="DS" /> </Node> </RegularDetection> <RegularDetection MonitorTypeStateID="Success"> <Node ID="HealthyCondition"> <Node ID="DS" /> </Node> </RegularDetection> <RegularDetection MonitorTypeStateID="Warning"> <Node ID="WarningCondition"> <Node ID="DS" /> </Node> </RegularDetection> </RegularDetections> </MonitorImplementation> </UnitMonitorType>
When we put it all together, in the example MP I have 4 rules and two monitors. All share a single script, and the script is only run a SINGLE time, but feeds information to all 6 workflows. Not to mention, in my test I had 4 network shares. That’s 24 workflows, but only a single script execution!
In this example I am discovering 4 network shares from a text file.
The UNCPath is discovered as a key property.
The script datasource simply maps a drive to each share, gathers performance data via PowerShell (such as free space), disconnects the drive, then outputs multiple propertybags with the perf data for each UNC path.
You can use verbose event logging in the script datasource to identify that there is only a single run of the script:
Totally correct Kevin. Some additions, especially be sure that any parameter passed to the last module (at Probe level) is always the same as Kevin mentioned. If any change, it will start a new probe module. This is also for the time intervals , isolation and the batching. Tip, play with the isolation levels to make it more stable. To visualize the story of Kevin have a look here:
https://www.youtube.com/watch?v=SClQ41ffkmY
Happy scoming!
Hi Kevin, I’m having an issue creating an MP to discover and monitor service accounts.
Here is my VS Solution file.
https://www.dropbox.com/scl/fo/m44rjfzpnemaglf25o7kz/h?rlkey=4f6s5kqmroh3n5kb7xr3znes1&dl=0
When I import the MP to my Dev SCOM environment, it only discovers one account. I haven’t added the monitor piece yet.
When the script runs to discover the service accounts, it can read all of them but only shows one in SCOM discovery inventory.
Can you take a look and tell me what I’m doing wrong?
Any help will be appreciated.
Thank you.
I forgot to mention that the discovery is disabled, and I was planning to enable it on a DC with an override.
Kevin,
I figured out what the issue was, and I was able to discover/monitor all the Service Accounts in my Dev Environment, but when I imported the MP to Prod, the service accounts were not discovered. I checked the Event Viewer and saw all the service accounts getting discovered, but they don’t show up in the SCOM console.
I enabled the discovery on one of the Prod Domain Controllers with an override and also specified the OUs path in the override.
The agent has no errors/warnings in SCOM.
Here is the updated VS Solution file
https://www.dropbox.com/scl/fi/pl5uut4pqce8pqii34nou/PEI.SRVC.Accounts.zip?rlkey=grd0u6m05231jmy06g2kshxo2&dl=0
Any help will be appreciated.
Thank you,
I found the error in the EVID “Invalid property length exception.” The value that it was complaining about was the description. There were some accounts with a long Description. I removed it from the MP and now it is working.
I’ll clean it a bit more and post it in my GitHub and then share the link for anyone that would like to do the same thing.
Kevin, do you have fragments for this Demo?