Run a Rule On-Demand

Ever Wanted to have a Rule or Recovery that you would quickly kick off without having to go into the SCOM console?  Specifically in my case I wanted to collect a list of the Top Processes and details about them on-demand, possibly allowing another program/user to take this snapshot without having access to the server or OpsMgr.

 

The solution I came up with was the write a Rule, using the AllowProxying attribute and then send events to the log with PowerShell remotely.  You can event follow the instructions at this post if you want to allow write-only access to a log – http://jpadda.wordpress.com/2010/08/08/event-log-write-permissions/

Allow Proxy on Event Based Rules

I thought I ran into this before but I couldn’t find a post with any concise details.

 

As noted in the below MSDN reference you can use the AllowProxying tag to enable you alerting and collection rules to fire on events that were not written from the local machine.  These include but are not limited to events you would get if you specified the computer name in the PowerShell write-eventLog cmdlet.

 

Just place <AllowProxying>true</AllowProxying> between the LogName and the Expression XML tags when authoring and you should be ready to rock.

 

<ComputerName>$Target/Property[Type=”Windows!Microsoft.Windows.Computer”]/NetworkName$</ComputerName>

  <LogName>UserLogonScripts</LogName>

  <AllowProxying>true</AllowProxying>

  <Expression>

    <And>

      <Expression>

 

 

MSDN Reference: http://msdn.microsoft.com/en-us/library/ee809339.aspx

A security warning on Domain Controllers

I ran across a post on TechNet today with questions on what security holes could be opened to administrators to SCOM; I wanted to point out one of the largest items that may not be immediately apparent.

This applies specifically to Domain Controllers, but once someone has access there its pretty much a free-for-all to your domain.

If your SCOM agent runs as local system on the Domain Controllers and scripts launched from the agent could potentially have Domain Administrator Level access to the AD infrastructure.

More on the Local-System and Domain interaction here: http://social.technet.microsoft.com/Forums/windowsserver/en-US/652e82f1-fdb6-46b7-b90e-c62fb37d583a/system-account-in-domain-controller?forum=winserverDS

Join-Object Crash Course

I stumbled across this wonderful PowerShell commandlet to Join two objects together based on specific properties.  The original information comes from the PowerShell blog here; and I’ve posted the code below and I would absolutely recommend checking out their Blog.

Using it is really this simple:

Join-Object -Left $employee -Right $entrance -Where {$args[0].Id -eq $args[1].EmployeeId} –LeftProperties "Name" –RightProperties "When" -Type OnlyIfInBoth

 

The Function Code; (Thanks again to the guys that wrote this, its great!)

function AddItemProperties($item, $properties, $output)
{
    if($item -ne $null)
    {
        foreach($property in $properties)
        {
            $propertyHash =$property -as [hashtable]
            if($propertyHash -ne $null)
            {
                $hashName=$propertyHash["name"] -as [string]
                if($hashName -eq $null)
                {
                    throw "there should be a string Name"  
                }
         
                $expression=$propertyHash["expression"] -as [scriptblock]
                if($expression -eq $null)
                {
                    throw "there should be a ScriptBlock Expression"  
                }
         
                $_=$item
                $expressionValue=& $expression
         
                $output | add-member -MemberType "NoteProperty" -Name $hashName -Value $expressionValue
            }
            else
            {
                # .psobject.Properties allows you to list the properties of any object, also known as "reflection"
                foreach($itemProperty in $item.psobject.Properties)
                {
                    if ($itemProperty.Name -like $property)
                    {
                        $output | add-member -MemberType "NoteProperty" -Name $itemProperty.Name -Value $itemProperty.Value
                    }
                }
            }
        }
    }
}

    
function WriteJoinObjectOutput($leftItem, $rightItem, $leftProperties, $rightProperties, $Type)
{
    $output = new-object psobject

    if($Type -eq "AllInRight")
    {
        # This mix of rightItem with LeftProperties and vice versa is due to
        # the switch of Left and Right arguments for AllInRight
        AddItemProperties $rightItem $leftProperties $output
        AddItemProperties $leftItem $rightProperties $output
    }
    else
    {
        AddItemProperties $leftItem $leftProperties $output
        AddItemProperties $rightItem $rightProperties $output
    }
    $output
}

<#
.Synopsis
   Joins two lists of objects
.DESCRIPTION
   Joins two lists of objects
.EXAMPLE
   Join-Object $a $b "Id" ("Name","Salary")
#>
function Join-Object
{
    [CmdletBinding()]
    [OutputType([int])]
    Param
    (
        # List to join with $Right
        [Parameter(Mandatory=$true,
                   Position=0)]
        [object[]]
        $Left,

        # List to join with $Left
        [Parameter(Mandatory=$true,
                   Position=1)]
        [object[]]
        $Right,

        # Condition in which an item in the left matches an item in the right
        # typically something like: {$args[0].Id -eq $args[1].Id}
        [Parameter(Mandatory=$true,
                   Position=2)]
        [scriptblock]
        $Where,

        # Properties from $Left we want in the output.
        # Each property can:
        # - Be a plain property name like "Name"
        # - Contain wildcards like "*"
        # - Be a hashtable like @{Name="Product Name";Expression={$_.Name}}. Name is the output property name
        #   and Expression is the property value. The same syntax is available in select-object and it is 
        #   important for join-object because joined lists could have a property with the same name
        [Parameter(Mandatory=$true,
                   Position=3)]
        [object[]]
        $LeftProperties,

        # Properties from $Right we want in the output.
        # Like LeftProperties, each can be a plain name, wildcard or hashtable. See the LeftProperties comments.
        [Parameter(Mandatory=$true,
                   Position=4)]
        [object[]]
        $RightProperties,

        # Type of join. 
        #   AllInLeft will have all elements from Left at least once in the output, and might appear more than once
        # if the where clause is true for more than one element in right, Left elements with matches in Right are 
        # preceded by elements with no matches. This is equivalent to an outer left join (or simply left join) 
        # SQL statement.
        #  AllInRight is similar to AllInLeft.
        #  OnlyIfInBoth will cause all elements from Left to be placed in the output, only if there is at least one
        # match in Right. This is equivalent to a SQL inner join (or simply join) statement.
        #  AllInBoth will have all entries in right and left in the output. Specifically, it will have all entries
        # in right with at least one match in left, followed by all entries in Right with no matches in left, 
        # followed by all entries in Left with no matches in Right.This is equivallent to a SQL full join.
        [Parameter(Mandatory=$false,
                   Position=5)]
        [ValidateSet("AllInLeft","OnlyIfInBoth","AllInBoth", "AllInRight")]
        [string]
        $Type="OnlyIfInBoth"
    )

    Begin
    {
        # a list of the matches in right for each object in left
        $leftMatchesInRight = new-object System.Collections.ArrayList

        # the count for all matches  
        $rightMatchesCount = New-Object "object[]" $Right.Count

        for($i=0;$i -lt $Right.Count;$i++)
        {
            $rightMatchesCount[$i]=0
        }
    }

    Process
    {
        if($Type -eq "AllInRight")
        {
            # for AllInRight we just switch Left and Right
            $aux = $Left
            $Left = $Right
            $Right = $aux
        }

        # go over items in $Left and produce the list of matches
        foreach($leftItem in $Left)
        {
            $leftItemMatchesInRight = new-object System.Collections.ArrayList
            $null = $leftMatchesInRight.Add($leftItemMatchesInRight)

            for($i=0; $i -lt $right.Count;$i++)
            {
                $rightItem=$right[$i]

                if($Type -eq "AllInRight")
                {
                    # For AllInRight, we want $args[0] to refer to the left and $args[1] to refer to right,
                    # but since we switched left and right, we have to switch the where arguments
                    $whereLeft = $rightItem
                    $whereRight = $leftItem
                }
                else
                {
                    $whereLeft = $leftItem
                    $whereRight = $rightItem
                }

                if(Invoke-Command -ScriptBlock $where -ArgumentList $whereLeft,$whereRight)
                {
                    $null = $leftItemMatchesInRight.Add($rightItem)
                    $rightMatchesCount[$i]++
                }
            
            }
        }

        # go over the list of matches and produce output
        for($i=0; $i -lt $left.Count;$i++)
        {
            $leftItemMatchesInRight=$leftMatchesInRight[$i]
            $leftItem=$left[$i]
                               
            if($leftItemMatchesInRight.Count -eq 0)
            {
                if($Type -ne "OnlyIfInBoth")
                {
                    WriteJoinObjectOutput $leftItem  $null  $LeftProperties  $RightProperties $Type
                }

                continue
            }

            foreach($leftItemMatchInRight in $leftItemMatchesInRight)
            {
                WriteJoinObjectOutput $leftItem $leftItemMatchInRight  $LeftProperties  $RightProperties $Type
            }
        }
    }

    End
    {
        #produce final output for members of right with no matches for the AllInBoth option
        if($Type -eq "AllInBoth")
        {
            for($i=0; $i -lt $right.Count;$i++)
            {
                $rightMatchCount=$rightMatchesCount[$i]
                if($rightMatchCount -eq 0)
                {
                    $rightItem=$Right[$i]
                    WriteJoinObjectOutput $null $rightItem $LeftProperties $RightProperties $Type
                }
            }
        }
    }
}

 

 

 

Balance Agents across Management Servers

The primary environment that I manage is crossing the 3000 server mark, which is what Microsoft lists as the supported max on a single management server.  To keep things under control and automated I created the quick below script from bits from around the internet, I thought it might be useful to someone else so I figured I share it.

Please post any questions you may have and I’ll try to answer them as soon as I can.

The script takes 3 parameters and needs to run on a server where the PowerShell module is installed.

  1. ManagementServer – This is the server you want the script to connect to when it runs.
  2. ServersToExclude – In my case I have a few servers set aside for Linux and SNMP monitoring, I added this to prevent the servers from being set as the Primary Management Server.  All Management servers are added to the FailOver List.
  3. UseServerDigits – This is a switch, I needed a way to distribute the agents that wasn’t random, this way each run of the script doesn’t re-shuffle the agents.  When this switch is enabled it tries to get the last digits in the servers name via regex and use those to place the server.

The Script Code:

param([string]$ManagementServer,[string]$ServersToExclude,[switch]$UseServerDigits)

#Import module and connect to the management server specified
Import-Module OperationsManager
New-SCOMManagementGroupConnection $ManagementServer

#Get the agents and management servers
$Agents = Get-SCOMAgent
$ManagementServers = Get-SCOMManagementServer

#Create two lists that will be used to hold the management servers, 
#$one will contain all servers for the failover list and the other will contain the servers to be set as primary
$AllMSList = New-Object 'System.Collections.Generic.List[Microsoft.EnterpriseManagement.Administration.ManagementServer]'
$PrimaryMSList = New-Object 'System.Collections.Generic.List[Microsoft.EnterpriseManagement.Administration.ManagementServer]'

#Process each management server we received
ForEach($MS in $ManagementServers)
    {
        #Add Every Managment Server to the first list
        $AllMSList.add($MS)
        If($ServersToExclude -notcontains $MS.ComputerName)
            {
                #Only add these servers if they are not in the servers to exclude list
                $PrimaryMSList.add($MS)
            }
    }
    
    
#loop through all servers
ForEach($Agent in $Agents)
    {
        If($Agent.DisplayName -match '(?<Digits>\d+)[^\d]*$' -and $UseServerDigits)
            {
                #Now that we have the rightmost number, do a modulus against the count of managment servers
                $ServerToAssignTo = $Matches.Digits % $PrimaryMSList.Count
               
                #Save some work if the server is already assigned here
                If($Agent.PrimaryManagementServerName -ne $PrimaryMSList[$ServerToAssignTo].DisplayName)
                    {
                        $Agent.SetManagementServers($PrimaryMSList[$ServerToAssignTo],$AllMSList)
                    }
            }
        Else
            {
                #This server appears to have no numbers in its name, or regex failed
                $ServerToAssign = Get-Random -Maximum $PrimaryMSList.Count -Minimum 0
              
                #Save some work if the server is already assigned here
                If($Agent.PrimaryManagementServerName -ne $PrimaryMSList[$ServerToAssignTo].DisplayName)
                    {
                        $Agent.SetManagementServers($PrimaryMSList[$ServerToAssignTo],$AllMSList)
                    }
            }
            
        
    }