SCOMHelper.psm1
####################################################################### # PRIVATE ####################################################################### <# .Link http://jdhitsolutions.com/blog/2013/01/ .Inputs Object .Outputs Simulated graph .Notes Version: 1.0? (Tyson: I'm not sure where I found this seemingly early version of this function. However, it's obvious to me that this is from Jeffery Hicks.) Author : Jeffery Hicks (http://jdhitsolutions.com/blog) #> Function Out-ConsoleGraph { [CmdletBinding()] Param( [Parameter(Position=0, ValueFromPipeline=$true)] [Object] $Object, [Parameter(Mandatory=$true)] [String] $Property, $Columns ) BEGIN { $Width = $Host.UI.RawUI.BufferSize.Width $Data = @() } PROCESS { # Add all of the objects from the pipeline into an array $Data += $Object } END { # Determine scale of graph Try { $Largest = $Data.$Property | Sort-Object | Select-Object -Last 1 } Catch { Write-Warning "Failed to find property $Property" Return } if ($Largest) { # Add the width of all requested columns to each object $Data = $Data | Select-Object -Property $Columns | ForEach-Object{ $Lengths = @() $Len = 0 $Item = $_ $Columns | ForEach-Object{ if ($Item.$($_)) { $Len += $Item.$($_).ToString().Length } } Add-Member -InputObject $Item -MemberType NoteProperty -Name Length -Value $Len -PassThru $Lengths += $Len } # Determine the available chart space based on width of all requested columns $Sample = $Lengths | Sort-Object -Property Length | Select-Object -Last 1 [Int]$Longest = $Sample.Length + ($Columns.Count * 33) $Available = $Width-$Longest-4 ForEach ($Obj in $Data) { # Set bar length to 0 if it is not a number greater than 0 if ($Obj.$Property -eq '-' -OR $Obj.$Property -eq 0 -or -not $Obj.$Property) { [Int]$Graph = 0 } else { $Graph = (($Obj.$Property) / $Largest) * $Available } # Based on bar size, use a different character to visualize the bar if ($Graph -ge 2) { [String]$G = [char]9608 } elseif ($Graph -gt 0 -AND $Graph -le 1) { [String]$G = [char]9612 $Graph = 1 } # Create the property that will contain the bar $Char = $G * $Graph $Obj | Select-Object -Property $Columns | Add-Member -MemberType NoteProperty -Name Graph -Value $Char -PassThru } # End ForEach } # End if ($Largest) } # End of END block } # End Out-ConsoleGraph ####################################################################### Function Get-ModuleRunasProfileName { Param( $Module, $Profiles ) $ModuleRunAs = $Profiles | Where-Object { $_.Id.Guid -eq $module.RunAs.Id.GUID } Return ($ModuleRunAs.Name+";"+$ModuleRunAs.DisplayName) } ####################################################################### # PRIVATE ####################################################################### <# .Synopsis Will create a graphical structure (.png file) that represents SCOM class taxonomy; all SCOM class attributes, properties, hosting relationships, and discovery relationships for a SCOM class. .DESCRIPTION This function will use the GraphViz package to produce a graph-like structure (.png file) that will represent one or more SCOM classes and it's full hierarchy which includes parents, attributes, properties, hosting relationships (both hosted and hosting), and any discoveries which are capable of discovering the related classes. Any number of SCOM class objects or names may be piped to the function. This function relies on the following modules. User will be prompted for permissio to install these: OpsMgrExtended: available from the PowerShell Gallery here: https://www.powershellgallery.com/packages/OpsMgrExtended GraphViz: from the Chocolatey repo. (https://www.graphviz.org/) PSGraph: https://github.com/KevinMarquette/PSGraph .EXAMPLE PS C:\> New-SCOMClassGraph -ClassName 'Microsoft.SQLServer.2012.DBEngine' .EXAMPLE PS C:\> New-SCOMClassGraph -ClassName 'Microsoft.SQLServer.2014.AlwaysOn.DatabaseReplica' -Caching:$False The above example will generate a new graph even if one already exists in the default storage directory. .EXAMPLE PS C:\> New-SCOMClassGraph -ClassName 'Microsoft.Windows.Server.6.2.LogicalDisk' -ShowDiscoveries:$false The above example will create a graph but will not include Discovery relationships. NOTE: If you are not seeing discovery data in your graphs it may be because the graphs are cached without discovery data. Try using the caching switch to force new graph files to be created. Discovery data will be included by default. Example: -Caching:$false .EXAMPLE PS C:\> Get-SCOMClass -Name *sql* | New-SCOMClassGraph -ShowGraph:$false The above example will create graph files for ALL SQL classes but will not open the graph files in the default application. Typically there are a tremendous number of SQL classes in the SQL management packs. .EXAMPLE PS C:\> New-SCOMClassGraph -ID 'ea99500d-8d52-fc52-b5a5-10dcd1e9d2bd' .EXAMPLE (Get-SCOMClassInstance -DisplayName "http://ms01.contoso.com:80/" ).getclasses().Name | New-SCOMClassGraph The above example will show the class graph for a specific instance of a class type. .EXAMPLE (Get-SCOMClassInstance -DisplayName "SQLEXPRESS" ).getclasses().Name | New-SCOMClassGraph The above example will show the class graph for a specific instance of a class type. .EXAMPLE Get-SCOMClass | Select Name,Displayname, @{N='ManagementPackName';E={$_.Identifier.Domain[0]} }| Out-GridView -PassThru | New-SCOMClassGraph This example above is a much fancier way to select one or more class names with the help of GridView. This command will get all SCOM classes in the management group and present Name, DisplayName, and ManagementPackName in GridView for easy browsing, filtering, and selection by the user. Select one or more classes from the Grid View, click OK. The selected class name(s) gets piped into the function: New-SCOMClassGraph .Parameter ClassName This is the name of a class. (not the DisplayName) .Parameter Class An Operations Manager class object. .Parameter ID ID of a class. .Parameter Caching This will enable/disable the use of preexisting graphs. If $false, a new graph will be created. If $true, the script will look for an existing graph for the class. If no graph exists, a new one will be created. .Parameter ShowGraph This allows the user to generate the graphs in the designated directory without opening them (with the default application associated to the .png file type) .Parameter ShowDiscoveries This will allow the user to omit the discovery relationships. .NOTES Author: Tyson Paul Blog: https://blogs.msdn.microsoft.com/tysonpaul/2018/07/18/scomhelper-powershell-module-a-scom-admins-best-friend/ Date: 2018.06.14 Version: 1.2 .LINK Get-SCOMMPFileInfo Get-SCOMClassInfo #> Function New-SCOMClassGraph { [CmdletBinding(DefaultParameterSetName='Parameter Set 1', SupportsShouldProcess=$true, PositionalBinding=$false, HelpUri = 'https://blogs.msdn.microsoft.com/tysonpaul/', ConfirmImpact='Medium')] Param ( [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromRemainingArguments=$false, Position=0, ParameterSetName='Parameter Set 1')] [Microsoft.EnterpriseManagement.Configuration.ManagementPackClass[]]$Class, [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, ValueFromRemainingArguments=$false, Position=0, ParameterSetName='Parameter Set 2')] [alias('Name', 'DisplayName')] [string[]]$ClassName, [Parameter(Mandatory=$true, ValueFromPipeline=$false, ValueFromRemainingArguments=$false, Position=0, ParameterSetName='Parameter Set 3')] [string[]]$ID, [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromRemainingArguments=$false, Position=1)] [string]$outDir, [switch]$Caching=$true, [switch]$ShowGraph=$true, [switch]$ShowDiscoveries=$true ) Begin{ Import-Module OperationsManager,PSGraph,PowerShellGet -ErrorAction SilentlyContinue If (-NOT ($PSVersionTable.PSVersion.Major -ge 5)){ Write-Error "PowerShell version: $($PSVersionTable.PSVersion.Major) detected! " Write-Error "Upgrade to PowerShell version 5 or greater to use this function." Break } 'OperationsManager' | ForEach-Object { If (-Not [bool](Get-Module -Name $_ -ErrorAction SilentlyContinue )) { Write-Error "Required module '$_' does not exist. Please install the module or run this function from a machine where the Operations Manager Console exists. Typically the required module will exists wherever the Console has been installed. Exiting." Break } } # Make sure GraphViz package is installed. If (-NOT ([bool](Get-Package -Name 'GraphViz' -ProviderName 'Chocolatey' -ErrorAction SilentlyContinue))){ Write-Error "Required package 'GraphViz' does not exist. Please install package." $choice = Read-Host "Install module: GraphViz ? (Y/N)" While ($choice -notmatch '[y]|[n]'){ $choice = Read-Host "Y/N?" } Switch ($choice){ 'y' { # Install 'GraphViz' from the Powershell Gallery Write-Host "Find-Package -Name 'GraphViz' -Source 'Chocolatey' | Install-Package -Verbose" -F Gray Find-Package -Name 'GraphViz' -Source "Chocolatey" | Install-Package -Verbose If ($?){ Write-Host "Package installed!" } Else { Write-Error "Problem installing package.`nExiting.`n"; Exit} } 'n' { Write-Host "Package will not be installed.`nExiting.`n" Break } } } # Make sure PSGraph is available/loaded # https://github.com/KevinMarquette/PSGraph If (-Not [bool](Get-Module -Name 'PSGraph' -ErrorAction SilentlyContinue )) { Write-Error "Required module 'PSGraph' does not exist. Please install module." $choice = Read-Host "Install module: PSGraph (Y/N) ?" While ($choice -notmatch '[y]|[n]'){ $choice = Read-Host "Y/N?" } Switch ($choice){ 'y' { # Install PSGraph from the Powershell Gallery Write-Host "Find-Module PSGraph | Install-Module -Verbose" Find-Module PSGraph | Install-Module -Verbose If ($?){ Write-Host "Module installed!" Import-Module PSGraph } Else { Write-Error "Problem installing module. You may manually download from this location: 'https://github.com/KevinMarquette/PSGraph'. Additional information here: 'https://kevinmarquette.github.io/2017-01-30-Powershell-PSGraph/'. For information on how to install a PowerShell module, see this article: 'https://msdn.microsoft.com/en-us/library/dd878350(v=vs.85).aspx'.`nExiting.`n"; Break } } 'n' { Write-Host "Module will not be installed.`nExiting.`n" Break } } } ############################################################### # This is a customized version of the original function located in the PSGraph module here: https://github.com/KevinMarquette/PSGraph Function Record { <# .SYNOPSIS Creates a record object for GraphViz. This is a customized version of the "Record" function that exists in the PSGraph module from Kevin Marquette. I added some additional parameters to control font styles. #> [OutputType('System.String')] [cmdletbinding(DefaultParameterSetName = 'Script')] param( [Parameter( Mandatory=$true, Position = 0 )] [alias('ID', 'Node')] [string] $Name, [Parameter( Position = 1, ValueFromPipeline=$true, ParameterSetName = 'Strings' )] [alias('Rows')] [Object[]] $Row, [Parameter( Position = 1, ParameterSetName = 'Script' )] [ScriptBlock] $ScriptBlock, [Parameter( Position = 2 )] [ScriptBlock] $RowScript, [string] $Label, # Added for color customization of header [string]$FONTCOLOR = 'white', [string]$BGCOLOR = 'black', # Added for color customization of table [string]$FILLCOLOR = 'white', [string]$STYLE = 'filled', [string]$TABLECOLOR = 'black' ) begin { $tableData = [System.Collections.ArrayList]::new() if ( [string]::IsNullOrEmpty($Label) ) { $Label = $Name } } process { if ( $null -ne $ScriptBlock ) { $Row = $ScriptBlock.Invoke() } if ( $null -ne $RowScript ) { $Row = foreach ( $node in $Row ) { @($node).ForEach($RowScript) } } $results = foreach ( $node in $Row ) { Row -Label $node } foreach ( $node in $results ) { [void]$tableData.Add($node) } } end { #$html = '<TABLE CELLBORDER="1" BORDER="0" CELLSPACING="0"><TR><TD bgcolor="black" align="center"><font color="white"><B>{0}</B></font></TD></TR>{1}</TABLE>' -f $Label, ($tableData -join '') $html = '<TABLE CELLBORDER="1" COLOR="'+$TABLECOLOR+'" BORDER="0" CELLSPACING="0"><TR><TD bgcolor="'+$BGCOLOR+'" align="center"><font color="'+$FONTCOLOR+'"><B>{0}</B></font></TD></TR>{1}</TABLE>' -f $Label, ($tableData -join '') #Node $Name @{label = $html; shape = 'none'; fontname = "Courier New"; style = "filled"; penwidth = 1; fillcolor = "white"} Node $Name @{label = $html; shape = 'none'; fontname = "Courier New"; style = $STYLE; penwidth = 1; fillcolor = $FILLCOLOR} } }#End Function ############################################################### Function Dig-Class { [CmdletBinding(DefaultParameterSetName='Parameter Set 1', SupportsShouldProcess=$true, PositionalBinding=$false, ConfirmImpact='Medium')] [Alias()] [OutputType([System.Object[]])] Param( [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=0, ParameterSetName='Parameter Set 1')] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [Microsoft.EnterpriseManagement.Configuration.ManagementPackClass]$Class, # Determines if this Class object is hosting another class. Will affect the shape/color [Parameter(Mandatory=$false, ValueFromPipeline=$false, Position=1, ParameterSetName='Parameter Set 1')] [switch]$IsHosting=$false ) Begin {} Process{ Write-Host $Class.Name -ForegroundColor Yellow $hashThisClass = @{} $hashCollection = @{} # If a base class exists, dig it If ([bool]$Class.Base.ID.Guid){ $BaseClass = (Get-SCClass -ID $Class.Base.ID.Guid ) #### TEST ### # If (-NOT $hashGraph.ContainsKey([string]$BaseClass.Name)) { #TEST $hashCollection = (Dig-Class $BaseClass) #Original [System.Object]$hashCollection[$BaseClass.Name]["Edges"] += @($Class.Name) #Original # } #TEST } #region TEST if we can add entire hosting chain of all hosted/hosting classes # If a hosting class exists, dig it. If ($Class.Hosted){ $hashGraph2= @{} $hostClass = $Class.FindHostClass() $hashHosting = (Dig-Class -Class $hostClass -IsHosting) $hashGraph2 = (Merge-HashTables -hmaster $hashCollection -htnew $hashHosting) $hashGraph2[$hostClass.Name].Edges += @($Class.Name) $hashCollection = $hashGraph2 } #endregion [System.Object[]]$PropNames =@() If ([bool]$Class.DisplayName) { $PropNames += Stylize-Row -Color $c_classAttribute -Bold $s_ClasAttributeBold -thisString "DisplayName: $($Class.DisplayName.ToString())" } $PropNames += Stylize-Row -Color $c_classAttribute -Bold $true -thisString "MP: $($Class.ManagementPackName)" If ($Class.Hosted) { $PropNames += Stylize-Row -Color $c_classAttribute -Bold $true -thisString "Hosted: $($Class.Hosted.ToString())" } Else { $PropNames += Stylize-Row -Color $c_classAttribute -Bold $s_ClasAttributeBold -thisString "Hosted: $($Class.Hosted.ToString())" } If ([bool]$Class.Abstract) { $PropNames += Stylize-Row -Color $c_classAttribute -Bold $TRUE -thisString "Abstract: $($Class.Abstract.ToString())" } Else{ $PropNames += Stylize-Row -Color $c_classAttribute -Bold $s_ClasAttributeBold -thisString "Abstract: $($Class.Abstract.ToString())" } If ([bool]$Class.Extension) { $PropNames += Stylize-Row -Color $c_ExtensionFont -Bold $s_ClasAttributeBold -thisString "Extension: $($Class.Extension.ToString())" } Else { $PropNames += Stylize-Row -Color $c_classAttribute -Bold $s_ClasAttributeBold -thisString "Extension: $($Class.Extension.ToString())" } # Get all properties of the class $PropNames += $Class.GetProperties().Name | Sort-Object # Identify the Key property (if any) $KeyName = $Class.GetProperties() | Where-Object {$_.Key -eq $true} | Select-Object -Property Name -ExpandProperty Name # If a Key property exists, format it as BOLD If ([Bool]$KeyName ) { $PropNames = $PropNames | Where-Object { (-not ([string]::IsNullOrEmpty($_))) } |ForEach-Object {$_ -Replace "^$KeyName$","<Font Color=`"Red`"><B>$($_)</B></Font>"} } $hashStyling = Get-DefaultStyling $hashThisClass = [Ordered]@{ #"Edges" = $Edges "PropNames" = $PropNames "Abstract" = $Class.Abstract "Hosted" = $Class.Hosted "Hosting" = $IsHosting } $hashThisClass['Styling'] = $hashStyling $hashCollection[$Class.Name] = $hashThisClass Return $hashCollection }#end Process End {} } ############################################################### Function Merge-HashTables { param( $hmaster, $htnew ) $hmaster.keys | ForEach-Object { $key = $_ If (-NOT $htnew.containskey($key)) { $htnew.Add($key,$hmaster.$key) } Else { [System.Object]$htnew[$_].Edges += $hmaster[$_].Edges } } #$htnew = $htold + $htnew return $htnew } ############################################################### Function Stylize-Record { # Modify colors for any hosted/hosting records Param( $hashtable ) $hashtable.Keys | ForEach-Object{ #<# If ($hashtable[$_].Abstract -eq $true){ # Table Header $hashtable[$_].Styling.FONTCOLOR = $c_AbstHdrFont $hashtable[$_].Styling.BGCOLOR = $c_AbstHdrBG # Table/Rows $hashtable[$_].Styling.FILLCOLOR = $c_AbstFill $hashtable[$_].Styling.Style = $c_AbstStyle $hashtable[$_].Styling.TABLECOLOR = $c_AbstTable } #> If ($hashtable[$_].Hosted -eq $true) { # Table Header $hashtable[$_].Styling.FONTCOLOR = $c_hostedHdrFont $hashtable[$_].Styling.BGCOLOR = $c_hostedHdrBG # Table/Rows $hashtable[$_].Styling.FILLCOLOR = $c_hostedFill $hashtable[$_].Styling.Style = $c_hostedStyling $hashtable[$_].Styling.TABLECOLOR = $c_hostedTable } If ($hashtable[$_].Hosting -eq $true){ # Table Header $hashtable[$_].Styling.FONTCOLOR = $c_hostingHdrFont $hashtable[$_].Styling.BGCOLOR = $c_hostingHdrBG # Table/Rows $hashtable[$_].Styling.FILLCOLOR = $c_hostingFill $hashtable[$_].Styling.Style = $c_hostingStyle #"filled" #$hashtable[$_].Styling.TABLECOLOR = $c_hostingTable } # Make sure the Abstract classes retain their header bg color If ($hashtable[$_].Abstract -eq $true){ # Table Header $hashtable[$_].Styling.BGCOLOR = $c_AbstHdrBG } If ($hashtable[$_].Discovery -eq $true){ # Table Header $hashtable[$_].Styling.FONTCOLOR = $c_DiscHdrFont $hashtable[$_].Styling.BGCOLOR = $c_DiscHdrBG # Table/Rows $hashtable[$_].Styling.FILLCOLOR = $c_DiscFill $hashtable[$_].Styling.Style = $c_DiscStyle $hashtable[$_].Styling.TABLECOLOR = $c_DiscTable } } Return $hashtable } ############################################################### Function Stylize-Row { [CmdletBinding(DefaultParameterSetName='Parameter Set 1', SupportsShouldProcess=$true, PositionalBinding=$false, ConfirmImpact='Medium')] [Alias()] [OutputType([System.Object[]])] Param( [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=0, ParameterSetName='Parameter Set 1')] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [System.String[]]$thisString, [string]$Color = 'black', [bool]$Bold = $false ) Begin{} Process{ ForEach ($string in $thisString){ $newString = ($String -replace '(<Font)|(Color="[a-zA-Z]*">)|(<B>)|(<\/B>)|(<\/Font>)','' ) $newString = $newString | Where-Object { (-not ([string]::IsNullOrEmpty($_))) } | ForEach-Object -Process {$_ -Replace "^$_$","<Font Color=`"$($Color)`"><B>$($_)</B></Font>"} If (-NOT $Bold){ $newString = ($newString -replace '(<B>)|(</B>)','' ) } Return $newString } } End{} } ############################################################### Function Get-DefaultStyling { # Default Record style $hashStyling = [Ordered]@{ # Table Header FONTCOLOR = $c_defaultHdrFont BGCOLOR = $c_defaultHdrBG # Table/Rows FILLCOLOR = $c_defaultFill Style = $c_defaultStyle TABLECOLOR = $c_defaultTable } Return $hashStyling } ############################################################### Function Get-Discoveries { Param( [hashtable]$hashMaster ) $discHash = @{} # Get only discoveries which potentially create classes (not relationships) $discs = Get-SCDiscovery | Where-Object { [bool]$_.DiscoveryClassCollection} ForEach ($disc in $discs) { If ($disc.DiscoveryClassCollection.Count -gt 1){ $c=1 } Else { $c=$null } ForEach ($discCollection in $disc.DiscoveryClassCollection) { $thisHash = @{} If ($hashMaster.containsKey([string]$discCollection.typeid.Identifier.path) -and (-NOT $thisHash.ContainsKey([string]$discCollection.typeid.Identifier.path)) ) { $PropNames = @() $PropNames += Stylize-Row -thisString "DisplayName: $($Disc.DisplayName)" -Color $c_DiscDisplayName -Bold $true $PropNames += Stylize-Row -thisString "Target: $([string]$disc.target.Identifier.Path)" -Color $c_DiscTarget -Bold $true ForEach ($PropID in $discCollection.propertycollection.PropertyID ) { $PropNames += Stylize-Row -thisString $PropID -Color $c_DiscRow } $thisHash.Add('PropNames',$PropNames) $thisHash.'Edges' += @([string]$discCollection.typeid.Identifier.path) $thisHash.'Label' = $disc.Name $thisHash.'Discovery' = $true $thisHash.'Styling' = Get-DefaultStyling $c++ $discHash.Add(([string]$disc.Name+$c),$thisHash) } } } Return $discHash } ############################################################### Function New-Graph { Graph { #region Classes $hashGraph.Keys | ForEach-Object -Process { # These will determine if Hosted/Hosting nodes appear in the Legend at the top of the .png If ([bool]$hashGraph[$_].Hosted) {$HostedExists = $true} If ([bool]$hashGraph[$_].Hosting) {$HostingExists = $true} $params = @{ Name = $_ Row = $hashGraph[$_].PropNames } $params += $hashGraph[$_].Styling Record @params Edge $_ -to ($hashGraph[$_].Edges | Select-Object -Unique) } #endregion Classes If ($ShowDiscoveries) { #region Discoveries $hashDiscoveries.Keys | ForEach-Object { $Params = @{ Name = $_ Label = $hashDiscoveries[$_].Label Row = $hashDiscoveries[$_].PropNames } $Params += $hashDiscoveries[$_].Styling Record @Params Edge $_ -to ($hashDiscoveries[$_].Edges | Select-Object -Unique) } #endregion Discoveries } #region Legend SubGraph -Attributes @{label='Legend'} -ScriptBlock { If ($HostedExists) { # Output Legend nodes Record -Name "Hosted Class" ` -Row ` @((Stylize-Row -thisString 'Class Attribute' -Color $c_classAttribute -Bold $s_ClasAttributeBold),` (Stylize-Row -thisString 'Key Property' -Color $c_Key -Bold $s_KeyBold),` 'Property') ` -BGCOLOR $c_hostedHdrBG ` -FONTCOLOR $c_hostedHdrFont ` -FILLCOLOR $c_hostedFill ` -STYLE $c_hostedStyling ` -TABLECOLOR $c_hostedTable } If ($HostingExists) { Record -Name "Hosting Class" ` -Row ` @((Stylize-Row -thisString 'Class Attribute' -Color $c_classAttribute -Bold $s_ClasAttributeBold),` (Stylize-Row -thisString 'Key Property' -Color $c_Key -Bold $s_KeyBold),` 'Property') ` -BGCOLOR $c_hostingHdrBG ` -FONTCOLOR $c_hostingHdrFont ` -FILLCOLOR $c_hostingFill ` -STYLE $c_hostingStyle ` -TABLECOLOR $c_hostingTable } Record -Name "Abstract Class" ` -Row ` @((Stylize-Row -thisString 'Class Attribute' -Color $c_classAttribute -Bold $s_ClasAttributeBold),` (Stylize-Row -thisString 'Key Property' -Color $c_Key -Bold $s_KeyBold),` 'Property') ` -BGCOLOR $c_AbstHdrBG ` -FONTCOLOR $c_AbstHdrFont ` -FILLCOLOR $c_AbstFill ` -STYLE $c_AbstStyle ` -TABLECOLOR $c_AbstTable Record -Name "Class" ` -Row ` @((Stylize-Row -thisString 'Class Attribute' -Color $c_classAttribute -Bold $s_ClasAttributeBold),` (Stylize-Row -thisString 'Key Property' -Color $c_Key -Bold $s_KeyBold),` 'Property') ` -BGCOLOR $c_defaultHdrBG ` -FONTCOLOR $c_defaultHdrFont ` -FILLCOLOR $c_defaultFill ` -STYLE $c_defaultStyle ` -TABLECOLOR $c_defaultTable If ($ShowDiscoveries -and ([bool]$hashDiscoveries.Count)) { Record -Name "Discovery" ` -Row ` (Stylize-Row -thisString 'Discovered Property' -Color $c_DiscRow )` -BGCOLOR $c_DiscHdrBG ` -FONTCOLOR $c_DiscHdrFont ` -FILLCOLOR $c_DiscFill ` -STYLE $c_DiscStyle ` -TABLECOLOR $c_DiscTable } } #end SubGraph #endregion Legend } | Export-PSGraph -DestinationPath $outPath -ShowGraph:$ShowGraph #-------------------------------- } ############################################################### # Styling/Colors $c_Key = 'Red' $s_KeyBold = $true $c_classAttribute = '#7503a5'#'#8e8a86' $s_ClasAttributeBold = $false #$true $c_ExtensionFont = 'green' $c_defaultHdrBG = 'black' $c_defaultHdrFont = 'white' $c_defaultFill = 'white' $c_defaultStyle = '' $c_defaultTable = 'black' $c_hostedHdrFont = '#d87d22' #'orange' #'#e0bc60' $c_hostedHdrBG = 'white' #gray $c_hostedFill = '' #'#e0bc60' $c_hostedStyling = '' $c_hostedTable = 'green' #'orange' #'#c66d00' $c_hostingHdrFont = '#c96d12' $c_hostingHdrBG = 'white' $c_hostingFill = '#ffd089'#'#f7be6a' $c_hostingStyle = 'filled' $c_hostingTable = $c_hostingFill #'#c96d12' $c_AbstHdrFont = 'white' $c_AbstHdrBG = '#c4c0bc' #'gray' $c_AbstFill = '' $c_AbstStyle = '' $c_AbstTable = '#9e9a97' $c_DiscHdrFont = 'white' $c_DiscHdrBG = '#206dea' #Blue $c_DiscFill = '' $c_DiscStyle = '' $c_DiscTable = '#002560' #DarkBlue $c_DiscRow = 'blue' $c_DiscDisplayName = '#0646ad' $c_DiscTarget = '#0646ad' If (-Not [Bool]$outDir ) { $outDir = (Join-Path $env:Temp 'New-SCOMClassGraph') } If (-NOT(Test-Path -Path $outDir -PathType Container)) { New-Item -Path $outDir -ItemType Directory -Force -ErrorAction SilentlyContinue } }#end Begin Process{ $hashGraph = @{} $hashDiscoveries = @{} If ([Bool]$ID){ $ID = $ID | ForEach-Object { $_ -Replace '{|}','' } $Class = (Get-SCClass -ID ($ID | Select-Object -Unique)) } ElseIf ([Bool]$ClassName){ $Class = (Get-SCClass -Name ($ClassName | Select-Object -Unique) ) If (-Not [Bool]$Class){ $Class = (Get-SCClass -DisplayName ($ClassName | Select-Object -Unique)) If (-Not [Bool]($Class)) { Write-Host "Unable to retrieve class: $($ClassName). Check spelling." -F Yellow Return; } } } ForEach ($thisClass in $Class) { $HostedExists = $false $HostingExists = $false $fileNameBase = $thisClass.Name $outPath = (Join-Path $OutDir ("$fileNameBase" +".png" )) # If the graph has already been created, no sense creating it again. Open exising file. If ( ($caching) -and (Test-Path -Path $outPath -PathType Leaf) ) { Invoke-Item $outPath Break } #This will dig into the Class taxonomy and identify the entire family tree, storing all parents/properties/relationships into a hash table. $hashGraph = (Dig-Class -Class $thisClass) If ($ShowDiscoveries) { #This will get any discoveries related to any of the classes $hashDiscoveries = Get-Discoveries -hashMaster $hashGraph # This will stylize the Discovery nodes. $hashDiscoveries = Stylize-Record -hashtable $hashDiscoveries } # This will stylize the class nodes (names, properties) based on their type (abstract, hosted, hosting, etc.) $hashGraph = Stylize-Record -hashtable $hashGraph # ...and now for the moment we've all been waiting for. New-Graph } #end ForEach Class }#end Process End{} }#end New-SCOMClassGraph ####################################################################### <# .SYNOPSIS This is used to generate a unique hash string from an ordinary string value. .NOTES Author: Tyson Paul Date: 2018.05.30 Blog: https://blogs.msdn.microsoft.com/tysonpaul/2018/07/18/scomhelper-powershell-module-a-scom-admins-best-friend/ History: Adapted from http://jongurgul.com/blog/get-stringhash-get-filehash/ #> Function Get-StringHash{ [CmdletBinding(DefaultParameterSetName='Parameter Set 1', SupportsShouldProcess=$true, PositionalBinding=$false, HelpUri = 'https://blogs.msdn.microsoft.com/tysonpaul/', ConfirmImpact='Medium')] Param ( [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, ValueFromRemainingArguments=$false, Position=0, ParameterSetName='Parameter Set 1')] [String] $String, $HashName = "MD5" ) $StringBuilder = New-Object System.Text.StringBuilder [System.Security.Cryptography.HashAlgorithm]::Create($HashName).ComputeHash([System.Text.Encoding]::UTF8.GetBytes($String))|ForEach-Object{ [Void]$StringBuilder.Append($_.ToString("x2")) } $StringBuilder.ToString() } ####################################################################### <# .SYNOPSIS Will export Operations Manager event log events to CSV .EXAMPLE PS C:\> Export-SCOMEventsToCSV The above example will output the newest 1000 SCOM events (default 1000) to a .CSV at C:\<computername>_OpsManEvents.csv .EXAMPLE PS C:\> Export-SCOMEventsToCSV -Newest 1500 -Path c:\Temp\SCOMlog.csv .NOTES Author: Tyson Paul Blog: https://blogs.msdn.microsoft.com/tysonpaul/2018/07/18/scomhelper-powershell-module-a-scom-admins-best-friend/ Original Date: 2018.03.22 History: #> Function Export-SCOMEventsToCSV { Param ( [int]$Newest = 1000, [string]$OutFileCSV="C:\$($env:COMPUTERNAME)_OpsManEvents.csv" ) Get-EventLog -LogName 'Operations Manager' -Newest $Newest | Export-Csv -Path $OutFileCSV -NoTypeInformation -Force } ####################################################################### <# .Synopsis Will return the friendly name of a RunAs account. .DESCRIPTION Often times RunAs account SSIDs will appear in Operations Manager event logs which makes it difficult to determine which account is involved. This will correlate the friendly name with the SSID provided. .EXAMPLE PS C:\> Get-SCOMRunAsAccountName -SSID '0000F61CD9E515695ED4A018518C053E3CD87251D500000000000000000000000000000000000000' .Parameter -SSID The SSID as it is presented in the Operations Manager event log; an 80 character string of Hex digits [0-9A-F] .NOTES Author: Tyson Paul Blog: https://blogs.msdn.microsoft.com/tysonpaul/2018/07/18/scomhelper-powershell-module-a-scom-admins-best-friend/ Original Date: 2013.09.23 History: I think I originally got most of this from a Technet blog: https://social.technet.microsoft.com/Forums/systemcenter/en-US/0b9bd679-a712-435e-9a27-8b3041cddac8/how-to-find-the-runasaccount-from-the-ssid?forum=operationsmanagergeneral #> Function Get-SCOMRunAsAccountName{ param ( [Parameter( Mandatory=$true, HelpMessage="Please enter the SSID")] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [string]$SSID ) Get-SCOMRunAsAccount | Sort-Object Name | ForEach-Object { $string = $null;$_.SecureStorageId | ForEach-Object { $string = $string + "{0:X2}" -f $_ } $RunAsAccountName = $_.Name [string]$RunAsAccountSSID = $string If ($SSID -match $RunAsAccountSSID) { Write-Host "The Run As Account Name is: $RunAsAccountName" } } }#end Function ####################################################################### <# .Synopsis Will display all known modules contained in all sealed management packs as well as basic schema information. Based on the original script found here: http://sc.scomurr.com/scom-2012-r2-mp-authoring-getting-modules-and-their-configurations/ .EXAMPLE PS C:\> Show-SCOMModules .EXAMPLE PS C:\> Show-SCOMModules | Out-GridView .NOTES Author: Tyson Paul Blog: https://blogs.msdn.microsoft.com/tysonpaul/2018/07/18/scomhelper-powershell-module-a-scom-admins-best-friend/ Original Date: 2017.12.13 History: 2018.01.22: Refined. 2017/12/13: First version #> Function Show-SCOMModules { $collection=@() $RAAP = Get-SCOMRunAsProfile ForEach ($MP in (Get-SCManagementPack | Where-Object{$_.Sealed -eq $True})) { ForEach($Module in ($MP.GetModuleTypes() | Sort-Object -Property Name | Sort-Object -Property XMLTag)) { $RunAs = '' $Object = New-Object PSObject $Object | Add-Member Noteproperty -Name Module -Value $Module.Name $Object | Add-Member Noteproperty -Name ManagementPackName -Value $MP.Name $Object | Add-Member Noteproperty -Name Accessibility -Value $Module.Accessibility If ([bool]$Module.RunAs){ $RunAs = Get-ModuleRunasProfileName -Module $Module -Profiles $RAAP } $Object | Add-Member Noteproperty -Name RunAs -Value $RunAs $Object | Add-Member Noteproperty -Name ID -Value $Module.ID if($null -ne $Module.Configuration.Schema) { # $set = ($Module.Configuration.schema.split("<") | Where-Object {$_ -match "^xsd:element.*"} | ` ForEach-Object -Process {$_.substring($_.indexof("name")).split("=")[1].split()[0]}) $Object | Add-Member Noteproperty Schema $set } $Object | Add-Member Noteproperty -Name Managed -Value $Module.Managed $collection += $object } } Return $collection } ####################################################################### <# .Synopsis Will standardize all of the aliases in one or more unsealed (.xml) management pack files. .DESCRIPTION This script will inventory all of the MP reference IDs that can be found in the single file or set of .XML files specified by the InputPath. It will create a customized set of condensed alias acronyms to be standardized across all of the unsealed management packs. It will then replace the aliases in the Manifest as well as throughout the elements where the aliases are used. .Parameter InputPath This can be a directory or full path to an unsealed management pack .xml file. .EXAMPLE PS C:\> Set-SCOMMPAliases -InputPath 'C:\UnSealedMPs\' .EXAMPLE PS C:\> Set-SCOMMPAliases -InputPath 'C:\UnSealedMPs\MyCustom.Monitoring.xml' .NOTES Author: Tyson Paul Blog: https://blogs.msdn.microsoft.com/tysonpaul/2018/07/18/scomhelper-powershell-module-a-scom-admins-best-friend/ Date: 2017/12/13 History: 2017/12/13: First version #> Function Set-SCOMMPAliases { Param ( [Parameter(Mandatory=$true)] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [ValidateScript({Test-Path -Path $_})] [string]$InputPath #Either a folder or full file path ) ##### TESTING ###### #$InputPath = 'C:\Test\Test\TempTest' ##### TESTING ###### # This will be used to seed the reference catalog with some hardcoded values for some common aliases $seed = @' Microsoft.SystemCenter = SC Microsoft.SystemCenter.Apm.Infrastructure = APMInf Microsoft.SystemCenter.Apm.Infrastructure.Monitoring = APMInfMon Microsoft.SystemCenter.Apm.Library = APMLib Microsoft.SystemCenter.Apm.NTServices = APMNT Microsoft.SystemCenter.Apm.Wcf = APMWcf Microsoft.SystemCenter.Apm.Web = APMWeb Microsoft.SystemCenter.DataWarehouse.ApmReports.Library = APMReports Microsoft.SystemCenter.InstanceGroup.Library = SCIGL Microsoft.SystemCenter.Internal = SCInt Microsoft.SystemCenter.Library =SCLib Microsoft.SystemCenter.WebApplication.Library = WebAppLib Microsoft.SystemCenter.WebApplicationSolutions.Library = WebAppSolLib Microsoft.SystemCenter.WebApplicationSolutions.Library.Resources.ENU = WebAppLibRes Microsoft.SystemCenter.WebApplicationTest.External.Library = WebAppTestExtLib Microsoft.SystemCenter.WebApplicationTest.Library = WebAppTestLib Microsoft.Windows.InternetInformationServices.2003 = IIS2003 Microsoft.Windows.Library = Windows Microsoft.Windows.Server.Library = WinSrvLib System.Health.Library = Health System.Library = System System.NetworkManagement.Library = NetManLib System.NetworkManagement.Monitoring = NetManMon System.NetworkManagement.Reports = NetManRpt System.NetworkManagement.Templates = NetManTmp System.Performance.Library = Performance '@ # $seed -split "`r`n" | Sort-Object ############################################################## # Will replace common/popular namespaces with smaller, friendly acronym Function AcronymCommonNames { Param ( [Parameter(Mandatory=$true)] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [string]$thisString ) $acronyms = ConvertFrom-StringData -StringData @" Microsoft.SQLServer = MSQL Microsoft.Windows.Server = MWS Microsoft.Windows.InternetInformationServices = IIS "@ If ( $acronyms.ContainsKey($thisString) ) { $arrNames = $acronyms.GetEnumerator().Name $arrNames | ForEach-Object { #Write-Host $_.Name $_.value $thisString = $thisString.Replace(($_),($acronyms.$_) ) } } Return $thisString } ############################################################## Function Create-Alias { Param( [Parameter(Mandatory=$true)] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [string]$Seed, [Parameter(Mandatory=$true)] [ValidateNotNull()] [ValidateNotNullOrEmpty()] # array of existing values, to prevent duplicates [system.Object[]]$Existing ) $array = AcronymCommonNames -thisString $Seed $array = $array.Split('.') $newAlias = $array[0].Replace('Microsoft','').Replace('Windows','Win').Replace('HewlettPackard','HP') For($i = 1; $i -lt $array.Count; $i++) { $fragment = $array[$i] If ( $fragment -match '\d+' ) { $newAlias += $fragment } ElseIf ( $fragment -match '^ApplicationMonitoring.?' ) { $newAlias += 'AppMon' } ElseIf ( $fragment -match '^Dashboard.?' ) { $newAlias += 'Dash' } ElseIf ( $fragment -match '^Discovery.?' ) { $newAlias += 'Disc' } ElseIf ( $fragment -match '^Library.?' ) { $newAlias += 'Lib' } ElseIf ( $fragment -match '^Linux.?' ) { $newAlias += 'Linux' } ElseIf ( $fragment -match '^Monitor.?' ) { $newAlias += 'Mon' } ElseIf ( $fragment -match '^NetworkManagement.?' ) { $newAlias += 'NetMan' } ElseIf ( $fragment -match '^Network.?' ) { $newAlias += 'Net' } ElseIf ( $fragment -match '^Report.?' ) { $newAlias += 'Rpt' } ElseIf ( $fragment -match '^Server.?' ) { $newAlias += 'Ser' } ElseIf ( $fragment -match '^System.?' ) { $newAlias += 'Sys' } ElseIf ( $fragment -match '^Template.?' ) { $newAlias += 'Tmp' } ElseIf ( $fragment -match '^Unix.?' ) { $newAlias += 'Unix' } ElseIf ( $fragment -match '^Visual.?' ) { $newAlias += 'Vis' } ElseIf ( $fragment -match '^Windows.?' ) { $newAlias += 'Win' } Else { # Use capitalized letters in MP name to build the acronym $tempchar = '' [char[]]$fragment | ForEach-Object { If ([char]::IsUpper($_) ){$tempchar+=$_ } } # $newAlias += $fragment[0] $newAlias += $tempchar } } $i = 2 $tempAlias = $newAlias While ($Existing -contains ($tempAlias)) { $tempAlias = $newAlias +"$i" $i++ } Return $tempAlias } ############################################################## # This function will replace all instances of existing aliases with the newly customized aliases. Function Standardize-AllReferenceAliases { Param( [Parameter(Mandatory=$true)] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [System.Object[]]$MPPaths, [Parameter(Mandatory=$true)] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [Hashtable]$Catalog ) ForEach ($MPPath in $MPPaths){ $mpxml = [xml](Get-Content $MPPath) $arrReferences = $mpxml.GetEnumerator().Manifest.references.reference [int]$i = 0 $content = (Get-Content $MPPath) # Replace aliases wherever used in xml ForEach ($ref in ($arrReferences) ) { <# Aliases typically appear in two scenarios... 1) Examples: referring to some element property: <Property>$MPElement[Name="Windows!Microsoft.Windows.Computer"]/PrincipalName$</Property> <MonitoringClass>$MPElement[Name="SCLib!Microsoft.SystemCenter.ManagedComputer"]$</MonitoringClass> <RelationshipClass>$MPElement[Name="SCIGL!Microsoft.SystemCenter.InstanceGroupContainsEntities"]$ #> $aliasrefbang = '="'+"$($ref.Alias)"+"!" $newAlias = ('="'+($Catalog.($ref.ID))+'!') $content = ($content -Replace ([regex]::Escape($aliasrefbang)), ([regex]::Escape($newAlias)) ) <# 2) Examples: referring to a workflow, type, datasource, etc.: <DataSource ID="GroupPopulationDataSource" TypeID="SCLib!Microsoft.SystemCenter.GroupPopulator"> <DiscoveryRelationship TypeID="SCIGL!Microsoft.SystemCenter.InstanceGroupContainsEntities" /> #> $aliasrefbang = '>'+"$($ref.Alias)"+"!" $newAlias = ('>'+($Catalog.($ref.ID))+'!') $content = ($content -Replace ([regex]::Escape($aliasrefbang)), ([regex]::Escape($newAlias)) ) #$content | Set-Content $MPPath -Force -Encoding UTF8 } # Reload the XML because it was just rewritten #$mpxml = [xml](Get-Content $MPPath) $mpxml = [xml]($content) $arrReferences = $mpxml.GetEnumerator().Manifest.references.reference [int]$i = 0 # Replace aliases in Manifest ForEach ($ref in ($arrReferences) ) { $ref.Alias = $Catalog.($ref.ID) $i++ } Write-Host "`t$i references updated! [ " -NoNewline -b Black -f Cyan Write-Host $(Split-Path -Path $MPPath -Leaf) -NoNewline Write-Host " ]" -b Black -f Cyan $mpxml.Save($MPPath) } } #endFunction ############################################################## <# This function is designed to append (or trim) a unique hash value to/from the aliases (in the Manifest as well as elsewhere in the management pack). This is necessary in the rare cases where existing aliases already match my customized new condensed acronyms in the $cat (catalog). "Windows!" is a good example of this. I've encountered random MPs which have the alias, "Windows" already assigned to a reference. This becomes problematic when I attempt to change references to the 'Microsoft.Windows.Library' after which the file ends up with multiple identical alias references (to different sealed MPs) using the exact same alias, "Windows". This is a clever way to make sure that all aliases are unique (no conflicts). This function should be run twice on the catalog; Once to append the unique string to aliases in the catalog (after the cat has been generated, of course), then modify the files. Then run again to set aliases to their final, desired values, then modify the files again to their final desired values. #> Function Modify-Aliases { Param( [Hashtable]$cat, [Switch]$Localized ) $hash = Get-StringHash $env:COMPUTERNAME If ($Localized){ @($cat.Keys) | ForEach-Object { $Cat[$_] = $cat[$_]+$hash } } Else{ @($cat.Keys) | ForEach-Object { $Cat[$_] = [string]$cat[$_] -Replace $hash, "" } } Return $cat } ############################################################## #http://jongurgul.com/blog/get-stringhash-get-filehash/ Function Get-StringHash { param ( [string] $String, [string] $HashName = "MD5" ) $StringBuilder = New-Object System.Text.StringBuilder [System.Security.Cryptography.HashAlgorithm]::Create($HashName).ComputeHash([System.Text.Encoding]::UTF8.GetBytes($String))|ForEach-Object{ [Void]$StringBuilder.Append($_.ToString("x2")) } $StringBuilder.ToString() } #------------------------------------------------------------- #################### MAIN ################################ ############################################################## # Create catalog of all existing references/aliases # The idea here is to seed the catalog with common pack names. # For other MPs dynamically create aliases based on doted name structure ############################################################## $cat = ConvertFrom-StringData -StringData $seed If (Test-Path -Path $InputPath -PathType Container -ErrorAction SilentlyContinue) { $MPPaths = (Get-ChildItem $InputPath -Recurse -Include *.xml -File ).FullName | Sort-Object Write-Host "$($MPPaths.Count) files found..." -F Yellow -B Black } ElseIf ((Test-Path -Path $InputPath -PathType Leaf -ErrorAction SilentlyContinue) -and ($InputPath -like "*.xml") ) { $MPPaths = $InputPath } Else { Write-Host "Something is wrong with `$InputPath: [$($InputPath)]."; Exit} $refIDs = @() [int]$i=0 ForEach ($MPPath in $MPPaths) { Write-Host $MPPath -F Green $mpxml = [xml](Get-Content $MPPath) #$mpxml.SelectNodes("//Reference") | % { $mpxml.ManagementPack.Manifest.References.Reference | ForEach-Object { $refIDs += $_.ID $_ Write-Progress -Activity "Creating master catalog of known References" -Status $_.ID -PercentComplete ($i / $MPPaths.Count*100) } $i++ } # Create array of all unique IDs $AllRefIDs = ($refIDs | Group-Object ).Name | Sort-Object $AllRefIDs | ForEach-Object { #If ID not in catalog yet, add it with customized alias If ( -not($cat.ContainsKey($_.ToString())) ){ $cat.Add($_,(Create-Alias -Seed $_ -Existing $cat.Values) ) } } # Update all aliases to unique values to prevent naming conflicts when modifying files $cat = Modify-Aliases -Localized:$true -cat $cat Write-Host "Updating references...(first pass, randomized unique values)" -b Black -f Yellow Standardize-AllReferenceAliases -MPPaths $MPPaths -Catalog $cat # Update all aliases to final, correct values, and modify files $cat = Modify-Aliases -Localized:$false -cat $cat Write-Host "Updating references...(final pass)" -b Black -f Green Standardize-AllReferenceAliases -MPPaths $MPPaths -Catalog $cat # Display the reference/alias Catalog $arrCat = @() ForEach ($key in $cat.Keys ){ #Write-Host $key " = " $cat[$key] $arrCat += ("$key = $($cat[$key])" ) } Return ($arrCat | Sort-Object) } #End Function ####################################################################### <# .Synopsis Will test any number of TCP ports on any number of hosts. .DESCRIPTION Will accept piped Computers/IPs or Ports and then test those ports on the targets for TCP connectivity. .Parameter Computer Computer NetBIOS name, FQDN, or IP .Parameter Port TCP Port number to test. .Parameter TimeoutMS The amount of time to wait (in milliseconds) before abandoning a connection attempt on a given port. .EXAMPLE PS C:\> Test-Port -Computer '8.8.8.8' -Port 53 .EXAMPLE PS C:\> 443,80,53,135,137,5723 | Test-Port -Computer 'MS01.contoso.com','DB01' | Sort-Object Computer,Port .EXAMPLE PS C:\> 'MS01.contoso.com','DB01' | Test-Port -Port 5723 .NOTES Author: Tyson Paul Blog: https://blogs.msdn.microsoft.com/tysonpaul/2018/07/18/scomhelper-powershell-module-a-scom-admins-best-friend/ Version: 1.3 Date: 2018.02.07 History: 2018.05.31: Improved error handling. Adapted from Boe Prox (https://gallery.technet.microsoft.com/scriptcenter/97119ed6-6fb2-446d-98d8-32d823867131) #> Function Test-Port { [CmdletBinding(DefaultParameterSetName='Parameter Set 1', SupportsShouldProcess=$true, PositionalBinding=$false, HelpUri = 'https://blogs.msdn.microsoft.com/tysonpaul/', ConfirmImpact='Medium')] Param ( [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromRemainingArguments=$false, Position=0, ParameterSetName='Parameter Set 1')] [string[]]$Computer, [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromRemainingArguments=$false, Position=1, ParameterSetName='Parameter Set 1')] [int[]]$Port, [int]$TimeoutMS=5000 ) Begin { $arrResults = @() $Error.Clear() } Process { If (-not $Port) { $Port = Read-host "Enter the port number to access" } ForEach ($thisComputer in $Computer) { ForEach ($thisPort in $Port) { $Status = "Failure" $objResult = New-Object -TypeName PSCustomObject $tcpobject = New-Object System.Net.Sockets.TcpClient #Connect to remote machine's port $connect = $tcpobject.BeginConnect($thisComputer,$thisPort,$null,$null) #Configure a timeout before quitting - time in milliseconds $wait = $connect.AsyncWaitHandle.WaitOne($TimeoutMS,$false) If (-Not $Wait) { $Message = "Connection Failure. Address:[$($thisComputer)], Port:[$($thisPort)] connection timed out [$($TimeoutMS) milliseconds].`n" } Else { Try{ $tcpobject.EndConnect($connect) #| out-Null $Status = "Success" $Message = "Connection Success. Address:[$($thisComputer)], Port:[$($thisPort)] connection successful.`n" }Catch{ #If ([bool]$Error[0]) { $Message = ("{0}" -f $error[0].Exception.InnerException) #} #Else { # $Status = "Success" # $Message = "Connection Success. Address:[$($thisComputer)], Port:[$($thisPort)] connection successful.`n" #} } } $objResult | Add-Member -MemberType NoteProperty -Name Computer -Value $thisComputer $objResult | Add-Member -MemberType NoteProperty -Name Port -Value $thisPort $objResult | Add-Member -MemberType NoteProperty -Name Status -Value $Status $objResult | Add-Member -MemberType NoteProperty -Name Result -Value $Message $arrResults += $objResult $Error.Clear() }#End ForEach Port }#End ForEach Computer }#End Process End { Return $arrResults } } #End Function ####################################################################### <# .SYNOPSIS Operations Manager Powershell script to output the effective monitoring configurations for a specified object ID, group, or Computer name. .DESCRIPTION Will recursively find contained instances of an object (or group of objects) and output the effective monitoring configurations/settings to individual output files. Then will merge all resulting files into one .csv file, delimited by pipes '|', then output all data to Grid View. .EXAMPLE PS C:\> Export-EffectiveMonitoringConfiguration -SCOMGroupName "All Windows Computers" -TargetFolder "C:\Export\MyConfigFiles" -OutputFileName "AllWindowsComputers_MonitoringConfigs.CSV" .EXAMPLE PS C:\> Export-EffectiveMonitoringConfiguration -ComputerName "SQL01.contoso.com" -TargetFolder "C:\Export" -OutputFileName "Output.csv" .EXAMPLE The following example returns the Monitoring object ID of a Windows Computer instance with name: "db01.contoso.com". The ID is then used as a parameter. PS C:\> $ComputerID = (Get-SCOMClass -name "Microsoft.windows.computer" | Get-SCOMClassInstance | ? {$_.DisplayName -like "db01.contoso.com"}).Id PS C:\> Export-EffectiveMonitoringConfiguration -ID $ComputerID -TargetFolder "C:\Export" -OutputFileName "Output.csv" .EXAMPLE The following example will output all monitoring configuration info for a specific computer to a csv file. There will be no confirmation prompt to proceed. Any previously stored DisplayName file will be removed and recreated. This will increase run time of script as it will have to retrieve all of the workflow Displaynames and this is very expensive on the SQL database. It will then display that information in GridView. The -PassThru parameter will allow the user to filter the view and then export any selected GridView line items to a new csv file named "FilteredOutput.csv" PS C:\> Export-EffectiveMonitoringConfiguration -ComputerName "win01.contoso.com" -TargetFolder "C:\Export" -OutputFileName "WIN01_Output.csv" -NoConfirm -ClearCache -NoGridView PS C:\> Import-Csv 'C:\Export\Merged_WIN01_Output.csv' -Delimiter '|' | Out-GridView -PassThru | Export-Csv -Path 'C:\Export\FilteredOutput.csv' -NoTypeInformation -Force .NOTES Author : Author: Tyson Paul Blog: : https://blogs.msdn.microsoft.com/tysonpaul/2018/07/18/scomhelper-powershell-module-a-scom-admins-best-friend/ Requires : Operations Manager Powershell Console Version : 1.15 Original Date: 7-25-2014 History 2018.04.06: Added column to output: Rule/Monitor DisplayName 2017.11.28: Fixed paramter types. 2017.10.11: Added ability to specify object ID or single computer name. Improved Help content. Improved output formatting. 2014.8.6: Fixed output file name/path problem. .PARAMETER -ComputerName Accepts a single FQDN (Fully Qualified Domain Name) name of an monitored computer. .PARAMETER -ID Accepts a single guid. Guid should contain 32 digits with 4 dashes (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx). .PARAMETER -SCOMGroupName Accepts a single group name. .PARAMETER -TargetFolder The full path to where the script should output the configuration files. .PARAMETER -OutputFileName The name of the complete output file in which all other configuration files will be compiled. .PARAMETER -NoConfirm Switch. Will not prompt user for confirmation to proceed. (Caution should be taken when targeting large groups.) .PARAMETER -NoGridView Switch. Will not display configuration in PowerShell GridView .PARAMETER -ClearCache Switch. Will delete any existing DisplayName file. This is a file that is saved in the $ENV:TEMP path. It will save a list of workflow Names and DisplayNames from previous executions of this script. This will significantly speed up the runtime of the script. This option is only useful if management packs have been updated and DisplayNames of workflows have been modified since the script was last run. When enabled, this parameter will force the script to retrieve all of the DisplayNames from the SDK instead of the locally stored file. #> Function Export-EffectiveMonitoringConfiguration { [CmdletBinding(DefaultParameterSetName='P1', SupportsShouldProcess=$true, PositionalBinding=$false)] Param ( # 1 [Parameter(Mandatory=$true, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=0, ParameterSetName='P1')] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [string]$ComputerName, #2 [Parameter(Mandatory=$true, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=0, ParameterSetName='P2')] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [string]$SCOMGroupName, #3 [Parameter(Mandatory=$true, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=0, ParameterSetName='P3')] [ValidateNotNull()] [ValidateNotNullOrEmpty()] # Validate GUID pattern [ValidatePattern("[a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9]-[a-f0-9][a-f0-9][a-f0-9][a-f0-9]-[a-f0-9][a-f0-9][a-f0-9][a-f0-9]-[a-f0-9][a-f0-9][a-f0-9][a-f0-9]-[a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9][a-f0-9]")] [System.Guid]$ID, #4 [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=1)] [string]$TargetFolder = "C:\SCOM_Export", #5 [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=2 )] [string]$OutputFileName, #6 [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=3 )] [switch]$NoConfirm, #6 [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=4 )] [switch]$NoGridview=$false, #7 [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=5 )] [switch]$ClearCache=$false ) New-Variable -Name StartupVariables -Force -Value (Get-Variable -Scope Global | Select-Object -ExpandProperty Name) ##################################################################################################### Function Cleanup(){ $ErrorActionPreference = "SilentlyContinue" #Depending on when this is called, some variables may not be initialized and clearing could throw benign error. Supress. Write-Host "`nPerforming cleanup..." -ForegroundColor Cyan #Cleanup Get-Variable | Where-Object { $StartupVariables -notcontains $_.Name } | ForEach-Object { Remove-Variable -Name "$($_.Name)" -Force -Scope 1 } } ######################################################################################################## # Will clean up names/strings with special characters (like URLs and Network paths) Function CleanName { Param( [string]$uglyString ) # Remove problematic characters and leading/trailing spaces $prettyString = (($uglyString.Replace(':','_')).Replace('/','_')).Replace('\','_').Trim() # If the string has been modified, output that info If ($uglyString -ne $prettyString) { Write-Verbose "There was a small problem with the characters in this parameter: [$($uglyString)]..." Write-Verbose "Original Name:`t`t$($uglyString)" Write-Verbose "Modified Name:`t`t$($prettyString)" } Return $prettyString #> } ######################################################################################################## # Function MergeFiles # Will find .csv files and merge them together. Function MergeFiles{ Param( [string]$strPath, [string]$strOutputFileName ) $strOutputFilePath = (Join-Path $strPath $strOutputFileName) # If output file already exists, remove it. If (Test-Path $strOutputFilePath -PathType Leaf) { Write-Verbose "Output file [ $($strOutputFilePath) ] already exists. Removing..." Remove-Item -Path $strOutputFilePath -Force } If (Test-Path $strOutputFilePath) { Write-Error "Cannot remove $strOutputFilePath and therefore cannot generate merged output file." -ForegroundColor Yellow -BackgroundColor Black Write-Error "Remove this file first: [ $($strOutputFilePath) ]" Write-Error "Exiting ! " Cleanup Exit } Get-ChildItem -Path $strPath -File -Filter *.csv -Exclude $strOutputFileName -Recurse | ForEach-Object { $intThisHeaderLength = (Get-Content -LiteralPath $_.FullName)[0].Length If ($intThisHeaderLength -gt $intLongestHeaderLength) { $objLongestHeaderFile = $_ $intLongestHeaderLength = $intThisHeaderLength } } Write-Host "Has largest set of file headers: [ $($objLongestHeaderFile.FullName) ] " # Create the master merge file seeded by the data from the existing CSV file with the most headers out of the entire set of CSVs. Try{ Get-Content $objLongestHeaderFile.FullName | Out-File -LiteralPath $strOutputFilePath -Force -Encoding UTF8 }Catch{ Write-Error $error[0] Write-Host "Something is wrong with this path [$($strOutputFilePath)]." -ForegroundColor Red -BackgroundColor Yellow Write-Host "Exiting..." Exit } # Iterate through all of the CSVs, append all of them into the master (except for the one already used as the seed above and except for the master merge file itself.) $i=0 $tempArray = @() Get-ChildItem -Path $strPath -File -Filter *.csv -Exclude "merged*" -Recurse | ForEach-Object { If( ( $_.FullName -eq $objLongestHeaderFile.FullName ) -or ($_.FullName -like (Get-Item $strOutputFilePath).FullName) ){ Write-Host "Skip this file: `t" -NoNewline; Write-Host "$($_.FullName)" -ForegroundColor Red -BackgroundColor Yellow } Else { Write-Host "Merge this file: `t" -NoNewline; Write-Host "$($_.FullName)" -BackgroundColor Green -ForegroundColor Black $tempArray += ((((Get-Content -Raw -Path $_.FullName) -Replace "\n"," " ) -Split "\r") | Select-Object -Skip 1 ) $i++ } } $tempArray | Out-File -LiteralPath $strOutputFilePath -Append -Encoding UTF8 "" # Cheap formatting Write-Host "Total files merged: `t" -NoNewline; Write-Host "$i" -BackgroundColor Black -ForegroundColor Green Write-Host "Master output file: `t" -NoNewline; Write-Host "$strOutputFilePath" -BackgroundColor black -ForegroundColor Green } # EndFunction ################################################################################################### Function MakeObject { Param( [string]$strMergedFilePath ) $mainArray = @() [string]$rootFolder = Split-Path $strMergedFilePath -Parent $tmpFileName = "ExportEffectiveMonitoringConfiguration.ps1_DisplayNamesCSV.tmp" Try { [string]$savedDNs = (Join-Path $env:Temp $tmpFileName ) New-Item -ItemType File -Path $savedDNs -ErrorAction SilentlyContinue }Catch{ [string]$savedDNs = (Join-Path $rootFolder $tmpFileName ) } If ($ClearCache){ Write-Host "Removing saved DisplayNames file: [$($savedDNs)]" -F Gray Remove-Item -Path $savedDNs -Force } If (!($strMergedFilePath)) { Write-Host "Cannot find [ $($strMergedFilePath) ] and therefore cannot compile object for Grid View." -ForegroundColor Yellow -BackgroundColor Black Write-Host "Exiting..." Cleanup Exit } $Headers = @() $Headers= (Get-Content -LiteralPath $strMergedFilePath | Select-Object -First 1).Split('|') $FileContents = (Get-Content -LiteralPath $strMergedFilePath | Select-Object -Skip 1 ).Replace("`0",'') $r=1 <# The Export-SCOMEffectiveMonitoringConfiguration cmdlet does not include DisplayName in it's default output set. Querying the SDK for the workflow DisplayName is expensive. In the code below we try to benefit from a saved list of Name->DisplayName pairs. If the list does not already exist, we will create one. If it does already exist, we will import it into a hash table for fast DisplayName lookup while building the rows of the master file. #> $DNHash = @{} Try { [System.Object[]]$arrDN = (Import-Csv -Path $savedDNs -ErrorAction SilentlyContinue) } Catch { $arrDN = @() } # If a previous list of Name/DisplayName pairs exists, let's use it to build our fast hash table. If ([bool]$arrDN ){ ForEach ($item in $arrDN) { $DNHash.Add($item.'Rule/Monitor Name',$item.'Rule/Monitor DisplayName') } } $arrTmpDN = @() ForEach ($Row in $FileContents) { $percent = [math]::Round(($r / $FileContents.count*100),0) Write-Progress -Activity "** What's happening? **" -status "Formatting your data! [Percent: $($percent)]" -percentComplete $percent If ($Row.Length -le 1) { Continue; } $c=0 $arrRow = @() $arrRow = $Row.Split('|') # If the ForEach has already executed one iteration and thus the full object template has already been created, # duplicate the template instead of building a new object and adding members to it for each column. This is about 3x faster than building the object every iteration. If ([bool]($templateObject)) { $object = $templateObject.PsObject.Copy() $object.Index = $r.ToString("0000") } Else { $object = New-Object -TypeName PSObject $object | Add-Member -MemberType NoteProperty -Name "Index" -Value $r.ToString("0000") } ForEach ($Column in $Headers) { If ( ($arrRow[$c] -eq '') -or ($arrRow[$c] -eq ' ') ) { $arrRow[$c] = 'N/A' } # Some header values repeat. If header already exists, give it a unique name [int]$Position=1 $tempColumn = $Column # The first 10 columns are unique. However, beyond 10, the column names repeat: # Parameter Name, Default Value, Effective Value # A clever way to assign each set of repeats a unique name is to append an incremental instance number. # Each set (of 3 column names) gets an occurance number provided by the clever math below. # Example: Parameter Name1, Default Value1, Effective Value1, Parameter Name2, Default Value2, Effective Value2 If ($c -ge 10) { $Position = [System.Math]::Ceiling(($c / 3)-3) $tempColumn = $Column + "$Position" } If ([bool]($templateObject)) { $object.$tempColumn = $arrRow[$c] } Else { $object | Add-Member -MemberType NoteProperty -Name $tempColumn -Value "$($arrRow[$c])" } If ($Column -eq 'Rule/Monitor Name') { # If DisplayName (DN) does not already exist in set If (-not [bool]($DN = $DNHash.($arrRow[$c])) ) { # Find the DisplayName switch ($arrRow[7]) #Assuming this column header is consistently "Type" { 'Monitor' { $DN = (Get-SCOMMonitor -Name $arrRow[$c]).DisplayName } 'Rule' { $DN = (Get-SCOMRule -Name $arrRow[$c]).DisplayName } Default {Write-Host "SWITCH DEFAULT IN FUNCTION: 'MAKEOBJECT', SOMETHING IS WRONG." -F Red -B Yellow} } # If no DN exists for the workflow, set a default If (-Not([bool]$DN)) { $DN = "N/A" } Else{ $DNHash.Add($arrRow[$c],$DN) } } # DN Exists, add it to the hash table for fast lookup. Also add it to the catalog/array of known DNs so it can be saved and used again # next time for fast lookup. If ([bool]($templateObject)) { $object.'Rule/Monitor DisplayName' = $DN } Else { $object | Add-Member -MemberType NoteProperty -Name "Rule/Monitor DisplayName" -Value $DN } } $c++ } $r++ $mainArray += $object If (-not [bool]($templateObject)) { $templateObject = $object.PsObject.Copy() } Remove-Variable -name object,DN -ErrorAction SilentlyContinue } # Build a simple array to hold unique Name,DisplayName values so that it can be exported easily to a CSV file. # This cached csv file will significantly speed up the script next time it runs. ForEach ($Key in $DNHash.Keys){ $tmpObj = New-Object -TypeName PSObject $tmpObj | Add-Member -MemberType NoteProperty -Name "Rule/Monitor Name" -Value $Key $tmpObj | Add-Member -MemberType NoteProperty -Name "Rule/Monitor DisplayName" -Value $DNHash.$Key $arrTmpDN += $tmpObj } $mainArray | Export-Csv -Path $strMergedFilePath -Force -Encoding UTF8 -Delimiter '|' -NoTypeInformation $arrTmpDN | Export-Csv -Path $savedDNs -Force -Encoding UTF8 -NoTypeInformation Return $mainArray } ################################################################################################### # The export cmdlet (Export-SCOMEffectiveMonitoringConfiguration) seems to include rogue LF linefeeds which causes problems. These LF characters need to be removed. # This will affect LF and CRLF so that only CR remain, which is fine for later use of Get-Content. Function FixLineFeeds { Param ( [String]$TargetFolder ) } # --------------------------------------------------------------------------------------------------------------------------------------------------- If (!(Test-Path $TargetFolder)) { Write-Verbose "TargetFolder [ $($TargetFolder) ] does not exist. Creating it now..." new-item -ItemType Directory -Path $TargetFolder If (!(Test-Path $TargetFolder)) { Write-Error "Unable to create TargetFolder: $TargetFolder. Exiting." Cleanup Exit } Else { Write-Verbose "Created TargetFolder successfully. " } } $elapsed_enumeration = [System.Diagnostics.Stopwatch]::StartNew() # If group name is provided... If ($SCOMGroupName) { $choice='group' $objects = @(Get-SCOMGroup -DisplayName $SCOMGroupName | Get-SCOMClassInstance) If (-not($objects)) {Write-Error "Unable to get group: [ $($SCOMGroupName) ]." Write-Verbose "To troubleshoot, run this command:`n`n Get-SCOMGroup -DisplayName '$SCOMGroupName' | Get-SCOMClassInstance `n" Write-Error "Exiting..."; Cleanup Exit } Else { Write-Verbose "Success getting group: [ $($SCOMGroupName) ]." } $TempName = $SCOMGroupName $count = $objects.GetRelatedMonitoringObjects().Count "" # Cheap formatting "" Write-Host "This will output ALL monitoring configuration for group: " -ForegroundColor Cyan -BackgroundColor Black -NoNewline; ` Write-Host "[" -ForegroundColor Red -BackgroundColor Black -NoNewline; ` Write-Host "$($SCOMGroupName)" -ForegroundColor Yellow -BackgroundColor Black -NoNewline; ` Write-Host "]" -ForegroundColor Red -BackgroundColor Black Write-Host "There are: " -ForegroundColor Green -BackgroundColor Black -NoNewline; ` Write-Host "[" -ForegroundColor Red -BackgroundColor Black -NoNewline; ` Write-Host "$($count)" -ForegroundColor Yellow -BackgroundColor Black -NoNewline; ` Write-Host "]" -ForegroundColor Red -BackgroundColor Black -NoNewline; ` Write-Host " nested objects in that group." -ForegroundColor Green -BackgroundColor Black Write-Host "This might take a little while depending on how large the group is and how many hosted objects exist!" -ForegroundColor Green -BackgroundColor Black "" # Cheap formatting } # If ID is provided... ElseIf ($ID) { $choice='ID' Write-Verbose "Getting class instance with ID: [ $($ID) ] " $objects = (Get-SCOMClassInstance -Id $ID) If (-not($objects)) { Write-Error "Unable to get class instance for ID: [ $($ID) ] " Write-Verbose "To troubleshoot, use this command:`n`n Get-SCOMClassInstance -Id '$ID' `n" Write-Error "Exiting..."; Cleanup Exit } Else { Write-Verbose "Success getting class instance with ID: [ $($ID) ], DisplayName: [ $($ID.DisplayName) ]." } $TempName = $ID $count = $objects.GetRelatedMonitoringObjects().Count "" # Cheap formatting "" Write-Host "This will output ALL monitoring configuration for object: " -ForegroundColor Cyan -BackgroundColor Black -NoNewline; ` Write-Host "[" -ForegroundColor Red -BackgroundColor Black -NoNewline; ` Write-Host "$($objects.DisplayName) , " -ForegroundColor Yellow -BackgroundColor Black -NoNewline; ` Write-Host "ID: $ID " -ForegroundColor Gray -BackgroundColor Black -NoNewline; ` Write-Host "]" -ForegroundColor Red -BackgroundColor Black Write-Host "There are: " -ForegroundColor Green -BackgroundColor Black -NoNewline; ` Write-Host "[" -ForegroundColor Red -BackgroundColor Black -NoNewline; ` Write-Host "$($count)" -ForegroundColor Yellow -BackgroundColor Black -NoNewline; ` Write-Host "]" -ForegroundColor Red -BackgroundColor Black -NoNewline; ` Write-Host " related monitoring objects." -ForegroundColor Green -BackgroundColor Black Write-Host "This might take a little while depending on how hosted objects exist !" -ForegroundColor Green -BackgroundColor Black "" # Cheap formatting } # Assume individul computer name is provided... ElseIf ($ComputerName){ $choice='ComputerName' # $objects = @(Get-SCOMClass -Name "Microsoft.Windows.Computer" | Get-SCOMClassInstance | Where-Object {$ComputerName -contains $_.DisplayName } ) # This approach should prove to be more efficient for environments with more than 40-ish Computers/agents. $ClassName = 'Microsoft.Windows.Computer' $ComputerClass = (Get-SCClass -Name $ClassName) If (-not($ComputerClass)) { Write-Error "Unable to get class: [ $ClassName ]." Write-Verbose "To troubleshoot, use this command:`n`n Get-SCOMClass -Name '$ClassName' `n" Write-Error "Exiting..."; Cleanup Exit } Else { Write-Verbose "Success getting class object with name: [ $($ClassName) ]." } Write-Verbose "Getting class instance of [ $($ClassName) ] with DisplayName of [ $($ComputerName) ]..." $objects = @(Get-SCOMClassInstance -DisplayName $ComputerName | Where-Object {$_.LeastDerivedNonAbstractManagementPackClassId -like $ComputerClass.Id.Guid} ) If (-not($objects)) { Write-Error "Unable to get class instance for DisplayName: [ $($ComputerName) ] " Write-Verbose "To troubleshoot, use this command:`n`n `$ComputerClass = (Get-SCOMClass -Name '$ClassName') " Write-Verbose " Get-SCOMClassInstance -DisplayName '$ComputerName' | Where-Object {`$_.LeastDerivedNonAbstractManagementPackClassId -like `$ComputerClass.Id.Guid} `n" Write-Error "Exiting..."; Cleanup Exit } Else { Write-Verbose "Success getting class instance for DisplayName: [ $($ComputerName) ] " } $TempName = $ComputerName $count = $objects.GetRelatedMonitoringObjects().Count "" # Cheap formatting "" Write-Host "This will output ALL monitoring configuration for Computer: " -ForegroundColor Cyan -BackgroundColor Black -NoNewline; ` Write-Host "[" -ForegroundColor Red -BackgroundColor Black -NoNewline; ` Write-Host "$($objects.DisplayName)" -ForegroundColor Yellow -BackgroundColor Black -NoNewline; ` Write-Host "]" -ForegroundColor Red -BackgroundColor Black Write-Host "There are: " -ForegroundColor Green -BackgroundColor Black -NoNewline; ` Write-Host "[" -ForegroundColor Red -BackgroundColor Black -NoNewline; ` Write-Host "$($count)" -ForegroundColor Yellow -BackgroundColor Black -NoNewline; ` Write-Host "]" -ForegroundColor Red -BackgroundColor Black -NoNewline; ` Write-Host " related monitoring objects." -ForegroundColor Green -BackgroundColor Black Write-Host "This might take a little while depending on how many hosted objects exist !" -ForegroundColor Green -BackgroundColor Black "" # Cheap formatting } Else{ #This should never happen because of parameter validation Write-Host "No input provided. Exiting..." Cleanup Exit } # If no OutputFileName exists, then simply use the DisplayName of the class instance. If (-not($OutputFileName)) { $OutputFileName = "Merged_"+$TempName+".csv" } Else { $tempIndex = $OutputFileName.LastIndexOf('.') If ($tempIndex -gt 1) { $temp = $OutputFileName.Substring(0, $tempIndex) } Else { $temp = $OutputFileName } $OutputFileName = "Merged_"+$temp+".csv" } # If output directory already contains file, this will notify the user. You may not want the merge operation to include other/older/foreign CSV files. $existingFiles = Get-ChildItem -LiteralPath $TargetFolder If ($existingFiles){ Write-Host "CAUTION: Files already exist in the output directory [ $($TargetFolder) ]! You probably want to remove them first." -ForegroundColor Red -BackgroundColor Yellow } If (-not($NoConfirm)){ # Force user to acknowledge prompt. While (-not($readin)){ $readin = Read-Host -Prompt "Continue? (y/n) `n" Switch ($readin) { "y" {Write-Host "Proceed..." -BackgroundColor Black -ForegroundColor Cyan } "n" {Write-Host "Exiting..." -BackgroundColor Yellow -ForegroundColor Red; Exit; } Default {Write-Host "Must select 'y' to proceed or 'n' to exit." -BackgroundColor Yellow -ForegroundColor Red; $readin ="" ;} } } } # Iterators used for nicely formatted output. $i=1 # Iterate through the objects (including hosted instances) and dig out all related configs for rules/monitors. $objects | ForEach-Object ` { $DN = (CleanName -uglyString $_.DisplayName) $path = (Join-Path $TargetFolder "($( CleanName -uglyString $_.Path ))_$($DN).csv" ) Export-SCOMEffectiveMonitoringConfiguration -Instance $_ -Path $path Write-Host "$($i): " -ForegroundColor Cyan -NoNewline; ` Write-Host "[" -ForegroundColor Red -NoNewline; ` Write-Host "$($_.Path)" -ForegroundColor Yellow -NoNewline; ` Write-Host "]" -ForegroundColor Red -NoNewline; ` Write-Host " $($_.FullName)" -ForegroundColor Green $r=1 #for progress bar calculation below $related = @($_.GetRelatedMonitoringObjects()) Write-Verbose "There are $($related.Count) 'related' monitoring objects for $($_.DisplayName)." $related | ForEach-Object ` -Process { $percent = [math]::Round((($r / $related.Count) *100),0) Write-Progress -Activity "** What's happening? **" -status "Getting your data. Be patient! [Percent: $($percent)]" -PercentComplete $percent $DN = (($($_.DisplayName).Replace(':','_')).Replace('/','_')).Replace('\','_') $path= (Join-Path $TargetFolder "($($_.Path))_$($DN).csv" ) Export-SCOMEffectiveMonitoringConfiguration -Instance $_ -Path $path Write-Host "$($i): " -ForegroundColor Cyan -NoNewline; ` Write-Host "[" -ForegroundColor Red -NoNewline; ` Write-Host "$($_.Path)" -ForegroundColor Yellow -NoNewline; ` Write-Host "]" -ForegroundColor Red -NoNewline; ` Write-Host " $($_.FullName)" -ForegroundColor Green $i++ # formatting, total line numbers $r++ # this object's hosted items, for progress bar calculation above } } $Enumeration_TimeSeconds = "{0:N4}" -f $elapsed_enumeration.Elapsed.TotalSeconds $elapsed_merge = [System.Diagnostics.Stopwatch]::StartNew() # ------ Merge Operation ------ MergeFiles -strPath $TargetFolder -strOutputFileName $OutputFileName # ------ Merge Operation ------ $Merge_TimeSeconds = "{0:N4}" -f $elapsed_merge.Elapsed.TotalSeconds Write-Host "Enumeration Duration: `t" -ForegroundColor Green -BackgroundColor Black -NoNewline; ` Write-Host "[" -ForegroundColor Red -BackgroundColor Black -NoNewline; ` Write-Host "$($Enumeration_TimeSeconds)" -ForegroundColor Yellow -BackgroundColor Black -NoNewline; ` Write-Host "]" -ForegroundColor Red -BackgroundColor Black -NoNewline; ` Write-Host " seconds." -ForegroundColor Green -BackgroundColor Black Write-Host "Merge Duration: `t" -ForegroundColor Green -BackgroundColor Black -NoNewline; ` Write-Host "[" -ForegroundColor Red -BackgroundColor Black -NoNewline; ` Write-Host "$($Merge_TimeSeconds)" -ForegroundColor Yellow -BackgroundColor Black -NoNewline; ` Write-Host "]" -ForegroundColor Red -BackgroundColor Black -NoNewline; ` Write-Host " seconds." -ForegroundColor Green -BackgroundColor Black Write-Host "Formatting output for Grid View. This might take a minute..." -ForegroundColor Cyan -BackgroundColor Black $elapsed_makeobject = [System.Diagnostics.Stopwatch]::StartNew() [string]$strMergedFilePath = (Join-Path $TargetFolder $OutputFileName) $objBlob = MakeObject -strMergedFilePath $strMergedFilePath $MakeObject_TimeSeconds = "{0:N4}" -f $elapsed_makeobject.Elapsed.TotalSeconds Write-Host "Grid View Format Duration: " -ForegroundColor Green -BackgroundColor Black -NoNewline; ` Write-Host "[" -ForegroundColor Red -BackgroundColor Black -NoNewline; ` Write-Host "$($MakeObject_TimeSeconds)" -ForegroundColor Yellow -BackgroundColor Black -NoNewline; ` Write-Host "]" -ForegroundColor Red -BackgroundColor Black -NoNewline; ` Write-Host " seconds." -ForegroundColor Green -BackgroundColor Black If (-not($NoGridview)){ $objBlob | Out-Gridview -Title "Your Effective Configuration for $choice :" } }#End Function ####################################################################### <# .SYNOPSIS This script will get all rule and monitor knowledge article content and output the information to separate files (Rules.html and Monitors.html) in the output folder path specified. .PARAMETER OutFolder The output folder path where output files will be created. Must be a container/directory, not a file. .PARAMETER ManagementPack A collection/array of one or more management pack objects. .PARAMETER MgmtServerFQDN Alias: ManagementServer Fully Qualified Domain Name of the SCOM management server. .PARAMETER NoKnowledgeExclude By default ALL workflows will be included in the output files. Enable this switch to exclude workflows which have no Knowledge Article content. .PARAMETER ExportCSV Export results to CSV files instead of default format (.html) .PARAMETER Topic This will customize the name of the output files. Useful when dumping multiple sets to the same output folder. .PARAMETER ShowResult Will open the Windows file Explorer to show output files. .EXAMPLE (Get-SCOMManagementPack -Name *.AD.*) | Export-SCOMKnowledge -OutFolder 'C:\MyReports' -Topic "AD_" -ShowResult In the example above the variable will be assigned a collection of all management pack objects with ".AD." in the name. That subset/collection of management pack objects will be passed into the script. The script will output workflow details for all Rules and Monitors contained in ALL management packs within that set. The output file names will be: "AD_Rules.html" and "AD_Monitors.html". Finally the script will open Windows File Explorer to the location of the output directory. .Example PS C:\> Export-SCOMKnowledge -OutFolder 'C:\Export' -ManagementPack (Get-SCOMManagementPack -Name "*ad.*") -Filter '201[0-6]' The command above will output all rules/monitors from management packs which contain 'ad.' in the Name and which contain '201x' in the Name or DisplayName of the workflow where 'x' represents any single digit 0-6 . .EXAMPLE PS C:\> Export-SCOMKnowledge -OutFolder "C:\Temp" -ManagementPack (Get-SCOMManagementPack -Name *SQL*) -Topic "SQL_Packs_" The command above will output workflow details for all Rules and Monitors contained in ALL management packs with "SQL" in the management pack Name. The output file names will be: "SQL_Packs_Rules.html" and "SQL_Packs_Monitors.html" .EXAMPLE PS C:\> Export-SCOMKnowledge -OutFolder "C:\MyReports" -ManagementServer "ms01.contoso.com" -NoKnowledgeExclude In the example above, the command will connect to management server: "ms01.contoso.com", will output workflow details for all Rules and Monitors (only if they contain a Knowledge Article) to two separate files in the specified folder. The output file names will be: "Rules.html" and "Monitors.html" .Example PS C:\> Export-SCOMKnowledge -OutFolder 'C:\Export' -ManagementPack (Get-SCOMManagementPack -Name "*ad.*") -Filter '(?=200[0-8])((?!Monitoring).)*$' The command above will output all rules/monitors from management packs which contain 'ad' in the Name and which contain '200x' in the Name or DisplayName of the workflow where 'x' is numbers 1-8 but excluding workflows that contain 'monitoring'. .Example This is a way to use management pack files (.mp and .xml) from a directory path with specific topics. The below script example will use management pack files which names match the array of Topics. ##---- CONFIGURE THESE VARIABLES ----## # Location of management pack files $inDir = 'C:\Program Files (x86)\System Center Management Packs\' # Output directory $outDir = 'C:\' # Management pack file names which match these strings will be selected $topics = "DHCP,DNS,FileServices,GroupPolicy,DFS,FileReplication" ##---- CONFIGURE THESE VARIABLES ----## $FilePaths =@() $h = @{} # Will export data for each management pack file name matching the topic name. ForEach ($topic in $topics.Split(',')){ Write-Host "Topic: $topic" -BackgroundColor Black -ForegroundColor Yellow $FilePaths = Get-ChildItem -Recurse -Include "*$topic*.mp*","*$topic*.xml*" -File -Path $inDir | Select fullname $FilePaths | ForEach {$h.(Split-Path $_.FullName -Leaf) = $_.FullName } If (!($h.Count)){ Continue;} #If no matching file found, skip to next topic $h.Keys "" $MPs = (Get-SCOMManagementPack -ManagementPackFile ($h.Values | % {$_} ) ) Export-SCOMKnowledge -OutFolder $outDir -ManagementPack $MPs -Topic $Topic } .LINK https://blogs.msdn.microsoft.com/tysonpaul/ .NOTES Version: 1.12 Author: Tyson Paul Date: 2016/5/3 Blog: https://blogs.msdn.microsoft.com/tysonpaul/2018/07/18/scomhelper-powershell-module-a-scom-admins-best-friend/ History: 2017/12/05: Added Topic parameter to customize output file names. Added ShowResult switch. 2017/10/16: Added support for regex filtering on DisplayName or Name of monitors/rules. 2016/05/03: Added option to export to CSV. Although embedded tables in the KnowledgeArticle don't convert very well. 2016/04/26: Improved formatting of output files. Now includes embedded parameter tables and restored article href links. 2016/04/26: Fixed script to accept ManagementPack pipeline input #> Function Export-SCOMKnowledge { [CmdletBinding(DefaultParameterSetName='Parameter Set 1', SupportsShouldProcess=$true, SupportsPaging = $true, PositionalBinding=$false)] Param( #1 [Parameter(Mandatory=$true, ValueFromPipeline=$false, Position=0, ParameterSetName='Parameter Set 1')] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [string]$OutFolder, #2 [Parameter(Mandatory=$false, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, ValueFromRemainingArguments=$false, Position=1, ParameterSetName='Parameter Set 1')] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [System.Object[]]$ManagementPack, #3 [Parameter(Mandatory=$false, ValueFromPipeline=$false, Position=2, ParameterSetName='Parameter Set 1')] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [Alias("ManagementServer")] [string]$MgmtServerFQDN, #4 [Parameter(Mandatory=$false, Position=3, ParameterSetName='Parameter Set 1')] [Switch]$NoKnowledgeExclude, #5 [Parameter(Mandatory=$false, ValueFromPipeline=$false, Position=4, ParameterSetName='Parameter Set 1')] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [string]$OpsDBServer, #6 [Parameter(Mandatory=$false, ValueFromPipeline=$false, Position=5, ParameterSetName='Parameter Set 1')] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [string]$OpsDBName, #7 [Parameter(Mandatory=$false, ValueFromPipeline=$false, Position=6, ParameterSetName='Parameter Set 1')] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [string]$Topic, #8 [Parameter(Mandatory=$false, Position=7, ParameterSetName='Parameter Set 1')] [string[]]$Filter, #9 [Parameter(Mandatory=$false, Position=8, ParameterSetName='Parameter Set 1')] [switch]$ExportCSV, #10 [Parameter(Mandatory=$false, Position=9, ParameterSetName='Parameter Set 1')] [switch]$ShowResult=$false ) #### UNCOMMENT FOR TESTING #### #$OutFolder = "c:\Test" #region Functions Begin { #------------------------------------------------------------------------------------ Function ProcessArticle { Param( $article ) If ($article -ne "None") #some rules don't have any knowledge articles { #Retrieve and format article content $MamlText = $null $HtmlText = $null If ($null -ne $article.MamlContent) { $MamlText = $article.MamlContent $articleContent = fnMamlToHtml($MamlText) } If ($null -ne $article.HtmlContent) { $HtmlText = $article.HtmlContent $articleContent = CleanHTML($HtmlText) } } If ($null -eq $articleContent) { $articleContent = "No resolutions were found for this alert." } Return $articleContent } #------------------------------------------------------------------------------------ Function ProcessWorkflows { Param( $Workflows, [string]$WFType ) $thisWFType = $WFType $myWFCollectionObj = @() [int]$row=1 ForEach ($thisWF in $Workflows) { Write-Progress -Activity "Processing $WFType" -status "Getting Alert [$($row)]: $($thisWF.DisplayName) " -percentComplete ($row / $($Workflows.count) * 100) $ErrorActionPreference = 'SilentlyContinue' $article = $thisWF.GetKnowledgeArticle($cultureInfo) If ($? -eq $false){ $error.Remove($Error[0]) $article = "None" If ($NoKnowledgeExclude){ Continue; } } Else{ $articleContent = ProcessArticle $article } If ($ExportCSV) { $WFName = $($thisWF.Name) $WFDisplayName = $($thisWF.DisplayName) } Else { $WFName = '<name>' + $($thisWF.Name) + '</name>' $WFDisplayName = '<displayname>' + $($thisWF.DisplayName) + '</displayname>' } $WFDescription = $thisWF.Description If ($WFDescription.Length -lt 1) {$WFDescription = "None"} #region Get_alert_name If ($WFType -like "Rule") { $thisWFType = "$($WFType): " + "$($thisWF.WriteActionCollection.Name)" # Proceed only if rule is an "alert" rule. If ($thisWF.WriteActionCollection.Name -Like "GenerateAlert"){ $AlertMessageID = $($thisWF.WriteActionCollection.Configuration).Split('"',3)[1] $Query = @" Select [LTValue] FROM [OperationsManager].[dbo].[LocalizedText] WHERE ElementName like '$($AlertMessageID)' AND LTStringType = '1' "@ $AlertDisplayName = Invoke-CLSqlCmd -Query $Query -Server $OpsDBServer -Database $OpsDBName If ($ExportCSV) { $AlertName = $AlertDisplayName } Else { $AlertName = '<alertname>' + $AlertDisplayName + '</alertname>' } } Else { If ($ExportCSV) { $AlertName = 'N/A' } Else { $AlertName = '<noalertname>N/A</noalertname>' } } } Else { # Workflow is not a rule, therefore it is a monitor. $Query = @" Select LocalizedText.LTValue From [OperationsManager].[dbo].[LocalizedText] INNER JOIN [OperationsManager].[dbo].[Monitor] on LocalizedText.LTStringId=Monitor.AlertMessage Where Monitor.MonitorID like '$($thisWF.ID)' AND LocalizedText.LTStringType = '1' "@ $AlertDisplayName = Invoke-CLSqlCmd -Query $Query -Server $OpsDBServer -Database $OpsDBName # Not all monitors generate an alert. If ($AlertDisplayName -like "EMPTYRESULT") { If ($ExportCSV) { $AlertName = 'N/A' } Else { $AlertName = '<noalertname>N/A</noalertname>' } } Else { If ($ExportCSV) { $AlertName = $AlertDisplayName } Else { $AlertName = '<alertname>' + $AlertDisplayName + '</alertname>' } } } #endregion Get_alert_name $WFID = $thisWF.ID $WFMgmtPackID = $thisWF.GetManagementPack().Name # Build the custom object to represent the workflow, add properties/values. $myWFObj = New-Object -TypeName System.Management.Automation.PSObject $myWFObj | Add-Member -MemberType NoteProperty -Name "Row" -Value $Row $myWFObj | Add-Member -MemberType NoteProperty -Name "DisplayName" -Value $WFDisplayName $myWFObj | Add-Member -MemberType NoteProperty -Name "AlertName" -Value $AlertName $myWFObj | Add-Member -MemberType NoteProperty -Name "KnowledgeArticle" -Value $articleContent $myWFObj | Add-Member -MemberType NoteProperty -Name "Description" -Value $WFDescription $myWFObj | Add-Member -MemberType NoteProperty -Name "ManagementPackID" -Value $WFMgmtPackID $myWFObj | Add-Member -MemberType NoteProperty -Name "Name" -Value $WFName $myWFObj | Add-Member -MemberType NoteProperty -Name "WorkflowType" -Value $thisWFType $myWFObj | Add-Member -MemberType NoteProperty -Name "ID" -Value $WFID $myWFCollectionObj += $myWFObj $Row++ } Return $myWFCollectionObj } #------------------------------------------------------------------------------------ Function fnMamlToHTML{ param ( $MAMLText ) $HTMLText = ""; $HTMLText = $MAMLText -replace ('xmlns:maml="http://schemas.microsoft.com/maml/2004/10"'); $HTMLText = $HTMLText -replace ("<maml:section>"); $HTMLText = $HTMLText -replace ("<maml:section >"); $HTMLText = $HTMLText -replace ("</maml:section>"); $HTMLText = $HTMLText -replace ("<section >"); #ui = underline. Not going to bother with this html conversion $HTMLText = $HTMLText -replace ("<maml:ui>", ""); $HTMLText = $HTMLText -replace ("</maml:ui>", ""); #Only convert the maml tables if not exporting to CSV IF ($ExportCSV) { $HTMLText = $HTMLText -replace ("<maml:para>", " "); $HTMLText = $HTMLText -replace ("<maml:para />", " "); $HTMLText = $HTMLText -replace ("</maml:para>", " "); $HTMLText = $HTMLText -replace ("<maml:title>", ""); $HTMLText = $HTMLText -replace ("</maml:title>", ""); $HTMLText = $HTMLText -replace ("<maml:list>", ""); $HTMLText = $HTMLText -replace ("</maml:list>", ""); $HTMLText = $HTMLText -replace ("<maml:listitem>", ""); $HTMLText = $HTMLText -replace ("</maml:listitem>", ""); $HTMLText = $HTMLText -replace ("<maml:table>", ""); $HTMLText = $HTMLText -replace ("</maml:table>", ""); $HTMLText = $HTMLText -replace ("<maml:row>", ""); $HTMLText = $HTMLText -replace ("</maml:row>", ""); $HTMLText = $HTMLText -replace ("<maml:entry>", ""); $HTMLText = $HTMLText -replace ("</maml:entry>", ""); $HTMLText = $HTMLText -replace ('<tr><td><p>Name</p></td><td><p>Description</p></td><td><p>Default Value</p></td></tr>', 'Name Description DefaultValue'); } Else { $HTMLText = $HTMLText -replace ("maml:para", "p"); $HTMLText = $HTMLText -replace ("<maml:table>", "<table>"); $HTMLText = $HTMLText -replace ("</maml:table>", "</table>"); $HTMLText = $HTMLText -replace ("<maml:row>", "<tr>"); $HTMLText = $HTMLText -replace ("</maml:row>", "</tr>"); $HTMLText = $HTMLText -replace ("<maml:entry>", "<td>"); $HTMLText = $HTMLText -replace ("</maml:entry>", "</td>"); $HTMLText = $HTMLText -replace ("<maml:title>", "<h3>"); $HTMLText = $HTMLText -replace ("</maml:title>", "</h3>"); $HTMLText = $HTMLText -replace ("<maml:list>", "<ul>"); $HTMLText = $HTMLText -replace ("</maml:list>", "</ul>"); $HTMLText = $HTMLText -replace ("<maml:listitem>", "<li>"); $HTMLText = $HTMLText -replace ("</maml:listitem>", "</li>"); $HTMLText = $HTMLText -replace ('<tr><td><p>Name</p></td><td><p>Description</p></td><td><p>Default Value</p></td></tr>', '<th>Name</th><th>Description</th><th>Default Value</th>'); } # Replace all maml links with html href formatted links while ($HTMLText -like "*<maml:navigationLink>*"){ If ($HtmlText -like "*uri condition*" ) { $HTMLText = Fix-HREF -mystring $HTMLText -irregular } Else { $HTMLText = Fix-HREF -mystring $HTMLText } } Return $HTMLText; } #------------------------------------------------------------------------------------ Function CleanHTML{ param ( $HTMLText ) $TrimedText = ""; $TrimedText = $HTMLText -replace ("<", "<") $TrimedText = $TrimedText -replace (">", ">") $TrimedText = $TrimedText -replace (""", '"') $TrimedText = $TrimedText -replace ("&", '&') $TrimedText; } #------------------------------------------------------------------------------------ #Remove maml link formatting, replace with HTML Function Fix-HREF{ Param( [string]$mystring, [switch]$irregular ) If ($irregular){ $href_link_tag_begin = '<maml:uri condition' } Else { $href_link_tag_begin = '<maml:uri href="' $href_name_tag_begin = '<maml:navigationLink><maml:linkText>' $href_name_tag_end = '</maml:linkText>' $href_link_tag_end = '" /></maml:navigationLink>' $href_name_length = ($mystring.IndexOf($href_name_tag_end)) - ( $mystring.IndexOf($href_name_tag_begin) + $href_name_tag_begin.Length) $href_name = $mystring.Substring( ($mystring.IndexOf($href_name_tag_begin) + $href_name_tag_begin.Length ), $href_name_length) $href_link_length = ($mystring.IndexOf($href_link_tag_end)) - ( $mystring.IndexOf($href_link_tag_begin) + $href_link_tag_begin.Length -1) $href_link = $mystring.Substring( ($mystring.IndexOf($href_link_tag_begin) + $href_link_tag_begin.Length ), $href_link_length -1) } $Chunk_Name = $mystring.Substring( $mystring.IndexOf($href_name_tag_begin), (($mystring.IndexOf($href_name_tag_end) + $href_name_tag_end.Length - 1) - $mystring.IndexOf($href_name_tag_begin) +1 ) ) $Chunk_HREF = $mystring.Substring( $mystring.IndexOf($href_link_tag_begin), (($mystring.IndexOf($href_link_tag_end) + $href_link_tag_end.Length - 1) - $mystring.IndexOf($href_link_tag_begin) +1 ) ) If ($irregular){ $newstring = $mystring.Replace(("$Chunk_Name" + "$Chunk_HREF"), '' ) } Else { #Example: <a href="http://www.bing.com">here</a> to go to Bing. $newstring = $mystring.Replace(("$Chunk_Name" + "$Chunk_HREF"), ('<a href="' + $href_link + '">' + "$href_name" + '</a>') ) } Return $newstring } #------------------------------------------------------------------------------------ Function Invoke-CLSqlCmd { param( [string]$Server, [string]$Database, [string]$Query, [int]$QueryTimeout = 30, #The time in seconds to wait for the command to execute. The default is 30 seconds. [int]$ConnectionTimeout = 15 #The time (in seconds) to wait for a connection to open. The default value is 15 seconds. ) BEGIN { } PROCESS { try { $sqlConnection = New-Object System.Data.SqlClient.SqlConnection $sqlConnection.ConnectionString = "Server=$Server;Database=$Database;Trusted_Connection=True;Connection Timeout=$ConnectionTimeout;" $sqlConnection.Open() try { $sqlCmd = New-Object System.Data.SqlClient.SqlCommand $sqlCmd.CommandText = $Query $sqlCmd.CommandTimeout = $QueryTimeout $sqlCmd.Connection = $SqlConnection try { $Value = @() $sqlReader = $sqlCmd.ExecuteReader() #The default position of the SqlDataReader is before the first record. #Therefore, you must call Read to begin accessing any data. #Return Value: true if there are more rows; otherwise false. If ($sqlReader.Read()) { # $NUL #function returns true. pipe to nowhere. $Value = ($sqlReader.GetValue(0), $sqlReader.GetName(0)) If ( ($Value[1].ToString().Length -eq 0) -and ($sqlReader.Fieldcount -eq 2) ) { #Write-Host "NoName!" -foreground Yellow -background Red try{ $ResultName = $sqlReader.GetValue(1) } finally { } If ($ResultName) { $Value[1]=$ResultName } Else{ $Value[1]='Name' } } #Write-Host "Value2: $Value2" } Else{ $Value = ("EMPTYRESULT",'Result') } } finally { $sqlReader.Close() } } finally { $sqlCmd.Dispose() } } finally { $sqlConnection.Close() } } END { Return $Value[0] } } #endregion Invoke-CLSqlCmd #------------------------------------------------------------------------------------ ##################################################################################### $ThisScript = $MyInvocation.MyCommand.Path $Rules=@() $Monitors=@() $InstallDirectory = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Microsoft Operations Manager\3.0\Setup" -Name "InstallDirectory").InstallDirectory $PoshModulePath = Join-Path (Split-Path $InstallDirectory) PowerShell $env:PSModulePath += (";" + "$PoshModulePath") # If Topic is provided, make sure it contains an underscore. If (($Topic.Length -ge 2) -and (($Topic.IndexOf('_')+1) -ne $Topic.Length) ) { $Topic = "$Topic"+"_" } #Get OpsDB Server name if not provided If (!($OpsDBServer)){ $OpsDBServer = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Microsoft Operations Manager\3.0\Setup" -Name "DatabaseServerName").DatabaseServerName } #Get OpsDB name if not provided If (!($OpsDBName)){ $OpsDBName = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Microsoft Operations Manager\3.0\Setup" -Name "DatabaseName").DatabaseName } # Add 2012 Functionality/cmdlets Import-Module OperationsManager If ($Error) { $modulepaths=$env:PSModulePath.split(';') $Error.Clear() } # If no mgmt server name has been set, assume that local host is the mgmt server. It's worth a shot. If ($MgmtServerFQDN -eq "") { #Get FQDN of local host executing the script (could be any mgmt server when using SCOM2012 Resource Pools) $objIPProperties = [System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties() If ($null -eq $objIPProperties.DomainName) { $MgmtServerFQDN = $objIPProperties.HostName } Else { $MgmtServerFQDN = $objIPProperties.HostName + "." +$objIPProperties.DomainName } } $Error.Clear() #Connect to Localhost Note: perhaps this can be done differently/cleaner/faster if connecting to self? Write-Host "Connecting to: $MgmtServerFQDN ..." -ForegroundColor Gray -BackgroundColor Black New-SCManagementGroupConnection -Computer $MgmtServerFQDN # | Out-Null If ($Error) { Write-Host "Failed to connect to: $MgmtServerFQDN ! Exiting. " -ForegroundColor Red -BackgroundColor Yellow Exit } Else { Write-Host "Connected to: $MgmtServerFQDN " -ForegroundColor Magenta -BackgroundColor Black } #Set Culture Info $cultureInfo = [System.Globalization.CultureInfo]'en-US' $cssHead = @" <style> displayname { color: blue; font: 16px Arial, sans-serif; } alertname { color: red; font: 16px Arial, sans-serif; } noalertname { color: grey; font: 16px Arial, sans-serif; } name { font: 10px Arial, sans-serif; } body { font: normal 14px Verdana, Arial, sans-serif; } table, th, td { border-collapse: collapse; border: 1px solid black; } th, td { padding: 10px; text-align: left; } tr:hover {background-color: #ccffcc} th { background-color: #4CAF50; color: white; } </style> "@ # Make sure output folder exists If (-not (Test-Path -Path $OutFolder -PathType Container )) { New-Item -Path $OutFolder -ItemType Directory -Force -ErrorAction Stop } # If output files already exist, remove them "Rules.html","Monitors.html" | ForEach-Object { If (Test-Path -PathType Leaf (Join-Path $OutFolder $_) ) { Remove-Item -Path (Join-Path $OutFolder $_) -Force } } } #endregion Begin #region Process { # If a set of MPs is specified, only process workflows contained in the MPs. If ($ManagementPack){ $ManagementPack | Select-Object DisplayName,Name,ID,Version | Format-Table -AutoSize # If filter keyword(s) exist, then filter the results according to the regex patterns submitted in the parameter value. If ($Filter) { Foreach ($ThisRegex in $Filter) { Write-Host "Getting all filtered Rules..." -ForegroundColor Yellow $Rules += (Get-SCOMRule -ManagementPack $ManagementPack | Where-Object {( $_.Name -match "$ThisRegex") -OR ( $_.DisplayName -match "$ThisRegex")} ) Write-Host "Getting all filtered Monitors..." -ForegroundColor Yellow $Monitors += (Get-SCOMMonitor -ManagementPack $ManagementPack | Where-Object {( $_.Name -match "$ThisRegex") -OR ( $_.DisplayName -match "$ThisRegex")} ) } } # If no filter(s) exist, then return all rules/mons from the designated MP. Else { Write-Host "Getting ALL Rules..." -ForegroundColor Yellow $Rules += (Get-SCOMRule -ManagementPack $ManagementPack ) Write-Host "Getting ALL Monitors..." -ForegroundColor Yellow $Monitors += (Get-SCOMMonitor -ManagementPack $ManagementPack ) } } # Else, get ALL workflows in ALL MPs Else { # If filter(s) exist, then filter the results according to the regex patterns submitted in the parameter value. If ($Filter) { Foreach ($ThisRegex in $Filter) { Write-Host "Getting all filtered Rules..." -ForegroundColor Yellow $Rules += (Get-SCOMRule | Where-Object {( $_.Name -match "$ThisRegex") -OR ( $_.DisplayName -match "$ThisRegex")} ) Write-Host "Getting all filtered Monitors..." -ForegroundColor Yellow $Monitors += (Get-SCOMMonitor | Where-Object {( $_.Name -match "$ThisRegex") -OR ( $_.DisplayName -match "$ThisRegex")} ) } } Else{ Write-Host "Getting ALL Rules..." -ForegroundColor Yellow $Rules += (Get-SCOMRule) Write-Host "Getting ALL Monitors..." -ForegroundColor Yellow $Monitors += (Get-SCOMMonitor) } } } #endregion #region End { Write-Host "`nTotal Rules Found: $($Rules.Count)" -BackgroundColor Black -ForegroundColor Green $myRulesObj = ProcessWorkflows -Workflows $Rules -WFType "Rule" If (($ExportCSV)) { $myRulesObjTemp = $myRulesObj | ConvertTo-Csv $myRulesObj = CleanHTML $myRulesObjTemp | ConvertFrom-CSV Write-Host "Exporting rules to CSV: "$(Join-Path $OutFolder ($Topic +"Rules.csv")) -F Cyan #Export to CSV $myRulesObj | Export-Csv -Path $(Join-Path $OutFolder ($Topic +"Rules.csv")) -NoTypeInformation } Else { $RulesTempContent = $myRulesObj | ConvertTo-HTML -Title "SCOM Rules" -Head $cssHead $RulesTempContent = CleanHTML $RulesTempContent $RulesTempContent = $RulesTempContent -Replace '<table>', '<table border="1" cellpadding="20">' $RulesTempContent >> (Join-Path $OutFolder ($Topic +"Rules.html") ) } Write-Host "Total Monitors Found: $($Monitors.Count)" -BackgroundColor Black -ForegroundColor Green $myMonitorObj = ProcessWorkflows -Workflows $Monitors -WFType "Monitor" If (($ExportCSV)) { $myMonitorObjTemp = $myMonitorObj | ConvertTo-CSV $myMonitorObj = CleanHTML $myMonitorObjTemp | ConvertFrom-CSV Write-Host "Exporting monitors to CSV: "$(Join-Path $OutFolder ($Topic + "Monitors.csv")) -F Cyan #Export to CSV $myMonitorObj | Export-Csv -Path $(Join-Path $OutFolder ($Topic + "Monitors.csv")) -NoTypeInformation } Else { $MonitorTempContent = $myMonitorObj | ConvertTo-HTML -Title "SCOM Monitors" -Head $cssHead $MonitorTempContent = CleanHTML $MonitorTempContent $MonitorTempContent = $MonitorTempContent -Replace '<table>', '<table border="1" cellpadding="20">' $MonitorTempContent >> (Join-Path $OutFolder ($Topic + "Monitors.html") ) } Write-host "Output folder: $OutFolder" -BackgroundColor Black -ForegroundColor Yellow If ($ShowResult) { Explorer.exe $OutFolder } } #endregion }#End Function ####################################################################### <# .SYNOPSIS This script will get workflow details for one or more alerts, including the Knowledge Article content (neatly formatted in HTML). .DESCRIPTION Accepts one parameter, the alert object array of one or more SCOM alert objects: Microsoft.EnterpriseManagement.Monitoring.MonitoringAlert[] .PARAMETER Alert The alert object(s) to be queried for the workflow details including the Knowledge article (if one exists). .PARAMETER MgmtServerFQDN Fully Qualified Domain Name of the SCOM management server. Alias: ManagementServer .PARAMETER OutputArticleOnly Will output only the Knowledge Article(s) content (neatly formatted in HTML). This may abe useful for situations where a service/ticketing connector is used to get alert properties. .EXAMPLE Get-SCOMAlert | Select-Object -First 1 | Get-SCOMAlertKnowledge Will display alert info (including Knowledge Article in HTML format) for the first alert object returned. .EXAMPLE PS C:\> Get-SCOMAlertKnowledge -Alert (Get-SCOMAlert | Select-Object -First 3) -OutputArticleOnly Will output only the HTML Knowledge Articles for the first 3 alert objects returned. .EXAMPLE PS C:\> Get-SCOMAlertKnowledge -Alert (Get-SCOMAlert -Name *sql*) Will output alert info (including Knowledge Article in HTML format) for any alerts with "sql" in the alert name. .NOTES Author: Tyson Paul Date: 2016/3/31 Blog: https://blogs.msdn.microsoft.com/tysonpaul/2018/07/18/scomhelper-powershell-module-a-scom-admins-best-friend/ #> Function Get-SCOMAlertKnowledge { [CmdletBinding(DefaultParameterSetName='Parameter Set 1', SupportsShouldProcess=$true, SupportsPaging = $true, PositionalBinding=$true)] Param( [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=0, ParameterSetName='Parameter Set 1')] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [Microsoft.EnterpriseManagement.Monitoring.MonitoringAlert[]]$Alert, [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=1, ParameterSetName='Parameter Set 1')] [Alias("ManagementServer")] [string]$MgmtServerFQDN = "", [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=2, ParameterSetName='Parameter Set 1')] [Switch]$OutputArticleOnly = $false ) Begin { #------------------------------------------------------------------------------------ Function ProcessArticle { Param( $article ) If ($article -ne "None") #some rules don't have any knowledge articles { #Retrieve and format article content $MamlText = $null $HtmlText = $null If ($null -ne $article.MamlContent) { $MamlText = $article.MamlContent $articleContent = fnMamlToHtml($MamlText) } If ($null -ne $article.HtmlContent) { $HtmlText = $article.HtmlContent $articleContent = fnTrimHTML($HtmlText) } } If ($null -eq $articleContent) { $articleContent = "No resolutions were found for this alert." } Return $articleContent } #------------------------------------------------------------------------------------ Function ProcessWorkflow { Param( $Workflows, [string]$WFType ) $myWFCollectionObj = @() ForEach ($thisWF in $Workflows) { $ErrorActionPreference = 'SilentlyContinue' $article = $thisWF.GetKnowledgeArticle($cultureInfo) If ($? -eq $false){ $error.Remove($Error[0]) $article = "None" } Else{ $articleContent = ProcessArticle $article } $WFName = $thisWF.Name $WFDisplayName = $thisWF.DisplayName $WFDescription = $thisWF.Description $WFID = $thisWF.ID If ($WFDescription.Length -lt 1) {$WFDescription = "None"} #Note: Only a small subset of alert properties are gathered here. Add additional Note Properties as needed using the format below. $myWFObj = New-Object -TypeName System.Management.Automation.PSObject $myWFObj | Add-Member -MemberType NoteProperty -Name "WorkflowType" -Value $WFType $myWFObj | Add-Member -MemberType NoteProperty -Name "Name" -Value $WFName $myWFObj | Add-Member -MemberType NoteProperty -Name "DisplayName" -Value $WFDisplayName $myWFObj | Add-Member -MemberType NoteProperty -Name "Description" -Value $WFDescription $myWFObj | Add-Member -MemberType NoteProperty -Name "ID" -Value $WFID $myWFObj | Add-Member -MemberType NoteProperty -Name "KnowledgeArticle" -Value $articleContent $myWFCollectionObj += $myWFObj } Return $myWFCollectionObj } #------------------------------------------------------------------------------------ Function fnMamlToHTML{ param ( $MAMLText ) $HTMLText = ""; $HTMLText = $MAMLText -replace ('xmlns:maml="http://schemas.microsoft.com/maml/2004/10"'); $HTMLText = $HTMLText -replace ("maml:para", "p"); $HTMLText = $HTMLText -replace ("maml:"); $HTMLText = $HTMLText -replace ("</section>"); $HTMLText = $HTMLText -replace ("<section>"); $HTMLText = $HTMLText -replace ("<section >"); $HTMLText = $HTMLText -replace ("<title>", "<h3>"); $HTMLText = $HTMLText -replace ("</title>", "</h3>"); $HTMLText = $HTMLText -replace ("<listitem>", "<li>"); $HTMLText = $HTMLText -replace ("</listitem>", "</li>"); $HTMLText; } #------------------------------------------------------------------------------------ Function fnTrimHTML($HTMLText){ $TrimedText = ""; $TrimedText = $HTMLText -replace ("<", "<") $TrimedText = $TrimedText -replace (">", ">") <# $TrimedText = $TrimedText -replace ("<html>") $TrimedText = $TrimedText -replace ("<HTML>") $TrimedText = $TrimedText -replace ("</html>") $TrimedText = $TrimedText -replace ("</HTML>") $TrimedText = $TrimedText -replace ("<body>") $TrimedText = $TrimedText -replace ("<BODY>") $TrimedText = $TrimedText -replace ("</body>") $TrimedText = $TrimedText -replace ("</BODY>") $TrimedText = $TrimedText -replace ("<h1>", "<h3>") $TrimedText = $TrimedText -replace ("</h1>", "</h3>") $TrimedText = $TrimedText -replace ("<h2>", "<h3>") $TrimedText = $TrimedText -replace ("</h2>", "</h3>") $TrimedText = $TrimedText -replace ("<H1>", "<h3>") $TrimedText = $TrimedText -replace ("</H1>", "</h3>") $TrimedText = $TrimedText -replace ("<H2>", "<h3>") $TrimedText = $TrimedText -replace ("</H2>", "</h3>") #> $TrimedText; } #------------------------------------------------------------------------------------ ########################################################################################### $ThisScript = $MyInvocation.MyCommand.Path $InstallDirectory = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Microsoft Operations Manager\3.0\Setup" -Name "InstallDirectory").InstallDirectory $PoshModulePath = Join-Path (Split-Path $InstallDirectory) PowerShell $env:PSModulePath += (";" + "$PoshModulePath") # Add 2012 Functionality/cmdlets Import-Module OperationsManager If ($Error) { $modulepaths=$env:PSModulePath.split(';') # LogIt -EventID 9995 -Type $warn -Force -Message "Import-Module error. Env:psmodulepath: `n$($modulepaths)" ; $Error.Clear() } # If no mgmt server name has been set, assume that local host is the mgmt server. It's worth a shot. If ($MgmtServerFQDN -eq "") { #Get FQDN of local host executing the script (could be any mgmt server when using SCOM2012 Resource Pools) $objIPProperties = [System.Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties() If ($null -eq $objIPProperties.DomainName) { $MgmtServerFQDN = $objIPProperties.HostName } Else { $MgmtServerFQDN = $objIPProperties.HostName + "." +$objIPProperties.DomainName } } #Connect to Localhost Note: perhaps this can be done differently/cleaner/faster if connecting to self? New-SCManagementGroupConnection -Computer $MgmtServerFQDN | Out-Null # If ($Error) { LogIt -EventID 9995 -Type $warn -Message "Log any Errors..." ; $Error.Clear() } #Set Culture Info $cultureInfo = [System.Globalization.CultureInfo]'en-US' $objWFCollection = @() } Process{ #Depending on how the alert object(s) are passed in, the ForEach may be needed. (parameter vs. piped) ForEach ($objAlert in $Alert) { $workflowID = $objAlert.MonitoringRuleId $bIsMonitorAlert = $objAlert.IsMonitorAlert If ($bIsMonitorAlert -eq $false) { $WFType = "Rule" $workflow = Get-SCOMRule -Id $workflowID } ElseIf ($bIsMonitorAlert -eq $true) { $WFType = "Monitor" $workflow = Get-SCOMMonitor -Id $workflowID } # The funciton being called is designed to accept one or more workflows. # It will return one or more custom objects with workfow details, including (most importantly) the KnowlegeArticle. $objWFCollection += ProcessWorkflow -Workflows $Workflow -WFType $WFType } } #End Process End{ If ($OutputArticleOnly){ Return $objWFCollection.KnowledgeArticle } Else{ Return $objWFCollection } } }#End Function ####################################################################### <# .Synopsis Will clear SCOM agent cache on local machine .EXAMPLE PS C:\> Clear-SCOMCache -WaitSeconds 60 -HealthServiceStatePath 'D:\Program Files\Microsoft System Center 2012 R2\Operations Manager\Server\Health Service State' .EXAMPLE PS C:\> Clear-SCOMCache -WaitSeconds 15 .INPUTS None .OUTPUTS None .NOTES Author: Tyson Paul Blog: https://blogs.msdn.microsoft.com/tysonpaul/2018/07/18/scomhelper-powershell-module-a-scom-admins-best-friend/ Version: 1.2 Date: 2012.10.09 History: 2018.05.25 Cleaned up. Revised a few things. #> Function Clear-SCOMCache { Param( [int]$WaitSeconds=30, [string]$HealthServiceStatePath = (Join-Path (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Microsoft Operations Manager\3.0\Setup" -Name "InstallDirectory").InstallDirectory 'Health Service State' ) ) $ServiceName = 'HealthService' Write-Host "Stopping service: $ServiceName" -f Cyan $StartTime = Get-Date Stop-Service $ServiceName -Verbose Do { $i++ Write-Host "Waiting for $ServiceName to stop..." -f Cyan Start-Sleep -Milliseconds 3000 If ($i -ge 8) { Write-Host "Failed to stop service: $ServiceName in the time allowed! " -b Yellow -f Red Start-Service $ServiceName | Out-Null } } while ( ((Get-Service $ServiceName).Status -ne "Stopped") -AND ((Get-Date) -le ($StartTime.AddSeconds($WaitSeconds) )) ) Write-Host "Attempting to remove cache folder: $HealthServiceStatePath" -f Cyan Get-Item $HealthServiceStatePath | Remove-Item -Recurse -Force -Verbose Start-Sleep -Milliseconds 1000 If (Test-Path -PathType Container $HealthServiceStatePath) { Write-Host "Failed to remove folder: $HealthServiceStatePath ! " -b Yellow -f Red } Else { Write-Host "Success! Folder removed." -f Green } Write-Host "Starting service: $ServiceName" -f Cyan $StartTime = Get-Date Start-Service $ServiceName -Verbose Do { $i++ Write-Host "Waiting for $ServiceName to start..." -f Cyan Start-Sleep -Milliseconds 3000 If ($i -ge 8) { Write-Host "Failed to start service: $ServiceName in the time allowed! " -b Yellow -f Red } } while ( ((Get-Service $ServiceName).Status -ne "Running") -AND ((Get-Date) -le ($StartTime.AddSeconds($WaitSeconds) )) ) If ( (Test-Path -PathType Container $HealthServiceStatePath) ){ Write-Host "Cache folder auto-created: $HealthServiceStatePath ! " -f Green } Write-Host "SCOM agent Cache has been cleared. " -f Green } #End Function ####################################################################### <# .Synopsis Will remove obsolete aliases from unsealed management packs. .DESCRIPTION This will not alter the original file but rather will output modified versions to the designated output folder. .EXAMPLE PS C:\> Remove-SCOMObsoleteReferenceFromMPFile -inFile 'C:\Unsealed MPs\*.xml' -outDir 'C:\Usealed MPs\Modified MPs' .EXAMPLE PS C:\> Remove-SCOMObsoleteReferenceFromMPFile -inFile 'C:\Unsealed MPs\MyOverrides.xml' -outDir 'C:\Usealed MPs\Modified MPs' .EXAMPLE PS C:\> Remove-SCOMObsoleteReferenceFromMPFile -inFile (GetChildItem 'C:\Unsealed MPs\*.xml').FullName .EXAMPLE PS C:\> Get-ChildItem -Path 'C:\Unsealed MPs\*.xml' | Remove-SCOMObsoleteReferenceFromMPFile .NOTES Author: Tyson Paul Blog: https://blogs.msdn.microsoft.com/tysonpaul/2018/07/18/scomhelper-powershell-module-a-scom-admins-best-friend/ Version: 1.0 Date: 20018.05.09 #> Function Remove-SCOMObsoleteReferenceFromMPFile { [CmdletBinding()] Param ( # Path to input file(s) (YourUnsealedPack.xml) [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, Position=0)] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [Alias("FullName")] [string[]]$inFile, # Path to output folder, to save modified .xml file(s) [Parameter(Mandatory=$false, ValueFromPipeline=$false, Position=1)] [string]$outDir ) Begin{} Process{ [System.Object[]]$paths = (Get-ChildItem -Path $inFile ) ForEach ($FilePath in $paths){ If (-not [bool]$outDir){ $outDir = (Join-Path (Split-Path $FilePath -Parent) "MODIFIED") } If (-NOT (Test-Path -Path $outDir) ){ New-Item -Path $outDir -ItemType Directory -Force -ErrorAction SilentlyContinue | Out-Null } Write-Host "Input File: " -NoNewline; Write-Host $FilePath -F Cyan Try{ [xml]$xml = Get-Content -Path $FilePath $References = $xml.ManagementPack.Manifest.References }Catch{ Continue } If (-NOT [bool]$References) {Continue} $References.SelectNodes("*") | ForEach-Object { # If neither the Alias or ID of the referenced MP is found within the body of the MP, remove the reference. If (-NOT (($xml.InnerXml -match ("$($_.Alias)!")) -or ($xml.InnerXml -match ("$($_.ID)!"))) ) { Write-Host "Obsolete Reference Removed: $($_.Alias) : $($_.ID)" -F Green $References.RemoveChild($_) | Out-Null } } $outFile = (Join-Path $outDir (Split-Path $FilePath -Leaf)) Write-Host "Saving to file: " -NoNewline; Write-Host $outFile -F Yellow $Xml.Save($outFile) } } End{} }# END FUNCTION ####################################################################### <# .Synopsis .DESCRIPTION Tim Culham of www.culham.net wrote an awesome lightweight Daily Health Check Script which can be found here: http://www.culham.net/powershell/scom-2012-scom-2012-r2-daily-check-powershell-script-html-report/ I have been meaning to write something similiar for awhile so decided to take his wonderfully written script/structure and extend it by adding in a number of the more in depth Database Health Checks/KH Useful SQL Queries that I frequently find myself asking customers to run when they are experiencing performance issues with the Ops & DW DB's. I will cleanup my code in a later version and add additional functionality but for now Kudo's to Tim! MBullwin 11/3/2014 www.OpsConfig.com [ https://gallery.technet.microsoft.com/SCOM-Health-Check-fd2272ec ] As with all scripts I post, this is provided AS-IS without warrenty so please test first and use at your own risk. What this version of the script will give you:(Some of these are just features which are carried over from the original, many are added) 01. Version/Service Pack/Edition of SQL for each SCOM DB Server 02. Disk Space Info for Ops DB, DW DB, and associated Temp DB's 03. Database Backup Status for all DB's except Temp. 04. Top 25 Largest Tables for Ops DB and DW DB 05. Number of Events Generated Per Day (Ops DB) 06. Top 10 Event Generating Computers (Ops DB) 07. Top 25 Events by Publisher (Ops DB) 08. Number of Perf Insertions Per Day (Ops DB) 09. Top 25 Perf Insertions by Object/Counter Name (Ops DB) 10. Top 25 Alerts by Alert Count 11. Alerts with a Repeat Count higher than 200 12. Stale State Change Data 13. Top 25 Monitors Changing State in the last 7 Days 14. Top 25 Monitors Changing State By Object 15. Ops DB Grooming History 16. Snapshot of DW Staging Tables 17. DW Grooming Retention 18. Management Server checks (Works well on prem, seems to have some issues with gateways due to remote calls-if you see some errors flash by have no fear though I wouldn't necessarily trust the results coming back from a Gateway server in the report depending on firewall settings) 19. Daily KPI 20. MP's Modified in the Last 24 hours 21. Overrides in Default MP Check 22. Unintialized Agents 23. Agent Stats (Healthy, Warning, Critical, Unintialized, Total) 24. Agent Pending Management Summary 25. Alert Summary 26. Servers in Maintenance Mode For more details on this version checkout: www.OpsConfig.com .NOTES Author: MBullwin Blog: www.OpsConfig.com Version: v1.1 Date: 11/3/2014 Original Script: https://gallery.technet.microsoft.com/SCOM-Health-Check-fd2272ec #> Function Get-SCOMHealthCheckOpsConfig { ################################################################################################################### # # Tim Culham of www.culham.net wrote an awesome lightweight Daily Health Check Script # which can be found here: # # http://www.culham.net/powershell/scom-2012-scom-2012-r2-daily-check-powershell-script-html-report/ # # I have been meaning to write something similiar for awhile so decided to take his wonderfully written # script/structure and extend it by adding in a number of the more in depth Database Health Checks/KH Useful # SQL Queries that I frequently find myself asking customers to run when they are experiencing performance # issues with the Ops & DW DB's. # # I will cleanup my code in a later version and add additional functionality but for now Kudo's to Tim! # # # MBullwin 11/3/2014 # www.OpsConfig.com # # As with all scripts I post, this is provided AS-IS without warrenty so please test first and use at your own risk. ###################################################################################################################### $StartTime=Get-Date # Check if the OperationsManager Module is loaded if(-not (Get-Module | Where-Object {$_.Name -eq "OperationsManager"})) { "The Operations Manager Module was not found...importing the Operations Manager Module" Import-module OperationsManager } else { "The Operations Manager Module is loaded" } # Connect to localhost when running on the management server or define a server to connect to. $connect = New-SCManagementGroupConnection -ComputerName localhost # The Name and Location of are we going to save this Report $CreateReportLocation = [System.IO.Directory]::CreateDirectory("c:\SCOM_Health_Check") $ReportLocation = "c:\SCOM_Health_Check" $ReportName = "$(get-date -format "yyyy-M-dd")-SCOM-HealthCheck.html" $ReportPath = "$ReportLocation\$ReportName" # Create header for HTML Report $Head = "<style>" $Head +="BODY{background-color:#CCCCCC;font-family:Calibri,sans-serif; font-size: small;}" $Head +="TABLE{border-width: 1px;border-style: solid;border-color: black;border-collapse: collapse; width: 98%;}" $Head +="TH{border-width: 1px;padding: 0px;border-style: solid;border-color: black;background-color:#293956;color:white;padding: 5px; font-weight: bold;text-align:left;}" $Head +="TD{border-width: 1px;padding: 0px;border-style: solid;border-color: black;background-color:#F0F0F0; padding: 2px;}" $Head +="</style>" # Retrieve the name of the Operational Database and Data WareHouse Servers from the registry. $reg = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Microsoft Operations Manager\3.0\Setup\" $OperationsManagerDBServer = $reg.DatabaseServerName $OperationsManagerDWServer = $reg.DataWarehouseDBServerName # If the value is empty in this key, then we'll use the Get-SCOMDataWarehouseSetting cmdlet. If (!($OperationsManagerDWServer)) {$OperationsManagerDWServer = Get-SCOMDataWarehouseSetting | Select-Object -expandProperty DataWarehouseServerName} $OperationsManagerDBServer = $OperationsManagerDBServer.ToUpper() $OperationsManagerDWServer = $OperationsManagerDWServer.ToUpper() $ReportingURL = Get-SCOMReportingSetting | Select-Object -ExpandProperty ReportingServerUrl $WebConsoleURL = Get-SCOMWebAddressSetting | Select-Object -ExpandProperty WebConsoleUrl <# # The number of days before Database Grooming # These are my settings, I use this to determine if someone has changed something # Feel free to comment this part out if you aren't interested $AlertDaysToKeep = 2 $AvailabilityHistoryDaysToKeep = 2 $EventDaysToKeep = 1 $JobStatusDaysToKeep = 1 $MaintenanceModeHistoryDaysToKeep = 2 $MonitoringJobDaysToKeep = 2 $PerformanceDataDaysToKeep = 2 $StateChangeEventDaysToKeep = 2 # SCOM Agent Heartbeat Settings $AgentHeartbeatInterval = 180 $MissingHeartbeatThreshold = 3 #> # SQL Server Function to query the Operational Database Server function Run-OpDBSQLQuery { Param($sqlquery) $SqlConnection = New-Object System.Data.SqlClient.SqlConnection $SqlConnection.ConnectionString = "Server=$OperationsManagerDBServer;Database=OperationsManager;Integrated Security=True" $SqlCmd = New-Object System.Data.SqlClient.SqlCommand $SqlCmd.CommandText = $sqlquery $SqlCmd.Connection = $SqlConnection $SqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter $SqlAdapter.SelectCommand = $SqlCmd $DataSet = New-Object System.Data.DataSet $SqlAdapter.Fill($DataSet) | Out-Null $SqlConnection.Close() $DataSet.Tables[0] } # SQL Server Function to query the Data Warehouse Database Server function Run-OpDWSQLQuery { Param($sqlquery) $SqlConnection = New-Object System.Data.SqlClient.SqlConnection $SqlConnection.ConnectionString = "Server=$OperationsManagerDWServer;Database=OperationsManagerDW;Integrated Security=True" $SqlCmd = New-Object System.Data.SqlClient.SqlCommand $SqlCmd.CommandText = $sqlquery $SqlCmd.Connection = $SqlConnection $SqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter $SqlAdapter.SelectCommand = $SqlCmd $DataSet = New-Object System.Data.DataSet $SqlAdapter.Fill($DataSet) | Out-Null $SqlConnection.Close() $DataSet.Tables[0] } # Retrieve the Data for the Majority of the Report # Truth is we probably don't need all of this data, but even on a busy environment it only takes a couple of mins to run. Write-Host "Retrieving Agents" $Agents = Get-SCOMAgent Write-Host "Retrieving Alerts" $Alerts = Get-SCOMAlert Write-Host "Retrieving Groups" $Groups = Get-SCOMGroup Write-Host "Retrieving Management Group" $ManagementGroup = Get-SCOMManagementGroup Write-Host "Retrieving Management Packs" $ManagementPacks = Get-SCManagementPack Write-Host "Retrieving Management Servers" $ManagementServers = Get-SCOMManagementServer Write-Host "Retrieving Monitors" $Monitors = Get-SCOMMonitor Write-Host "Retrieving Rules" $Rules = Get-SCOMRule # Check to see if the Reporting Server Site is OK $ReportingServerSite = New-Object System.Net.WebClient $ReportingServerSite = [net.WebRequest]::Create($ReportingURL) $ReportingServerSite.UseDefaultCredentials = $true $ReportingServerStatus = $ReportingServerSite.GetResponse() | Select-Object -expandProperty statusCode # This code can convert the "OK" Result to an Integer, like 200 # (($web.GetResponse()).Statuscode) -as [int] # Check to see if the Web Server Site is OK $WebConsoleSite = New-Object System.Net.WebClient $WebConsoleSite = [net.WebRequest]::Create($WebConsoleURL) $WebConsoleSite.UseDefaultCredentials = $true $WebConsoleStatus = $WebConsoleSite.GetResponse() | Select-Object -expandProperty statusCode # SQL Server Function to query Size of the Database Server $DatabaseSize = @" select a.FILEID, [FILE_SIZE_MB]=convert(decimal(12,2),round(a.size/128.000,2)), [SPACE_USED_MB]=convert(decimal(12,2),round(fileproperty(a.name,'SpaceUsed')/128.000,2)), [FREE_SPACE_MB]=convert(decimal(12,2),round((a.size-fileproperty(a.name,'SpaceUsed'))/128.000,2)) , [GROWTH_MB]=convert(decimal(12,2),round(a.growth/128.000,2)), NAME=left(a.NAME,15), FILENAME=left(a.FILENAME,60) from dbo.sysfiles a "@ #SQL Server Function to query Size of the TempDB $TempDBSize =@" USE tempdb select a.FILEID, [FILE_SIZE_MB]=convert(decimal(12,2),round(a.size/128.000,2)), [SPACE_USED_MB]=convert(decimal(12,2),round(fileproperty(a.name,'SpaceUsed')/128.000,2)), [FREE_SPACE_MB]=convert(decimal(12,2),round((a.size-fileproperty(a.name,'SpaceUsed'))/128.000,2)) , [GROWTH_MB]=convert(decimal(12,2),round(a.growth/128.000,2)), NAME=left(a.NAME,15), FILENAME=left(a.FILENAME,60) from dbo.sysfiles a "@ #SQL Server Function to query the version of SQL $SQLVersion =@" SELECT SERVERPROPERTY('productversion') AS "Product Version", SERVERPROPERTY('productlevel') AS "Service Pack", SERVERPROPERTY ('edition') AS "Edition" "@ # Run the Size Query against the Operational Database and Data Warehouse Database Servers $OPDBSize = Run-OpDBSQLQuery $DatabaseSize $DWDBSize = Run-OpDWSQLQuery $DatabaseSize $OPTPSize = Run-OpDBSQLQuery $TempDBSize $DWTPSize = Run-OpDWSQLQuery $TempDBSize $OPSQLVER = Run-OpDBSQLQuery $SQLVersion $DWSQLVER = Run-OpDWSQLQuery $SQLVersion # Insert the Database Server details into the Report $ReportOutput += "<h2>Database Servers</h2>" $ReportOutput += "<p>Operational Database Server : $OperationsManagerDBServer</p>" $ReportOutput += $OPSQLVER | Select-Object "Product Version", "Service Pack", Edition | ConvertTo-Html -Fragment $ReportOutput += "<p>Data Warehouse Database Server : $OperationsManagerDWServer</p>" $ReportOutput += $DWSQLVER | Select-Object "Product Version", "Service Pack", Edition | ConvertTo-Html -Fragment # Insert the Size Results for the Operational Database into the Report $ReportOutput += "<h3>$OperationsManagerDBServer Operations Manager DB</h4>" $ReportOutput += $OPDBSize | Select-Object Name, FILE_SIZE_MB, SPACE_USED_MB, FREE_SPACE_MB, FILENAME | ConvertTo-HTML -fragment $ReportOutput += "<br></br>" $ReportOutput += "<h3>Operations Temp DB</h4>" $ReportOutput += $OPTPSize | Select-Object Name, FILE_SIZE_MB, SPACE_USED_MB, FREE_SPACE_MB, FILENAME | ConvertTo-HTML -fragment # Insert the Size Results for the Data Warehouse Database and TempDB into the Report $ReportOutput += "<br>" $ReportOutput += "<h3>$OperationsManagerDWServer Data Warehouse DB</h4>" $ReportOutput += $DWDBSize | Select-Object Name, FILE_SIZE_MB, SPACE_USED_MB, FREE_SPACE_MB, FILENAME | ConvertTo-HTML -fragment $ReportOutput += "<br></br>" $ReportOutput += "<h3>Data Warehouse Temp DB</h4>" $ReportOutput += $DWTPSize | Select-Object Name, FILE_SIZE_MB, SPACE_USED_MB, FREE_SPACE_MB, FILENAME | ConvertTo-HTML -fragment # SQL Query to find out how many State Changes there were yesterday $StateChangesYesterday = @" -- How Many State Changes Yesterday?: select count(*) from StateChangeEvent where cast(TimeGenerated as date) = cast(getdate()-1 as date) "@ $StateChanges = Run-OpDBSQLQuery $StateChangesYesterday | Select-Object -ExpandProperty Column1 | Out-String $AllStats = @() $StatSummary = New-Object psobject $StatSummary | Add-Member noteproperty "Open Alerts" (($Alerts | Where-Object {$_.ResolutionState -ne 255}).count) $StatSummary | Add-Member noteproperty "Groups" ($Groups.Count) $StatSummary | Add-Member noteproperty "Monitors" ($Monitors.Count) $StatSummary | Add-Member noteproperty "Rules" ($Rules.Count) $StatSummary | Add-Member noteproperty "State Changes Yesterday" ($StateChanges | ForEach-Object {$_.TrimStart()} | ForEach-Object {$_.TrimEnd()}) $AllStats += $StatSummary #SQL Query Top 10 Event generating computers $TopEventGeneratingComputers = @" SELECT top 10 LoggingComputer, COUNT(*) AS TotalEvents FROM EventallView GROUP BY LoggingComputer ORDER BY TotalEvents DESC "@ #SQL Query number of Events Generated per day $NumberOfEventsPerDay = @" SELECT CASE WHEN(GROUPING(CONVERT(VARCHAR(20), TimeAdded, 101)) = 1) THEN 'All Days' ELSE CONVERT(VARCHAR(20), TimeAdded, 101) END AS DayAdded, COUNT(*) AS NumEventsPerDay FROM EventAllView GROUP BY CONVERT(VARCHAR(20), TimeAdded, 101) WITH ROLLUP ORDER BY DayAdded DESC "@ #SQL Query Most Common Events by Publishername $MostCommonEventsByPub = @" SELECT top 25 Number AS "Event Number", Publishername, COUNT(*) AS TotalEvents FROM EventAllView GROUP BY Number, Publishername ORDER BY TotalEvents DESC "@ #SQL Query the Number of Performance Insertions per Day $NumberofPerInsertsPerDay = @" SELECT CASE WHEN(GROUPING(CONVERT(VARCHAR(20), TimeSampled, 101)) = 1) THEN 'All Days' ELSE CONVERT(VARCHAR(20), TimeSampled, 101) END AS DaySampled, COUNT(*) AS NumPerfPerDay FROM PerformanceDataAllView GROUP BY CONVERT(VARCHAR(20), TimeSampled, 101) WITH ROLLUP ORDER BY DaySampled DESC "@ #SQL Query the Most common perf insertions by perf counter name $MostCommonPerfByN = @" select top 25 pcv.objectname, pcv.countername, count (pcv.countername) as total from performancedataallview as pdv, performancecounterview as pcv where (pdv.performancesourceinternalid = pcv.performancesourceinternalid) group by pcv.objectname, pcv.countername order by count (pcv.countername) desc "@ #SQL Query the Top 25 Alerts by Alert Count $MostCommonAByAC = @" SELECT Top 25 AlertStringName, Name, SUM(1) AS AlertCount, SUM(RepeatCount+1) AS AlertCountWithRepeatCount FROM Alertview WITH (NOLOCK) GROUP BY AlertStringName, Name ORDER BY AlertCount DESC "@ #SQL Query for Stale State Change Data $StaleStateChangeData = @" declare @statedaystokeep INT SELECT @statedaystokeep = DaysToKeep from PartitionAndGroomingSettings WHERE ObjectName = 'StateChangeEvent' SELECT COUNT(*) as 'Total StateChanges', count(CASE WHEN sce.TimeGenerated > dateadd(dd,-@statedaystokeep,getutcdate()) THEN sce.TimeGenerated ELSE NULL END) as 'within grooming retention', count(CASE WHEN sce.TimeGenerated < dateadd(dd,-@statedaystokeep,getutcdate()) THEN sce.TimeGenerated ELSE NULL END) as '> grooming retention', count(CASE WHEN sce.TimeGenerated < dateadd(dd,-30,getutcdate()) THEN sce.TimeGenerated ELSE NULL END) as '> 30 days', count(CASE WHEN sce.TimeGenerated < dateadd(dd,-90,getutcdate()) THEN sce.TimeGenerated ELSE NULL END) as '> 90 days', count(CASE WHEN sce.TimeGenerated < dateadd(dd,-365,getutcdate()) THEN sce.TimeGenerated ELSE NULL END) as '> 365 days' from StateChangeEvent sce "@ #SQL Query Noisest monitors changing state in the past 7 days $NoisyMonitorData = @" select distinct top 25 m.DisplayName as MonitorDisplayName, m.Name as MonitorIdName, mt.typename AS TargetClass, count(sce.StateId) as NumStateChanges from StateChangeEvent sce with (nolock) join state s with (nolock) on sce.StateId = s.StateId join monitorview m with (nolock) on s.MonitorId = m.Id join managedtype mt with (nolock) on m.TargetMonitoringClassId = mt.ManagedTypeId where m.IsUnitMonitor = 1 -- Scoped to within last 7 days AND sce.TimeGenerated > dateadd(dd,-7,getutcdate()) group by m.DisplayName, m.Name,mt.typename order by NumStateChanges desc "@ #SQL Query Top 25 Monitors changing state by Object $NoisyMonitorByObject =@" select distinct top 25 bme.DisplayName AS ObjectName, bme.Path, m.DisplayName as MonitorDisplayName, m.Name as MonitorIdName, mt.typename AS TargetClass, count(sce.StateId) as NumStateChanges from StateChangeEvent sce with (nolock) join state s with (nolock) on sce.StateId = s.StateId join BaseManagedEntity bme with (nolock) on s.BasemanagedEntityId = bme.BasemanagedEntityId join MonitorView m with (nolock) on s.MonitorId = m.Id join managedtype mt with (nolock) on m.TargetMonitoringClassId = mt.ManagedTypeId where m.IsUnitMonitor = 1 -- Scoped to specific Monitor (remove the "--" below): -- AND m.MonitorName like ('%HealthService%') -- Scoped to specific Computer (remove the "--" below): -- AND bme.Path like ('%sql%') -- Scoped to within last 7 days AND sce.TimeGenerated > dateadd(dd,-7,getutcdate()) group by s.BasemanagedEntityId,bme.DisplayName,bme.Path,m.DisplayName,m.Name,mt.typename order by NumStateChanges desc "@ #SQL Query Grooming Settings for the Operational Database $OpsDBGrooming =@" SELECT ObjectName, GroomingSproc, DaysToKeep, GroomingRunTime, DataGroomedMaxTime FROM PartitionAndGroomingSettings WITH (NOLOCK) "@ #SQL Query DW DB Staging Tables $DWDBStagingTables = @" select count(*) AS "Alert Staging Table" from Alert.AlertStage "@ $DWDBStagingTablesEvent =@" select count (*) AS "Event Staging Table" from Event.eventstage "@ $DWDBStagingTablesPerf =@" select count (*) AS "Perf Staging Table" from Perf.PerformanceStage "@ $DWDBStagingTablesState =@" select count (*) AS "State Staging Table" from state.statestage "@ #SQL Query DW Grooming Retention $DWDBGroomingRetention =@" select ds.datasetDefaultName AS 'Dataset Name', sda.AggregationTypeId AS 'Agg Type 0=raw, 20=Hourly, 30=Daily', sda.MaxDataAgeDays AS 'Retention Time in Days' from dataset ds, StandardDatasetAggregation sda WHERE ds.datasetid = sda.datasetid ORDER by "Retention Time in Days" desc "@ #SQL function to Query the Top 25 largest tables in a database $DWDBLargestTables =@" SELECT TOP 25 a2.name AS [tablename], (a1.reserved + ISNULL(a4.reserved,0))* 8 AS reserved, a1.rows as row_count, a1.data * 8 AS data, (CASE WHEN (a1.used + ISNULL(a4.used,0)) > a1.data THEN (a1.used + ISNULL(a4.used,0)) - a1.data ELSE 0 END) * 8 AS index_size, (CASE WHEN (a1.reserved + ISNULL(a4.reserved,0)) > a1.used THEN (a1.reserved + ISNULL(a4.reserved,0)) - a1.used ELSE 0 END) * 8 AS unused, (row_number() over(order by (a1.reserved + ISNULL(a4.reserved,0)) desc))%2 as l1, a3.name AS [schemaname] FROM (SELECT ps.object_id, SUM (CASE WHEN (ps.index_id < 2) THEN row_count ELSE 0 END) AS [rows], SUM (ps.reserved_page_count) AS reserved, SUM (CASE WHEN (ps.index_id < 2) THEN (ps.in_row_data_page_count + ps.lob_used_page_count + ps.row_overflow_used_page_count) ELSE (ps.lob_used_page_count + ps.row_overflow_used_page_count) END ) AS data, SUM (ps.used_page_count) AS used FROM sys.dm_db_partition_stats ps GROUP BY ps.object_id) AS a1 LEFT OUTER JOIN (SELECT it.parent_id, SUM(ps.reserved_page_count) AS reserved, SUM(ps.used_page_count) AS used FROM sys.dm_db_partition_stats ps INNER JOIN sys.internal_tables it ON (it.object_id = ps.object_id) WHERE it.internal_type IN (202,204) GROUP BY it.parent_id) AS a4 ON (a4.parent_id = a1.object_id) INNER JOIN sys.all_objects a2 ON ( a1.object_id = a2.object_id ) INNER JOIN sys.schemas a3 ON (a2.schema_id = a3.schema_id) WHERE a2.type <> N'S' and a2.type <> N'IT' "@ #SQL Function to query and check backup status of SQL Databases $SQLBackupStatus =@" SELECT d.name, DATEDIFF(Day, COALESCE(MAX(b.backup_finish_date), d.create_date), GETDATE()) AS [DaysSinceBackup] FROM sys.databases d LEFT OUTER JOIN msdb.dbo.backupset b ON d.name = b.database_name WHERE d.is_in_standby = 0 AND source_database_id is null AND d.name NOT LIKE 'tempdb' AND (b.[type] IN ('D', 'I') OR b.[type] IS NULL) GROUP BY d.name, d.create_date "@ # Run additional SQL Queries against the Operational Database $OPTOPALERT = Run-OpDBSQLQuery $TopEventGeneratingComputers $OPNUMEPERDAY = Run-OpDBSQLQuery $NumberOfEventsPerDay $OPMOSTCOMEVENT = Run-OpDBSQLQuery $MostCommonEventsByPub $OPPERFIPERD = Run-OpDBSQLQuery $NumberofPerInsertsPerDay $OPPERFIBYN = Run-OpDBSQLQuery $MostCommonPerfByN $OPTOPALERTC = Run-OpDBSQLQuery $MostCommonAByAC $OPSTALESTD = Run-OpDBSQLQuery $StaleStateChangeData $OPNOISYMON = Run-OpDBSQLQuery $NoisyMonitorData $OPNOISYMONOBJ = Run-OpDBSQLQuery $NoisyMonitorByObject $OPDBGROOM = Run-OpDBSQLQuery $OpsDBGrooming $OPLARGTAB = Run-OpDBSQLQuery $DWDBLargestTables $OPDBBACKUP = Run-OpDBSQLQuery $SQLBackupStatus #Run additional SQL Queries against the DW DB $DWDBSGTB = Run-OpDWSQLQuery $DWDBStagingTables $DWDBSGTBEV = Run-OpDWSQLQuery $DWDBStagingTablesEvent $DWDBSGTBPE = Run-OpDWSQLQuery $DWDBStagingTablesPerf $DWDBSGTBST = Run-OpDWSQLQuery $DWDBStagingTablesState $DWDBGRET = Run-OpDWSQLQuery $DWDBGroomingRetention $DWDBLARGETAB = Run-OpDWSQLQuery $DWDBLargestTables $DWDBBACKUP = Run-OpDWSQLQuery $SQLBackupStatus #Output to HTML Report $ReportOutput += "<h2>Operational Database Health</h2>" $ReportOutput += "<h3>Operations Database Backup Status</h3>" $ReportOutput += $OPDBBACKUP | Select-Object name, DaysSinceBackup | ConvertTo-HTML -Fragment $ReportOutput += "<h3>Operations Database Top 25 Largest Tables</h3>" $ReportOutput += $OPLARGTAB | Select-Object tablename, reserved, row_count, data, index_size, unused |ConvertTo-Html -Fragment $ReportOutput += "<h3>Number of Events Generated Per Day</h3>" $ReportOutput += $OPNUMEPERDAY | Select-Object NumEventsPerDay, DayAdded | ConvertTo-HTML -Fragment $ReportOutput += "<h3>Top 10 Event Generating Computers</h3>" $ReportOutput += $OPTOPALERT | Select-Object LoggingComputer, TotalEvents | ConvertTo-HTML -Fragment $ReportOutput += "<h3>Top 25 Events By Publisher</h3>" $ReportOutput += $OPMOSTCOMEVENT | Select-Object "Event Number", Publishername, TotalEvents | ConvertTo-Html -Fragment $ReportOutput += "<h3>Number of Perf Insertions Per Day</h3>" $ReportOutput += $OPPERFIPERD | Select-Object DaySampled, NumPerfPerDay | ConvertTo-Html -Fragment $ReportOutput += "<h3>Top 25 Perf Insertions by Object/Counter Name</h3>" $ReportOutput += $OPPERFIBYN | Select-Object objectname, countername, total | ConvertTo-Html -Fragment $ReportOutput += "<h3>Top 25 Alerts by Alert Count</h3>" $ReportOutput += $OPTOPALERTC | Select-Object AlertStringName, Name, AlertCount, AlertCountWithRepeatCount | ConvertTo-Html -Fragment # Get the alerts with a repeat count higher than the variable $RepeatCount $RepeatCount = 200 $ReportOutput += "<br>" $ReportOutput += "<h3>Alerts with a Repeat Count higher than $RepeatCount</h3>" # Produce a table of all Open Alerts above the repeatcount and add it to the Report $ReportOutput += $Alerts | Where-Object {$_.RepeatCount -ge $RepeatCount -and $_.ResolutionState -ne 255} | Select-Object Name, Category, NetBIOSComputerName, RepeatCount | Sort-Object repeatcount -desc | ConvertTo-HTML -fragment #Output to HTML report $ReportOutput += "<h3>Stale State Change Data</h3>" $ReportOutput += $OPSTALESTD | Select-Object "Total StateChanges", "within grooming retention", "> grooming retention","> 30 days","> 90 days","> 365 days"| ConvertTo-Html -Fragment $ReportOutput += "<h3>Top 25 Monitors Changing State in the last 7 Days</h3>" $ReportOutput += $OPNOISYMON | Select-Object MonitorDisplayName, MonitorIdName, TargetClass, NumStateChanges | ConvertTo-Html -Fragment $ReportOutput += "<h3>Top 25 Monitors Changing State By Object</h3>" $ReportOutput += $OPNOISYMONOBJ | Select-Object ObjectName, Path, MonitorDisplayName, MonitorIdName,TargetClass, NumStateChanges | ConvertTo-Html -Fragment $ReportOutput += "<h3>Operations Database Grooming History</h3>" $ReportOutput += $OPDBGROOM | Select-Object ObjectName, GroomingSproc, DaysToKeep, GroomingRunTime,DataGroomedMaxTime | ConvertTo-HTML -Fragment # SQL Query to find out what Grooming Jobs have run in the last 24 hours $DidGroomingRun = @" -- Did Grooming Run?: SELECT [InternalJobHistoryId] ,[TimeStarted] ,[TimeFinished] ,[StatusCode] ,[Command] ,[Comment] FROM [dbo].[InternalJobHistory] WHERE [TimeStarted] >= DATEADD(day, -2, GETDATE()) Order by [TimeStarted] "@ # Produce a table of Grooming History and add it to the Report $ReportOutput += "<h3>Grooming History From The Past 48 Hours</h3>" $ReportOutput += Run-OpDBSQLQuery $DidGroomingRun InternalJobHistoryId, TimeStarted, TimeFinished, StatusCode, Command, Comment | Select-Object | ConvertTo-HTML -fragment #Produce Table of DW DB Health $ReportOutput +="<h2>Data Warehouse Database Health</h2>" $ReportOutput +="<h3>Data Warehouse DB Backup Status</h3>" $ReportOutput +=$DWDBBACKUP | Select-Object name, DaysSinceBackup | ConvertTo-Html -Fragment $ReportOutput +="<h3>Data Warehouse Top 25 Largest Tables</h3>" $ReportOutput +=$DWDBLARGETAB | Select-Object tablename, reserved, row_count, data, index_size, unused |ConvertTo-Html -fragment $ReportOutput +="<h3>Data Warehouse Staging Tables</h3>" $ReportOutput +=$DWDBSGTB | Select-Object "Alert Staging Table", Table | ConvertTo-Html -Fragment $ReportOutput +=$DWDBSGTBEV | Select-Object "Event Staging Table", Table | ConvertTo-Html -Fragment $ReportOutput +=$DWDBSGTBPE | Select-Object "Perf Staging Table", Table | ConvertTo-Html -Fragment $ReportOutput +=$DWDBSGTBST | Select-Object "State Staging Table", Table| ConvertTo-Html -Fragment $ReportOutput +="<h3>Data Warhouse Grooming Retention</h3>" $ReportOutput +=$DWDBGRET | Select-Object "Dataset Name", "Agg Type 0=raw, 20=Hourly, 30=Daily","Retention Time in Days"| ConvertTo-Html -Fragment # Insert the Results for the Number of Management Servers into the Report $ReportOutput += "<p>Number of Management Servers : $($ManagementServers.count)</p>" # Retrieve the data for the Management Servers $ReportOutput += "<br>" $ReportOutput += "<h2>Management Servers</h2>" $AllManagementServers = @() ForEach ($ManagementServer in $ManagementServers) { # Find out the Server Uptime for each of the Management Servers #Original query referenced -computer $ManagementServer.Name this was an error I modified to .Displayname to fix #TPaul: updated this to use CIM instead of WMI $lastboottime = (Get-CimInstance -ClassName win32_operatingsystem ).LastBootUpTime $sysuptime =New-TimeSpan $lastboottime (Get-Date) $totaluptime = "" + $sysuptime.days + " days " + $sysuptime.hours + " hours " + $sysuptime.minutes + " minutes " + $sysuptime.seconds + " seconds" # Find out the Number of WorkFlows Running on each of the Management Servers $perfWorkflows = Get-Counter -ComputerName $ManagementServer.DisplayName -Counter "\Health Service\Workflow Count" -SampleInterval 1 -MaxSamples 1 # The Performance Counter seems to return a lot of other characters and spaces...I only want the number of workflows, let's dump the rest [int]$totalWorkflows = $perfWorkflows.readings.Split(':')[-1] | ForEach-Object {$_.TrimStart()} | ForEach-Object {$_.TrimEnd()} $ManagementServerProperty = New-Object psobject $ManagementServerProperty | Add-Member noteproperty "Management Server" ($ManagementServer.DisplayName) $ManagementServerProperty | Add-Member noteproperty "Health State" ($ManagementServer.HealthState) $ManagementServerProperty | Add-Member noteproperty "Version" ($ManagementServer.Version) $ManagementServerProperty | Add-Member noteproperty "Action Account" ($ManagementServer.ActionAccountIdentity) $ManagementServerProperty | Add-Member noteproperty "System Uptime" ($totaluptime) $ManagementServerProperty | Add-Member noteproperty "Workflows" ($totalWorkflows) $AllManagementServers += $ManagementServerProperty } # Insert the Results for the Management Servers into the Report $ReportOutput += $AllManagementServers | Select-Object "Management Server", "Health State", "Version", "Action Account", "System Uptime", "Workflows" | Sort-Object "Management Server" | ConvertTo-HTML -fragment # Insert the Results for the Stats and State Changes into the Report $ReportOutput += "<br>" $ReportOutput += "<h2>Daily KPI</h2>" $ReportOutput += $AllStats | Select-Object "Open Alerts", "Groups", "Monitors", "Rules", "State Changes Yesterday" | ConvertTo-HTML -fragment # Retrieve and Insert the Results for the Management Packs that have been modified into the Report Write-Host "Checking for Management Packs that have been modified in the last 24 hours" $ReportOutput += "<br>" $ReportOutput += "<h2>Management Packs Modified in the Last 24 Hours</h2>" If (!($ManagementPacks | Where-Object {$_.LastModified -ge (Get-Date).addhours(-24)})) { $ReportOutput += "<p>No Management Packs have been updated in the last 24 hours</p>" } Else { $ReportOutput += $ManagementPacks | Where-Object {$_.LastModified -ge (Get-Date).addhours(-24)} | Select-Object Name, LastModified | ConvertTo-HTML -fragment } # Retrieve and Insert the Results for the Default Management Pack into the Report # This 'should be empty'...don't store stuff here! Write-Host "Checking for the Default Management Pack for Overrides" $ReportOutput += "<br>" $ReportOutput += "<h2>The Default Management Pack</h2>" # Excluding these 2 ID's as they are part of the default MP for DefaultUser and Language Code Overrides $excludedID = "5a67584f-6f63-99fc-0d7a-55587d47619d", "e358a914-c851-efaf-dda9-6ca5ef1b3eb7" $defaultMP = $ManagementPacks | Where-Object {$_.Name -match "Microsoft.SystemCenter.OperationsManager.DefaultUser"} ##Changed below line for compat with PowerShell 2.0 ##If (!($defaultMP.GetOverrides() | ? {$_.ID -notin $excludedID})) If (!($defaultMP.GetOverrides() | Where-Object {$excludedID -NotContains $_.ID})) { $ReportOutput += "<p>There are no Overrides being Stored in the Default Management Pack</p>" } Else { ##Changed below line for compat with PowerShell 2.0 #$foundOverride = Get-SCOMClassInstance -id ($defaultMP.GetOverrides() | ? {$_.ID -notin $excludedID -AND $_.ContextInstance -ne $guid} | select -expandproperty ContextInstance -ea SilentlyContinue) $foundOverride = Get-SCOMClassInstance -id ($defaultMP.GetOverrides() | Where-Object {$excludedID -NotContains $_.ID -AND $_.ContextInstance -ne $guid} | Select-Object -expandproperty ContextInstance -ea SilentlyContinue) $ReportOutput += "<p>Found overrides against the following targets: $foundOverride.Displayname</p>" ##PowerShell 2.0 Compat ##$ReportOutput += $($defaultMP.GetOverrides() | ? {$_.ID -notin $excludedID} | Select Name, Property, Value, LastModified, TimeAdded) | ConvertTo-HTML -fragment $ReportOutput += $($defaultMP.GetOverrides() | Where-Object {$excludedID -NotContains $_.ID} | Select-Object -Property Name, Property, Value, LastModified, TimeAdded) | ConvertTo-HTML -fragment } # Show all Agents that are in an Uninitialized State Write-Host "Checking for Uninitialized Agents" $ReportOutput += "<br>" $ReportOutput += "<h2>Uninitialized Agents</h2>" If (!($Agents | Where-Object -FilterScript {$_.HealthState -eq "Uninitialized"} | Select-Object -Property Name)) { $ReportOutput += "<p>No Agents are in the Uninitialized State</p>" } Else { $ReportOutput += $Agents | Where-Object -FilterScript {$_.HealthState -eq "Uninitialized"} | Select-Object -Property Name | ConvertTo-HTML -fragment } # Show a Summary of all Agents States $healthy = $uninitialized = $warning = $critical = 0 Write-Host "Checking Agent States" $ReportOutput += "<br>" $ReportOutput += "<h3>Agent Stats</h3>" switch ($Agents | Select-Object HealthState ) { {$_.HealthState -like "Success"} {$healthy++} {$_.HealthState -like "Uninitialized"} {$uninitialized++} {$_.HealthState -like "Warning"} {$warning++} {$_.HealthState -like "Error"} {$critical++} } $totalagents = ($healthy + $warning + $critical + $uninitialized) $AllAgents = @() $iAgent = New-Object psobject $iAgent | Add-Member noteproperty -Name "Agents Healthy" -Value $healthy $iAgent | Add-Member noteproperty -Name "Agents Warning" -Value $warning $iAgent | Add-Member noteproperty -Name "Agents Critical" -Value $critical $iAgent | Add-Member noteproperty -Name "Agents Uninitialized" -Value $uninitialized $iAgent | Add-Member noteproperty -Name "Total Agents" -Value $totalagents $AllAgents += $iAgent $ReportOutput += $AllAgents | Select-Object -Property "Agents Healthy", "Agents Warning", "Agents Critical", "Agents Uninitialized", "Total Agents" | ConvertTo-HTML -fragment # Agent Pending Management States Write-Host "Checking Agent Pending Management States" $ReportOutput += "<br>" $ReportOutput += "<h3>Agent Pending Management Summary</h3>" $pushInstall = $PushInstallFailed = $ManualApproval = $RepairAgent = $RepairFailed = $UpdateFailed = 0 $agentpending = Get-SCOMPendingManagement switch ($agentpending | Select-Object AgentPendingActionType ) { {$_.AgentPendingActionType -like "PushInstall"} {$pushInstall++} {$_.AgentPendingActionType -like "PushInstallFailed"} {$PushInstallFailed++} {$_.AgentPendingActionType -like "ManualApproval"} {$ManualApproval++} {$_.AgentPendingActionType -like "RepairAgent"} {$RepairAgent++} {$_.AgentPendingActionType -like "RepairFailed"} {$RepairFailed++} {$_.AgentPendingActionType -like "UpdateFailed"} {$UpdateFailed++} } $AgentsPending = @() $AgentSummary = New-Object psobject $AgentSummary | Add-Member noteproperty "Push Install" ($pushInstall) $AgentSummary | Add-Member noteproperty "Push Install Failed" ($PushInstallFailed) $AgentSummary | Add-Member noteproperty "Manual Approval" ($ManualApproval) $AgentSummary | Add-Member noteproperty "Repair Agent" ($RepairAgent) $AgentSummary | Add-Member noteproperty "Repair Failed" ($RepairFailed) $AgentSummary | Add-Member noteproperty "Update Failed" ($UpdateFailed) $AgentsPending += $AgentSummary # Produce a table of all Agent Pending Management States and add it to the Report $ReportOutput += $AgentsPending | Select-Object "Push Install", "Push Install Failed", "Manual Approval", "Repair Agent", "Repair Failed", "Update Failed" | ConvertTo-HTML -fragment $ReportOutput += "<br>" $ReportOutput += "<h2>Alerts</h2>" $AlertsAll = ($Alerts | Where-Object {$_.ResolutionState -ne 255}).Count $AlertsWarning = ($Alerts | Where-Object {$_.Severity -eq "Warning" -AND $_.ResolutionState -ne 255}).Count $AlertsError = ($Alerts | Where-Object {$_.Severity -eq "Error" -AND $_.ResolutionState -ne 255}).Count $AlertsInformation = ($Alerts | Where-Object {$_.Severity -eq "Information" -AND $_.ResolutionState -ne 255}).Count $Alerts24hours = ($Alerts | Where-Object {$_.TimeRaised -ge (Get-Date).addhours(-24) -AND $_.ResolutionState -ne 255}).count $AllAlerts = @() $AlertSeverity = New-Object psobject $AlertSeverity | Add-Member noteproperty "Warning" ($AlertsWarning) $AlertSeverity | Add-Member noteproperty "Error" ($AlertsError) $AlertSeverity | Add-Member noteproperty "Information" ($AlertsInformation) $AlertSeverity | Add-Member noteproperty "Last 24 Hours" ($Alerts24hours) $AlertSeverity | Add-Member noteproperty "Total Open Alerts" ($AlertsAll) $AllAlerts += $AlertSeverity # Produce a table of all alert counts for warning, error, information, Last 24 hours and Total Alerts and add it to the Report $ReportOutput += $AllAlerts | Select-Object "Warning", "Error", "Information", "Last 24 Hours", "Total Open Alerts" | ConvertTo-HTML -fragment <# # Check if the Action Account is a Local Administrator on Each Management Server # This will only work if the account is a member of the Local Administrators Group directly. # If it has access by Group Membership, you can look for that Group instead # $ActionAccount = "YourGrouptoCheck" # Then replace all 5 occurrences below of $ManagementServer.ActionAccountIdentity with $ActionAccount Write-Host "Checking if the Action Account is a member of the Local Administrators Group for each Management Server" $ReportOutput += "<br>" $ReportOutput += "<h2>SCOM Action Account</h2>" ForEach ($ms in $ManagementServers.DisplayName | sort DisplayName) { $admins = @() $group =[ADSI]"WinNT://$ms/Administrators" $members = @($group.psbase.Invoke("Members")) $members | foreach { $obj = new-object psobject -Property @{ Server = $Server Admin = $_.GetType().InvokeMember("Name", 'GetProperty', $null, $_, $null) } $admins += $obj } If ($admins.admin -match $ManagementServer.ActionAccountIdentity) { # Write-Host "The user $($ManagementServer.ActionAccountIdentity) is a Local Administrator on $ms" $ReportOutput += "<p>The user $($ManagementServer.ActionAccountIdentity) is a Local Administrator on $ms</p>" } Else { # Write-Host "The user $($ManagementServer.ActionAccountIdentity) is NOT a Local Administrator on $ms" $ReportOutput += "<p><span style=`"color: `#CC0000;`">The user $($ManagementServer.ActionAccountIdentity) is NOT a Local Administrator on $ms</span></p>" } } #> # Objects in Maintenance Mode #SQL Query Servers in MMode $ServersInMM =@" select DisplayName from dbo.MaintenanceMode mm join dbo.BaseManagedEntity bm on mm.BaseManagedEntityId = bm.BaseManagedEntityId where Path is NULL and IsInMaintenanceMode = 1 "@ $OpsDBSIMM = Run-OpDBSQLQuery $ServersInMM $ReportOutput += "<br>" $ReportOutput += "<h2>Servers in Maintenance Mode</h2>" If (!($OpsDBSIMM)) { $ReportOutput += "<p>There are no objects in Maintenance Mode</p>" } Else { $ReportOutput += $OpsDBSIMM | Select-Object DisplayName | ConvertTo-HTML -fragment } <# # Global Grooming Settings # Simple comparisons against the values set at the beginning of this script # I use this to see if anyone has changed the settings. So set the values at the top of this script to match the values that your environment 'should' be set to. $ReportOutput += "<br>" $ReportOutput += "<h2>SCOM Global Settings</h2>" $SCOMDatabaseGroomingSettings = Get-SCOMDatabaseGroomingSetting If ($SCOMDatabaseGroomingSettings.AlertDaysToKeep -ne $AlertDaysToKeep) {$ReportOutput += "<p><span style=`"color: `#CC0000;`">Alert Days to Keep has been changed! Reset back to $AlertDaysToKeep</span></p>"} Else {$ReportOutput += "<p>Alert Days is correctly set to $AlertDaysToKeep</p>"} If ($SCOMDatabaseGroomingSettings.AvailabilityHistoryDaysToKeep -ne $AvailabilityHistoryDaysToKeep) {$ReportOutput += "<p><span style=`"color: `#CC0000;`">Availability History Days has been changed! Reset back to $AvailabilityHistoryDaysToKeep</span></p>"} Else {$ReportOutput += "<p>Availability History Days is correctly set to $AvailabilityHistoryDaysToKeep</p>"} If ($SCOMDatabaseGroomingSettings.EventDaysToKeep -ne $EventDaysToKeep) {$ReportOutput += "<p><span style=`"color: `#CC0000;`">Event Days has been changed! Reset back to $EventDaysToKeep</span></p>"} Else {$ReportOutput += "<p>Event Days is correctly set to $EventDaysToKeep</p>"} If ($SCOMDatabaseGroomingSettings.JobStatusDaysToKeep -ne $JobStatusDaysToKeep) {$ReportOutput += "<p><span style=`"color: `#CC0000;`">Job Days (Task History) has been changed! Reset back to $JobStatusDaysToKeep</span></p>"} Else {$ReportOutput += "<p>Job Days (Task History) is correctly set to $JobStatusDaysToKeep</p>"} If ($SCOMDatabaseGroomingSettings.MaintenanceModeHistoryDaysToKeep -ne $MaintenanceModeHistoryDaysToKeep) {$ReportOutput += "<p><span style=`"color: `#CC0000;`">Maintenance Mode History has been changed! Reset back to $MaintenanceModeHistoryDaysToKeep</span></p>"} Else {$ReportOutput += "<p>Maintenance Mode History is correctly set to $MaintenanceModeHistoryDaysToKeep</p>"} If ($SCOMDatabaseGroomingSettings.MonitoringJobDaysToKeep -ne $MonitoringJobDaysToKeep) {$ReportOutput += "<p><span style=`"color: `#CC0000;`">Monitoring Job Data has been changed! Reset back to $MonitoringJobDaysToKeep</span></p>"} Else {$ReportOutput += "<p>Monitoring Job Data is correctly set to $MonitoringJobDaysToKeep</p>"} If ($SCOMDatabaseGroomingSettings.PerformanceDataDaysToKeep -ne $PerformanceDataDaysToKeep) {$ReportOutput += "<p><span style=`"color: `#CC0000;`">Performance Data has been changed! Reset back to $PerformanceDataDaysToKeep</span></p>"} Else {$ReportOutput += "<p>Performance Data is correctly set to $PerformanceDataDaysToKeep</p>"} If ($SCOMDatabaseGroomingSettings.StateChangeEventDaysToKeep -ne $StateChangeEventDaysToKeep) {$ReportOutput += "<p><span style=`"color: `#CC0000;`">State Change Data has been changed! Reset back to $StateChangeEventDaysToKeep</span></p>"} Else {$ReportOutput += "<p>State Change Data is correctly set to $StateChangeEventDaysToKeep</p>"} # SCOM Agent Heartbeat Settings $HeartBeatSetting = Get-SCOMHeartbeatSetting If ($HeartBeatSetting.AgentHeartbeatInterval -ne 180 -AND $HeartBeatSetting.MissingHeartbeatThreshold -ne 3) {$ReportOutput += "<p><span style=`"color: `#CC0000;`">The HeartBeat Settings have been changed! Reset back to $AgentHeartbeatInterval and $MissingHeartbeatThreshold</span></p>"} Else {$ReportOutput += "<p>The HeartBeat Settings are correctly set to Interval: $AgentHeartbeatInterval and Missing Threshold: $MissingHeartbeatThreshold</p>"} #> # How long did this script take to run? $EndTime=Get-Date $TotalRunTime=$EndTime-$StartTime # Add the time to the Report $ReportOutput += "<br>" $ReportOutput += "<p>Total Script Run Time: $($TotalRunTime.hours) hrs $($TotalRunTime.minutes) min $($TotalRunTime.seconds) sec</p>" # Close the Body of the Report $ReportOutput += "</body>" Write-Host "Saving HTML Report to $ReportPath" # Save the Final Report to a File ConvertTo-HTML -head $Head -body "$ReportOutput" | Out-File $ReportPath Invoke-Item "$ReportPath" <# # Send Final Report by email... Write-Host "Emailing Report" $SMTPServer ="your.smtpserver.com" $Body = ConvertTo-HTML -head $Head -body "$ReportOutput" $SmtpClient = New-Object Net.Mail.SmtpClient($smtpServer); $mailmessage = New-Object system.net.mail.mailmessage $mailmessage.from = "sender@company.com" $mailmessage.To.add("recipient@company.com") # Want more recipient's? Just add a new line $mailmessage.To.add("anotherrecipient@company.com") $mailmessage.Subject = "Tims SCOM Healthcheck Report" $MailMessage.IsBodyHtml = $true $mailmessage.Body = $Body $smtpclient.Send($mailmessage) #> <# # Insert the Results for the Reporting Server URL into the Report $ReportOutput += "<h2>Reporting Server</h2>" $ReportOutput += "<p>Reporting Server URL : <a href=`"$ReportingURL/`">$ReportingURL</a></p>" $ReportOutput += "<p>The Reporting Server URL Status : $ReportingServerStatus</p>" # Insert the Results for the Web Console URL into the Report $ReportOutput += "<h2>Web Console Servers</h2>" $ReportOutput += "<p>Web Console URL : <a href=`"$WebConsoleURL/`">$WebConsoleURL</a></p>" $ReportOutput += "<p>The Web Console URL Status : $WebConsoleStatus</p>" #> # A bit of cleanup #Clear-Variable Agents, Alerts, Groups, ManagementGroup, ManagementPacks, ManagementServer, Monitors, Rules, ReportOutput, StartTime, EndTime, TotalRunTime, SCOMDatabaseGroomingSettings } ####################################################################### <# .Synopsis Will unseal MP and MPB files along with any additional resources contained therein and place into version-coded folders. .EXAMPLE PS C:\> Unseal-SCOMMP -inDir C:\MyMPs -outDir C:\MyMPs\UnsealedMPs .EXAMPLE PS C:\> Unseal-SCOMMP -inDir 'C:\Program Files (x86)\System Center Management Packs' -outDir 'C:\Temp\Unsealtest_OUT' .EXAMPLE PS C:\> Unseal-SCOMMP -inDir 'C:\en_system_center_2012_r2_operations_manager_x86_and_x64_dvd_2920299\ManagementPacks' -outDir 'C:\Temp\Unsealtest_OUT' .INPUTS Strings (directory paths) .OUTPUTS XML files (+ any resource files: .dll, .bmp, .ico etc.) .NOTES Author (Revised by): Tyson Paul (I modified this heavily but the original that I used as a framework was not written by me.) Original Author: I can't seem to find where I originally got this script from. Jon Almquist maybe? Blog: https://blogs.msdn.microsoft.com/tysonpaul/2018/07/18/scomhelper-powershell-module-a-scom-admins-best-friend/ Version: 1.0 Date: 2018.05.25 .LINK Get-SCOMMPFileInfo Get-SCOMClassInfo #> Function Unseal-SCOMMP { Param( $inDir="C:\Temp\Sealed", $outDir="C:\Temp\AllUnsealed" ) [Reflection.Assembly]::LoadWithPartialName("Microsoft.EnterpriseManagement.Core") [Reflection.Assembly]::LoadWithPartialName("Microsoft.EnterpriseManagement.Packaging") $mpStore = new-Object Microsoft.EnterpriseManagement.Configuration.IO.ManagementPackFileStore($inDir) $mps = get-childitem $inDir *.mp -Recurse If ($null -ne $mps) { ForEach ($file in $mps) { $MPFilePath = $file.FullName $mp = new-object Microsoft.EnterpriseManagement.Configuration.ManagementPack($MPFilePath) $newOutDirPath = ($outDir + "\"+ $file + "($($mp.Version.ToString()))") New-Item -Path $newOutDirPath -ItemType Directory -ErrorAction SilentlyContinue Write-Host $file $mpWriter = new-object Microsoft.EnterpriseManagement.Configuration.IO.ManagementPackXmlWriter( $newOutDirPath ) $mpWriter.WriteManagementPack($mp) } } #now dump MPB files $mps = Get-ChildItem $inDir *.mpb -Recurse $mpbReader = [Microsoft.EnterpriseManagement.Packaging.ManagementPackBundleFactory]::CreateBundleReader() If ($null -ne $mps) { ForEach ($mp in $mps) { $bundle = $mpbReader.Read($mp.FullName, $mpStore) $newOutDirPath = ($outDir + "\"+ $mp + "($($bundle.ManagementPacks.Version.ToString()))") New-Item -Path $newOutDirPath -ItemType Directory -ErrorAction SilentlyContinue Write-Host $mp $mpWriter = New-Object Microsoft.EnterpriseManagement.Configuration.IO.ManagementPackXmlWriter($newOutDirPath) ForEach($bundleItem in $bundle.ManagementPacks) { #write the xml $mpWriter.WriteManagementPack($bundleItem) $streams = $bundle.GetStreams($bundleItem) ForEach($stream in $streams.Keys) { $mpElement = $bundleItem.FindManagementPackElementByName($stream) $fileName = $mpElement.FileName If ($null -eq $fileName) { $outpath = (Join-Path $newOutDirPath ('.' + $stream + '.bin')) } Else { If ($fileName.IndexOf('\') -gt 0) { #$outpath = Split-Path $fileName -Leaf $outpath = (Join-Path $newOutDirPath (Split-Path $fileName -Leaf)) } Else { $outpath = (Join-Path $newOutDirPath $fileName) } } Write-Host "`t$outpath" $fs = [system.io.file]::Create($outpath) $streams[$stream].WriteTo($fs) $fs.Close() } } } } }#End Function ####################################################################### <# .Synopsis List version and file path information for all management pack files (*.mp, *.mpb, *.xml). .DESCRIPTION Will display MP file and version information. .EXAMPLE Get-SCOMMPFileInfo -inDir 'C:\Program Files (x86)\System Center Management Packs' -Verbose .EXAMPLE Get-SCOMMPFileInfo -inDir 'C:\Management Packs' -GridView .INPUTS System.String, Switch .OUTPUTS Custom object .NOTES Author: Tyson Paul Blog: https://blogs.msdn.microsoft.com/tysonpaul/2018/07/18/scomhelper-powershell-module-a-scom-admins-best-friend/ Date: 2017.06.01 Version: v1.0 History: 2018.05.30: Added GridView option as well as some other cleanup. .FUNCTIONALITY Useful for identifying specific versions of management packs in your local archive. .LINK Get-SCOMClassInfo New-SCOMClassGraph Unseal-SCOMMP #> Function Get-SCOMMPFileInfo { [CmdletBinding(DefaultParameterSetName='Parameter Set 1', SupportsShouldProcess=$true, PositionalBinding=$false)] Param ( [Parameter(Mandatory=$true, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=0, ParameterSetName='Parameter Set 1')] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [Alias("Path")] [string]$inDir, [switch]$GridView=$true, [switch]$passthru=$true ) $MPs = @() $arrMaster = @() $MPFiles = Get-ChildItem -Path $inDir -Recurse -Include *.mp,*.mpb,*.xml ForEach ($File in $MPFiles.FullName) { Switch ($File) { {$_ -Match '\.mp$|\.xml$'} { $MP = Get-SCManagementPack -ManagementPackFile $File break; } {$_ -Match '\.mpb$'} { $MP = Get-SCManagementPack -BundleFile $File break; } }#end Switch $obj = New-Object PSCustomObject $obj | Add-Member -Type NoteProperty -Name Name -Value $MP.Name $obj | Add-Member -Type NoteProperty -Name DisplayName -Value $MP.DisplayName $obj | Add-Member -Type NoteProperty -Name Version -Value $MP.Version $obj | Add-Member -Type NoteProperty -Name Description -Value $MP.Description $obj | Add-Member -Type NoteProperty -Name FullName -Value $File $obj | Add-Member -Type NoteProperty -Name ID -Value $MP.ID.GUID Write-Verbose "$File" $arrMaster += $obj }#end ForEach If ($GridView) { $arrMaster | Sort-Object Name, Version | Out-GridView -PassThru:$passthru } Else{ $arrMaster | Sort-Object Name, Version } }#End Function ####################################################################### <# .Synopsis Will display statistics about the total number of SCOM class instances and the management packs from which they originate. .EXAMPLE Get-SCOMClassInfo -Top 10 .EXAMPLE Get-SCOMClassInfo -Top 30 -MPStats .EXAMPLE Get-SCOMClassInfo -MPStats -ShowGraph .INPUTS None .OUTPUTS Custom Object: {InstanceCount,ClassName,MPName}, {MPName,TotalInstances} .NOTES Author: Tyson Paul Blog: https://blogs.msdn.microsoft.com/tysonpaul/2018/07/18/scomhelper-powershell-module-a-scom-admins-best-friend/ Version: 1.0 Original Date: 2018.05.25 .FUNCTIONALITY Statistical reporting. .LINK Get-SCOMMPFileInfo New-SCOMClassGraph Unseal-SCOMMP #> Function Get-SCOMClassInfo { Param ( #Will return top N results by InstanceCount [int]$Top=30, [switch]$MPStats, [switch]$ShowGraph ) # Get all MPs that were not installed OotB. $MPs = Get-SCManagementPack $MPs = $MPs | Sort-Object TimeCreated | Select-Object -Last ($MPs.Count - 98) $Classes = Get-SCClass -ManagementPack $MPs | Where-Object {$_.Singleton -ne $true} #$arrClasses = @() ForEach ($Class in $Classes) { $obj = New-Object PSCustomObject $Instances = $Class | Get-SCOMClassInstance $obj | Add-Member -MemberType NoteProperty -Name Instances -Value $Instances.Count $obj | Add-Member -MemberType NoteProperty -Name ClassName -Value $Class.Name $obj | Add-Member -MemberType NoteProperty -Name MPName -Value $Class.ManagementPackName [System.Object[]]$arrClasses += $obj } If ($MPStats){ $Results = (($arrClasses | Group-Object -Property MPName ) | Select-Object @{Name='MPName';E={$_.Name} },@{Name='TotalInstances';E={($_.Group.Instances | Measure-Object -Sum).Sum}} |Sort-Object TotalInstances -Descending | Select-Object -First $Top) If ([bool]$ShowGraph) { $Results | Out-ConsoleGraph -Property TotalInstances -Columns TotalInstances,MPName } Else { Return $Results } } Else{ $Results = ($arrClasses | Sort-Object Instances -Descending | Select-Object -First $Top) If ([bool]$ShowGraph) { $Results | Out-ConsoleGraph -Property Instances -Columns Instances,ClassName } Else { Return $Results } } } #End Function ####################################################################### <# .Synopsis Will disable all event collection rules for the provided sealed management pack(s). .DESCRIPTION Feed this function one or more management packs and it will create a corresponding unsealed pack for each in which it will store overrides which disable any/all event collection rules that are currently enabled. The resulting overrides will target the workflow class target. .EXAMPLE PS C:\> # PS C:\> # Create an array of MP names PS C:\> $names=@" Microsoft.SystemCenter.2007 Microsoft.Windows.Server.2012.Monitoring Microsoft.Windows.Server.2012.R2.Monitoring Microsoft.Windows.Server.AD.2012.R2.Monitoring Microsoft.SystemCenter.Apm.Infrastructure.Monitoring Microsoft.SystemCenter.ACS.Internal Microsoft.Windows.Server.DNS "@ -split "\n" -replace "`r","" PS C:\> # Retrieve array of management packs. Pipe MPs to function. PS C:\> Get-SCOMManagementPack -Name $names | Disable-SCOMAllEventRules .EXAMPLE PS C:\> Disable-SCOMAllEventRules -ManagementPack (Get-SCOMManagementPack -Name 'Microsoft.Windows.Server.2012.Monitoring') .EXAMPLE PS C:\> Disable-SCOMAllEventRules -ManagementPack (Get-SCOMManagementPack -Name @('Microsoft.SQLServer.2012.Mirroring.Discovery','Microsoft.SQLServer.2008.Discovery') ) .INPUTS Operations Manager Management Pack object .OUTPUTS Status messages (strings) .NOTES Author: Tyson Paul Blog: https://blogs.msdn.microsoft.com/tysonpaul/2018/07/18/scomhelper-powershell-module-a-scom-admins-best-friend/ Date: 2018.06.26 Version: 1.1 #> Function Disable-SCOMAllEventRules { Param ( [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=0, ParameterSetName='Parameter Set 2')] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [ValidateScript({$_.Sealed -eq $true})] [Alias('ManagementPack')] [Microsoft.EnterpriseManagement.Configuration.ManagementPack[]]$MP, [string]$MgmtServer = 'Localhost', [string]$LogFile ) #region Begin Begin{ #--------------------------------------------------------------- <# This function will help to organize properties of a custom object in the preferred order #> Function Combine-Objects { Param ( [Parameter(mandatory=$true)]$Obj1, [Parameter(mandatory=$true)]$Obj2 ) $hash = @{} ForEach ( $Property in $Obj1.psobject.Properties) { $hash[$Property.Name] = $Property.value } ForEach ( $Property in $Obj2.psobject.Properties) { $hash[$Property.Name] = $Property.value } $Object3 = New-Object -TypeName PSObject -Prop $hash #$Object3 = [Pscustomobject]$arguments Return $Object3 } #--------------------------------------------------------------- <# This will create the necessary unsealed MP in which to store overrides #> Function Verify-OverrideMP { #If override pack does not already exist, create it If (-NOT [bool](Get-SCManagementPack -Name $OverridesMP['Name'])) { Write-Host "Override pack does not exist. Name: $($OverridesMP['Name'])" Write-Host "Attempt to create unsealed MP: $($OverridesMP['Name'])" $MPCreated = New-OMManagementPack -SDK $MgmtServer -Name $OverridesMP['Name'] -DisplayName $OverridesMP['DisplayName'] -Description "Bulk disabling of Event Collections." If ($MPCreated) { Write-Host "Success! New ManagementPack:" -F Green Write-Host "DisplayName: " -NoNewline Write-Host "$($OverridesMP['DisplayName'])" -F Cyan Write-Host "Name: $($OverridesMP['Name'])" Return $true } Else { Write-Host "Failed to create override pack with name: $($OverridesMP['Name']). Aborting." Return $false } } Else { Return $true } } #--------------------------------------------------------------- $USERNAME = whoami $MPs = $MP # make sure a connection to an SDK exists If (-NOT [bool](Get-SCManagementGroupConnection) ) { $mg = New-SCManagementGroupConnection -ComputerName $MgmtServer } Import-Module OpsMgrExtended -ErrorAction SilentlyContinue # Make sure required module is available/installed If (-Not [bool](Get-Module -Name 'OpsMgrExtended' -ErrorAction SilentlyContinue )) { Write-Error "Required module 'OpsMgrExtended' does not exist. Please install module." $choice = Read-Host "Install module: OpsMgrExtended (Y/N) ?" While ($choice -notmatch '[y]|[n]'){ $choice = Read-Host "Y/N?" } Switch ($choice){ 'y' { # Install module from the Powershell Gallery Write-Host "Find-Module OpsMgrExtended | Install-Module -Verbose" Find-Module OpsMgrExtended | Install-Module -Verbose If ($?){ Write-Host "Module installed!" Import-Module OpsMgrExtended } Else { Write-Error "Problem installing module. You may manually download from this location: 'https://github.com/tyconsulting/OpsMgrExtended-PS-Module'. Additional information here: 'http://blog.tyang.org/2015/06/24/automating-opsmgr-part-1-introducing-opsmgrextended-powershell-sma-module/ , https://www.powershellgallery.com/packages/OpsMgrExtended/1.3.0'. For information on how to install a PowerShell module, see this article: 'https://msdn.microsoft.com/en-us/library/dd878350(v=vs.85).aspx'.`nExiting.`n"; Break } } 'n' { Write-Host "Module will not be installed.`nExiting.`n" Break } } } # If Logfile path not provided, set path to user temp directory. If (-NOT $LogFile ){ $LogFile = Join-Path $env:TEMP "Disable-SCOMAllEventRules.csv" } If (-NOT (Test-Path $LogFile -PathType Leaf)){ New-Item -Path $LogFile -ItemType File -Force | Out-Null } }#endregion Begin #region Process{ ForEach ($MP in $MPs) { # These are the event collection write actions to focus on $WA_Collections = "Microsoft.SystemCenter.CollectEvent","Microsoft.SystemCenter.DataWarehouse.PublishEventData" Write-Host "Getting Event Collection rules from MP: $($MP.Name). This may take a minute ..." # Get any event collection rules from the MP $EventRules = Get-SCOMRule -ManagementPack $MP | Where-Object { ([System.Object[]]$_.WriteActionCollection.TypeID.Identifier.Path -contains $WA_Collections[0]) ` -OR ` ([System.Object[]]$_.WriteActionCollection.TypeID.Identifier.Path -contains $WA_Collections[1]) ` } Write-Host "$($EventRules.Count) Event Collection rules found." $OverridesMP = @{} $OverridesMP['Name'] = ($MP.Name.ToString())+".DisabledEventCollection.OVERRIDES" $OverridesMP['DisplayName'] = ($MP.DisplayName.ToString())+" DisabledEventCollection (OVERRIDES)" [int]$i=1 ForEach ($thisRule in $EventRules) { Write-Progress -Activity "Creating Overrides" -Status "Evaluating rule status..." -PercentComplete ($i / $EventRules.Count *100) [bool]$Disabled = $false $Overrides = Get-SCOMOverride -Rule $thisRule #Determine if rule is 'Disabled" by default or already disabled via override. If so, skip. ForEach ($thisOverride in $Overrides) { If ( ($thisRule.Enabled -eq 'false') -OR ( ($thisOverride.Property -eq 'Enabled') -AND ($thisOverride.Value -eq 'false') )) { $Disabled=$true } } # If the rule is a valid candidate to be disabled, proceed to create override. If (-NOT $Disabled) { $ORName = "$($OverridesMP['Name']).OverrideForRule"+"$($thisRule.Name.Replace('.',''))ForContext$($thisRule.Target.Identifier.Path.Replace('.',''))" If ( $ORName.Length -gt 256) { $ORName = $ORName.Substring(0,255) } #Create a hashtable to use for splat $Params = @{ sDK = $MgmtServer MPName = $OverridesMP['Name'] OverrideType = 'RulePropertyOverride' OverrideName = $ORName OverrideWorkflow = $thisRule.Name Target = [string]$thisRule.Target.Identifier.Path OverrideProperty = 'Enabled' OverrideValue = 'False' Enforced = $false IncreaseMPVersion=$true } # Create timestamp for logging later on $now = (Get-Date -F yyyyMMdd-hhmmss) # Verify the necessary unsealed MP exists If (-NOT (Verify-OverrideMP) ) { Return } # Splat. Create the override $Result = New-OMPropertyOverride @Params # Create custom object. This will make it easy to pipe out data to a log file (csv) $object = New-Object PSCustomObject $object | Add-Member -MemberType NoteProperty -Name '#' -Value $i $object | Add-Member -MemberType NoteProperty -Name 'Date' -Value $now If ($Result) { $object | Add-Member -MemberType NoteProperty -Name 'Status' -Value 'SUCCESS' } Else { $object | Add-Member -MemberType NoteProperty -Name 'Status' -Value 'FAILED' } $object | Add-Member -MemberType NoteProperty -Name 'User' -Value $USERNAME $objParams = New-Object -TypeName PSObject -Prop $Params $object = Combine-Objects -Obj1 $object -Obj2 $objParams $object | Select-Object -Property "Date","Status","User","SDK","OverrideName","OverrideWorkflow","OverrideType","OverrideProperty","OverrideValue","Target","MPName","IncreaseMPVersion","Enforced" | Export-Csv -Path $LogFile -NoTypeInformation -Append $object | Select-Object -Property "#",@{N='Disabled';E={$_.Status}},OverrideWorkflow | Format-Table -AutoSize Remove-Variable -Name Params,Result,object,objParams $i++ } } } }#endregion Process End { Write-Host "`nLogfile: " -NoNewline Write-Host "$LogFile`n" -F Yellow } }#end Function ####################################################################### <# .SYNOPSIS Will ping all entries in your HOSTS file (C:\WINDOWS\System32\drivers\etc\hosts) very quickly. .EXAMPLE PS C:\> Ping-AllHosts -Count 30 Will ping all hosts a total of 30 times, pausing for the default 10 seconds inbetween iterations. .EXAMPLE PS C:\> Ping-AllHosts -t -DelayMS 30000 Will continue to ping hosts waiting 30 seconds inbetween iterations. .EXAMPLE PS C:\> Ping-AllHosts -Count 10 Will ping all hosts a total of 10 times. .NOTES Name: Ping-AllHosts Author: Tyson Paul Blog: https://blogs.msdn.microsoft.com/tysonpaul/2018/07/18/scomhelper-powershell-module-a-scom-admins-best-friend/ Date: 2018.04.30 History: v1.0 #> Function Ping-AllHosts { Param ( [int]$Count=1, [int]$DelayMS=10000, [switch]$t ) $HOSTSfile = (Join-Path $env:SystemRoot '\System32\drivers\etc\hosts') If (-NOT (Test-Path $HOSTSfile -PathType Leaf) ) { Write-Error "Cannot find 'hosts' file at: $HOSTSFile . Exiting." Return } If ($t) { $Count = 9999999999999 Write-Host "`$Count = $Count" -ForegroundColor Yellow } [long]$i=1 While ($i -le $Count) { Write-Host "Attempt: $i. Remaining attempts: $($Count - $i)" -F Green # Parse the Hosts file, extract only the lines that do NOT begin with hash (#). Discard any leading tabs or spaces on all lines. Ignore blank lines $lines = (( (Get-Content $HOSTSfile).Replace("`t",'#').Replace(" ",'#').Replace("##",'#') | Where-Object {$_ -match '^[^#]'})) | Where-Object {$_.Length -gt 1} $hash = @{} # Assign names and IPs to hash table $lines | ForEach-Object { $hash[$_.Split('#')[1]] = $_.Split('#')[0] } If (-Not [bool]$lines) { Write-Host "No valid hosts found in file. Exiting." Return } # Perform simultaneous address connection testing, as job. $job = Test-Connection -ComputerName ($hash.Keys | Out-String -stream) -Count 1 -AsJob Do{ Write-Host 'Waiting for ping test to complete...' -F Yellow Start-Sleep 3 }While($job.JobStateInfo.State -eq "Running") Receive-Job $job | Select-Object @{N="Source"; E={$_.PSComputerName}}, @{N="Target"; E={$_.Address}}, IPV4Address, @{N='Time(ms)';E={($_.'ResponseTime')} }, @{N='Alive?';E={([bool]$_.'ResponseTime')} } | Sort-Object Target | Format-Table -AutoSize If ($i -eq $Count) {Return} $i++ Write-Host "Sleeping $($DelayMS / 1000) Seconds before next ping test..." Start-Sleep -Milliseconds $DelayMS } }#end Function ####################################################################### $Functions = @' Clear-SCOMCache Disable-SCOMAllEventRules Export-EffectiveMonitoringConfiguration Export-SCOMEventsToCSV Export-SCOMKnowledge Get-SCOMAlertKnowledge Get-SCOMClassInfo Get-SCOMHealthCheckOpsConfig Get-SCOMMPFileInfo Get-SCOMRunAsAccountName Get-StringHash New-SCOMClassGraph Ping-AllHosts Remove-SCOMObsoleteReferenceFromMPFile Set-SCOMMPAliases Show-SCOMModules Test-Port Unseal-SCOMMP '@ -split "\n" -replace "`r","" | Export-ModuleMember |