Demo SCOM Script Template


 

image

 

I thought I’d take a moment to publish my SCOM script template.

Whenever I am writing a SCOM script for monitoring, discovery, or automation, there are some “standards” that I want in all my scripts.

 

1.  I personally feel that all script running in SCOM should at the MINIMUM log at script starting event, and a script completed event with runtime in seconds.  This helps anyone evaluating the server, or agent, just how many scripts are running and on what kind of frequency.

2.  I like to log “who” the script is executed by (what account, whether RunAs or default agent action account.

3.  I like to have an examples section for manually assigning script variables, which is very handy when testing/troubleshooting.

4.  I assign a ScriptName and EventID variables in the script, for consistency when logging events.

5.  I load examples for discovery scripts, propertybags for monitoring scripts, and just remove what isn't needed.  I find this easier and more consistent than going and grabbing an example from some other script I wrote previously.

6.  I have a section on connecting to the SCOM SDK, for scripts that will run automation on the SCOM management server.  I found this method to be the most reliable, as there are scenarios where commandlets just stop working under the MonitoringHost.exe process.

 

I don’t have a lot of “fluff” in here…. I never like it when I have to page down 3 or 4 pages to get to what a script is actually doing…. this is mostly just the meat and potatoes.

 

#================================================================================= # Describe Script Here # # Author: Kevin Holman # v1.2 #================================================================================= param($SourceId, $ManagedEntityId, $ComputerName, $Param1, $Param2) # Manual Testing section - put stuff here for manually testing script - typically parameters: #================================================================================= # $SourceId = '{00000000-0000-0000-0000-000000000000}' # $ManagedEntityId = '{00000000-0000-0000-0000-000000000000}' # $ComputerName = "computername.domain.com" # $Param1 = "foo" # $Param2 = "bar" #================================================================================= # 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 = "CompanyID.AppName.Workflow.RuleMonitorDiscoveryDSWA.ps1" $EventID = "1234" #================================================================================= # 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,"`n Script is starting. `n Running as ($whoami).") #================================================================================= # Discovery Script section - Discovery scripts get this #================================================================================= # Load SCOM Discovery module $DiscoveryData = $momapi.CreateDiscoveryData(0, $SourceId, $ManagedEntityId) #================================================================================= # PropertyBag Script section - Monitoring scripts get this #================================================================================= # Load SCOM PropertyBag function $bag = $momapi.CreatePropertyBag() #================================================================================= # Connect to local SCOM Management Group Section - If required #================================================================================= # I have found this to be the most reliable method to load SCOM modules for scripts running on Management Servers # Clear any previous errors $Error.Clear() # Import the OperationsManager module and connect to the management group $SCOMPowerShellKey = "HKLM:\SOFTWARE\Microsoft\System Center Operations Manager\12\Setup\Powershell\V2" $SCOMModulePath = Join-Path (Get-ItemProperty $SCOMPowerShellKey).InstallDirectory "OperationsManager" Import-module $SCOMModulePath New-DefaultManagementGroupConnection "localhost" IF ($Error) { $momapi.LogScriptEvent($ScriptName,$EventID,1,"`n FATAL ERROR: Unable to load OperationsManager module or unable to connect to Management Server. `n Terminating script. `n Error is: ($Error).") EXIT } #================================================================================= # Begin MAIN script section #================================================================================= #Put your stuff in here #================================================================================= # End MAIN script section # Discovery Script section - Discovery scripts get this #================================================================================= # Example discovery of a class with properties $instance = $DiscoveryData.CreateClassInstance("$MPElement[Name='Your.Custom.Class']$") $instance.AddProperty("$MPElement[Name='Windows!Microsoft.Windows.Computer']/PrincipalName$", $ComputerName) $instance.AddProperty("$MPElement[Name='System!System.Entity']/DisplayName$", $ComputerName) $instance.AddProperty("$MPElement[Name='Your.Custom.Class']/Property1$", $Param1) $instance.AddProperty("$MPElement[Name='Your.Custom.Class']/Property2$", $Param2) $DiscoveryData.AddInstance($instance) # Return Discovery Items Normally $DiscoveryData # Return Discovery Bag to the command line for testing (does not work from ISE) # $momapi.Return($DiscoveryData) #================================================================================= # PropertyBag Script section - Monitoring scripts get this #================================================================================= # Output a fixed Result = BAD for a monitor example $bag.AddValue("Result","BAD") # Output other data from script into bag $bag.AddValue("Param1",$Param1) $bag.AddValue("Param2",$Param2) # Return all bags $bag #================================================================================= # 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,"`n Script Completed. `n Script Runtime: ($ScriptTime) seconds.") #================================================================================= # End of script

 

Do you have stuff you like to place in every script?  If so – let me know in the comments!


Comments (4)

  1. Reidar Johansen says:

    Thank you for sharing this.
    In the starting script section I usually also have:
    [Threading.Thread]::CurrentThread.CurrentCulture=’en-US’;
    [Threading.Thread]::CurrentThread.CurrentUICulture=’en-US’;
    [string]$scriptUser=[System.Security.Principal.WindowsIdentity]::GetCurrent().Name;
    [string]$scriptComputer=[System.Environment]::MachineName;
    [string]$scriptVersion=’1.20170911.1′;

    The $scriptComputer I would usually add as propertybag in unhosted monitors so it will be visible in the context of the state change events (usefull to see what management server executed the script).

    I also include some details in the event logging by adding some functions and use those functions to create the events, like this:

    function Get-ScriptParam
    {
    [CmdletBinding()]
    param
    (
    [Parameter(Mandatory=$true)]
    $Invocation,
    [Parameter(Mandatory=$true)]
    $BoundParameters
    )
    $builtin=@(‘ErrorAction’,’WarningAction’,’Verbose’,’ErrorVariable’,’WarningVariable’,’OutVariable’,’OutBuffer’,’Debug’,’Confirm’,’PipelineVariable’,’WhatIf’);
    $pc=$Invocation.MyCommand.Parameters;
    if(-not($pc)){return;};
    $keys=$pc.Keys;
    $boundKeys=$BoundParameters.Keys;
    $ret=@();
    foreach($k in $boundKeys){$v=[string]$BoundParameters.Item($k);$ret+=if($k -match ‘password’ -and $v){$k+’=”******”’;}else{$k+’=”’+$v+””;};};
    foreach($key in $keys)
    {
    if($boundKeys -notcontains $key -and $builtin -notcontains $key)
    {
    $var=(Get-Variable -Scope Script -ErrorAction SilentlyContinue)|Where-Object{$_.Name -eq $key};
    if($var)
    {
    $val=[string]$var.Value;
    if($val){$ret+=if($key -match ‘password’){$key+’=”******”’;}else{$key+’=”’+$val+””;};};
    };
    };
    };
    $ret -join [char]10;
    };
    function Write-OMEventLog
    {
    [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact=’Medium’)]
    param
    (
    [Parameter(Mandatory=$true)]
    [string]$ScriptName,
    [Parameter(Mandatory=$true)]
    [string]$ScriptVersion,
    [Parameter(Mandatory=$true)]
    $MOMScripAPI,
    [Parameter(Mandatory=$true)]
    [int]$EventId,
    [ValidateSet(‘Info’,’Error’,’Warning’)]
    [string]$EventSeverity=’Info’,
    [Parameter(Mandatory=$true)]
    [string]$Message,
    [switch]$DebugIndependent,
    $DebugMode
    )
    $snv='{0} : Version [{1}]’ -f $ScriptName,$ScriptVersion;
    # Fallback to prevent any error. SCOM only supports EventIDs up to 20000
    if($EventId -ge 20000){$EventId=19999;};
    $es=switch($EventSeverity){‘Warning'{2;};’Error'{1;};default{0;};};
    # Support WhatIf parameter before making any changes
    if($PSCmdlet.ShouldProcess($Message))
    {
    if($DebugIndependent -and $MOMScripAPI){$MOMScripAPI.LogScriptEvent($snv,$EventId,$es,$Message);}
    else
    {
    if($DebugMode)
    {
    $Message='{0}: {1}’ -f ‘DEBUG’,$Message;
    if($MOMScripAPI){$MOMScripAPI.LogScriptEvent($snv,$EventId,$es,$Message);};
    };
    };
    };
    Write-Verbose $Message;
    };
    function Get-ErrorMessageString
    {
    [CmdletBinding()]
    param
    (
    [Parameter(Mandatory=$true)]
    $e
    )
    $errtype=”;
    $msg=if([bool]($e|gm -Name Exception) -and [bool]($e.Exception|gm -Name InnerException) -and $e.Exception.InnerException)
    {
    $errtype=$e.Exception.InnerException.GetType().FullName;
    $e.Exception.InnerException.Message;
    }elseif([bool]($e|gm -Name Exception) -and $e.Exception)
    {
    $errtype=$e.Exception.GetType().FullName;
    $e.Exception.Message;
    }elseif([bool]($e|gm -Name ErrorRecord) -and [bool]($e.ErrorRecord|gm -Name Exception) -and [bool]($e.ErrorRecord.Exception|gm -Name InnerException) -and $e.ErrorRecord.Exception.InnerException)
    {
    $errtype=$e.ErrorRecord.Exception.InnerException.GetType().FullName;
    $e.ErrorRecord.Exception.InnerException.Message;
    }elseif([bool]($e|gm -Name ErrorRecord) -and [bool]($e.ErrorRecord|gm -Name Exception) -and $e.ErrorRecord.Exception)
    {
    $errtype=$e.ErrorRecord.Exception.GetType().FullName;
    $e.ErrorRecord.Exception.Message;
    }elseif([bool]($e|gm -Name Message))
    {
    $errtype=$e.GetType().FullName;
    $e.Message;
    }else{”;};
    if($msg -match ‘\n’)
    {
    $a=@($msg -split ‘\r\n’ -split ‘\n’);
    $b=$a -notmatch ‘^\+’ -notmatch ‘^$’;
    $msg=$b -join ‘ : ‘;
    };
    if($msg -match (‘^Exception calling ‘+[char]34+’.+’+[char]34+’ with ‘+[char]34+’\d+’+[char]34+’ argument\(s\): ‘+[char]34)){$msg=$msg -replace (‘^Exception calling ‘+[char]34+’.+’+[char]34+’ with ‘+[char]34+’\d+’+[char]34+’ argument\(s\): ‘+[char]34) -replace ([char]34+’$’),”;};
    if($msg -eq ”){($errtype+’ has occured without any error message.’).Trim();}else{$msg+’.’ -replace ‘\t|\r\n|\n|\r|\s+’,’ ‘ -replace ‘\.+’,’.’;};
    };
    function Get-ErrorListAsString
    {
    if($Error.Count -gt 0)
    {
    $i=0;
    foreach($e in $Error)
    {
    $stacktrace=if([bool]($e|gm -Name InvocationInfo) -and $e.InvocationInfo)
    {
    ‘At line:’+$e.InvocationInfo.ScriptLineNumber+’ char:’+$e.InvocationInfo.OffsetInLine;
    }elseif([bool]($e|gm -Name ErrorRecord) -and [bool]($e.ErrorRecord|gm -Name InvocationInfo) -and $e.ErrorRecord.InvocationInfo)
    {
    ‘At line:’+$e.ErrorRecord.InvocationInfo.ScriptLineNumber+’ char:’+$e.ErrorRecord.InvocationInfo.OffsetInLine;
    }else{‘At line:0 char:0’;};
    $fqerror=if([bool]($e|gm -Name FullyQualifiedErrorId) -and $e.FullyQualifiedErrorId)
    {
    $e.FullyQualifiedErrorId;
    }elseif([bool]($e|gm -Name ErrorRecord) -and [bool]($e.ErrorRecord|gm -Name FullyQualifiedErrorId) -and $e.ErrorRecord.FullyQualifiedErrorId)
    {
    $e.ErrorRecord.FullyQualifiedErrorId;
    }else{‘Unknown’;};
    $msg=Get-ErrorMessageString $e;
    $rt=[char]36+’Error[‘+$i+’]’;
    if($fqerror){$rt+=’ : ‘+$fqerror;};
    if($stacktrace){$rt+=’ : ‘+$stacktrace;};
    if($msg){$rt+=’ : ‘+$msg;};
    $rt -replace ‘\s+’,’ ‘;
    $i++;
    };
    };
    };

    $messageparam=Get-ScriptParam -Invocation $MyInvocation -BoundParameters $PSBoundParameters -ErrorAction SilentlyContinue;
    if($messageparam){$messageparam=’Script parameters:’+[char]10+$messageparam;};
    $message=’Script executed by user ‘+$scriptUser+’ at ‘+$scriptStart.ToShortTimeString()+[char]10;

    #… do stuff
    $message+=’Add some more details to the event’;

    $scriptRuntime=(Get-Date)-$scriptStart;
    $scriptRuntimeinSeconds='{0:N2}’ -f $scriptRuntime.TotalSeconds;
    $message+=[char]10+$messageparam+[char]10+[char]10+’Script completed after ‘+$scriptRuntimeinSeconds+’ seconds.’+[char]10;

    # Get errors that have occured, if any
    $ef=@(Get-ErrorListAsString);
    # If we have errors during script execution
    $severity=if($ef){$message+=[char]10+’Errors found ‘+$ef.Count+’:’+[char]10+$($ef -join [char]10);’Error’;}else{$severity;};
    # Event log have a limit on text size, so truncate message if to large
    $message=$message.Trim([char]10);
    if($message.Length -gt 31389){$message=$message.Substring(0,31389);};

    Write-OMEventLog -ScriptName $scriptName -ScriptVersion $scriptVersion -MOMScripAPI $oAPI -EventId $scriptEventID -EventSeverity $severity -Message $message -DebugIndependent -DebugMode $DebugMode -ErrorAction Stop;

    1. rob1974 says:

      I also set the culture
      [Threading.Thread]::CurrentThread.CurrentCulture=’en-US’;
      [Threading.Thread]::CurrentThread.CurrentUICulture=’en-US’;

      And my template also includes the testing return code to see if the bag does return what i want:
      $api.Return($Bag) (which you’ve done for the discovery data as well)

  2. Mark Woolaway says:

    When it comes to assigning event IDs, (in your script $EventID = “1234”) what is a good range to use please? I don’t want to pick something already used by another process. Thank you.

    1. Kevin Holman says:

      Unfortunately there isnt one. Different authors always use different event ID’s…. including Microsoft. The best feedback I can give is to look at what’s in your environment and see if there seems to be a range not in use. But there is no guarantee it wont. The point here – is that any alert rules that use the OpsMgr health service script source, should include “event description contains” and use your script name as a criteria, then it doesn’t matter if there is event ID overlap.

Skip to main content