SCOMHelper.psm1
<#
2022.06.20 - Added new function: Convert-MAML2HTML 2022.06.10 - Added Get-SCOMRunningWorkflows. Improved Export-SCOMEffectiveMonitoringConfigurationReport function to eliminate empty Path issue. 2020.11.19 - Update-SCOMComputerGroup: improved a little bit. Update-OMGroupDiscovery2: added default Mgmtservername value and failure logging. 2020.08.18 - Added new function: Update-SCOMComputerGroup. Fixed small issue with New-SCOMClassGraph. 2020.08.11 - Added a Private.ps1 file to include common functions. 2020.07.22 - Remove 'Import-Module OperationsManager' #> # Load private functions . (Join-Path $PSScriptRoot Private.ps1) ####################################################################### ####################################################################### ####################################################################### ####################################################################### <# .Synopsis Will clear SCOM agent cache on local machine .EXAMPLE Clear-SCOMCache -HealthServiceStatePath 'D:\Program Files\Microsoft System Center 2012 R2\Operations Manager\Server\Health Service State' .EXAMPLE Clear-SCOMCache -TimeoutSeconds 60 .INPUTS None .OUTPUTS Boolean [true|false] .NOTES Author: Tyson Paul Blog: https://monitoringguys.com/ Version History: 2020.07.21 - Improved error handling a bit. Added cmdletbinding. Changed outputtype to Bool. 2020.02.27 - Cleaned up code a bit. Improved logic. Removed WaitSeconds parameter as it was not effective. 2018.05.25 - Cleaned up. Revised a few things. #> Function Clear-SCOMCache { [CmdletBinding(DefaultParameterSetName='Parameter Set 1', SupportsShouldProcess=$false, PositionalBinding=$false, HelpUri = 'http://www.microsoft.com/', ConfirmImpact='Medium')] [OutputType([BOOL])] Param( # How many seconds to wait before aborting [int]$TimeoutSeconds = 30, # If the registry does not contain the correct path you can specify a path here. [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-Verbose "Stopping service: $ServiceName" $StartTime = Get-Date Try { Stop-Service -Name $ServiceName -Verbose -Force -PassThru While ( ((Get-Service -Name $ServiceName).Status -ne "Stopped") -AND ((Get-Date) -le ($StartTime.AddSeconds($TimeoutSeconds) )) ) { Write-Verbose "Waiting for $ServiceName to stop..." Start-Sleep -Seconds 5 } } Catch { Write-Warning "Problem while stopping service: $($_). Continuing..." } If ((Get-Service -Name $ServiceName).Status -ne "Stopped") { Write-Warning "Failed to stop service: $ServiceName in the time allowed! Will attempt to leave HealthService in 'Started' state." Start-Service $ServiceName -Verbose Return $false } Else { Write-Verbose "Attempting to remove cache folder: $HealthServiceStatePath" Try { Get-Item $HealthServiceStatePath | Remove-Item -Recurse -Force -Verbose -ErrorAction Stop Start-Sleep -Seconds 1 } Catch { Write-Warning "Error while deleting cache files. $($_)" } If (Test-Path -PathType Container $HealthServiceStatePath) { Write-Warning "Failed to remove folder: [$($HealthServiceStatePath)] !" } Else { [bool]$folderRemoved = $true Write-Verbose "Health Service State folder removed succesfully: [$($HealthServiceStatePath)]." } $StartTime = Get-Date Try { Start-Service -Name $ServiceName -Verbose While ( ((Get-Service -Name $ServiceName).Status -ne "Running") -AND ((Get-Date) -le ($StartTime.AddSeconds($TimeoutSeconds) )) ) { Start-Sleep -Seconds 3 } If ((Get-Service -Name $ServiceName).Status -ne "Running") { Write-Warning "Failed to start service: $ServiceName in the time allowed! Verify service manually." } Else { If ( (Test-Path -PathType Container $HealthServiceStatePath) -AND ([bool]$folderRemoved)){ Write-Verbose "Cache folder auto-created: $HealthServiceStatePath !" Write-Verbose "SCOM agent Cache has been cleared." Return $true } } } Catch { Throw Return $false } If (-NOT [bool]$folderRemoved){ Write-Warning "SCOM cache was not fully cleared; not all files/folders were deleted. For best results, try again or reboot and try again. Alternatively you could try clearing cache manually." Return $false } #default scenario, assumed something didn't go quite right Return $false } } #End Function ####################################################################### <# .Synopsis An easy way to compare individual characters of two strings. .EXAMPLE Compare-String -String1 "Captain Jack will get you by tonight" -String2 "Çaptain JAck" # Example Output Index String1/ASCII String2/ASCII Compare Algorithm String1_Hash String2_Hash ----- ------------- ------------- ------- --------- ------------ ------------ 0 C:67 Ç:199 Different SHA1 32096c2e0eff33d844ee6d675407ace18289357d 1d866b5fef7cdae2d4deba53c7ae4bdbf76ca671 1 a:97 a:97 Same SHA1 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 2 p:112 p:112 Same SHA1 516b9783fca517eecbd1d064da2d165310b19759 516b9783fca517eecbd1d064da2d165310b19759 3 t:116 t:116 Same SHA1 8efd86fb78a56a5145ed7739dcb00c78581c5375 8efd86fb78a56a5145ed7739dcb00c78581c5375 4 a:97 a:97 Same SHA1 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 5 i:105 i:105 Same SHA1 042dc4512fa3d391c5170cf3aa61e6a638f84342 042dc4512fa3d391c5170cf3aa61e6a638f84342 6 n:110 n:110 Same SHA1 d1854cae891ec7b29161ccaf79a24b00c274bdaa d1854cae891ec7b29161ccaf79a24b00c274bdaa 7 :32 :32 Same SHA1 b858cb282617fb0956d960215c8e84d1ccf909c6 b858cb282617fb0956d960215c8e84d1ccf909c6 8 J:74 J:74 Same SHA1 58668e7669fd564d99db5d581fcdb6a5618440b5 58668e7669fd564d99db5d581fcdb6a5618440b5 9 a:97 A:65 Different SHA1 86f7e437faa5a7fce15d1ddcb9eaeaea377667b8 6dcd4ce23d88e2ee9568ba546c007c63d9131c1b 10 c:99 c:99 Same SHA1 84a516841ba77a5b4648de2cd0dfcb30ea46dbb4 84a516841ba77a5b4648de2cd0dfcb30ea46dbb4 11 k:107 k:107 Same SHA1 13fbd79c3d390e5d6585a21e11ff5ec1970cff0c 13fbd79c3d390e5d6585a21e11ff5ec1970cff0c The Example above will compare the individual characters of the shortest parameter to that of the other parameter and display ASCII values for each while indicating any differences between characters of the same index/location. .NOTES Author: Tyson Paul Blog: https://monitoringguys.com/ Version: 1.0 Version History: 2020.02.26 - Slight revision. Added commenting, better Help content. Added to SCOMHelper module. 2018.04.20 - Original #> Function Compare-String { Param( [Parameter(Mandatory=$true, ValueFromPipeline=$false, Position=0, ParameterSetName='Parameter Set 1')] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [string]$String1, [Parameter(Mandatory=$true, ValueFromPipeline=$false, Position=1, ParameterSetName='Parameter Set 1')] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [string]$String2, [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=2, ParameterSetName='Parameter Set 1')] [ValidateSet("SHA", "SHA1", "MD5", "SHA256", "SHA384", "SHA512","RIPEMD160")] $Algorithm = "SHA1" #SHA1 is default. Ref: https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.hashalgorithm.create?view=netframework-4.8 ) ######################### MAIN ################################ [System.Collections.ArrayList]$arr = @() 0..([math]::min(($String1.Length -1),($String2.Length -1))) | ForEach-Object { $String1_char = [string]$String1[$_] $String2_char = [string]$String2[$_] $String1_hash = Get-StringHash $String1_char -Algorithm $Algorithm $String2_hash = Get-StringHash $String2_char -Algorithm $Algorithm $obj = New-Object PSCustomObject $obj | Add-Member -Name 'Index' -Value $_ -MemberType NoteProperty $obj | Add-Member -Name 'String1/ASCII' -Value ("$($String1_char):$([double][char]$String1_char)") -MemberType NoteProperty $obj | Add-Member -Name 'String2/ASCII' -Value ("$($String2_char):$([double][char]$String2_char)") -MemberType NoteProperty If ($String1_hash -ne $String2_hash){ $obj | Add-Member -Name 'Compare' -Value "Different" -MemberType NoteProperty } Else{ $obj | Add-Member -Name 'Compare' -Value "Same" -MemberType NoteProperty } $obj | Add-Member -Name "Algorithm" -Value $Algorithm -MemberType NoteProperty $obj | Add-Member -Name "String1_Hash" -Value $String1_hash -MemberType NoteProperty $obj | Add-Member -Name "String2_Hash" -Value $String2_hash -MemberType NoteProperty $null = $arr.Add($obj) } Return ($arr | Format-Table -AutoSize) } #End Function ####################################################################### <# .DESCRIPTION Will convert MAML text or file to HTML. .EXAMPLE # Convert knowledge article and write to .html file #Example PS C:\> $monitor = Get-SCOMMonitor -DisplayName 'M365 Teams - Chat Synthetic Test Performance Monitor' PS C:\> $Article = $monitor.GetKnowledgeArticle(([System.Globalization.CultureInfo]'en-US')) PS C:\> Convert-MAMLToHTML -XML $Article.MamlContent | Set-Content C:\temp\KnowledgeArticle.html .NOTES Author: Tyson Paul (https://monitoringguys.com/) References: https://devio.wordpress.com/2009/09/15/command-line-xslt-processor-with-powershell/ https://systemcenter.wiki/ Version History: 2022.06.20 - v1 #> Function Convert-MAMLToHTML { [CmdletBinding(DefaultParameterSetName='Parameter Set 1', PositionalBinding=$false, HelpUri = 'https://monitoringguys.com/')] Param ( # Path to XMLFile [Parameter(Mandatory=$true, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=0, ParameterSetName='XMLFileExists')] [ValidateNotNullOrEmpty()] [string]$XMLFile, # XML data (raw) [Parameter(Mandatory=$true, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=0, ParameterSetName='XMLData')] [ValidateNotNullOrEmpty()] [string]$XML, # Path to XSL transform file. [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=1)] [string]$XSLFile ) ################# FUNCTIONS ################# Function Test-XMLFile { <# .SYNOPSIS Test the validity of an XML file .NOTES https://stackoverflow.com/questions/14423861/how-to-validate-xml-for-correct-syntax-format #> [CmdletBinding()] param ( [parameter(mandatory=$true)][ValidateNotNullorEmpty()][string]$xmlFilePath ) # Check the file exists if (!(Test-Path -Path $xmlFilePath)){ throw "$xmlFilePath is not valid. Please provide a valid path to the .xml fileh" } # Check for Load or Parse errors when loading the XML file $xml = New-Object System.Xml.XmlDocument try { $xml.Load((Get-ChildItem -Path $xmlFilePath).FullName) return $true } catch [System.Xml.XmlException] { Write-Verbose "$xmlFilePath : $($_.toString())" return $false } } ############################################## Function Write-DefaultTransformFile { # https://systemcenter.wiki/ $XSLData = @" <?xml version="1.0" encoding="utf-8"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" xmlns:maml="http://schemas.microsoft.com/maml/2004/10" exclude-result-prefixes="msxsl maml"> <xsl:output method="html" indent="no" encoding="utf-8" /> <xsl:template match="/"> <xsl:apply-templates /> </xsl:template> <xsl:template match="maml:section"> <xsl:apply-templates /> </xsl:template> <xsl:template match="maml:lineBreak"> <br /> </xsl:template> <xsl:template match="maml:navigationLink"> <a> <xsl:attribute name="href"> <xsl:value-of select="maml:uri/@href"/> </xsl:attribute> <xsl:choose> <xsl:when test="maml:uri/@condition!=''"> <xsl:attribute name="condition"> <xsl:value-of select="maml:uri/@condition" /> </xsl:attribute> </xsl:when> </xsl:choose> <xsl:choose> <xsl:when test="maml:uri/@uri!=''"> <xsl:attribute name="uri"> <xsl:value-of select="maml:uri/@uri" /> </xsl:attribute> </xsl:when> </xsl:choose> <xsl:value-of select="maml:linkText"/> </a> </xsl:template> <xsl:template match="maml:list"> <ul> <xsl:apply-templates /> </ul> </xsl:template> <xsl:template match="maml:listItem"> <li> <xsl:apply-templates /> </li> </xsl:template> <xsl:template match="maml:title"> <h1> <xsl:value-of select="."/> </h1> </xsl:template> <xsl:template match="maml:subTitle"> <h2> <xsl:value-of select="." /> </h2> </xsl:template> <xsl:template match="text()"> <xsl:value-of select="."/> </xsl:template> <xsl:template match="maml:example"> <pre> <xsl:apply-templates /> </pre> </xsl:template> <xsl:template match="maml:codeInline"> <code> <xsl:apply-templates /> </code> </xsl:template> <xsl:template match="maml:computerOutputInline"> <pre> <xsl:apply-templates /> </pre> </xsl:template> <xsl:template match="maml:procedure"> <ol> <xsl:apply-templates /> </ol> </xsl:template> <xsl:template match="maml:step"> <li> <xsl:apply-templates /> </li> </xsl:template> <xsl:template match="maml:para"> <p> <xsl:apply-templates /> </p> </xsl:template> <xsl:template match="maml:ui"> <b> <xsl:apply-templates /> </b> </xsl:template> <xsl:template match="maml:entry"> <td> <xsl:apply-templates /> </td> </xsl:template> <xsl:template match="maml:headerEntry"> <th> <xsl:apply-templates /> </th> </xsl:template> <xsl:template match="maml:tableHeader"> <thead> <xsl:apply-templates /> </thead> </xsl:template> <xsl:template match="maml:row"> <tr> <xsl:apply-templates /> </tr> </xsl:template> <xsl:template match="maml:table"> <table> <xsl:apply-templates /> </table> </xsl:template> </xsl:stylesheet> "@ $XSLFile = Join-Path $TempDir 'maml2html.xsl' $XSLData | Set-Content -Path $XSLFile -Force -Encoding UTF8 } ############## END FUNCTIONS ################# ############################################## if ((-NOT $XMLFile) -AND (-NOT $XML)) { Get-Help Convert-MAMLToHtml -Examples Return } # Setup temp dir for output file. Using a redirector to an output file is the only way I could find to obtain the property bag data. $TempDir = (Join-Path (Join-Path $env:Windir "Temp") (Join-Path 'SCOMHelper' 'Export-SCOMKnowledge') ) New-Item -Type Directory -Path $TempDir -ErrorAction SilentlyContinue | Out-Null If (-NOT(Test-Path -Path $TempDir -PathType Container)) { Write-Error "Unable to create/access directory: [$($TempDir)]. " Return $false } If ($XSLFile.Length) { If( -NOT (Test-Path $XSLFile)) { Throw "XSL input file not found: $($_)" Exit } } Else { #dot-sourced . Write-DefaultTransformFile } $tmpTransformFile = New-TemporaryFile If ($XML) { $XMLFile = (New-TemporaryFile).FullName $XML | Set-Content -Path $XMLFile -Force -Encoding UTF8 } If (-NOT (Test-XMLFile -xmlFilePath $XMLFile) ){ $tmpXMLFile = (New-TemporaryFile).FullName # Make sure XML has a root "<Root>$(Get-Content -Path $XMLFile)</Root>" | Set-Content -Path $tmpXMLFile -Force -Encoding UTF8 If (-NOT (Test-XMLFile -xmlFilePath $tmpXMLFile) ){ # Make sure XML has a root Throw "Error loading XML data from path: $XMLFile" Return } # Set path to new tmp file which contains root element. $XMLFile = $tmpXMLFile } $xslt = New-Object System.Xml.Xsl.XslCompiledTransform; Try { $xslt.Load($XSLFile); } Catch { Write-Warning "Failed to load transform file [$($XSLFile)]. Will use default 'maml2html' transform instead." . Write-DefaultTransformFile Try { $xslt.Load($XSLFile); } Catch { Write-Error "Fatal Error: Failed to load default transform file [$($XSLFile)]. " Return } } $xslt.Transform($XMLFile, $tmpTransformFile.FullName); Return (Get-Content -Path $tmpTransformFile.FullName); } ####################################################################### <# .DESCRIPTION Will install MOMAgent.msi on one or more target computers. Agent will need to be manually approved unless security settings allow automatic approval. .EXAMPLE # # This example will set two target computer names, prompt for a credential, then deploy the agent to the target computers. # The installation scriptblock will wait 120 seconds for the service to install and present a status of "Running" before returning the service status to the output. PS C:\> $Computers = "scorch1.contoso.com","devdb01.contoso.com" PS C:\> $MyCred = Get-Credential -UserName 'Contoso\tpaul' -Message "Enter Action credential (Local Admin on targets)" PS C:\> Deploy-SCOMAgent -USE_SETTINGS_FROM_AD 0 -USE_MANUALLY_SPECIFIED_SETTINGS 1 -MANAGEMENT_GROUP "SCOMLAB" -MANAGEMENT_SERVER_AD_NAME "MS01.CONTOSO.COM" -MANAGEMENT_SERVER_DNS "MS01.CONTOSO.COM" -ACTIONS_USE_COMPUTER_ACCOUNT 1 -NOAPM 1 -InstallCredential $MyCred -TargetComputer $Computers -WaitForServiceSeconds 120 .EXAMPLE # # This example will pipe the the array of computer names to the function. This is slower than passing the array as a parameter with '-TargetComputer'. # The installation scriptblock will wait 120 seconds for the service to install and present a status of "Running" before returning the service status to the output. PS C:\> $Computers = "scorch1.contoso.com","devdb01.contoso.com" PS C:\> $Computers | Deploy-SCOMAgent -USE_SETTINGS_FROM_AD 0 -USE_MANUALLY_SPECIFIED_SETTINGS 1 -MANAGEMENT_GROUP "SCOMLAB" -MANAGEMENT_SERVER_AD_NAME "MS01.CONTOSO.COM" -MANAGEMENT_SERVER_DNS "MS01.CONTOSO.COM" -ACTIONS_USE_COMPUTER_ACCOUNT 1 .EXAMPLE # # This example demonstrates how you would specify an alternate Default Action Account. This is a very, very rare requirement but here's an example anyway. PS C:\> $InstallCred = Get-Credential -UserName 'Contoso\tpaul' -Message "Enter Action credential (Local Admin on targets)" PS C:\> $DefaultActionCred = Get-Credential -UserName 'Contoso\svc_LowPrivAccount' -Message "Enter Default Action credential (for very, very rare low-priv scenarios)" PS C:\> Deploy-SCOMAgent -USE_SETTINGS_FROM_AD 0 -USE_MANUALLY_SPECIFIED_SETTINGS 1 -MANAGEMENT_GROUP "SCOMLAB" -MANAGEMENT_SERVER_AD_NAME "MS01.CONTOSO.COM" -MANAGEMENT_SERVER_DNS "MS01.CONTOSO.COM" -ACTIONS_USE_COMPUTER_ACCOUNT 0 -NOAPM 1 -TargetComputer "devdb01.contoso.com" -WaitForServiceSeconds 180 -DefaultActionAccountCredential $DefaultActionCred -InstallCredential $InstallCred .INPUTS Function accepts an array of computer names as piped input. .OUTPUTS Outputs objects for each installation attempt. .NOTES Author: Tyson Paul Blog: https://monitoringguys.com/2019/11/12/scomhelper/ History: 2020.05.13 - First version For additional info on manual installation parameters see: https://docs.microsoft.com/en-us/system-center/scom/manage-deploy-windows-agent-manually?view=sc-om-2019 #> Function Deploy-SCOMAgent { [CmdletBinding(DefaultParameterSetName='Parameter Set 1', SupportsShouldProcess=$true, PositionalBinding=$false, HelpUri = 'http://www.microsoft.com/', ConfirmImpact='Medium')] Param ( # The account that will remotely execute the installation of the agent. This function relies on WSMAN therefore this credential must have rights to PSRemoting access to the target machines. This is not the Microsoft Monitoring Agent (HealthService) service credentials. Default: <current user creds> [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=0, ParameterSetName='Parameter Set 1')] [Alias("Credential")] [pscredential]$InstallCredential, # {0|1} Indicates whether the management group settings properties will be set on the command line. Use 0 if you want to set the properties at the command line. Use 1 to use the management group settings from Active Directory. [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=1, ParameterSetName='Parameter Set 1')] [ValidateSet('0','1')] [string]$USE_SETTINGS_FROM_AD = 0, # {0|1} If USE_SETTINGS_FROM_AD=1, then USE_MANUALLY_SPECIFIED_SETTINGS must equal 0. [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=2, ParameterSetName='Parameter Set 1')] [ValidateSet('0','1')] [string]$USE_MANUALLY_SPECIFIED_SETTINGS = 1, # Specifies the management group (name) that will manage the computer. [Parameter(Mandatory=$true, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=3, ParameterSetName='Parameter Set 1')] [string]$MANAGEMENT_GROUP, # Specifies the fully qualified domain name for the management server. To use a gateway server, enter the gateway server FQDN as MANAGEMENT_SERVER_DNS. [Parameter(Mandatory=$true, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=4, ParameterSetName='Parameter Set 1')] [ValidatePattern('(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{1,63}(?<!-)\.)+[a-zA-Z]{2,63}$)')] [string]$MANAGEMENT_SERVER_DNS, # Use this parameter if the computer's DNS and Active Directory names differ to set to the fully qualified Active Directory Domain Services name. [Parameter(Mandatory=$true, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=5, ParameterSetName='Parameter Set 1')] [ValidatePattern('(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{1,63}(?<!-)\.)+[a-zA-Z]{2,63}$)')] [string]$MANAGEMENT_SERVER_AD_NAME, # Sets the health service port number. It is rare to modify this and not advised. [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=6, ParameterSetName='Parameter Set 1')] [ValidateRange(1025,65535)] [int]$SECURE_PORT = 5723, # Optional parameter. Use this parameter if you want to install the agent to a path other than the default installation path. Note that \Agent will be appended to this value. [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=7, ParameterSetName='Parameter Set 1')] [System.IO.FileInfo]$INSTALLDIR, # Indicates whether the service will run as a specified user account (0) or the default 'LocalSystem' account (1). Default=1. [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=8, ParameterSetName='Parameter Set 1')] [ValidateSet('0','1')] [string]$ACTIONS_USE_COMPUTER_ACCOUNT=1, # This is the Microsoft Monitoring Agent (HealthService) Default Action Account credentials if other than 'LocalSystem'. Use of an alternate credential is very rare and typically unnecessary. Use with caution. [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=9, ParameterSetName='Parameter Set 1')] [pscredential]$DefaultActionAccountCredential, # {0|1} Optional parameter. Installs the Operations Manager agent without .NET Application Performance Monitoring. If you are using AVIcode 5.7, NOAPM=1 leaves the AVIcode agent in place. If you are using AVIcode 5.7 and install the Operations Manager agent by using momagent.msi without NOAPM=1, the AVIcode agent will not work correctly and an alert will be generated. [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=10, ParameterSetName='Parameter Set 1')] [ValidateSet('0','1')] [string]$NOAPM=1, # One or more target computers FQDN, comma-separated. [Parameter(Mandatory=$True, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, ValueFromRemainingArguments=$false, Position=11, ParameterSetName='Parameter Set 1')] [string[]]$TargetComputer, # The full path to the agent installation file. Default = '<SCOM Mgmt Server Install Directory>\AgentManagement\amd64\MOMAgent.msi' [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=12, ParameterSetName='Parameter Set 1')] [System.IO.FileInfo]$AgentMSIPath, # The number of seconds to wait for the service to appear after installation before returning from the remote PSSession. Default:180. [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=13, ParameterSetName='Parameter Set 1')] [int]$WaitForServiceSeconds = 180 ) #region Begin Begin { $hash = @{} $hash.msi = $null $hash.outDir = @((Join-Path $env:Windir "Temp"),'C:\Temp') If (-NOT $AgentMSIPath){ $hash.InstallDirectory = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Microsoft Operations Manager\3.0\Setup" -Name "InstallDirectory").InstallDirectory $hash.agentPath = (Join-Path $hash.InstallDirectory 'AgentManagement\amd64\MOMAgent.msi' ) } Try{ $hash.fileObj = Get-Item -Path $hash.agentPath -ErrorAction STOP $hash.msi = [System.IO.File]::ReadAllBytes($hash.fileObj.FullName) } Catch { Throw "Failure getting MOMAgent.msi from path: [$($hash.agentPath)]! Either specify the correct path to the file or put the file at this location. " Return 1 } # amount of time to wait for HealthService service to enumerate in Services before returning from Scriptblock $hash.WaitForServiceSeconds = $WaitForServiceSeconds [bool]$hash.WhatIf = (-Not ($PSCmdlet.ShouldProcess($hash))) $Params = @{ USE_SETTINGS_FROM_AD = $USE_SETTINGS_FROM_AD USE_MANUALLY_SPECIFIED_SETTINGS = $USE_MANUALLY_SPECIFIED_SETTINGS MANAGEMENT_GROUP = $MANAGEMENT_GROUP MANAGEMENT_SERVER_DNS = $MANAGEMENT_SERVER_DNS MANAGEMENT_SERVER_AD_NAME = $MANAGEMENT_SERVER_AD_NAME SECURE_PORT = $SECURE_PORT INSTALLDIR = $INSTALLDIR ACTIONS_USE_COMPUTER_ACCOUNT = $ACTIONS_USE_COMPUTER_ACCOUNT NOAPM = $NOAPM } If ($DefaultActionAccountCredential){ $Params.ACTIONSUSER = $DefaultActionAccountCredential.GetNetworkCredential().UserName Try { $Params.ACTIONSDOMAIN = $DefaultActionAccountCredential.GetNetworkCredential().Domain }Catch{ $Params.ACTIONSDOMAIN = '' } $Params.ACTIONSPASSWORD = $DefaultActionAccountCredential.GetNetworkCredential().Password } #------------------------------------------------- #region InstallationBlock $scriptBlock = [scriptblock]{ Param ( $hash, $Params ) # Buid custom object to return. Initially assume it doesn't exist. $obj = New-Object -TypeName PSCUSTOMOBJECT $obj | Add-Member -MemberType NoteProperty -Name 'ServerName' -Value "$($env:COMPUTERNAME).$($env:USERDNSDOMAIN)" $obj | Add-Member -MemberType NoteProperty -Name 'ServiceName' -Value 'HealthService' $obj | Add-Member -MemberType NoteProperty -Name 'Status' -Value 'NOT FOUND' $obj | Add-Member -MemberType NoteProperty -Name 'Comment' -Value '' Try{ # If service already exists, do nothing. Return. $Service = Get-Service -Name HealthService -ErrorAction Stop $obj.Status = $Service.Status.ToString() $obj.Comment = "Service already exists. No action taken." Return $obj }Catch { # Assume service doesn't appear yet. Proceed to install. } # Write the .msi to temp dir on remote target. Try { $exePath = (Join-Path $hash.outDir[0] $hash.fileObj.Name) [System.IO.File]::WriteAllBytes($exePath, $hash.msi) }Catch { Try{ $exePath = (Join-Path $hash.outDir[1] $hash.fileObj.Name) [System.IO.File]::WriteAllBytes($exePath, $hash.msi) } Catch { $obj.Comment = "Remote connection successful but failed to copy bits to path: [$($exePath)]. Service not installed. Error was $_" Return $obj } } #region Build CMD string $CMD = "msiexec.exe /i $exePath /passive /l*v $(Split-Path $exePath -Parent)\OMAgentinstall.log" ForEach ($Key in @($Params.Keys)) { switch ($Key) { # Wrap InstallDir in double quotes {($Key -match 'INSTALLDIR') -AND ($Params.$Key.LENGTH -ge 1) } {$CMD += " $($Key)=```"$($Params.$_)```""; Continue; } {$Params.$_.LENGTH -ge 1 } {$CMD += " $($Key)=$($Params.$_)"} Default {Continue} } } # it makes sense to hardcode this param=1 $CMD += " AcceptEndUserLicenseAgreement=1" #endregion Build CMD #Install Command If ($hash.WhatIf){ $obj.Status = "Running (WHATIF)" $obj.Comment = "(WHATIF) Service installed successfully. Bits were actually written to this path successfully: [$($exePath)]" Return $obj } Else { # Get down to business! Invoke-Expression $CMD #Set timer $ServiceTimer = [System.Diagnostics.Stopwatch]::StartNew() Do{ Try{ $Service = Get-Service -Name HealthService -ErrorAction Stop }Catch { # Assume service doesn't appear yet Start-Sleep -Seconds 3 } }While (($ServiceTimer.Elapsed.TotalSeconds -lt $hash.WaitForServiceSeconds) -AND ([string]($Service.Status) -ne "Running") ) $ServiceTimer.Stop() If ([bool]$Service ){ $obj.Status = [string]($Service.Status) $obj.Comment = "Service installed successfully." } Else { $obj.Comment = "Service not installed/found. Command line: `n$($CMD) `n`nTimerSeconds:$($ServiceTimer.Elapsed.TotalSeconds), WaitForServiceSeconds:$($hash.WaitForServiceSeconds), ServiceStatus:$($Service.Status), ServiceRetrievedSuccess:$([bool]$Service)." } } Return $obj } #endregion #region InstallationBlock #------------------------------------------------- } #endregion Begin #region Process Process { # ----------- Configure remote session ----------- #region ConfigureSessionRemotely $SessionConfigName = 'MaxDataSizeMB500' $ConfigureSessionBlock = [scriptblock]{ Param( $SessionConfigName ) Try { # If session option is already registered, get it $var = Get-PSSessionConfiguration -Name $SessionConfigName -ErrorAction Stop }Catch { $var = Register-PSSessionConfiguration -Name $SessionConfigName } $var = Set-PSSessionConfiguration -Name $SessionConfigName -MaximumReceivedDataSizePerCommandMB 500 -MaximumReceivedObjectSizeMB 500 } $SessionParams = @{ ComputerName = $TargetComputer } If ($InstallCredential) { $SessionParams.Credential = $InstallCredential } $Session = New-PSSession @SessionParams #Set session config on remote Try { Invoke-Command -Session $Session -ArgumentList $SessionConfigName -ScriptBlock $ConfigureSessionBlock -ErrorAction Stop } Catch { Write-Warning "WinRM Reset will typically cause an 'I/O operation has been aborted...' message." } Remove-PSSession -Session $Session #endregion ConfigureSessionRemotely #------------------------------------------------- #------------------------------------------------- #region SetMyLocalSessionConfig Try { # If session option is already registered, get it $var = (Get-PSSessionConfiguration -Name $SessionConfigName -ErrorAction Stop) }Catch { $var = Register-PSSessionConfiguration -Name $SessionConfigName } $var = Set-PSSessionConfiguration -Name $SessionConfigName -MaximumReceivedDataSizePerCommandMB 500 -MaximumReceivedObjectSizeMB 500 #endregion SetMyLocalSessionConfig #------------------------------------------------- #------------------------------------------------- #region PrepareInstallSession $SessionName = "MySCOMInstallSession" $SessionParams = @{ Name = $SessionName ComputerName = $TargetComputer ConfigurationName = $SessionConfigName } If ($InstallCredential) { $SessionParams.Credential = $InstallCredential } $Session = New-PSSession @SessionParams #endregion PrepareInstallSession #------------------------------------------------- # Execute installation $InvokeParams = @{ Session = $Session ScriptBlock = $scriptBlock ArgumentList = @($hash, $Params) Verbose = $true } $Result = Invoke-Command @InvokeParams Remove-PSSession -Session $Session Return $Result } #region Process End { } } #end Function ####################################################################### <# .SYNOPSIS Operations Manager Powershell function to output the effective monitoring configuration. .DESCRIPTION This will output a generous amount of information about monitors, rules, discoveries and the effective config for a given object (and all hosted/contained objects). CSV or HTML output. There exists a similar function in the standard OpsMan PowerShell module but AFAIK, even with the most recent UR10(SCOM2016) update at the time of this writing, it is limited in its output and I don’t find it very useful which is why I wrote this function. .EXAMPLE # #Example PS C:\> $ComputerName = "DB01.CONTOSO.COM" PS C:\> $objs = Get-SCOMClass -Name 'Microsoft.Windows.Computer' | Get-SCOMClassInstance | Where-Object Name -match "$ComputerName" PS C:\> $ExportPath = 'C:\Temp\EffectiveConfig\' PS C:\> Export-SCOMEffectiveMonitoringConfigurationReport -MonitoringObject $objs -ExportCSVPath $ExportPath -IndividualFiles -SortBy AlertPriority -SortOrder Ascending -Verbose The commands above will export a CSV file of monitoring configuration for the Windows Computer object and everything hosted on it to a CSV file sorted ascending by AlertPriorty, with verbose output to the screen. The command will also output individual CSV files for every contained or hosted object. .EXAMPLE # #Example PS C:\> $obj = Get-SCOMClass -Name 'Microsoft.SQLServer.2016.DBEngine' | Get-SCOMClassInstance | Where DisplayName -eq 'MySQLInstance' PS C:\> $ExportPath = 'C:\Temp\EffectiveConfig\' PS C:\> Export-SCOMEffectiveMonitoringConfigurationReport -MonitoringObject $obj -ExportHTMLPath $ExportPath -HTMLFormat Basic The commands above will output a fancy HTML report for the specified SQL instance(s). NOTE: The 'fancy' HTML report should only be used for instances with few hosted objects. This report does not perform well with a high row count. .EXAMPLE # #EXAMPLE PS C:\> $Name = 'DC3SQLDev01' PS C:\> $Date = (Get-date -f "yyyyMMdd_hhmmss") PS C:\> $OutFolder = (Join-Path -Path 'D:\Temp' -ChildPath $Date) PS C:\> $Computer = Get-SCOMClass -Name 'microsoft.windows.computer' | Get-SCOMClassInstance | Where-Object Name -match $Name PS C:\> Export-SCOMEffectiveMonitoringConfigurationReport -MonitoringObject $Computer -ExportCSVPath (Join-Path $OutFolder ("$($Name)_EffectiveConfig_$($Date)")) -SortBy WorkflowId .NOTES Author: Tyson Paul Original Date: 7-25-2014 History 2022.05.25: Seeing duplicate workflows for same instance, one has empty Path. 2020.07.16: Added another example. 2020.02.19: Found that override property "Enabled" value (true/false) is case-sensitive and therefore must be lower case for this report to run correctly. Added function to fix existing overrides if they are found while running this function/report. Ended up fixing Start-SCOMOverrideTool.ps1 script too. Clean-Name function: improved invalid/illegal character replacement strategy. 2019.08.26: Major revision. .PARAMETER MonitoringObject Accepts one or more SCOM Monitoring Objects (use Get-SCOMClassInstance to retrieve a monitoring object). .PARAMETER ExportCSVPath Path/folder to export CSV report(s). .PARAMETER ExportHTMLPath Path/folder to export HTML report(s). .PARAMETER HTMLFormat There are two options: Basic and Fancy. Both report styles include a text filter powered by Javascript. .PARAMETER IndividualFiles Will also output individual report files for each object that may be hosted on or contained by the MonitoringObject(s). .PARAMETER SortBy Report will be sorted by this column/property. .PARAMETER SortOrder Ascending or Descending. Default is Ascending. #> Function Export-SCOMEffectiveMonitoringConfigurationReport { [CmdletBinding(DefaultParameterSetName='P1', SupportsShouldProcess=$true, PositionalBinding=$false)] Param ( #Any valid monitoring object(s) (class instance or group) [Parameter(Mandatory=$true, ParameterSetName = "P1", ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=0)] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [Microsoft.EnterpriseManagement.Monitoring.MonitoringObject[]]$MonitoringObject, #Directory to export CSV fil [Parameter(Mandatory=$false, ParameterSetName = "P1", ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=1)] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [string]$ExportCSVPath, #Directory to export HTML file [Parameter(Mandatory=$false, ParameterSetName='P1', ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=2)] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [string]$ExportHTMLPath, #Formatting of HTML export [Parameter(Mandatory=$false, ParameterSetName='P1', ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=3)] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [ValidateSet("Fancy", "Basic")] [string]$HTMLFormat = "Basic", #Report rows will be sorted by this Workflow property [Parameter(Mandatory=$false, ParameterSetName='P1', ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=4)] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [ValidateSet("EntityName", "EntityId", "Path", "ClassName", "ClassId", "WorkflowName", "WorkflowDisplayName", "WorkflowType", "WorkflowId", "WorkflowMPName", "Enabled", "GeneratesAlert", "AlertSeverity", "AlertPriority", "Overridden", "Description", "Parameters", "Overrides")] [string]$SortBy = "EntityId", #Formatting of HTML export [Parameter(Mandatory=$false, ParameterSetName='P1', ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=5)] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [ValidateSet("Ascending", "Descending")] [string]$SortOrder = 'Ascending', # Will output individual csv or html files for all individual objects in addition to the merged/master file [switch]$IndividualFiles ) $TESTING = $FALSE <# UNCOMMENT FOR TESTING # This will avoid loading Mons/Rules every time while testing. WRITE-HOST 'TESTING' -F RED -B YELLOW $TESTING = $TRUE #> ################################################################### # Will clean up names/strings with special characters (like URLs and Network paths) Function Clean-Name { Param( [string]$uglyString ) # Remove problematic characters and leading/trailing spaces #$prettyString = (($uglyString.Replace(':','_')).Replace('/','_')).Replace('\','_').Trim() $prettyString = $uglyString.Split([IO.Path]::GetInvalidFileNameChars()) -join '_' # 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 Build-HTMLBasic { [CmdletBinding(DefaultParameterSetName='Parameter Set 1', SupportsShouldProcess=$true, PositionalBinding=$false)] Param ( #array of workflows to be exported to the Path [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=0, ParameterSetName='Parameter Set 1')] [System.Object[]]$arrWFs, #title of HTML doc [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=2, ParameterSetName='Parameter Set 1')] [string]$Title = "" ) $HTML = '' $HTML += @" <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>$Title</title> </head> <body> <style type="text/css"> a { text-decoration: none; } a:link, a:visited { color: blue; } a:hover { color: red; } .trrow {} .tg {border-collapse:collapse;border-spacing:1;} .tg td{font-family:Arial, sans-serif;font-size:14px;padding:6px 6px;border-style:solid;border-width:1px;overflow:hidden;word-break:normal;border-color:black;} .tg th{font-family:Arial, sans-serif;font-size:14px;font-weight:normal;padding:1px 6px;border-style:solid;border-width:1px;overflow:hidden;word-break:normal;border-color:black;} .tg .defaultstyle{border-color:inherit;vertical-align:center;text-align:left} .tg .headerstyle{font-weight:bold;font-size:100%;border-color:inherit;vertical-align:center} .tg .columnNameStyle{border-color:inherit;vertical-align:center;text-align:center} .tg .classnamestyle{font-weight:bold;border-color:inherit;vertical-align:center} .tg .keystyle{font-weight:bold;color:#fe0000;border-color:inherit;text-align:right;vertical-align:center} .tg .keypropertystyle{font-weight:bold;color:#fe0000;border-color:inherit;vertical-align:center} .tg .propertystyle{color:#22771a;border-color:inherit;vertical-align:center;horizontal-align:center} propertystyle @media screen and (max-width: 767px) { .tg {width: auto !important;} .tg col {width: auto !important;} .tg-wrap {overflow-x: auto;-webkit-overflow-scrolling: touch;} } #myInput { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAVCAYAAACpF6WWAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAEnQAABJ0Ad5mH3gAAAACYktHRAD/h4/MvwAAAAl2cEFnAAABKgAAASkAUBZlMQAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxMy0wNC0xMFQwNjo1OTowNy0wNzowMI5BiVEAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTMtMDQtMTBUMDY6NTk6MDctMDc6MDD/HDHtAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAABF0RVh0VGl0bGUAc2VhcmNoLWljb27Cg+x9AAACKklEQVQ4T6WUSavqQBCFK+2sII7gShFXLpUsBBHFf+1KcAQFwaWiolsnnBDn++4p0iHRqPDuByFJd/Wp6qrqVn5+IQP3+52m0ymtVis6Ho885na7KRgMUiKR4O9vmEQHgwGNx2NyOp0khCBFUXgcJo/Hg67XK8ViMcpkMjz+Dl200+nQZrMhh8PBE4gYQgDidrudvzEOm2KxyP9WsCginM1mHKEUS6VSFA6HOWI4G41GPAfx2+1GgUCAVFXVZMwovwY/lUqFPB4PiyFn+XxemzbT6/VovV6z8Ol0olwux+LPCBQFEQKIvhME2WyWbWGHFCD/VghUGVvE1rDlb6TTabbFmuVyqY2aEWgbFALeI5GINvyeUCjEtlgju+IZoRWfkS30CURoxFJUNjMEt9stf38CNjJKIFvNiMBJgTebzcZt843hcMhCELWqPBDxeJwulwtvC/3X7/e1qVfgFD0rC5tMJrUZM8Lr9VI0GmVBRDCfz6nZbHI/Sna7HXW7XZpMJtxSiBIP1lmhH9NqtaqfGKQDTmQREBnSgwfmMqfYYblc1o+2xHShtNttLgSiee4EmMEp3hDBPJzikimVSuRyuTTLJ1GwWCz4pCB3UhiL/X4/Hw50C5zjLSM+n898weCogxdRIzAGxigAdtNqtV6EC4UC+Xy+z6Kf2O/31Gg0TMK4ZBDxf4uCw+FA9XpdF0aaUOg/iQLcHbVaTb/p0Cl/FgXIJ/oYnaCqKv0DC6dltH6Ks84AAAAASUVORK5CYII=); /* Add a search icon to input */ background-position: 10px 12px; /* Position the search icon */ background-repeat: no-repeat; /* Do not repeat the icon image */ width: 100%; /* Full-width */ font-size: 16px; /* Increase font-size */ padding: 12px 20px 12px 40px; /* Add some padding */ border: 1px solid #ddd; /* Add a grey border */ margin-bottom: 6px; /* Add some space below the input */ } #myTable tr { /* Add a bottom border to all table rows */ border-bottom: 1px solid #ddd; } #myTable tr.header, #myTable tr:hover { /* Add a grey background color to the table header and on hover */ background-color: #f1f1f1; } </style> <script> function myFunction() { // Declare variables var input, filter, table, tr, td1,td2,td3,td4,td5,td6,td7,td8,td9, i; input = document.getElementById("myInput"); filter = input.value.toUpperCase(); table = document.getElementById("myTable"); //tr = table.getElementsByTagName("tr"); tr = table.getElementsByClassName("trrow"); // Loop through all table rows, and hide those who don't match the search query for (i = 0; i < tr.length; i++) { td1 = tr[i].getElementsByClassName("defaultstyle")[1]; td2 = tr[i].getElementsByClassName("defaultstyle")[2]; td3 = tr[i].getElementsByClassName("defaultstyle")[3]; td4 = tr[i].getElementsByClassName("defaultstyle")[4]; td5 = tr[i].getElementsByClassName("defaultstyle")[5]; td6 = tr[i].getElementsByClassName("defaultstyle")[6]; td7 = tr[i].getElementsByClassName("defaultstyle")[7]; td8 = tr[i].getElementsByClassName("defaultstyle")[8]; td9 = tr[i].getElementsByClassName("defaultstyle")[9]; if (td1) { if ( (td1.innerHTML.toUpperCase().indexOf(filter) > -1) || (td2.innerHTML.toUpperCase().indexOf(filter) > -1) || (td3.innerHTML.toUpperCase().indexOf(filter) > -1) || (td4.innerHTML.toUpperCase().indexOf(filter) > -1) || (td5.innerHTML.toUpperCase().indexOf(filter) > -1) || (td6.innerHTML.toUpperCase().indexOf(filter) > -1) || (td7.innerHTML.toUpperCase().indexOf(filter) > -1) || (td8.innerHTML.toUpperCase().indexOf(filter) > -1) || (td9.innerHTML.toUpperCase().indexOf(filter) > -1) ) { tr[i].style.display = ""; } else { tr[i].style.display = "none"; } } } } </script> <input type="text" id="myInput" onkeyup="myFunction()" placeholder="Search for text.."> <div class="tg-wrap"> <table id="myTable" class="tg"> "@ $headerHTML = '' $rows = '' ForEach ($header in $arrOrderedHeaders ) { $headerHTML += @" <th class="headerstyle">$($header)</th> "@ } $HTML += @" <tr> $headerHTML </tr> "@ $i =1 #Build rows ForEach ($WF in $arrWFs[0..($arrWFs.Count -1)]) { $percentComplete = "{0:N0}" -f (($i/([math]::Max(($arrWFs.Count),1 )) ) *100) Write-Progress -Activity "Building HTML report headers" -Status "$($percentComplete) percent complete." -PercentComplete $percentComplete $rows += @" <tr class="trrow"> "@ ForEach ($Header in $arrOrderedHeaders) { If ($Header -eq 'Index') { $rows += @" <td class="defaultstyle">$($i)</td> "@ } Else{ $rows += @" <td class="defaultstyle">$($WF.$Header)</td> "@ } }#end ForEach Header $rows += @" </tr> "@ $i++ }#end ForEach WF Write-Progress -Activity "Building HTML report" -Status "$($percentComplete) percent complete." -PercentComplete 100 -Completed $HTML += @" $rows </table> </div> </body> </html> "@ Return $HTML }# End Build-HTMLBasic ################################################################### Function Build-HTMLFancyResize { [CmdletBinding(DefaultParameterSetName='Parameter Set 1', SupportsShouldProcess=$true, PositionalBinding=$false)] Param ( #array of workflows to be exported to the Path [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=0, ParameterSetName='Parameter Set 1')] [System.Object[]]$arrWFs, #title of HTML doc [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=2, ParameterSetName='Parameter Set 1')] [string]$Title = "" ) $HTML = '' $HTML += @" <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>$Title</title> </head> <style type="text/css"> .trrow {} td{ text-indent:5px; color:#444; border-bottom:1px solid #bbb; border-left:1px solid #bbb; } "@ $headerHTML = @" <tr> "@ $rows = '' $CSSColumns ='' [int]$pxFactor = 8 $maxColumnSize = (50 * $pxFactor) #Build CSS style for each column and Header HTML ForEach ($header in $arrOrderedHeaders ) { #CSS #header name width in px Try{ #[int]$headerNamePx = [System.Math]::Max(($header.Length * $pxFactor), (($arrAllWorkflows.$header | Measure-Object -Maximum).Maximum.ToString().Length * $pxFactor) ) [int]$headerNamePx = [System.Math]::Max(($header.Length * $pxFactor), ([System.Math]::Min($maxColumnSize, (($arrWFs.$header | Where-Object Length -gt 0 | % {$_.Length} ) | Measure-Object -Maximum ).Maximum * $pxFactor )) ) } Catch { [int]$headerNamePx = ($header.length * $pxFactor) } $CSSColumns += @" td.$($header), th.$($header) { text-align: left; width:$($headerNamePx)px; min-width:$([math]::Min(30,($headerNamePx /4)))px; } "@ #Header HTML $headerHTML += @" <th class="$header">$($header)</th> "@ }#end ForEach $header $headerHTML += @" </tr> "@ $HTML += @" $CSSColumns "@ $HTML += @' td.Overrides, th.Overrides{ border-right:1px solid #2e638e; } td.bottom{ border-bottom:1px solid #2e638e; } td.disabled, th.disabled{ width:25px; text-align: center; min-width:25px; } #myInput { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAVCAYAAACpF6WWAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAEnQAABJ0Ad5mH3gAAAACYktHRAD/h4/MvwAAAAl2cEFnAAABKgAAASkAUBZlMQAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxMy0wNC0xMFQwNjo1OTowNy0wNzowMI5BiVEAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTMtMDQtMTBUMDY6NTk6MDctMDc6MDD/HDHtAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAABF0RVh0VGl0bGUAc2VhcmNoLWljb27Cg+x9AAACKklEQVQ4T6WUSavqQBCFK+2sII7gShFXLpUsBBHFf+1KcAQFwaWiolsnnBDn++4p0iHRqPDuByFJd/Wp6qrqVn5+IQP3+52m0ymtVis6Ho885na7KRgMUiKR4O9vmEQHgwGNx2NyOp0khCBFUXgcJo/Hg67XK8ViMcpkMjz+Dl200+nQZrMhh8PBE4gYQgDidrudvzEOm2KxyP9WsCginM1mHKEUS6VSFA6HOWI4G41GPAfx2+1GgUCAVFXVZMwovwY/lUqFPB4PiyFn+XxemzbT6/VovV6z8Ol0olwux+LPCBQFEQKIvhME2WyWbWGHFCD/VghUGVvE1rDlb6TTabbFmuVyqY2aEWgbFALeI5GINvyeUCjEtlgju+IZoRWfkS30CURoxFJUNjMEt9stf38CNjJKIFvNiMBJgTebzcZt843hcMhCELWqPBDxeJwulwtvC/3X7/e1qVfgFD0rC5tMJrUZM8Lr9VI0GmVBRDCfz6nZbHI/Sna7HXW7XZpMJtxSiBIP1lmhH9NqtaqfGKQDTmQREBnSgwfmMqfYYblc1o+2xHShtNttLgSiee4EmMEp3hDBPJzikimVSuRyuTTLJ1GwWCz4pCB3UhiL/X4/Hw50C5zjLSM+n898weCogxdRIzAGxigAdtNqtV6EC4UC+Xy+z6Kf2O/31Gg0TMK4ZBDxf4uCw+FA9XpdF0aaUOg/iQLcHbVaTb/p0Cl/FgXIJ/oYnaCqKv0DC6dltH6Ks84AAAAASUVORK5CYII=); /* Add a search icon to input */ background-position: 10px 12px; /* Position the search icon */ background-repeat: no-repeat; /* Do not repeat the icon image */ width: 100%; /* Full-width */ font-size: 16px; /* Increase font-size */ padding: 12px 20px 12px 40px; /* Add some padding */ border: 1px solid #ddd; /* Add a grey border */ margin-bottom: 6px; /* Add some space below the input */ } #normal tr:hover { /* Add a grey background color to the table header and on hover */ background-color: #f1f1f1; } body{ background-color: white; text-align:center; padding:55px; } .center{ text-align:left; } #table{ width:100%; max-width:1600px; } th{ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABkAAAAAeCAMAAABDhiIoAAAACXBIWXMAAAsTAAALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAAwBQTFRFAAAA////nMHblrzTjrbQhq/Ne6bJZ5rJYZjOWYOpSovFKlZ5DVeUCFiZB1eWBVebA1idAFifcLX7b7H2bK3vaajmZaDaY5zUYpnQA2W+A2a+A2W9BWW6BWO2DWOwVKTuUp/mUJreTpXWTZLQS47KSozHdrfyeLn0dbXva6Xbc7HpcKvhaqDRaJ3NZ5zMZ5vLZZnHZpnIZJbEYpPAYJC7XYu0W4euWoSri8P4iL7xhLnpgLLge6vXd6TOeafRdqPMpMvuo8ntpszuAGbAAGa/AGW+AGS7AGO5AGK3AGG0AF+wAF6tAFyoAFumAFqjAFmiAFmhAWW9AWS6AWK2AV2qAmGzA2a8A2W7A2S4A2O2A2K0A2CvA1ynA1ukA1qiA1mgA1mfBF6sBWW5BWS3BWK0BWGyBWGxBWCvBVulBma5BmW4BmW2BmOyBmGuBVmfBVidB2S0Bl6pBluhBlqeBlmcBleaB2SyB2KwB2KvB2CsB2CqB1+oB12lB1yjBleZCGW2CGS1CGS0CGOyCGGtB1ugB1iaCFqdCmW0Cma0CmSxCmOvC2azC2WyCmGpCl+lCVeYCVeWC2KsCludClqbClmZCliXC12iC1yfDWWyDWSwC1qaC1mYC1eWC1eVDWSvDWOuDWKtDWKrDWGqDWCpDWCnDFmaDV+lDV2iDVyfDVudDVqbDVqaDVmYDViXDVeWDleULmOOLWCKK1l+N2yXNmmUNWaPM2OKMmCGSICtTIa1VZLEa7HqdsD9dL35kcfzjcHriLrjhbXcgrDWfqrOfKfLAVidBFibBlmbB1iZDFiVLF2EKld6MV6CMl+DMV6BMV2Af63Rk8LlkL7gjrrbiLLRh7DPqNDwntP5ms7yl8ntlcbpirbVjLjXibTTqdz+oNDwp9j5pNT0lsHelL7akLjTncvpmcbjkrzXkbrVj7jSj7fRmsHZl73Vq9XwocnioMfgnsXencPcmL7Vps/os+L9nMTcmcDXqdTso8zksd32rtnyteH3uOb6vOv9w/P+////BZXfmQAAAQB0Uk5T////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////AFP3ByUAAAUwSURBVHja7J3NayRFGMaftzM9+ZhETDZmWbInBQ+i91UCLiz4kYvr5qx48uTRgysoKAh7WcSrBxFE8LKC6EZccXEh3gUv+wcYVt2PrEl6MvZMpjxUVVd1T013PM2UPD+Gmaq3qnuaruZ95q2vkc6XqEfC5jN3zgSsf4TroonzIIQQEhetb155IiQVt/XHU8rkE09RlATSvjUkPFJ7GUO2BCGExCYgwK8NVZSoUsZmnX1Y0pADAMCf7pi3mrXiHFuCEELiExAVVg1nV75aqGERUSgrFwsAgEFrAABopwC6j3eLwOOaH3+EI5FltgQhhMQnIEc1xalz/LaL6nDpwCgGAAzaAAZACqRIu4AAAwGS41kMWksmEOnoSoa8nVe/R9gShBASn4D8NWpNhknJvbvxjSGwD+zrUGPE/Q9KlhZ2MQAg2PeK0kKyBLAnpoAQQkiEAvJPbQUJC4kA6LvoRfzi+aN5FD1jWomOGnSCAkIIIREKyJ1xTl1BlH6rFoRVxab2sPeIMR2cUCUoIIQQEp2ACN4d79YVarWjIVvuCFN1l/EsW4IQQuKLQPZKXr6Ym5sMSwKQ1IYM3tzcmeOZYEWpCzZusyUIISQ+Abk7Lvw4+drA1XtYvbcK3B9fv76Til1YhBASG/Lk7mpjnVohqQpNeGn6yDnK7LIlCCEktgjkPi6PyELaTwuTACqxYyECINGrDMX0agkAUYBgD1ixaSRQlTlblrNaMdYL0VjfBV5nSxBCSGwC8tVFGyI4Y98mzJCIMgs2RNuMJLhREW1advUFQ0FYP4DfzwJYjyD6+CKSNnyNjzEhZBLIjxevBqw28REAfOAsYl9lCfGWA4rLSw7MIqwigWGPOKKQT6bkOrj5CyFkKgXE8+0VBfEExFOQh95hK94Zikim60pgO7HwW52akAae5i0ghExaQB7rvmeSMwFvfuwZimDC7WqF03YflLnyoX2bMwfl/vGwNrcn1mw0IqL4zBBCiPbxa9d/qnjIYjQk7adGQZx3H1GRwu3PeYZ+tTAsDnYzLDWl2tGPoglTPsWEkAkJSPb2iFuXko/XZDrd0bmuKSvvhSi+xCTjf7dfeedK5Ts/ZEv8V97nLSCETFhATh++UfuL1kQanUIhMmCtTkCQYcUubz/1QBds4WblSy7YxPbm9ub25rZf9jGbhRBCYhCQ7z6rxB+BZOqUBDmWIch0ZhFATycXvBlY2cgZRjqptkq5r0u5ebZLI5u8BYSQCdP6+3x5+qy/f2IxNtFPgZY3dq6kk1XP1F0YPbs7WfMwx6WX4rhjb07HZVxzyU/5GBNCJhKB3HjVrSh4IRQw3LCJF4si13cFPY/qUZO7u+j0RO+QMhgbgpT4vJT7fprv2Mt8aAghBABadifdDexsHGrbL8CGk5LnrOvPSkrQB9DOzZ8SPtQfB+iZyVhS7NFYmYg1RkfK5qnunuGSFUIIsQKi2QF2bFq5JLBhZlDZ2b26M0pSADly/b+2OYA2sAQc9IA5M+tKXPUiEVhGIVxaQQghMQqI+vnyqEcXuL6oHBlMNjOyoKdkdQEsIi9m+MKOfvd6wYgjCV/BrecBXGdLEEJIZMgPl84VquHbQ5lgd9RiuW9rHnZe1ti/kgp0At1iSxBCSGQUYYEav0mHuDoBDu3iQl16BLsoXQUPF+4GQggh/wtaCs+ES9qegmQdXwk6biV64D2DXUh4CsADYAtmGeGF8Zdxky1BCCGRId+epFKjrdoBVsqueWk/+kiGCfSLU5sIISQ6/gUAAP//AwB2PSXhkCthvAAAAABJRU5ErkJggg==); height:30px; background-repeat:no-repeat; color:white; text-shadow: #012b4d 2px 2px 2px; text-align: center; } .grip{ width:20px; height:15px; margin-top:-3px; background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAAhCAYAAACFtMg3AAAACXBIWXMAAAsTAAALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAB/5JREFUeNrkmHlsFNcdxz/v7doYwxpfJXXMDWkogZYYis2ppJEohZaGEKVBRVEplEIE5bIxmHCl5QqUcAVRpa0UqaqaSk16KEENpiVpS0qQaRMCxCngQGhMYL3rvbzHzLzXP9bH2Ltmdw1SUPuTRnozO/OZ932/37557ytisZjGFieDmk4XAGVZxAyDcDhKOBgi2BLG1xzg0383sGPJY3QNpVR7O1Ln7/SbpUxi0SjRSJRIOEQkGsUyFS2hABcuneOp7cva75VSJrBPZJ3F3kONxlSKiBkjGI0SikYIxiK4Q0GuXW/ipYrlnZ4XdsEng5oVq/eCZYIVQyiDVFFS3I/f712TVHCkzs/uympMFIY2MYVKycvvX0zNy7uTij6RdZbpRw+AoXGGTZyR1LxBvQt5f86+Dl4nsVUHyTQa3T6+tfonCdcjdX72VW3ImNd8w832b1clF1t7KGPe1bCH0a+u7BA8dv5m3mw2E8ROmlxGrOkaABOnjE/atov+wtxqxi89RNl3NtN4JpQgdlzFRBrDTQCML5+UtG0XvejBpyn/wSHGfO8Z/iTPJYidNuYBsi94AZjy4KikbbvoXi/MZeSWXYhfn2/UVTUHKSwqBKVAW2CZaK0QykRrjRSgzFinEXc64m3TUigNXp+fh2bMZdb0UVTVHGS2uy+F6hJKK0wUWmtMLLTWIASmthBSdIx8K09ZirDO5w2XYNqsOG/11ufxPOEn0jcGlgWGRpoKaSgwFVJKdNRCSonQ8X9oVmvxxoQGE/KO5TCjYj7SCGTziyOb8DQ1tU4CEi0EkyaNJeq+ClpRMXEsMe8nKKWomFxGxH2VmGESM0yUUnib/Ty7rZLFCx+mjfeH4gBuMYy2+WVsRQXXWm6itKasvJzGFjdaacomTOQ/ITeWYWEZFiGVR21fwZadHbyX9j9L4W9c5PiyW2cezdTRI8k6F+/zlAdGkl3fhFKKyV+6H+e5m0SVSVSZEFPkHcvhuaqNLF74MEJrzc/fuqgH3lfCoqe3UZCf35plA7QCZcXPtY6fdwmvL8DGLZUMGzGER4rjKbPzZrpzKTIvYaJQWmGhUFqjhE5ghelHrUtS86PkvAWrtuJ5LECkTwtYGmHEsywMC6EFqLbxiLOFJcir7c22lRvbeRJg4bQR4tL5Bo4cqMbr9bbVGCAQQgCtpSdkF7FB1q6vpKS0o3Ndea8XhXA7hyKEQNqel1p0Eeui1iVZ80z3vBd3b6DwFRc5gRwQoIVAC0BKtJC2KbhV7PFcNi3d2InXfsvSR0aL+vc/4vD+arzNvrg46YyD2o4uYldUVdJ/wFC+MVCKrtmy844WteB2DkMADiRSJBPrZNm6qpS8n+6qoei3LnJCvcEp0E6JdgiQHRUjtIO847lUL9qYwOukYtXXvyzO/vMyh55f25FpIVuzTPu51x9k8YqVFN5TyhPDhejuk2DnvVEQ5EbWYKSQ7aKlFu1iF65elTbv8HOtogM5aIdAO+IZ1kIitCDvWA4/fGp9Ul7CUmb9o2XinX9cZN+etXh9PhCOTln2+vzMX7SMvIISFozOFqm+g3ZebUGE61mDOzIr+nLclcW8Jcsz5h3Yvo6i3+WRE+yFloDUCA39juWy6PGqbnlCa50UXP3LOj1u3AAqq/dQ0M+FUCYebzNPLlhO/5JBVE7pJTJZANh5D3ljuAzF0TzB49+/Pd7qjbtomu3DzI5R8Mccvvvo2lvyuhUMsOTFd/TUigGs37AXlGLmk0so6F/Kjul9BD0IO09aihnz7gyveusejN5h5nxtVWqe1vqWx4IjZ/Sv6j16yOw1euVrhk51/93Ok6lG8dNAmDMXJcWFeVzzBrjd+Kx5tyzp/8WQ/J+Fs7sfXr18E78/8MVoJPqB56anornxRsvOxXPe7emL7hae0zCMpI5Hfn4eSimcDgeWYSIMg441Zvdxt/PSdjw8vgBXXj+UVgfbHA//e0F2VNZgojC1iYUFQFNuLqelM21eW/tNRz2zXtkfdzwiJo5InOe6kYvzVFZaPGe3jocjG+3Itn2+/BmVXKTOz+GqDeRqM8HiydKCTCfLdsejjwJDo6SJU8Z5Zo7AkSavW8cDYFLFaIzr9Rl1bOz8zZxym+xat6nT9SzhpLx8coK7kQ7vRCDEzNeOdLpu9nYy6StjEtyNlCXdneMRz2rc9cCRjedmI1ePvpCyZF6+cF1X1RxkUJ8Cpl75EIcVz3DbIsfE4pM8F3+XMiNe7rA+XPxqA5Y0oLXK21yPwo9c9DrhSIvXreNhdz20SH/l18a7GvJwsnQElm1b2eZ6ZFLMbbyWy0GG/WUg0pIgdILrkfZ3eP6EQnGl/mN+dngDHtuWECE5eeoc2SX3J+yFbxV2XkO0mbdLRyCQaK058/ZJSnsXpzOZJuVFLkUY+tdBCCRoeOvsBWKjCrEyWE1073hoBWYMYYQhGsxoFO28yzEfp+4dDnbHQ5k95sXqoww5OQCBzfHQVuYrrZSOR4Zh5100A9R9/r52x6MnYeeZF0wGnR7Q4XhIkbngtB2PDMLO+9Dycab/cOQd4qn3TAbWlcTtHYfsmeBbOh49DDvvA4K8WzwMh74zPP6luffsPRlNqknf/ON5E8SpU1fYs3MN3ubmHpV0d7zzIkhDfvEd4zlOC4oaCm5/t7Rr/jjx579dYce2VXh9/oxm1lS8j6PhO8qLNsTS56XrKAz+5qrbdifuBl7ajsfnivLvyH70s+b9dwCwRlJdGtc2+wAAAABJRU5ErkJggg==); margin-left:-5px; position:relative; z-index:88; cursor:e-resize; } .grip:hover{ background-position-x:-20px; } .JCLRLastGrip .grip{ background-position-y:-18px; left:-2px; } .dragging .grip{ background-position-x:-40px; } .sampleText{ position:relative; width:100%; } .dotted{ background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAECAYAAABCxiV9AAAACXBIWXMAAAsTAAALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAADdJREFUeNpi/P//PwMy4IuYWsfAwMDxaUV2FRMDJvBiYGDwZmBgYGBE14kMmBjwAAAAAAD//wMAlfQKloYQcBIAAAAASUVORK5CYII=); background-repeat:repeat-y; } input.check{ } #sample2Txt{ float:right; } label{ color:#0361ae; } .scroll{ overflow:scroll; max-width:100%; position:relative; } </style> <script type="text/javascript"> /*! * jQuery JavaScript Library v1.6.2 * http://jquery.com/ * * Copyright 2011, John Resig * Dual licensed under the MIT or GPL Version 2 licenses. * http://jquery.org/license * * Includes Sizzle.js * http://sizzlejs.com/ * Copyright 2011, The Dojo Foundation * Released under the MIT, BSD, and GPL Licenses. * * Date: Thu Jun 30 14:16:56 2011 -0400 */ (function(a,b){function cv(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cs(a){if(!cg[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){ch||(ch=c.createElement("iframe"),ch.frameBorder=ch.width=ch.height=0),b.appendChild(ch);if(!ci||!ch.createElement)ci=(ch.contentWindow||ch.contentDocument).document,ci.write((c.compatMode==="CSS1Compat"?"<!doctype html>":"")+"<html><body>"),ci.close();d=ci.createElement(a),ci.body.appendChild(d),e=f.css(d,"display"),b.removeChild(ch)}cg[a]=e}return cg[a]}function cr(a,b){var c={};f.each(cm.concat.apply([],cm.slice(0,b)),function(){c[this]=a});return c}function cq(){cn=b}function cp(){setTimeout(cq,0);return cn=f.now()}function cf(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ce(){try{return new a.XMLHttpRequest}catch(b){}}function b$(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g<i;g++){if(g===1)for(h in a.converters)typeof h=="string"&&(e[h.toLowerCase()]=a.converters[h]);l=k,k=d[g];if(k==="*")k=l;else if(l!=="*"&&l!==k){m=l+" "+k,n=e[m]||e["* "+k];if(!n){p=b;for(o in e){j=o.split(" ");if(j[0]===l||j[0]==="*"){p=e[j[1]+" "+k];if(p){o=e[o],o===!0?n=p:p===!0&&(n=o);break}}}}!n&&!p&&f.error("No conversion from "+m.replace(" "," to ")),n!==!0&&(c=n?n(c):p(o(c)))}}return c}function bZ(a,c,d){var e=a.contents,f=a.dataTypes,g=a.responseFields,h,i,j,k;for(i in g)i in d&&(c[g[i]]=d[i]);while(f[0]==="*")f.shift(),h===b&&(h=a.mimeType||c.getResponseHeader("content-type"));if(h)for(i in e)if(e[i]&&e[i].test(h)){f.unshift(i);break}if(f[0]in d)j=f[0];else{for(i in d){if(!f[0]||a.converters[i+" "+f[0]]){j=i;break}k||(k=i)}j=j||k}if(j){j!==f[0]&&f.unshift(j);return d[j]}}function bY(a,b,c,d){if(f.isArray(b))f.each(b,function(b,e){c||bC.test(a)?d(a,e):bY(a+"["+(typeof e=="object"||f.isArray(e)?b:"")+"]",e,c,d)});else if(!c&&b!=null&&typeof b=="object")for(var e in b)bY(a+"["+e+"]",b[e],c,d);else d(a,b)}function bX(a,c,d,e,f,g){f=f||c.dataTypes[0],g=g||{},g[f]=!0;var h=a[f],i=0,j=h?h.length:0,k=a===bR,l;for(;i<j&&(k||!l);i++)l=h[i](c,d,e),typeof l=="string"&&(!k||g[l]?l=b:(c.dataTypes.unshift(l),l=bX(a,c,d,e,l,g)));(k||!l)&&!g["*"]&&(l=bX(a,c,d,e,"*",g));return l}function bW(a){return function(b,c){typeof b!="string"&&(c=b,b="*");if(f.isFunction(c)){var d=b.toLowerCase().split(bN),e=0,g=d.length,h,i,j;for(;e<g;e++)h=d[e],j=/^\+/.test(h),j&&(h=h.substr(1)||"*"),i=a[h]=a[h]||[],i[j?"unshift":"push"](c)}}}function bA(a,b,c){var d=b==="width"?a.offsetWidth:a.offsetHeight,e=b==="width"?bv:bw;if(d>0){c!=="border"&&f.each(e,function(){c||(d-=parseFloat(f.css(a,"padding"+this))||0),c==="margin"?d+=parseFloat(f.css(a,c+this))||0:d-=parseFloat(f.css(a,"border"+this+"Width"))||0});return d+"px"}d=bx(a,b,b);if(d<0||d==null)d=a.style[b]||0;d=parseFloat(d)||0,c&&f.each(e,function(){d+=parseFloat(f.css(a,"padding"+this))||0,c!=="padding"&&(d+=parseFloat(f.css(a,"border"+this+"Width"))||0),c==="margin"&&(d+=parseFloat(f.css(a,c+this))||0)});return d+"px"}function bm(a,b){b.src?f.ajax({url:b.src,async:!1,dataType:"script"}):f.globalEval((b.text||b.textContent||b.innerHTML||"").replace(be,"/*$0*/")),b.parentNode&&b.parentNode.removeChild(b)}function bl(a){f.nodeName(a,"input")?bk(a):"getElementsByTagName"in a&&f.grep(a.getElementsByTagName("input"),bk)}function bk(a){if(a.type==="checkbox"||a.type==="radio")a.defaultChecked=a.checked}function bj(a){return"getElementsByTagName"in a?a.getElementsByTagName("*"):"querySelectorAll"in a?a.querySelectorAll("*"):[]}function bi(a,b){var c;if(b.nodeType===1){b.clearAttributes&&b.clearAttributes(),b.mergeAttributes&&b.mergeAttributes(a),c=b.nodeName.toLowerCase();if(c==="object")b.outerHTML=a.outerHTML;else if(c!=="input"||a.type!=="checkbox"&&a.type!=="radio"){if(c==="option")b.selected=a.defaultSelected;else if(c==="input"||c==="textarea")b.defaultValue=a.defaultValue}else a.checked&&(b.defaultChecked=b.checked=a.checked),b.value!==a.value&&(b.value=a.value);b.removeAttribute(f.expando)}}function bh(a,b){if(b.nodeType===1&&!!f.hasData(a)){var c=f.expando,d=f.data(a),e=f.data(b,d);if(d=d[c]){var g=d.events;e=e[c]=f.extend({},d);if(g){delete e.handle,e.events={};for(var h in g)for(var i=0,j=g[h].length;i<j;i++)f.event.add(b,h+(g[h][i].namespace?".":"")+g[h][i].namespace,g[h][i],g[h][i].data)}}}}function bg(a,b){return f.nodeName(a,"table")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function W(a,b,c){b=b||0;if(f.isFunction(b))return f.grep(a,function(a,d){var e=!!b.call(a,d,a);return e===c});if(b.nodeType)return f.grep(a,function(a,d){return a===b===c});if(typeof b=="string"){var d=f.grep(a,function(a){return a.nodeType===1});if(R.test(b))return f.filter(b,d,!c);b=f.filter(b,d)}return f.grep(a,function(a,d){return f.inArray(a,b)>=0===c})}function V(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function N(a,b){return(a&&a!=="*"?a+".":"")+b.replace(z,"`").replace(A,"&")}function M(a){var b,c,d,e,g,h,i,j,k,l,m,n,o,p=[],q=[],r=f._data(this,"events");if(!(a.liveFired===this||!r||!r.live||a.target.disabled||a.button&&a.type==="click")){a.namespace&&(n=new RegExp("(^|\\.)"+a.namespace.split(".").join("\\.(?:.*\\.)?")+"(\\.|$)")),a.liveFired=this;var s=r.live.slice(0);for(i=0;i<s.length;i++)g=s[i],g.origType.replace(x,"")===a.type?q.push(g.selector):s.splice(i--,1);e=f(a.target).closest(q,a.currentTarget);for(j=0,k=e.length;j<k;j++){m=e[j];for(i=0;i<s.length;i++){g=s[i];if(m.selector===g.selector&&(!n||n.test(g.namespace))&&!m.elem.disabled){h=m.elem,d=null;if(g.preType==="mouseenter"||g.preType==="mouseleave")a.type=g.preType,d=f(a.relatedTarget).closest(g.selector)[0],d&&f.contains(h,d)&&(d=h);(!d||d!==h)&&p.push({elem:h,handleObj:g,level:m.level})}}}for(j=0,k=p.length;j<k;j++){e=p[j];if(c&&e.level>c)break;a.currentTarget=e.elem,a.data=e.handleObj.data,a.handleObj=e.handleObj,o=e.handleObj.origHandler.apply(e.elem,arguments);if(o===!1||a.isPropagationStopped()){c=e.level,o===!1&&(b=!1);if(a.isImmediatePropagationStopped())break}}return b}}function K(a,c,d){var e=f.extend({},d[0]);e.type=a,e.originalEvent={},e.liveFired=b,f.event.handle.call(c,e),e.isDefaultPrevented()&&d[0].preventDefault()}function E(){return!0}function D(){return!1}function m(a,c,d){var e=c+"defer",g=c+"queue",h=c+"mark",i=f.data(a,e,b,!0);i&&(d==="queue"||!f.data(a,g,b,!0))&&(d==="mark"||!f.data(a,h,b,!0))&&setTimeout(function(){!f.data(a,g,b,!0)&&!f.data(a,h,b,!0)&&(f.removeData(a,e,!0),i.resolve())},0)}function l(a){for(var b in a)if(b!=="toJSON")return!1;return!0}function k(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(j,"$1-$2").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNaN(d)?i.test(d)?f.parseJSON(d):d:parseFloat(d)}catch(g){}f.data(a,c,d)}else d=b}return d}var c=a.document,d=a.navigator,e=a.location,f=function(){function J(){if(!e.isReady){try{c.documentElement.doScroll("left")}catch(a){setTimeout(J,1);return}e.ready()}}var e=function(a,b){return new e.fn.init(a,b,h)},f=a.jQuery,g=a.$,h,i=/^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/\d/,n=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,o=/^[\],:{}\s]*$/,p=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,q=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,r=/(?:^|:|,)(?:\s*\[)+/g,s=/(webkit)[ \/]([\w.]+)/,t=/(opera)(?:.*version)?[ \/]([\w.]+)/,u=/(msie) ([\w.]+)/,v=/(mozilla)(?:.*? rv:([\w.]+))?/,w=/-([a-z])/ig,x=function(a,b){return b.toUpperCase()},y=d.userAgent,z,A,B,C=Object.prototype.toString,D=Object.prototype.hasOwnProperty,E=Array.prototype.push,F=Array.prototype.slice,G=String.prototype.trim,H=Array.prototype.indexOf,I={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=n.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.6.2",length:0,size:function(){return this.length},toArray:function(){return F.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?E.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),A.done(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(F.apply(this,arguments),"slice",F.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:E,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j<k;j++)if((a=arguments[j])!=null)for(c in a){d=i[c],f=a[c];if(i===f)continue;l&&f&&(e.isPlainObject(f)||(g=e.isArray(f)))?(g?(g=!1,h=d&&e.isArray(d)?d:[]):h=d&&e.isPlainObject(d)?d:{},i[c]=e.extend(l,h,f)):f!==b&&(i[c]=f)}return i},e.extend({noConflict:function(b){a.$===e&&(a.$=g),b&&a.jQuery===e&&(a.jQuery=f);return e},isReady:!1,readyWait:1,holdReady:function(a){a?e.readyWait++:e.ready(!0)},ready:function(a){if(a===!0&&!--e.readyWait||a!==!0&&!e.isReady){if(!c.body)return setTimeout(e.ready,1);e.isReady=!0;if(a!==!0&&--e.readyWait>0)return;A.resolveWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").unbind("ready")}},bindReady:function(){if(!A){A=e._Deferred();if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",B,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",B),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&J()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a&&typeof a=="object"&&"setInterval"in a},isNaN:function(a){return a==null||!m.test(a)||isNaN(a)},type:function(a){return a==null?String(a):I[C.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;if(a.constructor&&!D.call(a,"constructor")&&!D.call(a.constructor.prototype,"isPrototypeOf"))return!1;var c;for(c in a);return c===b||D.call(a,c)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw a},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(o.test(b.replace(p,"@").replace(q,"]").replace(r,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(b,c,d){a.DOMParser?(d=new DOMParser,c=d.parseFromString(b,"text/xml")):(c=new ActiveXObject("Microsoft.XMLDOM"),c.async="false",c.loadXML(b)),d=c.documentElement,(!d||!d.nodeName||d.nodeName==="parsererror")&&e.error("Invalid XML: "+b);return c},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(w,x)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g<h;)if(c.apply(a[g++],d)===!1)break}else if(i){for(f in a)if(c.call(a[f],f,a[f])===!1)break}else for(;g<h;)if(c.call(a[g],g,a[g++])===!1)break;return a},trim:G?function(a){return a==null?"":G.call(a)}:function(a){return a==null?"":(a+"").replace(k,"").replace(l,"")},makeArray:function(a,b){var c=b||[];if(a!=null){var d=e.type(a);a.length==null||d==="string"||d==="function"||d==="regexp"||e.isWindow(a)?E.call(c,a):e.merge(c,a)}return c},inArray:function(a,b){if(H)return H.call(b,a);for(var c=0,d=b.length;c<d;c++)if(b[c]===a)return c;return-1},merge:function(a,c){var d=a.length,e=0;if(typeof c.length=="number")for(var f=c.length;e<f;e++)a[d++]=c[e];else while(c[e]!==b)a[d++]=c[e++];a.length=d;return a},grep:function(a,b,c){var d=[],e;c=!!c;for(var f=0,g=a.length;f<g;f++)e=!!b(a[f],f),c!==e&&d.push(a[f]);return d},map:function(a,c,d){var f,g,h=[],i=0,j=a.length,k=a instanceof e||j!==b&&typeof j=="number"&&(j>0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i<j;i++)f=c(a[i],i,d),f!=null&&(h[h.length]=f);else for(g in a)f=c(a[g],g,d),f!=null&&(h[h.length]=f);return h.concat.apply([],h)},guid:1,proxy:function(a,c){if(typeof c=="string"){var d=a[c];c=a,a=d}if(!e.isFunction(a))return b;var f=F.call(arguments,2),g=function(){return a.apply(c,f.concat(F.call(arguments)))};g.guid=a.guid=a.guid||g.guid||e.guid++;return g},access:function(a,c,d,f,g,h){var i=a.length;if(typeof c=="object"){for(var j in c)e.access(a,j,c[j],f,g,d);return a}if(d!==b){f=!h&&f&&e.isFunction(d);for(var k=0;k<i;k++)g(a[k],c,f?d.call(a[k],k,g(a[k],c)):d,h);return a}return i?g(a[0],c):b},now:function(){return(new Date).getTime()},uaMatch:function(a){a=a.toLowerCase();var b=s.exec(a)||t.exec(a)||u.exec(a)||a.indexOf("compatible")<0&&v.exec(a)||[];return{browser:b[1]||"",version:b[2]||"0"}},sub:function(){function a(b,c){return new a.fn.init(b,c)}e.extend(!0,a,this),a.superclass=this,a.fn=a.prototype=this(),a.fn.constructor=a,a.sub=this.sub,a.fn.init=function(d,f){f&&f instanceof e&&!(f instanceof a)&&(f=a(f));return e.fn.init.call(this,d,f,b)},a.fn.init.prototype=a.fn;var b=a(c);return a},browser:{}}),e.each("Boolean Number String Function Array Date RegExp Object".split(" "),function(a,b){I["[object "+b+"]"]=b.toLowerCase()}),z=e.uaMatch(y),z.browser&&(e.browser[z.browser]=!0,e.browser.version=z.version),e.browser.webkit&&(e.browser.safari=!0),j.test(" ")&&(k=/^[\s\xA0]+/,l=/[\s\xA0]+$/),h=e(c),c.addEventListener?B=function(){c.removeEventListener("DOMContentLoaded",B,!1),e.ready()}:c.attachEvent&&(B=function(){c.readyState==="complete"&&(c.detachEvent("onreadystatechange",B),e.ready())});return e}(),g="done fail isResolved isRejected promise then always pipe".split(" "),h=[].slice;f.extend({_Deferred:function(){var a=[],b,c,d,e={done:function(){if(!d){var c=arguments,g,h,i,j,k;b&&(k=b,b=0);for(g=0,h=c.length;g<h;g++)i=c[g],j=f.type(i),j==="array"?e.done.apply(e,i):j==="function"&&a.push(i);k&&e.resolveWith(k[0],k[1])}return this},resolveWith:function(e,f){if(!d&&!b&&!c){f=f||[],c=1;try{while(a[0])a.shift().apply(e,f)}finally{b=[e,f],c=0}}return this},resolve:function(){e.resolveWith(this,arguments);return this},isResolved:function(){return!!c||!!b},cancel:function(){d=1,a=[];return this}};return e},Deferred:function(a){var b=f._Deferred(),c=f._Deferred(),d;f.extend(b,{then:function(a,c){b.done(a).fail(c);return this},always:function(){return b.done.apply(b,arguments).fail.apply(this,arguments)},fail:c.done,rejectWith:c.resolveWith,reject:c.resolve,isRejected:c.isResolved,pipe:function(a,c){return f.Deferred(function(d){f.each({done:[a,"resolve"],fail:[c,"reject"]},function(a,c){var e=c[0],g=c[1],h;f.isFunction(e)?b[a](function(){h=e.apply(this,arguments),h&&f.isFunction(h.promise)?h.promise().then(d.resolve,d.reject):d[g](h)}):b[a](d[g])})}).promise()},promise:function(a){if(a==null){if(d)return d;d=a={}}var c=g.length;while(c--)a[g[c]]=b[g[c]];return a}}),b.done(c.cancel).fail(b.cancel),delete b.cancel,a&&a.call(b,b);return b},when:function(a){function i(a){return function(c){b[a]=arguments.length>1?h.call(arguments,0):c,--e||g.resolveWith(g,h.call(b,0))}}var b=arguments,c=0,d=b.length,e=d,g=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred();if(d>1){for(;c<d;c++)b[c]&&f.isFunction(b[c].promise)?b[c].promise().then(i(c),g.reject):--e;e||g.resolveWith(g,b)}else g!==a&&g.resolveWith(g,d?[a]:[]);return g.promise()}}),f.support=function(){var a=c.createElement("div"),b=c.documentElement,d,e,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u;a.setAttribute("className","t"),a.innerHTML=" <link/><table></table><a href='/a' style='top:1px;float:left;opacity:.55;'>a</a><input type='checkbox'/>",d=a.getElementsByTagName("*"),e=a.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=a.getElementsByTagName("input")[0],k={leadingWhitespace:a.firstChild.nodeType===3,tbody:!a.getElementsByTagName("tbody").length,htmlSerialize:!!a.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55$/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:a.className!=="t",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0},i.checked=!0,k.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,k.optDisabled=!h.disabled;try{delete a.test}catch(v){k.deleteExpando=!1}!a.addEventListener&&a.attachEvent&&a.fireEvent&&(a.attachEvent("onclick",function(){k.noCloneEvent=!1}),a.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),k.radioValue=i.value==="t",i.setAttribute("checked","checked"),a.appendChild(i),l=c.createDocumentFragment(),l.appendChild(a.firstChild),k.checkClone=l.cloneNode(!0).cloneNode(!0).lastChild.checked,a.innerHTML="",a.style.width=a.style.paddingLeft="1px",m=c.getElementsByTagName("body")[0],o=c.createElement(m?"div":"body"),p={visibility:"hidden",width:0,height:0,border:0,margin:0},m&&f.extend(p,{position:"absolute",left:-1e3,top:-1e3});for(t in p)o.style[t]=p[t];o.appendChild(a),n=m||b,n.insertBefore(o,n.firstChild),k.appendChecked=i.checked,k.boxModel=a.offsetWidth===2,"zoom"in a.style&&(a.style.display="inline",a.style.zoom=1,k.inlineBlockNeedsLayout=a.offsetWidth===2,a.style.display="",a.innerHTML="<div style='width:4px;'></div>",k.shrinkWrapBlocks=a.offsetWidth!==2),a.innerHTML="<table><tr><td style='padding:0;border:0;display:none'></td><td>t</td></tr></table>",q=a.getElementsByTagName("td"),u=q[0].offsetHeight===0,q[0].style.display="",q[1].style.display="none",k.reliableHiddenOffsets=u&&q[0].offsetHeight===0,a.innerHTML="",c.defaultView&&c.defaultView.getComputedStyle&&(j=c.createElement("div"),j.style.width="0",j.style.marginRight="0",a.appendChild(j),k.reliableMarginRight=(parseInt((c.defaultView.getComputedStyle(j,null)||{marginRight:0}).marginRight,10)||0)===0),o.innerHTML="",n.removeChild(o);if(a.attachEvent)for(t in{submit:1,change:1,focusin:1})s="on"+t,u=s in a,u||(a.setAttribute(s,"return;"),u=typeof a[s]=="function"),k[t+"Bubbles"]=u;o=l=g=h=m=j=a=i=null;return k}(),f.boxModel=f.support.boxModel;var i=/^(?:\{.*\}|\[.*\])$/,j=/([a-z])([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!l(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g=f.expando,h=typeof c=="string",i,j=a.nodeType,k=j?f.cache:a,l=j?a[f.expando]:a[f.expando]&&f.expando;if((!l||e&&l&&!k[l][g])&&h&&d===b)return;l||(j?a[f.expando]=l=++f.uuid:l=f.expando),k[l]||(k[l]={},j||(k[l].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?k[l][g]=f.extend(k[l][g],c):k[l]=f.extend(k[l],c);i=k[l],e&&(i[g]||(i[g]={}),i=i[g]),d!==b&&(i[f.camelCase(c)]=d);if(c==="events"&&!i[c])return i[g]&&i[g].events;return h?i[f.camelCase(c)]||i[c]:i}},removeData:function(b,c,d){if(!!f.acceptData(b)){var e=f.expando,g=b.nodeType,h=g?f.cache:b,i=g?b[f.expando]:f.expando;if(!h[i])return;if(c){var j=d?h[i][e]:h[i];if(j){delete j[c];if(!l(j))return}}if(d){delete h[i][e];if(!l(h[i]))return}var k=h[i][e];f.support.deleteExpando||h!=a?delete h[i]:h[i]=null,k?(h[i]={},g||(h[i].toJSON=f.noop),h[i][e]=k):g&&(f.support.deleteExpando?delete b[f.expando]:b.removeAttribute?b.removeAttribute(f.expando):b[f.expando]=null)}},_data:function(a,b,c){return f.data(a,b,c,!0)},acceptData:function(a){if(a.nodeName){var b=f.noData[a.nodeName.toLowerCase()];if(b)return b!==!0&&a.getAttribute("classid")===b}return!0}}),f.fn.extend({data:function(a,c){var d=null;if(typeof a=="undefined"){if(this.length){d=f.data(this[0]);if(this[0].nodeType===1){var e=this[0].attributes,g;for(var h=0,i=e.length;h<i;h++)g=e[h].name,g.indexOf("data-")===0&&(g=f.camelCase(g.substring(5)),k(this[0],g,d[g]))}}return d}if(typeof a=="object")return this.each(function(){f.data(this,a)});var j=a.split(".");j[1]=j[1]?"."+j[1]:"";if(c===b){d=this.triggerHandler("getData"+j[1]+"!",[j[0]]),d===b&&this.length&&(d=f.data(this[0],a),d=k(this[0],a,d));return d===b&&j[1]?this.data(j[0]):d}return this.each(function(){var b=f(this),d=[j[0],c];b.triggerHandler("setData"+j[1]+"!",d),f.data(this,a,c),b.triggerHandler("changeData"+j[1]+"!",d)})},removeData:function(a){return this.each(function(){f.removeData(this,a)})}}),f.extend({_mark:function(a,c){a&&(c=(c||"fx")+"mark",f.data(a,c,(f.data(a,c,b,!0)||0)+1,!0))},_unmark:function(a,c,d){a!==!0&&(d=c,c=a,a=!1);if(c){d=d||"fx";var e=d+"mark",g=a?0:(f.data(c,e,b,!0)||1)-1;g?f.data(c,e,g,!0):(f.removeData(c,e,!0),m(c,d,"mark"))}},queue:function(a,c,d){if(a){c=(c||"fx")+"queue";var e=f.data(a,c,b,!0);d&&(!e||f.isArray(d)?e=f.data(a,c,f.makeArray(d),!0):e.push(d));return e||[]}},dequeue:function(a,b){b=b||"fx";var c=f.queue(a,b),d=c.shift(),e;d==="inprogress"&&(d=c.shift()),d&&(b==="fx"&&c.unshift("inprogress"),d.call(a,function(){f.dequeue(a,b)})),c.length||(f.removeData(a,b+"queue",!0),m(a,b,"queue"))}}),f.fn.extend({queue:function(a,c){typeof a!="string"&&(c=a,a="fx");if(c===b)return f.queue(this[0],a);return this.each(function(){var b=f.queue(this,a,c);a==="fx"&&b[0]!=="inprogress"&&f.dequeue(this,a)})},dequeue:function(a){return this.each(function(){f.dequeue(this,a)})},delay:function(a,b){a=f.fx?f.fx.speeds[a]||a:a,b=b||"fx";return this.queue(b,function(){var c=this;setTimeout(function(){f.dequeue(c,b)},a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,c){function m(){--h||d.resolveWith(e,[e])}typeof a!="string"&&(c=a,a=b),a=a||"fx";var d=f.Deferred(),e=this,g=e.length,h=1,i=a+"defer",j=a+"queue",k=a+"mark",l;while(g--)if(l=f.data(e[g],i,b,!0)||(f.data(e[g],j,b,!0)||f.data(e[g],k,b,!0))&&f.data(e[g],i,f._Deferred(),!0))h++,l.done(m);m();return d.promise()}});var n=/[\n\t\r]/g,o=/\s+/,p=/\r/g,q=/^(?:button|input)$/i,r=/^(?:button|input|object|select|textarea)$/i,s=/^a(?:rea)?$/i,t=/^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i,u=/\:|^on/,v,w;f.fn.extend({attr:function(a,b){return f.access(this,a,b,!0,f.attr)},removeAttr:function(a){return this.each(function(){f.removeAttr(this,a)})},prop:function(a,b){return f.access(this,a,b,!0,f.prop)},removeProp:function(a){a=f.propFix[a]||a;return this.each(function(){try{this[a]=b,delete this[a]}catch(c){}})},addClass:function(a){var b,c,d,e,g,h,i;if(f.isFunction(a))return this.each(function(b){f(this).addClass(a.call(this,b,this.className))});if(a&&typeof a=="string"){b=a.split(o);for(c=0,d=this.length;c<d;c++){e=this[c];if(e.nodeType===1)if(!e.className&&b.length===1)e.className=a;else{g=" "+e.className+" ";for(h=0,i=b.length;h<i;h++)~g.indexOf(" "+b[h]+" ")||(g+=b[h]+" ");e.className=f.trim(g)}}}return this},removeClass:function(a){var c,d,e,g,h,i,j;if(f.isFunction(a))return this.each(function(b){f(this).removeClass(a.call(this,b,this.className))});if(a&&typeof a=="string"||a===b){c=(a||"").split(o);for(d=0,e=this.length;d<e;d++){g=this[d];if(g.nodeType===1&&g.className)if(a){h=(" "+g.className+" ").replace(n," ");for(i=0,j=c.length;i<j;i++)h=h.replace(" "+c[i]+" "," ");g.className=f.trim(h)}else g.className=""}}return this},toggleClass:function(a,b){var c=typeof a,d=typeof b=="boolean";if(f.isFunction(a))return this.each(function(c){f(this).toggleClass(a.call(this,c,this.className,b),b)});return this.each(function(){if(c==="string"){var e,g=0,h=f(this),i=b,j=a.split(o);while(e=j[g++])i=d?i:!h.hasClass(e),h[i?"addClass":"removeClass"](e)}else if(c==="undefined"||c==="boolean")this.className&&f._data(this,"__className__",this.className),this.className=this.className||a===!1?"":f._data(this,"__className__")||""})},hasClass:function(a){var b=" "+a+" ";for(var c=0,d=this.length;c<d;c++)if((" "+this[c].className+" ").replace(n," ").indexOf(b)>-1)return!0;return!1},val:function(a){var c,d,e=this[0];if(!arguments.length){if(e){c=f.valHooks[e.nodeName.toLowerCase()]||f.valHooks[e.type];if(c&&"get"in c&&(d=c.get(e,"value"))!==b)return d;d=e.value;return typeof d=="string"?d.replace(p,""):d==null?"":d}return b}var g=f.isFunction(a);return this.each(function(d){var e=f(this),h;if(this.nodeType===1){g?h=a.call(this,d,e.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.nodeName.toLowerCase()]||f.valHooks[this.type];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c=a.selectedIndex,d=[],e=a.options,g=a.type==="select-one";if(c<0)return null;for(var h=g?c:0,i=g?c+1:e.length;h<i;h++){var j=e[h];if(j.selected&&(f.support.optDisabled?!j.disabled:j.getAttribute("disabled")===null)&&(!j.parentNode.disabled||!f.nodeName(j.parentNode,"optgroup"))){b=f(j).val();if(g)return b;d.push(b)}}if(g&&!d.length&&e.length)return f(e[c]).val();return d},set:function(a,b){var c=f.makeArray(b);f(a).find("option").each(function(){this.selected=f.inArray(f(this).val(),c)>=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attrFix:{tabindex:"tabIndex"},attr:function(a,c,d,e){var g=a.nodeType;if(!a||g===3||g===8||g===2)return b;if(e&&c in f.attrFn)return f(a)[c](d);if(!("getAttribute"in a))return f.prop(a,c,d);var h,i,j=g!==1||!f.isXMLDoc(a);j&&(c=f.attrFix[c]||c,i=f.attrHooks[c],i||(t.test(c)?i=w:v&&c!=="className"&&(f.nodeName(a,"form")||u.test(c))&&(i=v)));if(d!==b){if(d===null){f.removeAttr(a,c);return b}if(i&&"set"in i&&j&&(h=i.set(a,d,c))!==b)return h;a.setAttribute(c,""+d);return d}if(i&&"get"in i&&j&&(h=i.get(a,c))!==null)return h;h=a.getAttribute(c);return h===null?b:h},removeAttr:function(a,b){var c;a.nodeType===1&&(b=f.attrFix[b]||b,f.support.getSetAttribute?a.removeAttribute(b):(f.attr(a,b,""),a.removeAttributeNode(a.getAttributeNode(b))),t.test(b)&&(c=f.propFix[b]||b)in a&&(a[c]=!1))},attrHooks:{type:{set:function(a,b){if(q.test(a.nodeName)&&a.parentNode)f.error("type property can't be changed");else if(!f.support.radioValue&&b==="radio"&&f.nodeName(a,"input")){var c=a.value;a.setAttribute("type",b),c&&(a.value=c);return b}}},tabIndex:{get:function(a){var c=a.getAttributeNode("tabIndex");return c&&c.specified?parseInt(c.value,10):r.test(a.nodeName)||s.test(a.nodeName)&&a.href?0:b}},value:{get:function(a,b){if(v&&f.nodeName(a,"button"))return v.get(a,b);return b in a?a.value:null},set:function(a,b,c){if(v&&f.nodeName(a,"button"))return v.set(a,b,c);a.value=b}}},propFix:{tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},prop:function(a,c,d){var e=a.nodeType;if(!a||e===3||e===8||e===2)return b;var g,h,i=e!==1||!f.isXMLDoc(a);i&&(c=f.propFix[c]||c,h=f.propHooks[c]);return d!==b?h&&"set"in h&&(g=h.set(a,d,c))!==b?g:a[c]=d:h&&"get"in h&&(g=h.get(a,c))!==b?g:a[c]},propHooks:{}}),w={get:function(a,c){return f.prop(a,c)?c.toLowerCase():b},set:function(a,b,c){var d;b===!1?f.removeAttr(a,c):(d=f.propFix[c]||c,d in a&&(a[d]=!0),a.setAttribute(c,c.toLowerCase()));return c}},f.support.getSetAttribute||(f.attrFix=f.propFix,v=f.attrHooks.name=f.attrHooks.title=f.valHooks.button={get:function(a,c){var d;d=a.getAttributeNode(c);return d&&d.nodeValue!==""?d.nodeValue:b},set:function(a,b,c){var d=a.getAttributeNode(c);if(d){d.nodeValue=b;return b}}},f.each(["width","height"],function(a,b){f.attrHooks[b]=f.extend(f.attrHooks[b],{set:function(a,c){if(c===""){a.setAttribute(b,"auto");return c}}})})),f.support.hrefNormalized||f.each(["href","src","width","height"],function(a,c){f.attrHooks[c]=f.extend(f.attrHooks[c],{get:function(a){var d=a.getAttribute(c,2);return d===null?b:d}})}),f.support.style||(f.attrHooks.style={get:function(a){return a.style.cssText.toLowerCase()||b},set:function(a,b){return a.style.cssText=""+b}}),f.support.optSelected||(f.propHooks.selected=f.extend(f.propHooks.selected,{get:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}})),f.support.checkOn||f.each(["radio","checkbox"],function(){f.valHooks[this]={get:function(a){return a.getAttribute("value")===null?"on":a.value}}}),f.each(["radio","checkbox"],function(){f.valHooks[this]=f.extend(f.valHooks[this],{set:function(a,b){if(f.isArray(b))return a.checked=f.inArray(f(a).val(),b)>=0}})});var x=/\.(.*)$/,y=/^(?:textarea|input|select)$/i,z=/\./g,A=/ /g,B=/[^\w\s.|`]/g,C=function(a){return a.replace(B,"\\$&")};f.event={add:function(a,c,d,e){if(a.nodeType!==3&&a.nodeType!==8){if(d===!1)d=D;else if(!d)return;var g,h;d.handler&&(g=d,d=g.handler),d.guid||(d.guid=f.guid++);var i=f._data(a);if(!i)return;var j=i.events,k=i.handle;j||(i.events=j={}),k||(i.handle=k=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.handle.apply(k.elem,arguments):b}),k.elem=a,c=c.split(" ");var l,m=0,n;while(l=c[m++]){h=g?f.extend({},g):{handler:d,data:e},l.indexOf(".")>-1?(n=l.split("."),l=n.shift(),h.namespace=n.slice(0).sort().join(".")):(n=[],h.namespace=""),h.type=l,h.guid||(h.guid=d.guid);var o=j[l],p=f.event.special[l]||{};if(!o){o=j[l]=[];if(!p.setup||p.setup.call(a,e,n,k)===!1)a.addEventListener?a.addEventListener(l,k,!1):a.attachEvent&&a.attachEvent("on"+l,k)}p.add&&(p.add.call(a,h),h.handler.guid||(h.handler.guid=d.guid)),o.push(h),f.event.global[l]=!0}a=null}},global:{},remove:function(a,c,d,e){if(a.nodeType!==3&&a.nodeType!==8){d===!1&&(d=D);var g,h,i,j,k=0,l,m,n,o,p,q,r,s=f.hasData(a)&&f._data(a),t=s&&s.events;if(!s||!t)return;c&&c.type&&(d=c.handler,c=c.type);if(!c||typeof c=="string"&&c.charAt(0)==="."){c=c||"";for(h in t)f.event.remove(a,h+c);return}c=c.split(" ");while(h=c[k++]){r=h,q=null,l=h.indexOf(".")<0,m=[],l||(m=h.split("."),h=m.shift(),n=new RegExp("(^|\\.)"+f.map(m.slice(0).sort(),C).join("\\.(?:.*\\.)?")+"(\\.|$)")),p=t[h];if(!p)continue;if(!d){for(j=0;j<p.length;j++){q=p[j];if(l||n.test(q.namespace))f.event.remove(a,r,q.handler,j),p.splice(j--,1)}continue}o=f.event.special[h]||{};for(j=e||0;j<p.length;j++){q=p[j];if(d.guid===q.guid){if(l||n.test(q.namespace))e==null&&p.splice(j--,1),o.remove&&o.remove.call(a,q);if(e!=null)break}}if(p.length===0||e!=null&&p.length===1)(!o.teardown||o.teardown.call(a,m)===!1)&&f.removeEvent(a,h,s.handle),g=null,delete t[h]}if(f.isEmptyObject(t)){var u=s.handle;u&&(u.elem=null),delete s.events,delete s.handle,f.isEmptyObject(s)&&f.removeData(a,b,!0)}}},customEvent:{getData:!0,setData:!0,changeData:!0},trigger:function(c,d,e,g){var h=c.type||c,i=[],j;h.indexOf("!")>=0&&(h=h.slice(0,-1),j=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i. shift(),i.sort());if(!!e&&!f.event.customEvent[h]||!!f.event.global[h]){c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.exclusive=j,c.namespace=i.join("."),c.namespace_re=new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)");if(g||!e)c.preventDefault(),c.stopPropagation();if(!e){f.each(f.cache,function(){var a=f.expando,b=this[a];b&&b.events&&b.events[h]&&f.event.trigger(c,d,b.handle.elem)});return}if(e.nodeType===3||e.nodeType===8)return;c.result=b,c.target=e,d=d!=null?f.makeArray(d):[],d.unshift(c);var k=e,l=h.indexOf(":")<0?"on"+h:"";do{var m=f._data(k,"handle");c.currentTarget=k,m&&m.apply(k,d),l&&f.acceptData(k)&&k[l]&&k[l].apply(k,d)===!1&&(c.result=!1,c.preventDefault()),k=k.parentNode||k.ownerDocument||k===c.target.ownerDocument&&a}while(k&&!c.isPropagationStopped());if(!c.isDefaultPrevented()){var n,o=f.event.special[h]||{};if((!o._default||o._default.call(e.ownerDocument,c)===!1)&&(h!=="click"||!f.nodeName(e,"a"))&&f.acceptData(e)){try{l&&e[h]&&(n=e[l],n&&(e[l]=null),f.event.triggered=h,e[h]())}catch(p){}n&&(e[l]=n),f.event.triggered=b}}return c.result}},handle:function(c){c=f.event.fix(c||a.event);var d=((f._data(this,"events")||{})[c.type]||[]).slice(0),e=!c.exclusive&&!c.namespace,g=Array.prototype.slice.call(arguments,0);g[0]=c,c.currentTarget=this;for(var h=0,i=d.length;h<i;h++){var j=d[h];if(e||c.namespace_re.test(j.namespace)){c.handler=j.handler,c.data=j.data,c.handleObj=j;var k=j.handler.apply(this,g);k!==b&&(c.result=k,k===!1&&(c.preventDefault(),c.stopPropagation()));if(c.isImmediatePropagationStopped())break}}return c.result},props:"altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),fix:function(a){if(a[f.expando])return a;var d=a;a=f.Event(d);for(var e=this.props.length,g;e;)g=this.props[--e],a[g]=d[g];a.target||(a.target=a.srcElement||c),a.target.nodeType===3&&(a.target=a.target.parentNode),!a.relatedTarget&&a.fromElement&&(a.relatedTarget=a.fromElement===a.target?a.toElement:a.fromElement);if(a.pageX==null&&a.clientX!=null){var h=a.target.ownerDocument||c,i=h.documentElement,j=h.body;a.pageX=a.clientX+(i&&i.scrollLeft||j&&j.scrollLeft||0)-(i&&i.clientLeft||j&&j.clientLeft||0),a.pageY=a.clientY+(i&&i.scrollTop||j&&j.scrollTop||0)-(i&&i.clientTop||j&&j.clientTop||0)}a.which==null&&(a.charCode!=null||a.keyCode!=null)&&(a.which=a.charCode!=null?a.charCode:a.keyCode),!a.metaKey&&a.ctrlKey&&(a.metaKey=a.ctrlKey),!a.which&&a.button!==b&&(a.which=a.button&1?1:a.button&2?3:a.button&4?2:0);return a},guid:1e8,proxy:f.proxy,special:{ready:{setup:f.bindReady,teardown:f.noop},live:{add:function(a){f.event.add(this,N(a.origType,a.selector),f.extend({},a,{handler:M,guid:a.handler.guid}))},remove:function(a){f.event.remove(this,N(a.origType,a.selector),a)}},beforeunload:{setup:function(a,b,c){f.isWindow(this)&&(this.onbeforeunload=c)},teardown:function(a,b){this.onbeforeunload===b&&(this.onbeforeunload=null)}}}},f.removeEvent=c.removeEventListener?function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)}:function(a,b,c){a.detachEvent&&a.detachEvent("on"+b,c)},f.Event=function(a,b){if(!this.preventDefault)return new f.Event(a,b);a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||a.returnValue===!1||a.getPreventDefault&&a.getPreventDefault()?E:D):this.type=a,b&&f.extend(this,b),this.timeStamp=f.now(),this[f.expando]=!0},f.Event.prototype={preventDefault:function(){this.isDefaultPrevented=E;var a=this.originalEvent;!a||(a.preventDefault?a.preventDefault():a.returnValue=!1)},stopPropagation:function(){this.isPropagationStopped=E;var a=this.originalEvent;!a||(a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0)},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=E,this.stopPropagation()},isDefaultPrevented:D,isPropagationStopped:D,isImmediatePropagationStopped:D};var F=function(a){var b=a.relatedTarget,c=!1,d=a.type;a.type=a.data,b!==this&&(b&&(c=f.contains(this,b)),c||(f.event.handle.apply(this,arguments),a.type=d))},G=function(a){a.type=a.data,f.event.handle.apply(this,arguments)};f.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(a,b){f.event.special[a]={setup:function(c){f.event.add(this,b,c&&c.selector?G:F,a)},teardown:function(a){f.event.remove(this,b,a&&a.selector?G:F)}}}),f.support.submitBubbles||(f.event.special.submit={setup:function(a,b){if(!f.nodeName(this,"form"))f.event.add(this,"click.specialSubmit",function(a){var b=a.target,c=b.type;(c==="submit"||c==="image")&&f(b).closest("form").length&&K("submit",this,arguments)}),f.event.add(this,"keypress.specialSubmit",function(a){var b=a.target,c=b.type;(c==="text"||c==="password")&&f(b).closest("form").length&&a.keyCode===13&&K("submit",this,arguments)});else return!1},teardown:function(a){f.event.remove(this,".specialSubmit")}});if(!f.support.changeBubbles){var H,I=function(a){var b=a.type,c=a.value;b==="radio"||b==="checkbox"?c=a.checked:b==="select-multiple"?c=a.selectedIndex>-1?f.map(a.options,function(a){return a.selected}).join("-"):"":f.nodeName(a,"select")&&(c=a.selectedIndex);return c},J=function(c){var d=c.target,e,g;if(!!y.test(d.nodeName)&&!d.readOnly){e=f._data(d,"_change_data"),g=I(d),(c.type!=="focusout"||d.type!=="radio")&&f._data(d,"_change_data",g);if(e===b||g===e)return;if(e!=null||g)c.type="change",c.liveFired=b,f.event.trigger(c,arguments[1],d)}};f.event.special.change={filters:{focusout:J,beforedeactivate:J,click:function(a){var b=a.target,c=f.nodeName(b,"input")?b.type:"";(c==="radio"||c==="checkbox"||f.nodeName(b,"select"))&&J.call(this,a)},keydown:function(a){var b=a.target,c=f.nodeName(b,"input")?b.type:"";(a.keyCode===13&&!f.nodeName(b,"textarea")||a.keyCode===32&&(c==="checkbox"||c==="radio")||c==="select-multiple")&&J.call(this,a)},beforeactivate:function(a){var b=a.target;f._data(b,"_change_data",I(b))}},setup:function(a,b){if(this.type==="file")return!1;for(var c in H)f.event.add(this,c+".specialChange",H[c]);return y.test(this.nodeName)},teardown:function(a){f.event.remove(this,".specialChange");return y.test(this.nodeName)}},H=f.event.special.change.filters,H.focus=H.beforeactivate}f.support.focusinBubbles||f.each({focus:"focusin",blur:"focusout"},function(a,b){function e(a){var c=f.event.fix(a);c.type=b,c.originalEvent={},f.event.trigger(c,null,c.target),c.isDefaultPrevented()&&a.preventDefault()}var d=0;f.event.special[b]={setup:function(){d++===0&&c.addEventListener(a,e,!0)},teardown:function(){--d===0&&c.removeEventListener(a,e,!0)}}}),f.each(["bind","one"],function(a,c){f.fn[c]=function(a,d,e){var g;if(typeof a=="object"){for(var h in a)this[c](h,d,a[h],e);return this}if(arguments.length===2||d===!1)e=d,d=b;c==="one"?(g=function(a){f(this).unbind(a,g);return e.apply(this,arguments)},g.guid=e.guid||f.guid++):g=e;if(a==="unload"&&c!=="one")this.one(a,d,e);else for(var i=0,j=this.length;i<j;i++)f.event.add(this[i],a,g,d);return this}}),f.fn.extend({unbind:function(a,b){if(typeof a=="object"&&!a.preventDefault)for(var c in a)this.unbind(c,a[c]);else for(var d=0,e=this.length;d<e;d++)f.event.remove(this[d],a,b);return this},delegate:function(a,b,c,d){return this.live(b,c,d,a)},undelegate:function(a,b,c){return arguments.length===0?this.unbind("live"):this.die(b,null,c,a)},trigger:function(a,b){return this.each(function(){f.event.trigger(a,b,this)})},triggerHandler:function(a,b){if(this[0])return f.event.trigger(a,b,this[0],!0)},toggle:function(a){var b=arguments,c=a.guid||f.guid++,d=0,e=function(c){var e=(f.data(this,"lastToggle"+a.guid)||0)%d;f.data(this,"lastToggle"+a.guid,e+1),c.preventDefault();return b[e].apply(this,arguments)||!1};e.guid=c;while(d<b.length)b[d++].guid=c;return this.click(e)},hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}});var L={focus:"focusin",blur:"focusout",mouseenter:"mouseover",mouseleave:"mouseout"};f.each(["live","die"],function(a,c){f.fn[c]=function(a,d,e,g){var h,i=0,j,k,l,m=g||this.selector,n=g?this:f(this.context);if(typeof a=="object"&&!a.preventDefault){for(var o in a)n[c](o,d,a[o],m);return this}if(c==="die"&&!a&&g&&g.charAt(0)==="."){n.unbind(g);return this}if(d===!1||f.isFunction(d))e=d||D,d=b;a=(a||"").split(" ");while((h=a[i++])!=null){j=x.exec(h),k="",j&&(k=j[0],h=h.replace(x,""));if(h==="hover"){a.push("mouseenter"+k,"mouseleave"+k);continue}l=h,L[h]?(a.push(L[h]+k),h=h+k):h=(L[h]||h)+k;if(c==="live")for(var p=0,q=n.length;p<q;p++)f.event.add(n[p],"live."+N(h,m),{data:d,selector:m,handler:e,origType:h,origHandler:e,preType:l});else n.unbind("live."+N(h,m),e)}return this}}),f.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error".split(" "),function(a,b){f.fn[b]=function(a,c){c==null&&(c=a,a=null);return arguments.length>0?this.bind(b,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0)}),function(){function u(a,b,c,d,e,f){for(var g=0,h=d.length;g<h;g++){var i=d[g];if(i){var j=!1;i=i[a];while(i){if(i.sizcache===c){j=d[i.sizset];break}if(i.nodeType===1){f||(i.sizcache=c,i.sizset=g);if(typeof b!="string"){if(i===b){j=!0;break}}else if(k.filter(b,[i]).length>0){j=i;break}}i=i[a]}d[g]=j}}}function t(a,b,c,d,e,f){for(var g=0,h=d.length;g<h;g++){var i=d[g];if(i){var j=!1;i=i[a];while(i){if(i.sizcache===c){j=d[i.sizset];break}i.nodeType===1&&!f&&(i.sizcache=c,i.sizset=g);if(i.nodeName.toLowerCase()===b){j=i;break}i=i[a]}d[g]=j}}}var a=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d=0,e=Object.prototype.toString,g=!1,h=!0,i=/\\/g,j=/\W/;[0,0].sort(function(){h=!1;return 0});var k=function(b,d,f,g){f=f||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return f;var i,j,n,o,q,r,s,t,u=!0,w=k.isXML(d),x=[],y=b;do{a.exec(""),i=a.exec(y);if(i){y=i[3],x.push(i[1]);if(i[2]){o=i[3];break}}}while(i);if(x.length>1&&m.exec(b))if(x.length===2&&l.relative[x[0]])j=v(x[0]+x[1],d);else{j=l.relative[x[0]]?[d]:k(x.shift(),d);while(x.length)b=x.shift(),l.relative[b]&&(b+=x.shift()),j=v(b,j)}else{!g&&x.length>1&&d.nodeType===9&&!w&&l.match.ID.test(x[0])&&!l.match.ID.test(x[x.length-1])&&(q=k.find(x.shift(),d,w),d=q.expr?k.filter(q.expr,q.set)[0]:q.set[0]);if(d){q=g?{expr:x.pop(),set:p(g)}:k.find(x.pop(),x.length===1&&(x[0]==="~"||x[0]==="+")&&d.parentNode?d.parentNode:d,w),j=q.expr?k.filter(q.expr,q.set):q.set,x.length>0?n=p(j):u=!1;while(x.length)r=x.pop(),s=r,l.relative[r]?s=x.pop():r="",s==null&&(s=d),l.relative[r](n,s,w)}else n=x=[]}n||(n=j),n||k.error(r||b);if(e.call(n)==="[object Array]")if(!u)f.push.apply(f,n);else if(d&&d.nodeType===1)for(t=0;n[t]!=null;t++)n[t]&&(n[t]===!0||n[t].nodeType===1&&k.contains(d,n[t]))&&f.push(j[t]);else for(t=0;n[t]!=null;t++)n[t]&&n[t].nodeType===1&&f.push(j[t]);else p(n,f);o&&(k(o,h,f,g),k.uniqueSort(f));return f};k.uniqueSort=function(a){if(r){g=h,a.sort(r);if(g)for(var b=1;b<a.length;b++)a[b]===a[b-1]&&a.splice(b--,1)}return a},k.matches=function(a,b){return k(a,null,null,b)},k.matchesSelector=function(a,b){return k(b,null,null,[a]).length>0},k.find=function(a,b,c){var d;if(!a)return[];for(var e=0,f=l.order.length;e<f;e++){var g,h=l.order[e];if(g=l.leftMatch[h].exec(a)){var j=g[1];g.splice(1,1);if(j.substr(j.length-1)!=="\\"){g[1]=(g[1]||"").replace(i,""),d=l.find[h](g,b,c);if(d!=null){a=a.replace(l.match[h],"");break}}}}d||(d=typeof b.getElementsByTagName!="undefined"?b.getElementsByTagName("*"):[]);return{set:d,expr:a}},k.filter=function(a,c,d,e){var f,g,h=a,i=[],j=c,m=c&&c[0]&&k.isXML(c[0]);while(a&&c.length){for(var n in l.filter)if((f=l.leftMatch[n].exec(a))!=null&&f[2]){var o,p,q=l.filter[n],r=f[1];g=!1,f.splice(1,1);if(r.substr(r.length-1)==="\\")continue;j===i&&(i=[]);if(l.preFilter[n]){f=l.preFilter[n](f,j,d,i,e,m);if(!f)g=o=!0;else if(f===!0)continue}if(f)for(var s=0;(p=j[s])!=null;s++)if(p){o=q(p,f,s,j);var t=e^!!o;d&&o!=null?t?g=!0:j[s]=!1:t&&(i.push(p),g=!0)}if(o!==b){d||(j=i),a=a.replace(l.match[n],"");if(!g)return[];break}}if(a===h)if(g==null)k.error(a);else break;h=a}return j},k.error=function(a){throw"Syntax error, unrecognized expression: "+a};var l=k.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,CLASS:/\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/,TAG:/^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/},leftMatch:{},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(a){return a.getAttribute("href")},type:function(a){return a.getAttribute("type")}},relative:{"+":function(a,b){var c=typeof b=="string",d=c&&!j.test(b),e=c&&!d;d&&(b=b.toLowerCase());for(var f=0,g=a.length,h;f<g;f++)if(h=a[f]){while((h=h.previousSibling)&&h.nodeType!==1);a[f]=e||h&&h.nodeName.toLowerCase()===b?h||!1:h===b}e&&k.filter(b,a,!0)},">":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!j.test(b)){b=b.toLowerCase();for(;e<f;e++){c=a[e];if(c){var g=c.parentNode;a[e]=g.nodeName.toLowerCase()===b?g:!1}}}else{for(;e<f;e++)c=a[e],c&&(a[e]=d?c.parentNode:c.parentNode===b);d&&k.filter(b,a,!0)}},"":function(a,b,c){var e,f=d++,g=u;typeof b=="string"&&!j.test(b)&&(b=b.toLowerCase(),e=b,g=t),g("parentNode",b,f,a,e,c)},"~":function(a,b,c){var e,f=d++,g=u;typeof b=="string"&&!j.test(b)&&(b=b.toLowerCase(),e=b,g=t),g("previousSibling",b,f,a,e,c)}},find:{ID:function(a,b,c){if(typeof b.getElementById!="undefined"&&!c){var d=b.getElementById(a[1]);return d&&d.parentNode?[d]:[]}},NAME:function(a,b){if(typeof b.getElementsByName!="undefined"){var c=[],d=b.getElementsByName(a[1]);for(var e=0,f=d.length;e<f;e++)d[e].getAttribute("name")===a[1]&&c.push(d[e]);return c.length===0?null:c}},TAG:function(a,b){if(typeof b.getElementsByTagName!="undefined")return b.getElementsByTagName(a[1])}},preFilter:{CLASS:function(a,b,c,d,e,f){a=" "+a[1].replace(i,"")+" ";if(f)return a;for(var g=0,h;(h=b[g])!=null;g++)h&&(e^(h.className&&(" "+h.className+" ").replace(/[\t\n\r]/g," ").indexOf(a)>=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(i,"")},TAG:function(a,b){return a[1].replace(i,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||k.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&k.error(a[0]);a[0]=d++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(i,"");!f&&l.attrMap[g]&&(a[1]=l.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(i,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=k(b[3],null,null,c);else{var g=k.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(l.match.POS.test(b[0])||l.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!k(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return b<c[3]-0},gt:function(a,b,c){return b>c[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=l.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||k.getText([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h<i;h++)if(g[h]===a)return!1;return!0}k.error(e)},CHILD:function(a,b){var c=b[1],d=a;switch(c){case"only":case"first":while(d=d.previousSibling)if(d.nodeType===1)return!1;if(c==="first")return!0;d=a;case"last":while(d=d.nextSibling)if(d.nodeType===1)return!1;return!0;case"nth":var e=b[2],f=b[3];if(e===1&&f===0)return!0;var g=b[0],h=a.parentNode;if(h&&(h.sizcache!==g||!a.nodeIndex)){var i=0;for(d=h.firstChild;d;d=d.nextSibling)d.nodeType===1&&(d.nodeIndex=++i);h.sizcache=g}var j=a.nodeIndex-f;return e===0?j===0:j%e===0&&j/e>=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=l.attrHandle[c]?l.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=l.setFilters[e];if(f)return f(a,c,b,d)}}},m=l.match.POS,n=function(a,b){return"\\"+(b-0+1)};for(var o in l.match)l.match[o]=new RegExp(l.match[o].source+/(?![^\[]*\])(?![^\(]*\))/.source),l.leftMatch[o]=new RegExp(/(^(?:.|\r|\n)*?)/.source+l.match[o].source.replace(/\\(\d+)/g,n));var p=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(q){p=function(a,b){var c=0,d=b||[];if(e.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var f=a.length;c<f;c++)d.push(a[c]);else for(;a[c];c++)d.push(a[c]);return d}}var r,s;c.documentElement.compareDocumentPosition?r=function(a,b){if(a===b){g=!0;return 0}if(!a.compareDocumentPosition||!b.compareDocumentPosition)return a.compareDocumentPosition?-1:1;return a.compareDocumentPosition(b)&4?-1:1}:(r=function(a,b){if(a===b){g=!0;return 0}if(a.sourceIndex&&b.sourceIndex)return a.sourceIndex-b.sourceIndex;var c,d,e=[],f=[],h=a.parentNode,i=b.parentNode,j=h;if(h===i)return s(a,b);if(!h)return-1;if(!i)return 1;while(j)e.unshift(j),j=j.parentNode;j=i;while(j)f.unshift(j),j=j.parentNode;c=e.length,d=f.length;for(var k=0;k<c&&k<d;k++)if(e[k]!==f[k])return s(e[k],f[k]);return k===c?s(a,f[k],-1):s(e[k],b,1)},s=function(a,b,c){if(a===b)return c;var d=a.nextSibling;while(d){if(d===b)return-1;d=d.nextSibling}return 1}),k.getText=function(a){var b="",c;for(var d=0;a[d];d++)c=a[d],c.nodeType===3||c.nodeType===4?b+=c.nodeValue:c.nodeType!==8&&(b+=k.getText(c.childNodes));return b},function(){var a=c.createElement("div"),d="script"+(new Date).getTime(),e=c.documentElement;a.innerHTML="<a name='"+d+"'/>",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(l.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},l.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(l.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="<a href='#'></a>",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(l.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=k,b=c.createElement("div"),d="__sizzle__";b.innerHTML="<p class='TEST'></p>";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){k=function(b,e,f,g){e=e||c;if(!g&&!k.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return p(e.getElementsByTagName(b),f);if(h[2]&&l.find.CLASS&&e.getElementsByClassName)return p(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return p([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return p([],f);if(i.id===h[3])return p([i],f)}try{return p(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var m=e,n=e.getAttribute("id"),o=n||d,q=e.parentNode,r=/^\s*[+~]/.test(b);n?o=o.replace(/'/g,"\\$&"):e.setAttribute("id",o),r&&q&&(e=e.parentNode);try{if(!r||q)return p(e.querySelectorAll("[id='"+o+"'] "+b),f)}catch(s){}finally{n||m.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)k[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}k.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!k.isXML(a))try{if(e||!l.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return k(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="<div class='test e'></div><div class='test'></div>";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;l.order.splice(1,0,"CLASS"),l.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?k.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?k.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:k.contains=function(){return!1},k.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var v=function(a,b){var c,d=[],e="",f=b.nodeType?[b]:b;while(c=l.match.PSEUDO.exec(a))e+=c[0],a=a.replace(l.match.PSEUDO,"");a=l.relative[a]?a+"*":a;for(var g=0,h=f.length;g<h;g++)k(a,f[g],d);return k.filter(e,d)};f.find=k,f.expr=k.selectors,f.expr[":"]=f.expr.filters,f.unique=k.uniqueSort,f.text=k.getText,f.isXMLDoc=k.isXML,f.contains=k.contains}();var O=/Until$/,P=/^(?:parents|prevUntil|prevAll)/,Q=/,/,R=/^.[^:#\[\.,]*$/,S=Array.prototype.slice,T=f.expr.match.POS,U={children:!0,contents:!0,next:!0,prev:!0};f.fn.extend({find:function(a){var b=this,c,d;if(typeof a!="string")return f(a).filter(function(){for(c=0,d=b.length;c<d;c++)if(f.contains(b[c],this))return!0});var e=this.pushStack("","find",a),g,h,i;for(c=0,d=this.length;c<d;c++){g=e.length,f.find(a,this[c],e);if(c>0)for(h=g;h<e.length;h++)for(i=0;i<g;i++)if(e[i]===e[h]){e.splice(h--,1);break}}return e},has:function(a){var b=f(a);return this.filter(function(){for(var a=0,c=b.length;a<c;a++)if(f.contains(this,b[a]))return!0})},not:function(a){return this.pushStack(W(this,a,!1),"not",a)},filter:function(a){return this.pushStack(W(this,a,!0),"filter",a)},is:function(a){return!!a&&(typeof a=="string"?f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h,i,j={},k=1;if(g&&a.length){for(d=0,e=a.length;d<e;d++)i=a[d],j[i]||(j[i]=T.test(i)?f(i,b||this.context):i);while(g&&g.ownerDocument&&g!==b){for(i in j)h=j[i],(h.jquery?h.index(g)>-1:f(g).is(h))&&c.push({selector:i,elem:g,level:k});g=g.parentNode,k++}}return c}var l=T.test(a)||typeof a!="string"?f(a,b||this.context):0;for(d=0,e=this.length;d<e;d++){g=this[d];while(g){if(l?l.index(g)>-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a||typeof a=="string")return f.inArray(this[0],a?f(a):this.parent().children());return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(V(c[0])||V(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling(a.parentNode.firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c),g=S.call(arguments);O.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!U[a]?f.unique(e):e,(this.length>1||Q.test(d))&&P.test(a)&&(e=e.reverse());return this.pushStack(e,a,g.join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var X=/ jQuery\d+="(?:\d+|null)"/g,Y=/^\s+/,Z=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,$=/<([\w:]+)/,_=/<tbody/i,ba=/<|&#?\w+;/,bb=/<(?:script|object|embed|option|style)/i,bc=/checked\s*(?:[^=]|=\s*.checked.)/i,bd=/\/(java|ecma)script/i,be=/^\s*<!(?:\[CDATA\[|\-\-)/,bf={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],area:[1,"<map>","</map>"],_default:[0,"",""]};bf.optgroup=bf.option,bf.tbody=bf.tfoot=bf.colgroup=bf.caption=bf.thead,bf.th=bf.td,f.support.htmlSerialize||(bf._default=[1,"div<div>","</div>"]),f.fn.extend({text:function(a){if(f.isFunction(a))return this.each(function(b){var c=f(this);c.text(a.call(this,b,c.text()))});if(typeof a!="object"&&a!==b)return this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a));return f.text(this)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){f(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f(arguments[0]).toArray());return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){if(a===b)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(X,""):null;if(typeof a=="string"&&!bb.test(a)&&(f.support.leadingWhitespace||!Y.test(a))&&!bf[($.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Z,"<$1></$2>");try{for(var c=0,d=this.length;c<d;c++)this[c].nodeType===1&&(f.cleanData(this[c].getElementsByTagName("*")),this[c].innerHTML=a)}catch(e){this.empty().append(a)}}else f.isFunction(a)?this.each(function(b){var c=f(this);c.html(a.call(this,b,c.html()))}):this.empty().append(a);return this},replaceWith:function(a){if(this[0]&&this[0].parentNode){if(f.isFunction(a))return this.each(function(b){var c=f(this),d=c.html();c.replaceWith(a.call(this,b,d))});typeof a!="string"&&(a=f(a).detach());return this.each(function(){var b=this.nextSibling,c=this.parentNode;f(this).remove(),b?f(b).before(a):f(c).append(a)})}return this.length?this.pushStack(f(f.isFunction(a)?a():a),"replaceWith",a):this},detach:function(a){return this.remove(a,!0)},domManip:function(a,c,d){var e,g,h,i,j=a[0],k=[];if(!f.support.checkClone&&arguments.length===3&&typeof j=="string"&&bc.test(j))return this.each(function(){f(this).domManip(a,c,d,!0)});if(f.isFunction(j))return this.each(function(e){var g=f(this);a[0]=j.call(this,e,c?g.html():b),g.domManip(a,c,d)});if(this[0]){i=j&&j.parentNode,f.support.parentNode&&i&&i.nodeType===11&&i.childNodes.length===this.length?e={fragment:i}:e=f.buildFragment(a,this,k),h=e.fragment,h.childNodes.length===1?g=h=h.firstChild:g=h.firstChild;if(g){c=c&&f.nodeName(g,"tr");for(var l=0,m=this.length,n=m-1;l<m;l++)d.call(c?bg(this[l],g):this[l],e.cacheable||m>1&&l<n?f.clone(h,!0,!0):h)}k.length&&f.each(k,bm)}return this}}),f.buildFragment=function(a,b,d){var e,g,h,i;b&&b[0]&&(i=b[0].ownerDocument||b[0]),i.createDocumentFragment||(i=c),a.length===1&&typeof a[0]=="string"&&a[0].length<512&&i===c&&a[0].charAt(0)==="<"&&!bb.test(a[0])&&(f.support.checkClone||!bc.test(a[0]))&&(g=!0,h=f.fragments[a[0]],h&&h!==1&&(e=h)),e||(e=i.createDocumentFragment(),f.clean(a,i,e,d)),g&&(f.fragments[a[0]]=h?e:1);return{fragment:e,cacheable:g}},f.fragments={},f.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){f.fn[a]=function(c){var d=[],e=f(c),g=this.length===1&&this[0].parentNode;if(g&&g.nodeType===11&&g.childNodes.length===1&&e.length===1){e[b](this[0]);return this}for(var h=0,i=e.length;h<i;h++){var j=(h>0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j )}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d=a.cloneNode(!0),e,g,h;if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bi(a,d),e=bj(a),g=bj(d);for(h=0;e[h];++h)bi(e[h],g[h])}if(b){bh(a,d);if(c){e=bj(a),g=bj(d);for(h=0;e[h];++h)bh(e[h],g[h])}}e=g=null;return d},clean:function(a,b,d,e){var g;b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);var h=[],i;for(var j=0,k;(k=a[j])!=null;j++){typeof k=="number"&&(k+="");if(!k)continue;if(typeof k=="string")if(!ba.test(k))k=b.createTextNode(k);else{k=k.replace(Z,"<$1></$2>");var l=($.exec(k)||["",""])[1].toLowerCase(),m=bf[l]||bf._default,n=m[0],o=b.createElement("div");o.innerHTML=m[1]+k+m[2];while(n--)o=o.lastChild;if(!f.support.tbody){var p=_.test(k),q=l==="table"&&!p?o.firstChild&&o.firstChild.childNodes:m[1]==="<table>"&&!p?o.childNodes:[];for(i=q.length-1;i>=0;--i)f.nodeName(q[i],"tbody")&&!q[i].childNodes.length&&q[i].parentNode.removeChild(q[i])}!f.support.leadingWhitespace&&Y.test(k)&&o.insertBefore(b.createTextNode(Y.exec(k)[0]),o.firstChild),k=o.childNodes}var r;if(!f.support.appendChecked)if(k[0]&&typeof (r=k.length)=="number")for(i=0;i<r;i++)bl(k[i]);else bl(k);k.nodeType?h.push(k):h=f.merge(h,k)}if(d){g=function(a){return!a.type||bd.test(a.type)};for(j=0;h[j];j++)if(e&&f.nodeName(h[j],"script")&&(!h[j].type||h[j].type.toLowerCase()==="text/javascript"))e.push(h[j].parentNode?h[j].parentNode.removeChild(h[j]):h[j]);else{if(h[j].nodeType===1){var s=f.grep(h[j].getElementsByTagName("script"),g);h.splice.apply(h,[j+1,0].concat(s))}d.appendChild(h[j])}}return h},cleanData:function(a){var b,c,d=f.cache,e=f.expando,g=f.event.special,h=f.support.deleteExpando;for(var i=0,j;(j=a[i])!=null;i++){if(j.nodeName&&f.noData[j.nodeName.toLowerCase()])continue;c=j[f.expando];if(c){b=d[c]&&d[c][e];if(b&&b.events){for(var k in b.events)g[k]?f.event.remove(j,k):f.removeEvent(j,k,b.handle);b.handle&&(b.handle.elem=null)}h?delete j[f.expando]:j.removeAttribute&&j.removeAttribute(f.expando),delete d[c]}}}});var bn=/alpha\([^)]*\)/i,bo=/opacity=([^)]*)/,bp=/([A-Z]|^ms)/g,bq=/^-?\d+(?:px)?$/i,br=/^-?\d/,bs=/^[+\-]=/,bt=/[^+\-\.\de]+/g,bu={position:"absolute",visibility:"hidden",display:"block"},bv=["Left","Right"],bw=["Top","Bottom"],bx,by,bz;f.fn.css=function(a,c){if(arguments.length===2&&c===b)return this;return f.access(this,a,c,!0,function(a,c,d){return d!==b?f.style(a,c,d):f.css(a,c)})},f.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=bx(a,"opacity","opacity");return c===""?"1":c}return a.style.opacity}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":f.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!!a&&a.nodeType!==3&&a.nodeType!==8&&!!a.style){var g,h,i=f.camelCase(c),j=a.style,k=f.cssHooks[i];c=f.cssProps[i]||i;if(d===b){if(k&&"get"in k&&(g=k.get(a,!1,e))!==b)return g;return j[c]}h=typeof d;if(h==="number"&&isNaN(d)||d==null)return;h==="string"&&bs.test(d)&&(d=+d.replace(bt,"")+parseFloat(f.css(a,c)),h="number"),h==="number"&&!f.cssNumber[i]&&(d+="px");if(!k||!("set"in k)||(d=k.set(a,d))!==b)try{j[c]=d}catch(l){}}},css:function(a,c,d){var e,g;c=f.camelCase(c),g=f.cssHooks[c],c=f.cssProps[c]||c,c==="cssFloat"&&(c="float");if(g&&"get"in g&&(e=g.get(a,!0,d))!==b)return e;if(bx)return bx(a,c)},swap:function(a,b,c){var d={};for(var e in b)d[e]=a.style[e],a.style[e]=b[e];c.call(a);for(e in b)a.style[e]=d[e]}}),f.curCSS=f.css,f.each(["height","width"],function(a,b){f.cssHooks[b]={get:function(a,c,d){var e;if(c){if(a.offsetWidth!==0)return bA(a,b,d);f.swap(a,bu,function(){e=bA(a,b,d)});return e}},set:function(a,b){if(!bq.test(b))return b;b=parseFloat(b);if(b>=0)return b+"px"}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return bo.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle;c.zoom=1;var e=f.isNaN(b)?"":"alpha(opacity="+b*100+")",g=d&&d.filter||c.filter||"";c.filter=bn.test(g)?g.replace(bn,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){var c;f.swap(a,{display:"inline-block"},function(){b?c=bx(a,"margin-right","marginRight"):c=a.style.marginRight});return c}})}),c.defaultView&&c.defaultView.getComputedStyle&&(by=function(a,c){var d,e,g;c=c.replace(bp,"-$1").toLowerCase();if(!(e=a.ownerDocument.defaultView))return b;if(g=e.getComputedStyle(a,null))d=g.getPropertyValue(c),d===""&&!f.contains(a.ownerDocument.documentElement,a)&&(d=f.style(a,c));return d}),c.documentElement.currentStyle&&(bz=function(a,b){var c,d=a.currentStyle&&a.currentStyle[b],e=a.runtimeStyle&&a.runtimeStyle[b],f=a.style;!bq.test(d)&&br.test(d)&&(c=f.left,e&&(a.runtimeStyle.left=a.currentStyle.left),f.left=b==="fontSize"?"1em":d||0,d=f.pixelLeft+"px",f.left=c,e&&(a.runtimeStyle.left=e));return d===""?"auto":d}),bx=by||bz,f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)});var bB=/%20/g,bC=/\[\]$/,bD=/\r?\n/g,bE=/#.*$/,bF=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bG=/^(?:color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bH=/^(?:about|app|app\-storage|.+\-extension|file|widget):$/,bI=/^(?:GET|HEAD)$/,bJ=/^\/\//,bK=/\?/,bL=/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,bM=/^(?:select|textarea)/i,bN=/\s+/,bO=/([?&])_=[^&]*/,bP=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bQ=f.fn.load,bR={},bS={},bT,bU;try{bT=e.href}catch(bV){bT=c.createElement("a"),bT.href="",bT=bT.href}bU=bP.exec(bT.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bQ)return bQ.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("<div>").append(c.replace(bL,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bM.test(this.nodeName)||bG.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bD,"\r\n")}}):{name:b.name,value:c.replace(bD,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.bind(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?f.extend(!0,a,f.ajaxSettings,b):(b=a,a=f.extend(!0,f.ajaxSettings,b));for(var c in{context:1,url:1})c in b?a[c]=b[c]:c in f.ajaxSettings&&(a[c]=f.ajaxSettings[c]);return a},ajaxSettings:{url:bT,isLocal:bH.test(bU[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":"*/*"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML}},ajaxPrefilter:bW(bR),ajaxTransport:bW(bS),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a?4:0;var o,r,u,w=l?bZ(d,v,l):b,x,y;if(a>=200&&a<300||a===304){if(d.ifModified){if(x=v.getResponseHeader("Last-Modified"))f.lastModified[k]=x;if(y=v.getResponseHeader("Etag"))f.etag[k]=y}if(a===304)c="notmodified",o=!0;else try{r=b$(d,w),c="success",o=!0}catch(z){c="parsererror",u=z}}else{u=c;if(!c||a)c="error",a<0&&(a=0)}v.status=a,v.statusText=c,o?h.resolveWith(e,[r,c,v]):h.rejectWith(e,[v,c,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.resolveWith(e,[v,c]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f._Deferred(),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bF.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.done,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bE,"").replace(bJ,bU[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bN),d.crossDomain==null&&(r=bP.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bU[1]&&r[2]==bU[2]&&(r[3]||(r[1]==="http:"?80:443))==(bU[3]||(bU[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),bX(bR,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bI.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bK.test(d.url)?"&":"?")+d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bO,"$1_="+x);d.url=y+(y===d.url?(bK.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", */*; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=bX(bS,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){status<2?w(-1,z):f.error(z)}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)bY(g,a[g],c,e);return d.join("&").replace(bB,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var b_=f.now(),ca=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+b_++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=b.contentType==="application/x-www-form-urlencoded"&&typeof b.data=="string";if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(ca.test(b.url)||e&&ca.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(ca,l),b.url===j&&(e&&(k=k.replace(ca,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var cb=a.ActiveXObject?function(){for(var a in cd)cd[a](0,1)}:!1,cc=0,cd;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ce()||cf()}:ce,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,cb&&delete cd[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n),m.text=h.responseText;try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cc,cb&&(cd||(cd={},f(a).unload(cb)),cd[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var cg={},ch,ci,cj=/^(?:toggle|show|hide)$/,ck=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,cl,cm=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],cn,co=a.webkitRequestAnimationFrame||a.mozRequestAnimationFrame||a.oRequestAnimationFrame;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(cr("show",3),a,b,c);for(var g=0,h=this.length;g<h;g++)d=this[g],d.style&&(e=d.style.display,!f._data(d,"olddisplay")&&e==="none"&&(e=d.style.display=""),e===""&&f.css(d,"display")==="none"&&f._data(d,"olddisplay",cs(d.nodeName)));for(g=0;g<h;g++){d=this[g];if(d.style){e=d.style.display;if(e===""||e==="none")d.style.display=f._data(d,"olddisplay")||""}}return this},hide:function(a,b,c){if(a||a===0)return this.animate(cr("hide",3),a,b,c);for(var d=0,e=this.length;d<e;d++)if(this[d].style){var g=f.css(this[d],"display");g!=="none"&&!f._data(this[d],"olddisplay")&&f._data(this[d],"olddisplay",g)}for(d=0;d<e;d++)this[d].style&&(this[d].style.display="none");return this},_toggle:f.fn.toggle,toggle:function(a,b,c){var d=typeof a=="boolean";f.isFunction(a)&&f.isFunction(b)?this._toggle.apply(this,arguments):a==null||d?this.each(function(){var b=d?a:f(this).is(":hidden");f(this)[b?"show":"hide"]()}):this.animate(cr("toggle",3),a,b,c);return this},fadeTo:function(a,b,c,d){return this.filter(":hidden").css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=f.speed(b,c,d);if(f.isEmptyObject(a))return this.each(e.complete,[!1]);a=f.extend({},a);return this[e.queue===!1?"each":"queue"](function(){e.queue===!1&&f._mark(this);var b=f.extend({},e),c=this.nodeType===1,d=c&&f(this).is(":hidden"),g,h,i,j,k,l,m,n,o;b.animatedProperties={};for(i in a){g=f.camelCase(i),i!==g&&(a[g]=a[i],delete a[i]),h=a[g],f.isArray(h)?(b.animatedProperties[g]=h[1],h=a[g]=h[0]):b.animatedProperties[g]=b.specialEasing&&b.specialEasing[g]||b.easing||"swing";if(h==="hide"&&d||h==="show"&&!d)return b.complete.call(this);c&&(g==="height"||g==="width")&&(b.overflow=[this.style.overflow,this.style.overflowX,this.style.overflowY],f.css(this,"display")==="inline"&&f.css(this,"float")==="none"&&(f.support.inlineBlockNeedsLayout?(j=cs(this.nodeName),j==="inline"?this.style.display="inline-block":(this.style.display="inline",this.style.zoom=1)):this.style.display="inline-block"))}b.overflow!=null&&(this.style.overflow="hidden");for(i in a)k=new f.fx(this,b,i),h=a[i],cj.test(h)?k[h==="toggle"?d?"show":"hide":h]():(l=ck.exec(h),m=k.cur(),l?(n=parseFloat(l[2]),o=l[3]||(f.cssNumber[i]?"":"px"),o!=="px"&&(f.style(this,i,(n||1)+o),m=(n||1)/k.cur()*m,f.style(this,i,m+o)),l[1]&&(n=(l[1]==="-="?-1:1)*n+m),k.custom(m,n,o)):k.custom(m,h,""));return!0})},stop:function(a,b){a&&this.queue([]),this.each(function(){var a=f.timers,c=a.length;b||f._unmark(!0,this);while(c--)a[c].elem===this&&(b&&a[c](!0),a.splice(c,1))}),b||this.dequeue();return this}}),f.each({slideDown:cr("show",1),slideUp:cr("hide",1),slideToggle:cr("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){f.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),f.extend({speed:function(a,b,c){var d=a&&typeof a=="object"?f.extend({},a):{complete:c||!c&&b||f.isFunction(a)&&a,duration:a,easing:c&&b||b&&!f.isFunction(b)&&b};d.duration=f.fx.off?0:typeof d.duration=="number"?d.duration:d.duration in f.fx.speeds?f.fx.speeds[d.duration]:f.fx.speeds._default,d.old=d.complete,d.complete=function(a){f.isFunction(d.old)&&d.old.call(this),d.queue!==!1?f.dequeue(this):a!==!1&&f._unmark(this)};return d},easing:{linear:function(a,b,c,d){return c+d*a},swing:function(a,b,c,d){return(-Math.cos(a*Math.PI)/2+.5)*d+c}},timers:[],fx:function(a,b,c){this.options=b,this.elem=a,this.prop=c,b.orig=b.orig||{}}}),f.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this),(f.fx.step[this.prop]||f.fx.step._default)(this)},cur:function(){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];var a,b=f.css(this.elem,this.prop);return isNaN(a=parseFloat(b))?!b||b==="auto"?0:b:a},custom:function(a,b,c){function h(a){return d.step(a)}var d=this,e=f.fx,g;this.startTime=cn||cp(),this.start=a,this.end=b,this.unit=c||this.unit||(f.cssNumber[this.prop]?"":"px"),this.now=this.start,this.pos=this.state=0,h.elem=this.elem,h()&&f.timers.push(h)&&!cl&&(co?(cl=!0,g=function(){cl&&(co(g),e.tick())},co(g)):cl=setInterval(e.tick,e.interval))},show:function(){this.options.orig[this.prop]=f.style(this.elem,this.prop),this.options.show=!0,this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur()),f(this.elem).show()},hide:function(){this.options.orig[this.prop]=f.style(this.elem,this.prop),this.options.hide=!0,this.custom(this.cur(),0)},step:function(a){var b=cn||cp(),c=!0,d=this.elem,e=this.options,g,h;if(a||b>=e.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),e.animatedProperties[this.prop]=!0;for(g in e.animatedProperties)e.animatedProperties[g]!==!0&&(c=!1);if(c){e.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){d.style["overflow"+b]=e.overflow[a]}),e.hide&&f(d).hide();if(e.hide||e.show)for(var i in e.animatedProperties)f.style(d,i,e.orig[i]);e.complete.call(d)}return!1}e.duration==Infinity?this.now=b:(h=b-this.startTime,this.state=h/e.duration,this.pos=f.easing[e.animatedProperties[this.prop]](this.state,h,0,1,e.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){for(var a=f.timers,b=0;b<a.length;++b)a[b]()||a.splice(b--,1);a.length||f.fx.stop()},interval:13,stop:function(){clearInterval(cl),cl=null},speeds:{slow:600,fast:200,_default:400},step:{opacity:function(a){f.style(a.elem,"opacity",a.now)},_default:function(a){a.elem.style&&a.elem.style[a.prop]!=null?a.elem.style[a.prop]=(a.prop==="width"||a.prop==="height"?Math.max(0,a.now):a.now)+a.unit:a.elem[a.prop]=a.now}}}),f.expr&&f.expr.filters&&(f.expr.filters.animated=function(a){return f.grep(f.timers,function(b){return a===b.elem}).length});var ct=/^t(?:able|d|h)$/i,cu=/^(?:body|html)$/i;"getBoundingClientRect"in c.documentElement?f.fn.offset=function(a){var b=this[0],c;if(a)return this.each(function(b){f.offset.setOffset(this,a,b)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return f.offset.bodyOffset(b);try{c=b.getBoundingClientRect()}catch(d){}var e=b.ownerDocument,g=e.documentElement;if(!c||!f.contains(g,b))return c?{top:c.top,left:c.left}:{top:0,left:0};var h=e.body,i=cv(e),j=g.clientTop||h.clientTop||0,k=g.clientLeft||h.clientLeft||0,l=i.pageYOffset||f.support.boxModel&&g.scrollTop||h.scrollTop,m=i.pageXOffset||f.support.boxModel&&g.scrollLeft||h.scrollLeft,n=c.top+l-j,o=c.left+m-k;return{top:n,left:o}}:f.fn.offset=function(a){var b=this[0];if(a)return this.each(function(b){f.offset.setOffset(this,a,b)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return f.offset.bodyOffset(b);f.offset.initialize();var c,d=b.offsetParent,e=b,g=b.ownerDocument,h=g.documentElement,i=g.body,j=g.defaultView,k=j?j.getComputedStyle(b,null):b.currentStyle,l=b.offsetTop,m=b.offsetLeft;while((b=b.parentNode)&&b!==i&&b!==h){if(f.offset.supportsFixedPosition&&k.position==="fixed")break;c=j?j.getComputedStyle(b,null):b.currentStyle,l-=b.scrollTop,m-=b.scrollLeft,b===d&&(l+=b.offsetTop,m+=b.offsetLeft,f.offset.doesNotAddBorder&&(!f.offset.doesAddBorderForTableAndCells||!ct.test(b.nodeName))&&(l+=parseFloat(c.borderTopWidth)||0,m+=parseFloat(c.borderLeftWidth)||0),e=d,d=b.offsetParent),f.offset.subtractsBorderForOverflowNotVisible&&c.overflow!=="visible"&&(l+=parseFloat(c.borderTopWidth)||0,m+=parseFloat(c.borderLeftWidth)||0),k=c}if(k.position==="relative"||k.position==="static")l+=i.offsetTop,m+=i.offsetLeft;f.offset.supportsFixedPosition&&k.position==="fixed"&&(l+=Math.max(h.scrollTop,i.scrollTop),m+=Math.max(h.scrollLeft,i.scrollLeft));return{top:l,left:m}},f.offset={initialize:function(){var a=c.body,b=c.createElement("div"),d,e,g,h,i=parseFloat(f.css(a,"marginTop"))||0,j="<div style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;'><div></div></div><table style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;' cellpadding='0' cellspacing='0'><tr><td></td></tr></table>";f.extend(b.style,{position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"}),b.innerHTML=j,a.insertBefore(b,a.firstChild),d=b.firstChild,e=d.firstChild,h=d.nextSibling.firstChild.firstChild,this.doesNotAddBorder=e.offsetTop!==5,this.doesAddBorderForTableAndCells=h.offsetTop===5,e.style.position="fixed",e.style.top="20px",this.supportsFixedPosition=e.offsetTop===20||e.offsetTop===15,e.style.position=e.style.top="",d.style.overflow="hidden",d.style.position="relative",this.subtractsBorderForOverflowNotVisible=e.offsetTop===-5,this.doesNotIncludeMarginInBodyOffset=a.offsetTop!==i,a.removeChild(b),f.offset.initialize=f.noop},bodyOffset:function(a){var b=a.offsetTop,c=a.offsetLeft;f.offset.initialize(),f.offset.doesNotIncludeMarginInBodyOffset&&(b+=parseFloat(f.css(a,"marginTop"))||0,c+=parseFloat(f.css(a,"marginLeft"))||0);return{top:b,left:c}},setOffset:function(a,b,c){var d=f.css(a,"position");d==="static"&&(a.style.position="relative");var e=f(a),g=e.offset(),h=f.css(a,"top"),i=f.css(a,"left"),j=(d==="absolute"||d==="fixed")&&f.inArray("auto",[h,i])>-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cu.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cu.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each(["Left","Top"],function(a,c){var d="scroll"+c;f.fn[d]=function(c){var e,g;if(c===b){e=this[0];if(!e)return null;g=cv(e);return g?"pageXOffset"in g?g[a?"pageYOffset":"pageXOffset"]:f.support.boxModel&&g.document.documentElement[d]||g.document.body[d]:e[d]}return this.each(function(){g=cv(this),g?g.scrollTo(a?f(g).scrollLeft():c,a?c:f(g).scrollTop()):this[d]=c})}}),f.each(["Height","Width"],function(a,c){var d=c.toLowerCase();f.fn["inner"+c]=function(){var a=this[0];return a&&a.style?parseFloat(f.css(a,d,"padding")):null},f.fn["outer"+c]=function(a){var b=this[0];return b&&b.style?parseFloat(f.css(b,d,a?"margin":"border")):null},f.fn[d]=function(a){var e=this[0];if(!e)return a==null?null:this;if(f.isFunction(a))return this.each(function(b){var c=f(this);c[d](a.call(this,b,c[d]()))});if(f.isWindow(e)){var g=e.document.documentElement["client"+c];return e.document.compatMode==="CSS1Compat"&&g||e.document.body["client"+c]||g}if(e.nodeType===9)return Math.max(e.documentElement["client"+c],e.body["scroll"+c],e.documentElement["scroll"+c],e.body["offset"+c],e.documentElement["offset"+c]);if(a===b){var h=f.css(e,d),i=parseFloat(h);return f.isNaN(i)?h:i}return this.css(d,typeof a=="string"?a:a+"px")}}),a.jQuery=a.$=f})(window); </script> <script type="text/javascript"> /** _ _____ _ _ _ | | __ \ (_) | | | | ___ ___ | | |__) |___ ___ _ ______ _| |__ | | ___ / __/ _ \| | _ // _ \/ __| |_ / _` | '_ \| |/ _ \ | (_| (_) | | | \ \ __/\__ \ |/ / (_| | |_) | | __/ \___\___/|_|_| \_\___||___/_/___\__,_|_.__/|_|\___| v1.6 - jQuery plugin created by Alvaro Prieto Lauroba Licences: MIT & GPL Feel free to use or modify this plugin as far as my full name is kept If you are going to use this plug-in in production environments it is strongly recommended to use its minified version: colResizable.min.js */ (function($){ var d = $(document); //window object var h = $("head"); //head object var drag = null; //reference to the current grip that is being dragged var tables = {}; //object of the already processed tables (table.id as key) var count = 0; //internal count to create unique IDs when needed. //common strings for packing var ID = "id"; var PX = "px"; var SIGNATURE ="JColResizer"; var FLEX = "JCLRFlex"; //short-cuts var I = parseInt; var M = Math; var ie = navigator.userAgent.indexOf('Trident/4.0')>0; var S; try{S = sessionStorage;}catch(e){} //Firefox crashes when executed as local file system //append required CSS rules h.append("<style type='text/css'> .JColResizer{table-layout:fixed;} .JColResizer > tbody > tr > td, .JColResizer > tbody > tr > th{overflow:hidden;padding-left:0!important; padding-right:0!important;} .JCLRgrips{ height:0px; position:relative;} .JCLRgrip{margin-left:-5px; position:absolute; z-index:5; } .JCLRgrip .JColResizer{position:absolute;background-color:red;filter:alpha(opacity=1);opacity:0;width:10px;height:100%;cursor: e-resize;top:0px} .JCLRLastGrip{position:absolute; width:1px; } .JCLRgripDrag{ border-left:1px dotted black; } .JCLRFlex{width:auto!important;} .JCLRgrip.JCLRdisabledGrip .JColResizer{cursor:default; display:none;}</style>"); /** * Function to allow column resizing for table objects. It is the starting point to apply the plugin. * @param {DOM node} tb - reference to the DOM table object to be enhanced * @param {Object} options - some customization values */ var init = function( tb, options){ var t = $(tb); //the table object is wrapped t.opt = options; //each table has its own options available at anytime t.mode = options.resizeMode; //shortcuts t.dc = t.opt.disabledColumns; if(t.opt.disable) return destroy(t); //the user is asking to destroy a previously colResized table var id = t.id = t.attr(ID) || SIGNATURE+count++; //its id is obtained, if null new one is generated t.p = t.opt.postbackSafe; //short-cut to detect postback safe if(!t.is("table") || tables[id] && !t.opt.partialRefresh) return; //if the object is not a table or if it was already processed then it is ignored. if (t.opt.hoverCursor !== 'e-resize') h.append("<style type='text/css'>.JCLRgrip .JColResizer:hover{cursor:"+ t.opt.hoverCursor +"!important}</style>"); //if hoverCursor has been set, append the style t.addClass(SIGNATURE).attr(ID, id).before('<div class="JCLRgrips"/>'); //the grips container object is added. Signature class forces table rendering in fixed-layout mode to prevent column's min-width t.g = []; t.c = []; t.w = t.width(); t.gc = t.prev(); t.f=t.opt.fixed; //t.c and t.g are arrays of columns and grips respectively if(options.marginLeft) t.gc.css("marginLeft", options.marginLeft); //if the table contains margins, it must be specified if(options.marginRight) t.gc.css("marginRight", options.marginRight); //since there is no (direct) way to obtain margin values in its original units (%, em, ...) t.cs = I(ie? tb.cellSpacing || tb.currentStyle.borderSpacing :t.css('border-spacing'))||2; //table cellspacing (not even jQuery is fully cross-browser) t.b = I(ie? tb.border || tb.currentStyle.borderLeftWidth :t.css('border-left-width'))||1; //outer border width (again cross-browser issues) // if(!(tb.style.width || tb.width)) t.width(t.width()); //I am not an IE fan at all, but it is a pity that only IE has the currentStyle attribute working as expected. For this reason I can not check easily if the table has an explicit width or if it is rendered as "auto" tables[id] = t; //the table object is stored using its id as key createGrips(t); //grips are created }; /** * This function allows to remove any enhancements performed by this plugin on a previously processed table. * @param {jQuery ref} t - table object */ var destroy = function(t){ var id=t.attr(ID), t=tables[id]; //its table object is found if(!t||!t.is("table")) return; //if none, then it wasn't processed t.removeClass(SIGNATURE+" "+FLEX).gc.remove(); //class and grips are removed delete tables[id]; //clean up data }; /** * Function to create all the grips associated with the table given by parameters * @param {jQuery ref} t - table object */ var createGrips = function(t){ var th = t.find(">thead>tr:first>th,>thead>tr:first>td"); //table headers are obtained if(!th.length) th = t.find(">tbody>tr:first>th,>tr:first>th,>tbody>tr:first>td, >tr:first>td"); //but headers can also be included in different ways th = th.filter(":visible"); //filter invisible columns t.cg = t.find("col"); //a table can also contain a colgroup with col elements t.ln = th.length; //table length is stored if(t.p && S && S[t.id])memento(t,th); //if 'postbackSafe' is enabled and there is data for the current table, its coloumn layout is restored th.each(function(i){ //iterate through the table column headers var c = $(this); //jquery wrap for the current column var dc = t.dc.indexOf(i)!=-1; //is this a disabled column? var g = $(t.gc.append('<div class="JCLRgrip"></div>')[0].lastChild); //add the visual node to be used as grip g.append(dc ? "": t.opt.gripInnerHtml).append('<div class="'+SIGNATURE+'"></div>'); if(i == t.ln-1){ //if the current grip is the las one g.addClass("JCLRLastGrip"); //add a different css class to stlye it in a different way if needed if(t.f) g.html(""); //if the table resizing mode is set to fixed, the last grip is removed since table with can not change } g.bind('touchstart mousedown', onGripMouseDown); //bind the mousedown event to start dragging if (!dc){ //if normal column bind the mousedown event to start dragging, if disabled then apply its css class g.removeClass('JCLRdisabledGrip').bind('touchstart mousedown', onGripMouseDown); }else{ g.addClass('JCLRdisabledGrip'); } g.t = t; g.i = i; g.c = c; c.w =c.width(); //some values are stored in the grip's node data as shortcut t.g.push(g); t.c.push(c); //the current grip and column are added to its table object c.width(c.w).removeAttr("width"); //the width of the column is converted into pixel-based measurements g.data(SIGNATURE, {i:i, t:t.attr(ID), last: i == t.ln-1}); //grip index and its table name are stored in the HTML }); t.cg.removeAttr("width"); //remove the width attribute from elements in the colgroup t.find('td, th').not(th).not('table th, table td').each(function(){ $(this).removeAttr('width'); //the width attribute is removed from all table cells which are not nested in other tables and dont belong to the header }); if(!t.f){ t.removeAttr('width').addClass(FLEX); //if not fixed, let the table grow as needed } syncGrips(t); //the grips are positioned according to the current table layout //there is a small problem, some cells in the table could contain dimension values interfering with the //width value set by this plugin. Those values are removed }; /** * Function to allow the persistence of columns dimensions after a browser postback. It is based in * the HTML5 sessionStorage object, which can be emulated for older browsers using sessionstorage.js * @param {jQuery ref} t - table object * @param {jQuery ref} th - reference to the first row elements (only set in deserialization) */ var memento = function(t, th){ var w,m=0,i=0,aux =[],tw; if(th){ //in deserialization mode (after a postback) t.cg.removeAttr("width"); if(t.opt.flush){ S[t.id] =""; return;} //if flush is activated, stored data is removed w = S[t.id].split(";"); //column widths is obtained tw = w[t.ln+1]; if(!t.f && tw){ //if not fixed and table width data available its size is restored t.width(tw*=1); if(t.opt.overflow) { //if overfolw flag is set, restore table width also as table min-width t.css('min-width', tw + PX); t.w = tw; } } for(;i<t.ln;i++){ //for each column aux.push(100*w[i]/w[t.ln]+"%"); //width is stored in an array since it will be required again a couple of lines ahead th.eq(i).css("width", aux[i] ); //each column width in % is restored } for(i=0;i<t.ln;i++) t.cg.eq(i).css("width", aux[i]); //this code is required in order to create an inline CSS rule with higher precedence than an existing CSS class in the "col" elements }else{ //in serialization mode (after resizing a column) S[t.id] =""; //clean up previous data for(;i < t.c.length; i++){ //iterate through columns w = t.c[i].width(); //width is obtained S[t.id] += w+";"; //width is appended to the sessionStorage object using ID as key m+=w; //carriage is updated to obtain the full size used by columns } S[t.id]+=m; //the last item of the serialized string is the table's active area (width), //to be able to obtain % width value of each columns while deserializing if(!t.f) S[t.id] += ";"+t.width(); //if not fixed, table width is stored } }; /** * Function that places each grip in the correct position according to the current table layout * @param {jQuery ref} t - table object */ var syncGrips = function (t){ t.gc.width(t.w); //the grip's container width is updated for(var i=0; i<t.ln; i++){ //for each column var c = t.c[i]; t.g[i].css({ //height and position of the grip is updated according to the table layout left: c.offset().left - t.offset().left + c.outerWidth(false) + t.cs / 2 + PX, height: t.opt.headerOnly? t.c[0].outerHeight(false) : t.outerHeight(false) }); } }; /** * This function updates column's width according to the horizontal position increment of the grip being * dragged. The function can be called while dragging if liveDragging is enabled and also from the onGripDragOver * event handler to synchronize grip's position with their related columns. * @param {jQuery ref} t - table object * @param {number} i - index of the grip being dragged * @param {bool} isOver - to identify when the function is being called from the onGripDragOver event */ var syncCols = function(t,i,isOver){ var inc = drag.x-drag.l, c = t.c[i], c2 = t.c[i+1]; var w = c.w + inc; var w2= c2.w- inc; //their new width is obtained c.width( w + PX); t.cg.eq(i).width( w + PX); if(t.f){ //if fixed mode c2.width(w2 + PX); t.cg.eq(i+1).width( w2 + PX); }else if(t.opt.overflow) { //if overflow is set, incriment min-width to force overflow t.css('min-width', t.w + inc); } if(isOver){ c.w=w; c2.w= t.f ? w2 : c2.w; } }; /** * This function updates all columns width according to its real width. It must be taken into account that the * sum of all columns can exceed the table width in some cases (if fixed is set to false and table has some kind * of max-width). * @param {jQuery ref} t - table object */ var applyBounds = function(t){ var w = $.map(t.c, function(c){ //obtain real widths return c.width(); }); t.width(t.w = t.width()).removeClass(FLEX); //prevent table width changes $.each(t.c, function(i,c){ c.width(w[i]).w = w[i]; //set column widths applying bounds (table's max-width) }); t.addClass(FLEX); //allow table width changes }; /** * Event handler used while dragging a grip. It checks if the next grip's position is valid and updates it. * @param {event} e - mousemove event binded to the window object */ var onGripDrag = function(e){ if(!drag) return; var t = drag.t; //table object reference var oe = e.originalEvent.touches; var ox = oe ? oe[0].pageX : e.pageX; //original position (touch or mouse) var x = ox - drag.ox + drag.l; //next position according to horizontal mouse position increment var mw = t.opt.minWidth, i = drag.i ; //cell's min width var l = t.cs*1.5 + mw + t.b; var last = i == t.ln-1; //check if it is the last column's grip (usually hidden) var min = i? t.g[i-1].position().left+t.cs+mw: l; //min position according to the contiguous cells var max = t.f ? //fixed mode? i == t.ln-1? t.w-l: t.g[i+1].position().left-t.cs-mw: Infinity; //max position according to the contiguous cells x = M.max(min, M.min(max, x)); //apply bounding drag.x = x; drag.css("left", x + PX); //apply position increment if(last){ //if it is the last grip var c = t.c[drag.i]; //width of the last column is obtained drag.w = c.w + x- drag.l; } if(t.opt.liveDrag){ //if liveDrag is enabled if(last){ c.width(drag.w); if(!t.f && t.opt.overflow){ //if overflow is set, incriment min-width to force overflow t.css('min-width', t.w + x - drag.l); }else { t.w = t.width(); } }else{ syncCols(t,i); //columns are synchronized } syncGrips(t); var cb = t.opt.onDrag; //check if there is an onDrag callback if (cb) { e.currentTarget = t[0]; cb(e); } //if any, it is fired } return false; //prevent text selection while dragging }; /** * Event handler fired when the dragging is over, updating table layout * @param {event} e - grip's drag over event */ var onGripDragOver = function(e){ d.unbind('touchend.'+SIGNATURE+' mouseup.'+SIGNATURE).unbind('touchmove.'+SIGNATURE+' mousemove.'+SIGNATURE); $("head :last-child").remove(); //remove the dragging cursor style if(!drag) return; drag.removeClass(drag.t.opt.draggingClass); //remove the grip's dragging css-class if (!(drag.x - drag.l == 0)) { var t = drag.t; var cb = t.opt.onResize; //get some values var i = drag.i; //column index var last = i == t.ln-1; //check if it is the last column's grip (usually hidden) var c = t.g[i].c; //the column being dragged if(last){ c.width(drag.w); c.w = drag.w; }else{ syncCols(t, i, true); //the columns are updated } if(!t.f) applyBounds(t); //if not fixed mode, then apply bounds to obtain real width values syncGrips(t); //the grips are updated if (cb) { e.currentTarget = t[0]; cb(e); } //if there is a callback function, it is fired if(t.p && S) memento(t); //if postbackSafe is enabled and there is sessionStorage support, the new layout is serialized and stored } drag = null; //since the grip's dragging is over }; /** * Event handler fired when the grip's dragging is about to start. Its main goal is to set up events * and store some values used while dragging. * @param {event} e - grip's mousedown event */ var onGripMouseDown = function(e){ var o = $(this).data(SIGNATURE); //retrieve grip's data var t = tables[o.t], g = t.g[o.i]; //shortcuts for the table and grip objects var oe = e.originalEvent.touches; //touch or mouse event? g.ox = oe? oe[0].pageX: e.pageX; //the initial position is kept g.l = g.position().left; g.x = g.l; d.bind('touchmove.'+SIGNATURE+' mousemove.'+SIGNATURE, onGripDrag ).bind('touchend.'+SIGNATURE+' mouseup.'+SIGNATURE, onGripDragOver); //mousemove and mouseup events are bound h.append("<style type='text/css'>*{cursor:"+ t.opt.dragCursor +"!important}</style>"); //change the mouse cursor g.addClass(t.opt.draggingClass); //add the dragging class (to allow some visual feedback) drag = g; //the current grip is stored as the current dragging object if(t.c[o.i].l) for(var i=0,c; i<t.ln; i++){ c=t.c[i]; c.l = false; c.w= c.width(); } //if the colum is locked (after browser resize), then c.w must be updated return false; //prevent text selection }; /** * Event handler fired when the browser is resized. The main purpose of this function is to update * table layout according to the browser's size synchronizing related grips */ var onResize = function(){ for(var t in tables){ if( tables.hasOwnProperty( t ) ) { t = tables[t]; var i, mw=0; t.removeClass(SIGNATURE); //firefox doesn't like layout-fixed in some cases if (t.f) { //in fixed mode t.w = t.width(); //its new width is kept for(i=0; i<t.ln; i++) mw+= t.c[i].w; //cell rendering is not as trivial as it might seem, and it is slightly different for //each browser. In the beginning i had a big switch for each browser, but since the code //was extremely ugly now I use a different approach with several re-flows. This works //pretty well but it's a bit slower. For now, lets keep things simple... for(i=0; i<t.ln; i++) t.c[i].css("width", M.round(1000*t.c[i].w/mw)/10 + "%").l=true; //c.l locks the column, telling us that its c.w is outdated }else{ //in non fixed-sized tables applyBounds(t); //apply the new bounds if(t.mode == 'flex' && t.p && S){ //if postbackSafe is enabled and there is sessionStorage support, memento(t); //the new layout is serialized and stored for 'flex' tables } } syncGrips(t.addClass(SIGNATURE)); } } }; //bind resize event, to update grips position $(window).bind('resize.'+SIGNATURE, onResize); /** * The plugin is added to the jQuery library * @param {Object} options - an object that holds some basic customization values */ $.fn.extend({ colResizable: function(options) { var defaults = { //attributes: resizeMode: 'fit', //mode can be 'fit', 'flex' or 'overflow' draggingClass: 'JCLRgripDrag', //css-class used when a grip is being dragged (for visual feedback purposes) gripInnerHtml: '', //if it is required to use a custom grip it can be done using some custom HTML liveDrag: false, //enables table-layout updating while dragging minWidth: 15, //minimum width value in pixels allowed for a column headerOnly: false, //specifies that the size of the the column resizing anchors will be bounded to the size of the first row hoverCursor: "e-resize", //cursor to be used on grip hover dragCursor: "e-resize", //cursor to be used while dragging postbackSafe: false, //when it is enabled, table layout can persist after postback or page refresh. It requires browsers with sessionStorage support (it can be emulated with sessionStorage.js). flush: false, //when postbakSafe is enabled, and it is required to prevent layout restoration after postback, 'flush' will remove its associated layout data marginLeft: null, //in case the table contains any margins, colResizable needs to know the values used, e.g. "10%", "15em", "5px" ... marginRight: null, //in case the table contains any margins, colResizable needs to know the values used, e.g. "10%", "15em", "5px" ... disable: false, //disables all the enhancements performed in a previously colResized table partialRefresh: false, //can be used in combination with postbackSafe when the table is inside of an updatePanel, disabledColumns: [], //column indexes to be excluded //events: onDrag: null, //callback function to be fired during the column resizing process if liveDrag is enabled onResize: null //callback function fired when the dragging process is over } var options = $.extend(defaults, options); //since now there are 3 different ways of resizing columns, I changed the external interface to make it clear //calling it 'resizeMode' but also to remove the "fixed" attribute which was confusing for many people options.fixed = true; options.overflow = false; switch(options.resizeMode){ case 'flex': options.fixed = false; break; case 'overflow': options.fixed = false; options.overflow = true; break; } return this.each(function() { init( this, options); }); } }); })(jQuery); </script> <script type="text/javascript"> $(function(){ $("#normal").colResizable({ liveDrag:true, gripInnerHtml:"<div class='grip'></div>", draggingClass:"dragging", resizeMode:'fit' }); $("#flex").colResizable({ liveDrag:true, gripInnerHtml:"<div class='grip'></div>", draggingClass:"dragging", resizeMode:'flex' }); $("#overflow").colResizable({ liveDrag:true, gripInnerHtml:"<div class='grip'></div>", draggingClass:"dragging", resizeMode:'overflow' }); $("#disabled").colResizable({ liveDrag:true, gripInnerHtml:"<div class='grip'></div>", draggingClass:"dragging", resizeMode:'overflow', disabledColumns: [2] }); }); </script> <script> function myFunction() { // Declare variables var input, filter, table, tr, td1,td2,td3,td4,td5,td6,td7,td8,td9, i; input = document.getElementById("myInput"); filter = input.value.toUpperCase(); table = document.getElementById("normal"); //tr = table.getElementsByTagName("tr"); tr = table.getElementsByClassName("trrow"); // Loop through all table rows, and hide those who don't match the search query for (i = 0; i < tr.length; i++) { td1 = tr[i].getElementsByClassName("EntityName")[0]; td2 = tr[i].getElementsByClassName("EntityId")[0]; td3 = tr[i].getElementsByClassName("Path")[0]; td4 = tr[i].getElementsByClassName("ClassName")[0]; td5 = tr[i].getElementsByClassName("ClassId")[0]; td6 = tr[i].getElementsByClassName("WorkflowName")[0]; td7 = tr[i].getElementsByClassName("WorkflowDisplayName")[0]; td8 = tr[i].getElementsByClassName("WorkflowType")[0]; td9 = tr[i].getElementsByClassName("WorkflowId")[0]; if (td1) { if ( (td1.innerHTML.toUpperCase().indexOf(filter) > -1) || (td2.innerHTML.toUpperCase().indexOf(filter) > -1) || (td3.innerHTML.toUpperCase().indexOf(filter) > -1) || (td4.innerHTML.toUpperCase().indexOf(filter) > -1) || (td5.innerHTML.toUpperCase().indexOf(filter) > -1) || (td6.innerHTML.toUpperCase().indexOf(filter) > -1) || (td7.innerHTML.toUpperCase().indexOf(filter) > -1) || (td8.innerHTML.toUpperCase().indexOf(filter) > -1) || (td9.innerHTML.toUpperCase().indexOf(filter) > -1) ) { tr[i].style.display = ""; } else { tr[i].style.display = "none"; } } } } </script> </head> <body> <input type="text" id="myInput" onkeyup="myFunction()" placeholder="Search for text.."> <div class="center" > <table id="normal" width="400%" border="0" cellpadding="5" cellspacing="0"> '@ $HTML += @" $headerHTML "@ $i =1 #Build rows ForEach ($WF in $arrWFs) { $rows += @" <tr class="trrow"> "@ ForEach ($header in $arrOrderedHeaders) { If ($header -eq 'Index') { $rows += @" <td class="$($Header)">$($i)</td> "@ } Else { $rows += @" <td class="$($Header)">$($WF.$Header)</td> "@ } } #end ForEach Header $i++ } #end ForEach WF $rows += @" </tr> "@ $HTML += @" $rows </table> </div> </body> </html> "@ Return $HTML } #End Function Build-HTMLFancyResize ################################################################### Function Export-HTMLFile { [CmdletBinding(DefaultParameterSetName='Parameter Set 1', SupportsShouldProcess=$true, PositionalBinding=$false)] Param ( #array of workflows to be exported to the Path [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=0, ParameterSetName='Parameter Set 1')] [System.Object[]]$arrWFs, #export file path [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=1, ParameterSetName='Parameter Set 1')] [string]$Path, #title of HTML doc [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=2, ParameterSetName='Parameter Set 1')] [string]$Title = "", #which formatting type [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=3, ParameterSetName='Parameter Set 1')] [ValidateSet("Fancy", "Basic")] [string]$Format = 'Fancy' ) Write-Host "Building HTML document..." -ForegroundColor Gray Switch ($Format){ "Fancy" { $HTML = Build-HTMLFancyResize -arrWFs $arrWFs -Title $Title } "Basic" { $HTML = Build-HTMLBasic -arrWFs $arrWFs -Title $Title } Default { Throw "Problem with Export-HTMLFile. Exiting..." } } Try { $HTML | Set-Content -Path $Path -Force Write-Verbose "`nFiles written to directory [$(Split-Path $Path -Parent)]" Return $Path }Catch { Write-Error "Unable to write content to HTML file: [$Path]. `n$($Error[0].Message)" } } ################################################################### Function Export-CSVFile { Param ( #export file path [string]$Path, #array of workflows to be exported to the Path [System.Object[]]$arrWFs ) Try { $arrWFs | Export-Csv -Path $Path -NoTypeInformation -Force Write-Verbose "`nFiles written to directory [$(Split-Path $Path -Parent)]" Return $Path }Catch { Write-Error "Unable to write content to CSV file: [$Path]. `n$($Error[0].Message)" } } ################################################################### Function FormatObj-ForCSVExport { Param ( # New custom workflow object being built [System.Object]$obj, # Workflow (from effective config method) [System.Object]$WF ) $Params = '' Try { Switch ($WorkFlow.Type){ "Rule" { $Obj.WorkflowDisplayName = ($hashAllRules[$WorkFlow.WorkflowId.Guid].DisplayName) If ($hashAllRules[$WorkFlow.WorkflowId.Guid].DataSourceCollection.Configuration) { $Params = (([xml]"<Parameters>$($hashAllRules[$WorkFlow.WorkflowId.Guid].DataSourceCollection.Configuration)</Parameters>").ChildNodes ) } $Obj.WorkflowMPName = ($hashAllRules[$WorkFlow.WorkflowId.Guid].Identifier.Domain[0]) } "Monitor" { $Obj.WorkflowDisplayName = ($hashAllMonitors[$WorkFlow.WorkflowId.Guid].DisplayName) If ($hashAllMonitors[$WorkFlow.WorkflowId.Guid].Configuration) { $Params = (([xml]"<Parameters>$($hashAllMonitors[$WorkFlow.WorkflowId.Guid].Configuration)</Parameters>").ChildNodes ) } $Obj.WorkflowMPName = ($hashAllMonitors[$WorkFlow.WorkflowId.Guid].Identifier.Domain[0]) } default { $Obj.WorkflowDisplayName = ($WorkFlow.WorkflowName) Write-Host "WorkFlow.Type: '$($WorkFlow.Type)' not found!" -F Yellow } } } Catch { $Obj.WorkflowDisplayName = ($WorkFlow.WorkflowName) } If ($Params) { $propertyNames = '' $propertyNames = $Params | Get-Member -MemberType Property | Select-Object Name -ExpandProperty Name $ParamsFormattedCSV ='' ForEach ($Name in $propertyNames) { $ParamsFormattedCSV += @" $($Name): $($Params.$Name) "@ } $Obj.Parameters = $ParamsFormattedCSV } $OverridesFormattedCSV = '' If ($WorkFlow.Overridden) { ForEach ($Override in $WorkFlow.OverrideableParameters) { $propertyNames = $Override | Get-Member -MemberType Property | Select-Object Name -ExpandProperty Name $OverridesFormattedCSV += @" ParameterName: $($Override.ParameterName) DefaultValue: $($Override.DefaultValue) EffectiveValue: $($Override.EffectiveValue) <><><><><><><><><><><><><><><><><> "@ } #end ForEach Override $Obj | Add-Member -MemberType NoteProperty -Name "Overrides" -Value $OverridesFormattedCSV } Else { #No Overrides $Obj | Add-Member -MemberType NoteProperty -Name "Overrides" -Value '' } Return $Obj } ################################################################### <# This function is for formatting the Parameters and Overrides fields specifically for HTML. #> Function FormatObj-ForHTMLExport { Param ( # New custom workflow object being built [System.Object]$Obj ) $Params = '' Try { Switch ($WorkFlow.Type){ "Rule" { $Obj.WorkflowDisplayName = ($hashAllRules[$WorkFlow.WorkflowId.Guid].DisplayName) If ($hashAllRules[$WorkFlow.WorkflowId.Guid].DataSourceCollection.Configuration) { $Params = (([xml]"<Parameters>$($hashAllRules[$WorkFlow.WorkflowId.Guid].DataSourceCollection.Configuration)</Parameters>").ChildNodes ) } $Obj.WorkflowMPName = ($hashAllRules[$WorkFlow.WorkflowId.Guid].Identifier.Domain[0]) } "Monitor" { $Obj.WorkflowDisplayName = ($hashAllMonitors[$WorkFlow.WorkflowId.Guid].DisplayName) If ($hashAllMonitors[$WorkFlow.WorkflowId.Guid].Configuration) { $Params = (([xml]"<Parameters>$($hashAllMonitors[$WorkFlow.WorkflowId.Guid].Configuration)</Parameters>").ChildNodes ) } $Obj.WorkflowMPName = ($hashAllMonitors[$WorkFlow.WorkflowId.Guid].Identifier.Domain[0]) } default {$Obj.WorkflowDisplayName = ($WorkFlow.WorkflowName)} } } Catch { $Obj.WorkflowDisplayName = ($WorkFlow.WorkflowName) } If ($Params) { $propertyNames = '' $propertyNames = $Params | Get-Member -MemberType Property | Select-Object Name -ExpandProperty Name $embedTable = @" <table border=1 cellpadding="5"> <tr style="font-weight:bold"> <td>Name</td> <td>Value</td> </tr> "@ ForEach ($Name in $propertyNames) { $value = ( ( (($Params.$Name -Replace '&','&') -Replace '<', '<' ) -Replace '>', '>' ) -Replace '\n', $BR_Token) $embedTable += @" <tr> <td>$($Name)</td> <td>$($value)</td> </tr> "@ } $embedTable += @" </table> "@ $Obj.Parameters = $embedTable } If ($WorkFlow.Overridden) { $embedTable = @" <table border=1 cellpadding="5"> <tr style="font-weight:bold"> <td>ParameterName</td> <td>DefaultValue</td> <td>EffectiveValue</td> </tr> "@ ForEach ($Override in $WorkFlow.OverrideableParameters) { $propertyNames = $Override | Get-Member -MemberType Property | Select-Object Name -ExpandProperty Name $embedTable += @" <tr> <td>$($Override.ParameterName)</td> <td>$($Override.DefaultValue)</td> <td>$($Override.EffectiveValue)</td> </tr> "@ } #end ForEach Override $embedTable += @" </table> "@ $Obj | Add-Member -MemberType NoteProperty -Name "Overrides" -Value $embedTable } Else { #No Overrides $Obj | Add-Member -MemberType NoteProperty -Name "Overrides" -Value '' } Return $Obj } ################################################################### Function Get-CleanNames { Param ( # Should be a MonitoringObject [Microsoft.EnterpriseManagement.Monitoring.MonitoringObject]$MonitoringObject ) $objCleanNames = New-Object -TypeName pscustomobject $objCleanNames | Add-Member -MemberType NoteProperty -Name "Name" -Value (Clean-Name $MonitoringObject.Name) If ($MonitoringObject.DisplayName) { $objCleanNames | Add-Member -MemberType NoteProperty -Name "DN" -Value (Clean-Name $MonitoringObject.DisplayName) } Else { $objCleanNames | Add-Member -MemberType NoteProperty -Name "DN" -Value (Clean-Name $MonitoringObject.Name) } $objCleanNames | Add-Member -MemberType NoteProperty -Name "Path" -Value (Clean-Name $MonitoringObject.Path) Return $objCleanNames } ################################################################### Function Get-SCOMRelatedObjects { [CmdletBinding(DefaultParameterSetName='Parameter Set 1', SupportsShouldProcess=$true)] Param ( # This should be a SCOM MonitoringObject [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, ValueFromRemainingArguments=$true, Position=0, ParameterSetName='Parameter Set 1')] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [Microsoft.EnterpriseManagement.Monitoring.MonitoringObject[]]$object ) #region Begin Begin { [System.Collections.ArrayList]$arr = @() Function Dig-Object { [CmdletBinding(DefaultParameterSetName='Parameter Set 1', SupportsShouldProcess=$true)] Param ( # Param1 help description [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, ValueFromRemainingArguments=$true, Position=0, ParameterSetName='Parameter Set 1')] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [system.object]$object, [string]$indent ) Begin{ [System.Collections.ArrayList]$arr = @() #[System.Collections.ArrayList]$related = @() } Process { Write-Verbose "$indent $($_.DisplayName) , $($_.GetLeastDerivedNonAbstractClass())" $NULL = $arr.Add($object) $tmpRelated = $object.GetRelatedMonitoringObjects() If ($tmpRelated.Count -ge 1) { ForEach ($item in ($tmpRelated | Dig-Object -indent ($indent + "--"))) { $NULL = $arr.Add($item) } } } End { Return ($arr ) } } }#endregion Begin Process{ ForEach ($item in $object){ #$arr += $item | Dig-Object -indent "--" $item | Dig-Object -indent "--" | ForEach-Object {$NULL = $arr.Add($_)} } } End{ Return ($arr | Select-Object -Unique) } }#end Get-SCOMRelatedObjects ################################################################### Function Load-Cache { # Load all Monitors Write-Host "Getting monitors..." -F Gray $AllMonitors = Get-SCOMMonitor $hashAllMonitors = @{} $i =1 ForEach ($Mon in $AllMonitors) { $percent = [math]::Round((($i / $AllMonitors.Count) *100),0) Write-Progress -Activity "** What's happening? **" -status "Loading monitors. Be patient! [Percent: $($percent)]" -PercentComplete $percent $hashAllMonitors.Add($Mon.Id.Guid, $Mon) $i++ } Write-Progress -Activity "** What's happening? **" -status "Complete" -Completed # Load all rules Write-Host "Getting rules..." -F Gray $AllRules = Get-SCOMRule $hashAllRules = @{} $i=1 ForEach ($Rule in $AllRules) { $percent = [math]::Round((($i / $AllRules.Count) *100),0) Write-Progress -Activity "** What's happening? **" -status "Loading Rules. Be patient! [Percent: $($percent)]" -PercentComplete $percent $hashAllRules.Add($Rule.Id.Guid, $Rule) $i++ } Write-Progress -Activity "** What's happening? **" -status "Complete" -Completed } ################################################################### #--------------------------------------------------------------- #--------------------------- MAIN ------------------------------ #Build headers # 2022.06.20 removed "Index" as it was not meaningful when recursively enumerating workflows $arrOrderedHeaders = @" EntityName EntityId Path ClassName ClassId WorkflowName WorkflowDisplayName WorkflowType WorkflowId WorkflowMPName Enabled GeneratesAlert AlertSeverity AlertPriority Overridden Description Parameters Overrides "@ -split '\r\n' Switch ($SortOrder ) { 'Ascending' { [BOOL]$SortOrder = $false } 'Descending' { [BOOL]$SortOrder = $true } Default { [BOOL]$SortOrder = $false } } $BR_Token = '<BR />' #Validate arguments/paths If ( (-NOT($ExportCSVPath)) -AND (-NOT($ExportHTMLPath)) ) { Throw 'You must provide at least one valid export path/directory with -ExportHTMLPath or -ExportCSVPath. Exiting...' } If ($ExportCSVPath) { Try{ If (-NOT(Test-Path -Path $ExportCSVPath -PathType Container)){ Write-Host "Path: [$($ExportCSVPath)] does not exist. Creating folder now..." New-Item -Path $ExportCSVPath -ItemType Directory -ErrorAction Stop } } Catch{ Write-Error "Invalid path: [$($ExportCSVPath)]. Must be valid directory. Unable to create directory." Return 1 } } If ($ExportHTMLPath) { Try{ If (-NOT(Test-Path -Path $ExportHTMLPath -PathType Container)){ Write-Host "Path: [$($ExportHTMLPath)] does not exist. Creating folder now..." New-Item -Path $ExportHTMLPath -ItemType Directory -ErrorAction Stop } Else { $SearchIconPath = Join-Path -Path $ExportHTMLPath -ChildPath 'searchicon.png' } } Catch{ Write-Error "Invalid path: [$($ExportHTMLPath)]. Must be valid directory. Unable to create directory." Return 1 } } If (-NOT $TESTING){ Write-Output "Loading all rules and monitors..." . Load-Cache } #--------------------------------------------------------------- #Thanks to Craig Pero for pointing me in the right direction here! $omMg = Get-SCOMManagementGroup | Select-Object -First 1 #array to contain master list of all workflows of all related objects [System.Collections.ArrayList]$arrAllWorkflowsCSV = @() [System.Collections.ArrayList]$arrAllWorkflowsHTML = @() $timer = [System.Diagnostics.Stopwatch]::StartNew() #array to contain all related objects $arrObjects = Get-SCOMRelatedObjects -object $MonitoringObject Write-Host "$($timer.Elapsed.TotalSeconds) second to retrieve all related objects" -ForegroundColor Gray Write-Host "`n`n`n`n" #Start below Write-Progress bar $hashSCI = @{} #Keep a collection of each class instance retrieved. Retrieve each class instance only once. $i = 1 #counter for the progress bar $timer = [System.Diagnostics.Stopwatch]::StartNew() [bool]$SkipFixOR = $false #region ForEach ($obj in ($arrObjects |Sort-Object -Property DisplayName) ) { $CleanNames = '' $CleanName = Get-CleanNames -MonitoringObject $obj Write-Host "$($i): " -ForegroundColor Cyan -NoNewline; ` Write-Host "[" -ForegroundColor Red -NoNewline; ` Write-Host "$($obj.Path)" -ForegroundColor Yellow -NoNewline; ` Write-Host "]" -ForegroundColor Red -NoNewline; ` Write-Host " $($obj.FullName)" -ForegroundColor Green $percent = [math]::Round((($i / $arrObjects.Count) *100),0) Write-Progress -Activity "** What's happening? **" -status "Getting your data. Be patient! [Percent: $($percent)]" -PercentComplete $percent Try{ $Workflows = $omMg.EffectiveMonitoring.GetAllEffectiveMonitoringWorkflows($obj.Id,$true) } Catch{ # It's possible to have "not lowercase" true|false values for the Enabled property which causes this report/method to puke Write-Verbose ('Unable to execute: $Workflows = $omMg.EffectiveMonitoring.GetAllEffectiveMonitoringWorkflows($obj.id,$true)' + " for instance of class: Microsoft.Windows.Computer. $($obj.FullName) , ID: $($obj.Id)") Write-Error $Error[0].Exception Write-Host "Skipping item: $($obj.FullName) , ID: $($obj.Id)" -F Gray #<# If ( ($Error[0].Exception -match 'true|false') -AND ($Error[0].Exception -cmatch '[^true|^false]') -AND (-NOT $SkipFixOR) ) { $choice = '' $choice = $null While ($choice -notmatch '^n$|^a$') { Write-Host "Problem detected with `"Enabled`" property value. This value must be all lowercase. Would you like to attempt to fix overrides?" -ForegroundColor Yellow -BackgroundColor Red $choice = (Read-Host "fix [a]ll or [n]o ?").ToLower() } # Fix overrides If ($choice -eq 'a') { If ($ExportCSVPath){ $tmpPath = $ExportCSVPath } Else { $tmpPath = $ExportHTMLPath} Set-OverrideEnabledCase -ExportPath $tmpPath Write-Host "`nOverride Values should be fixed. Try to run this report again. `n`nExiting..." -F Yellow Exit } #Do not fix overrides. Don't prompt again. Else { Write-Host "Will not prompt again to repair overrides. " -F Gray $SkipFixOR = $true #user will not be prompted again to fix } } Continue; } #end Catch #> [System.Collections.ArrayList]$arrThisObjWorkflowsCSV = @() [System.Collections.ArrayList]$arrThisObjWorkflowsHTML = @() #[int]$w = 1 #region ForEach ($WorkFlow in ($Workflows | Sort-Object -Property EntityId -Descending:$SortOrder )) { $tmpObj = New-Object -TypeName PSCUSTOMOBJECT # $tmpObj | Add-Member -MemberType NoteProperty -Name "NAME" -Value "VALUE" #$tmpObj | Add-Member -MemberType NoteProperty -Name "Index" -Value $w $tmpObj | Add-Member -MemberType NoteProperty -Name "EntityName" -Value ($WorkFlow.EntityName) $tmpObj | Add-Member -MemberType NoteProperty -Name "EntityId" -Value ($WorkFlow.EntityId.Guid) # Retrieve each class instance only once. If (-NOT $hashSCI[($WorkFlow.EntityId.Guid)] ) { $hashSCI[($WorkFlow.EntityId.Guid)] = (Get-SCOMClassInstance -Id $WorkFlow.EntityId.Guid) } $tmpObj | Add-Member -MemberType NoteProperty -Name "Path" -Value (($hashSCI[($WorkFlow.EntityId.Guid)]).Path ) $tmpObj | Add-Member -MemberType NoteProperty -Name "ClassName" -Value ($WorkFlow.EntityTypeName) $tmpObj | Add-Member -MemberType NoteProperty -Name "ClassId" -Value ($WorkFlow.EntityTypeId.Guid) $tmpObj | Add-Member -MemberType NoteProperty -Name "WorkflowName" -Value ($WorkFlow.WorkflowName) $tmpObj | Add-Member -MemberType NoteProperty -Name "WorkflowDisplayName" -Value "PLACEHOLDER" $tmpObj | Add-Member -MemberType NoteProperty -Name "WorkflowType" -Value ($WorkFlow.Type) $tmpObj | Add-Member -MemberType NoteProperty -Name "WorkflowId" -Value ($WorkFlow.WorkflowId.Guid) $tmpObj | Add-Member -MemberType NoteProperty -Name "WorkflowMPName" -Value "PLACEHOLDER" $tmpObj | Add-Member -MemberType NoteProperty -Name "Enabled" -Value ($WorkFlow.Enabled.ToString()) $tmpObj | Add-Member -MemberType NoteProperty -Name "GeneratesAlert" -Value ($WorkFlow.GeneratesAlert.ToString()) $tmpObj | Add-Member -MemberType NoteProperty -Name "AlertSeverity" -Value ($WorkFlow.AlertSeverity) $tmpObj | Add-Member -MemberType NoteProperty -Name "AlertPriority" -Value ($WorkFlow.AlertPriority) $tmpObj | Add-Member -MemberType NoteProperty -Name "Overridden" -Value ($WorkFlow.Overridden.ToString()) $tmpObj | Add-Member -MemberType NoteProperty -Name "Description" -Value ($WorkFlow.Description) $tmpObj | Add-Member -MemberType NoteProperty -Name "Parameters" -Value '' If ($ExportCSVPath){ $tmpObjCSV = FormatObj-ForCSVExport -Obj $tmpObj.PsObject.Copy() $arrAllWorkflowsCSV.Add($tmpObjCSV) | Out-Null $arrThisObjWorkflowsCSV.Add($tmpObjCSV) | Out-Null } If ($ExportHTMLPath){ $tmpObjHTML = FormatObj-ForHTMLExport -Obj $tmpObj.PsObject.Copy() $arrAllWorkflowsHTML.Add($tmpObjHTML) | Out-Null $arrThisObjWorkflowsHTML.Add($tmpObjHTML) | Out-Null } $w++ } #endregion ForEach Workflow (of the Obj) If ($ExportCSVPath -AND $IndividualFiles){ Try { $ObjExportFilePath = (Join-Path $ExportCSVPath "($($CleanName.Path))_$($CleanName.DN).csv" ) Export-CSVFile -arrWFs $arrThisObjWorkflowsCSV -Path $ObjExportFilePath -NoTypeInformation -Force }Catch { Write-Error "Problem exporting to CSV [$ObjExportFilePath]. $($Error[0].Message)" } } If ($ExportHTMLPath -AND $IndividualFiles) { Try { $ObjExportFilePath = (Join-Path $ExportHTMLPath "($($CleanName.Path))_$($CleanName.DN).html" ) Export-HTMLFile -arrWFs $arrThisObjWorkflowsHTML -Path $ObjExportFilePath -Title $obj.Name -Format $HTMLFormat }Catch { Write-Error "Problem exporting to HTML [$ObjExportFilePath]. $($Error[0].Message)" } } $i++ } #endregion ForEach Obj Write-Progress -Activity "** What's happening? **" -status "Completed" -Completed $arrAllWorkflowsCSV = ($arrAllWorkflowsCSV | Sort-Object -Property $SortBy -Descending:$SortOrder) $arrAllWorkflowsHTML = ($arrAllWorkflowsHTML | Sort-Object -Property $SortBy -Descending:$SortOrder) $CleanNames = '' # Output master files if appropriate If ($ExportCSVPath){ Try { If ($MonitoringObject.Count -eq 1){ $CleanName = Get-CleanNames -MonitoringObject $MonitoringObject[0] $ObjExportFilePath = (Join-Path $ExportCSVPath "($($CleanName.Path))_$($CleanName.DN).csv" ) } Else { $ObjExportFilePath = (Join-Path $ExportCSVPath "MERGED.CSV" ) } Export-CSVFile -arrWFs $arrAllWorkflowsCSV -Path $ObjExportFilePath -NoTypeInformation -Force }Catch { Write-Error "Problem exporting to CSV [$ObjExportFilePath]. $($Error[0].Message)" } } If ($ExportHTMLPath) { Try { If ($MonitoringObject.Count -eq 1){ $CleanName = Get-CleanNames -MonitoringObject $MonitoringObject[0] $ObjExportFilePath = (Join-Path $ExportHTMLPath "($($CleanName.Path))_$($CleanName.DN).html" ) } Else { $ObjExportFilePath = (Join-Path $ExportHTMLPath "MERGED.HTML" ) } Export-HTMLFile -arrWFs $arrAllWorkflowsHTML -Path $ObjExportFilePath -Title 'All Workflows' -Format $HTMLFormat }Catch { Write-Error "Problem exporting to HTML [$ObjExportFilePath]. $($Error[0].Message)" } } Write-Host "$($timer.Elapsed.TotalSeconds) seconds to complete workflow export for $($i) objects." -ForegroundColor Gray #--------------------------------------------------------------- } #End Function <# .SYNOPSIS Will export Operations Manager event log events to CSV .EXAMPLE Export-SCOMEventsToCSV The above example will output the newest 1000 SCOM events (default 1000) to a .CSV at C:\<computername>_OpsManEvents.csv .EXAMPLE Export-SCOMEventsToCSV -Newest 1500 -Path c:\Temp\SCOMlog.csv .NOTES Author: Tyson Paul Blog: https://monitoringguys.com/ Original Date: 2018.03.22 History: #> Function Export-SCOMEventsToCSV { [CmdletBinding(DefaultParameterSetName='Parameter Set 1', SupportsShouldProcess=$true, PositionalBinding=$false, ConfirmImpact='Medium')] Param ( [int]$Newest = 1000, # Param1 help description [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=1, ParameterSetName='Parameter Set 1')] [Alias("Path")] [string]$OutFileCSV = "C:\$($env:COMPUTERNAME)_OpsManEvents.csv" ) Get-EventLog -LogName 'Operations Manager' -Newest $Newest | Export-Csv -Path $OutFileCSV -NoTypeInformation -Force Return (Get-Item $OutFileCSV) } ####################################################################### <# .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 #Export SCOM Knowledge for rules/monitors Get-SCOMManagementGroupConnection | Remove-SCOMManagementGroupConnection -Verbose New-SCOMManagementGroupConnection -ComputerName YOUR_SERVERNAME $SQLMPs = Get-SCOMManagementPack -Name *sql* $Outfolder = 'D:\SCOMReports' # Workflows to separate files ForEach ($MP in $SQLMPs){ $rules = $MP.GetRules().Count $mons = $MP.GetMonitors().Count If (($rules + $mons) -gt 0){ Export-SCOMKnowledge -OutFolder $Outfolder -ManagementPack $MP -Topic "$($MP.Name)_" } Else{ Write-Host "No monitor/rule workflows found: " -NoNewline; Write-Host "$($MP.DisplayName)" -f Yellow } } #All MPs combined Export-SCOMKnowledge -OutFolder $Outfolder -ManagementPack $SQLMPs -Topic "ALL_SQL_" .LINK https://monitoringguys.com/ .NOTES Author: Tyson Paul Blog: https://monitoringguys.com/ History: 2022.06.20: Used arraylist to improve speed when gathering rules/mons. Leveraged new Convert-MAML2HTML function for article conversion. 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=$false, 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')] [Alias("WorkflowFilter")] [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) $articleContent = ($MamlText | ForEach-Object { Convert-MAMLToHtml -XML $_ }) } 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 = $NULL Try { $article = $thisWF.GetKnowledgeArticle($cultureInfo) } Catch { #Empty catch is fine here } If (-NOT $article){ $error.Remove($Error[0]) $articlecontent = "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 [System.Collections.ArrayList]$Rules=@() [System.Collections.ArrayList]$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-SCOMPowerShellModule 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 ( (Get-SCOMRule -ManagementPack $ManagementPack | Where-Object {( $_.Name -match "$ThisRegex") -OR ( $_.DisplayName -match "$ThisRegex")} ) | ForEach-Object {$NULL = $Rules.Add($_) }) Write-Host "Getting all filtered Monitors..." -ForegroundColor Yellow ( (Get-SCOMMonitor -ManagementPack $ManagementPack | Where-Object {( $_.Name -match "$ThisRegex") -OR ( $_.DisplayName -match "$ThisRegex")} ) | ForEach-Object {$NULL = $Monitors.Add($_) }) } } # If no filter(s) exist, then return all rules/mons from the designated MP. Else { Write-Host "Getting ALL Rules..." -ForegroundColor Yellow ( (Get-SCOMRule -ManagementPack $ManagementPack ) | ForEach-Object {$NULL = $Rules.Add($_) }) Write-Host "Getting ALL Monitors..." -ForegroundColor Yellow ( (Get-SCOMMonitor -ManagementPack $ManagementPack ) | ForEach-Object {$NULL = $Monitors.Add($_) }) } } # 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 ( (Get-SCOMRule | Where-Object {( $_.Name -match "$ThisRegex") -OR ( $_.DisplayName -match "$ThisRegex")}) | ForEach-Object {$NULL = $Rules.Add($_) }) Write-Host "Getting all filtered Monitors..." -ForegroundColor Yellow ( (Get-SCOMMonitor | Where-Object {( $_.Name -match "$ThisRegex") -OR ( $_.DisplayName -match "$ThisRegex")} ) | ForEach-Object {$NULL = $Monitors.Add($_) }) } } Else{ Write-Host "Getting ALL Rules..." -ForegroundColor Yellow ( (Get-SCOMRule) | ForEach-Object {$NULL = $Rules.Add($_) }) Write-Host "Getting ALL Monitors..." -ForegroundColor Yellow ( (Get-SCOMMonitor) | ForEach-Object {$NULL = $Monitors.Add($_) }) } } } #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 -Encoding UTF8 } Else { $RulesTempContent = $myRulesObj | ConvertTo-HTML -Title "SCOM Rules" -Head $cssHead $RulesTempContent = CleanHTML $RulesTempContent $RulesTempContent = $RulesTempContent -Replace '<table>', '<table border="1" cellpadding="20">' $RulesTempContent | Set-Content (Join-Path $OutFolder ($Topic +"Rules.html")) -Encoding UTF8 } 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 -Encoding UTF8 } Else { $MonitorTempContent = $myMonitorObj | ConvertTo-HTML -Title "SCOM Monitors" -Head $cssHead $MonitorTempContent = CleanHTML $MonitorTempContent $MonitorTempContent = $MonitorTempContent -Replace '<table>', '<table border="1" cellpadding="20">' $MonitorTempContent | Set-Content (Join-Path $OutFolder ($Topic + "Monitors.html")) -Encoding UTF8 } Write-host "Output folder: $OutFolder" -BackgroundColor Black -ForegroundColor Yellow If ($ShowResult) { Explorer.exe $OutFolder } } #endregion } ####################################################################### <# .Synopsis This script will export all SCOM overrides to various file formats. .DESCRIPTION Will export a generous amount of information about overrides. Output may be filtered by management pack and exported to html, csv, xml, and json file formats. .PARAMETER OutDir Directory where output files will be created. Folder will get created if it doesn't already exist. .PARAMETER ExportFormats File format. TypeInformation is excluded. "CSV","JSON","HTML","XML" .PARAMETER CSS Any custom CSS for the HTML report. This will be inserted into the <Head> tag of the document. See examples. .PARAMETER FilterMPNames This can be a comma-separated string of management pack names or it can be an array of single names or a combination of both. See examples. The report(s) will only include overrides that exist in these MPs. .PARAMETER SortBy Sort rows by column name. .PARAMETER SortOrder Ascending or descending based on SortBy parameter. .PARAMETER DateStampFile By default the output files will include a datestamp in the name. Specify $false to produce a consistent filename. See examples. .PARAMETER WriteToEventLog Will write debugging info to Opsman event log. .EXAMPLE Export-SCOMOverrides -OutDir C:\Reports -ExportFormats HTML -FilterMPNames 'URLGenie.OVERRIDES,OpsMgr.Self.Maintenance.Overrides','PortGenie.OVERRIDES','WindowsService.OVERRIDES' -Verbose This example demonstrates that you can supply a comma-separated list of MP Names or an array of names or both. .EXAMPLE Export-SCOMOverrides -SortBy ORMPName -SortOrder Ascending -DateStampFile:$false -Verbose .EXAMPLE #Example PS C:\> [string]$CSS = @' <style> table, th, td { border: 1px solid black; border-collapse: collapse; } th, td { padding: 5px; } td { text-align: center; } </style> '@ PS C:\> Export-SCOMOverrides -SortBy ORMPName -ExportFormats HTML -CSS $CSS -OutDir "C:\Reports" .NOTES Author: Tyson Paul Blog: https://monitoringguys.com/2019/11/12/scomhelper/ History: 2020.05.13 - First version .INPUTS Inputs to this cmdlet (if any) .OUTPUTS Will output to std output or up to 4 different output file formats. #> Function Export-SCOMOverrides { [CmdletBinding(DefaultParameterSetName='Parameter Set 1', SupportsShouldProcess=$true, PositionalBinding=$false, HelpUri = 'https://monitoringguys.com/', ConfirmImpact='Medium')] Param ( # Folder for exported files (HTML and CSV) [Parameter(Mandatory=$true, ParameterSetName='FileExport', ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=0)] [string]$OutDir, # Export Formats: <html | csv | xml | json | all> [Parameter(Mandatory=$false, ParameterSetName='FileExport', ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=1)] [ValidateSet( "All", "CSV", "JSON", "HTML", "XML")] $ExportFormats , # Optional CSS for HTML file export. Will appear in <Head> section of output file. [Parameter(Mandatory=$false, ParameterSetName='FileExport', ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=2)] [string]$CSS = @' <style> table, th, td { border: 1px solid black; border-collapse: collapse; } th, td { padding: 3px; } td { text-align: left; } </style> '@ , # Comma-separated list of management pack Names or array of names (or both) for which to include overrides from only THESE MPs. Otherwise ALL ORs from ALL MPs will be exported. [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=3)] [string[]]$FilterMPNames = '', #Report rows will be sorted by this Workflow property [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=4)] [ValidateSet("ContextDN", "ContextInstanceDN", "ContextIsGroup", "Enforced", "ORMPName", "ORName", "Parameter", "Property", "Value", "Workflow", "WorkflowDN", "WorkflowMPName", "WorkflowType")] [string]$SortBy = "WorkflowDN", #Formatting of HTML export [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=5)] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [ValidateSet("Ascending", "Descending")] [string]$SortOrder = 'Ascending', # Will export files with unique datestamp name. <true|false> [Parameter(Mandatory=$false, ParameterSetName='FileExport', ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=6)] [switch]$DateStampFile = $true, [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=7)] [switch]$WriteToEventLog ) [int]$maxLogLength = 31000 #max chars to allow for event log messages [string]$ScriptName = 'Export-Overrides.ps1' $ScriptNameInstanceGUID = (New-Guid).Guid.Substring(((New-Guid).Guid.Length ) -6).ToUpper() [string]$whoami = whoami.exe ######################################################## Function LogIt { Param( [switch]$Display, [int]$EventID, [int]$Line, $maxLogLength=31000, [string]$msg = '<No message provided>', [bool]$Proceed, [int]$Type = 2 ) $output = @" Message: $msg ThisScriptInstanceGUID: $ScriptNameInstanceGUID ScriptLine: $Line Running As: $whoami This script: $ScriptName Any Errors: $Error "@ If ($Proceed) { $oEvent = New-Object -ComObject 'MOM.ScriptAPI' If ($output.Length -gt $maxLogLength){ $output = ($output.Substring(0,([math]::Min($output.Length,$maxLogLength) )) + '...TRUNCATED...') } $oEvent.LogScriptEvent($ScriptName,$EventID,$Type,$output ) #Display output, appropriate for an agent task. } If ($Display ) { Write-Verbose $msg } } ############################################################################### Function Load-Cache { LogIt -msg "Building cache..." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display # Cache all classes LogIt -msg "Getting all Classes..." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display $AllClasses = Get-SCOMClass LogIt -msg "$($AllClasses.Count) found." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display $hashAllClasses = @{} $hashAllClassesID = @{} ForEach ($Class in $AllClasses) { $hashAllClasses.Add($Class.Name, $Class) $hashAllClassesID.Add($Class.ID.GUID,$Class) } # Cache all monitors LogIt -msg "Getting all Monitors..." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display $AllMonitors = Get-SCOMMonitor LogIt -msg "$($AllMonitors.Count) Monitors found." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display $hashAllMonitors = @{} ForEach ($Mon in $AllMonitors) { $hashAllMonitors.Add($Mon.Id.Guid, $Mon) } # Cache all rules LogIt -msg "Getting all Rules..." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display $AllRules = Get-SCOMRule LogIt -msg "$($AllRules.Count) Rules found." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display $hashAllRules = @{} ForEach ($Rule in $AllRules) { $hashAllRules.Add($Rule.Id.Guid, $Rule) } # Cache all discoveries LogIt -msg "Getting all Discoveries..." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display $AllDiscoveries = Get-SCOMDiscovery LogIt -msg "$($AllDiscoveries.Count) Discoveries found." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display $hashAllDiscoveries = @{} ForEach ($Disc in $AllDiscoveries) { $hashAllDiscoveries.Add($Disc.Id.Guid, $Disc) } # Cache all groups LogIt -msg "Getting all Groups..." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display $AllGroups = Get-SCOMGroup LogIt -msg "$($AllGroups.Count) Groups found." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display $hashAllGroups = @{} ForEach ($Group in $AllGroups) { $hashAllGroups.Add($Group.FullName, $Group) } } ######################################################## Function Load-MPs { Param( [ValidateSet("Sealed","Unsealed","All")] [string]$type="All" ) $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() If ($Reload -eq $true ){ $type = 'All'} switch ($type) { 'All' { LogIt -msg "Getting ALL MPs. This may take a minute..." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display $AllMPs = Get-SCOMManagementPack | Where-Object {$_.Name -notin $ExcludedMPs.Values} $hashAllMPs = @{} $AllMPs | ForEach-Object { $hashAllMPs.Add($_.Name,$_) } $tmpmsg = "$($AllMPs.Count ) MPs found..." } {$_ -in 'All','Sealed' } { LogIt -msg "Getting Sealed MPs. This may take a minute..." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display $SealedMPs = $AllMPs | Where-Object -FilterScript {$_.Sealed -eq $True } | Where-Object {$_.Name -notin $ExcludedMPs.Values} $hashSealedMPs = @{} $SealedMPs | ForEach-Object { $hashSealedMPs.Add($_.Name,$_) } $tmpmsg += " $($SealedMPs.Count ) Sealed MPs found..." } {$_ -in 'All','Unsealed' } { LogIt -msg "Getting unsealed MPs. This may take a minute..." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display $UnsealedMPs = $AllMPs | Where-Object -FilterScript {$_.Sealed -eq $False } | Where-Object {$_.Name -notin $ExcludedMPs.Values} $hashUnsealedMPs = @{} $UnsealedMPs | ForEach-Object { $hashUnsealedMPs.Add($_.Name,$_) } $tmpmsg += " $($UnsealedMPs.Count ) unsealed MPs found..." } Default {Write-Host "Default switch. Problem." -F Yellow} } [double]$elapsed = $stopwatch.Elapsed.TotalSeconds $stopwatch.Stop() LogIt -msg "$tmpmsg Operation took $($elapsed) seconds." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display } ######################################################## Function Select-Overrides { Param( [Parameter( Mandatory=$false, ParameterSetName='1')] [System.Object[]]$MPs, [System.Object[]]$Override, [Switch]$silent ) [System.Collections.ArrayList]$arrayOR = @() If ([bool]$MPs) { LogIt -msg "`nGetting overrides from $($MPs.Count) unsealed MP(s). This may take a minute..." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display # Return only the overrides from the previously selected MPs ForEach ($MP in $MPs) { Try{ $arrayOR += ( ($AllMPs | Where-Object { $_.Name -eq $MP.Name}).GetOverrides() ) }Catch{ Write-Host "No valid overrides found in MP: $($MP.Name)" } } } Else { LogIt -msg "`nGetting overrides from $($AllMPs.Count) Management Packs. This may take a minute..." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display $arrayOR = $AllMPs | ForEach-Object {$_.GetOverrides()} } If ($arrayOR.Count -eq 0){ LogIt -msg "No valid overrides found in MP(s). To be valid for selection, ORs must not reference locally defined targets or workflows (in the same unsealed pack)." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display } # Just set variable here. Function should be dot-sourced when called. $ORs = $arrayOR } ######################################################## Function Format-OR { Param( $ORs ) [System.Collections.ArrayList]$arrObject = @() [int]$i=0 LogIt -msg "Begin formatting ORs..." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display # 'MonitorPropertyOverride', 'RulePropertyOverride', 'DiscoveryPropertyOverride' # 'MonitorConfigurationOverride', 'RuleConfigurationOverride', 'DiscoveryConfigurationOverride' # I'm going to have to do this the old-fashioned way ForEach ($item in $ORs) { $i++ #Write-Progress -Activity "Formatting Data" -status "$i of ($ORs.Count)" -percentComplete ($i / ($ORs.Count*100)) $Object = New-Object PSObject # Will address Diagnostic and Recovery overrides at a later time. Maybe. If ($item.XMLTag -match 'Diagnostic|Recovery' ) { Continue; } $Object | add-member Noteproperty ORName ([string]$item.Name) $Object | add-member Noteproperty ORMPName ([string]$item.Identifier.Domain[0]) # is Monitor If ([bool]$item.Monitor.Identifier.Path.Count) { $Object | add-member Noteproperty WorkflowMPName ($item.Monitor.Identifier.Domain[0].ToString()) $Object | add-member Noteproperty WorkflowType "Monitor" $Object | add-member Noteproperty Workflow ($item.Monitor.Identifier.Path[0].ToString()) #$Object | add-member Noteproperty WorkflowDN ((Get-SCOMMonitor -Id $item.Monitor.Id.Guid).DisplayName ) $Object | add-member Noteproperty WorkflowDN (($hashAllMonitors[$item.Monitor.Id.Guid]).DisplayName ) $Object | add-member Noteproperty WorkflowId ([string]$item.Monitor.Id.Guid) } # is Rule ElseIf (($item.Rule.Identifier.Path.Count)) { $Object | add-member Noteproperty WorkflowMPName ($item.Rule.Identifier.Domain[0].ToString()) $Object | add-member Noteproperty WorkflowType "Rule" $Object | add-member Noteproperty Workflow ($item.Rule.Identifier.Path[0].ToString() ) #$Object | add-member Noteproperty WorkflowDN ((Get-SCOMRule -Id $item.Rule.Id.Guid).DisplayName ) $Object | add-member Noteproperty WorkflowDN (($hashAllRules[$item.Rule.Id.Guid]).DisplayName ) $Object | add-member Noteproperty WorkflowId ([string]$item.Rule.Id.Guid) } # is Discovery ElseIf ([bool]($item.Discovery.Identifier.Path.Count)) { $Object | add-member Noteproperty WorkflowMPName ($item.Discovery.Identifier.Domain[0].ToString()) $Object | add-member Noteproperty WorkflowType "Discovery" $Object | add-member Noteproperty Workflow ($item.Discovery.Identifier.Path[0].ToString()) #$Object | add-member Noteproperty WorkflowDN ((Get-SCOMDiscovery -Id $item.Discovery.Id.Guid).DisplayName ) $Object | add-member Noteproperty WorkflowDN (($hashAllDiscoveries[$item.Discovery.Id.Guid]).DisplayName ) $Object | add-member Noteproperty WorkflowId ([string]$item.Discovery.Id.Guid) } Else { $Object | add-member Noteproperty Workflow "ERROR" $Object | add-member Noteproperty WorkflowDN "ERROR" $Object | add-member Noteproperty WorkflowType "ERROR" } # To be added at some point # Diagnostic Task # Recovery Task # Note: $item.Target.Identifier.Path[0] If ([bool]($item.Property.Count)) { $Object | add-member Noteproperty Property ($item.Property.ToString() ) $Object | add-member Noteproperty Parameter "" } Else { $Object | add-member Noteproperty Property "" $Object | add-member Noteproperty Parameter $item.Parameter } $Object | add-member Noteproperty Value $item.Value If ([bool]($hashAllClassesID[$item.Context.Id.Guid])){ $Object | add-member Noteproperty Context ( $hashAllClassesID[$item.Context.Id.Guid].Name ) $Object | add-member Noteproperty ContextID ($item.Context.Id.Guid.ToString()) $Object | add-member Noteproperty ContextDN ( $hashAllClassesID[$item.Context.Id.Guid].DisplayName ) If ( $hashAllGroups.ContainsKey($hashAllClassesID[$item.Context.Id.Guid].Name) ) { $Object | add-member Noteproperty ContextIsGroup 'True' } Else { $Object | add-member Noteproperty ContextIsGroup 'False' } } #If the Context/Target does not exist, then add the ORName to a dirty list Else { Try{ # Need to figure out what to do with this collection of duds. $StaleORNames.Add($item.Name,'Stale') } Catch { #typically it's a bad idea to have an empty Catch but it's fine in this situation } $Object | add-member Noteproperty Context "DOES NOT EXIST - STALE REFERENCE" $Object | add-member Noteproperty ContextID "" $Object | add-member Noteproperty ContextDN "" } If ([Bool]($item.ContextInstance.Count)) { $Object | add-member Noteproperty ContextInstanceID ($item.ContextInstance.Guid.ToString() ) If ([bool]($DN = (Get-SCOMClassInstance -Id $item.ContextInstance.Guid).DisplayName) ) { # This might prove to be very time consuming $Object | add-member Noteproperty ContextInstanceDN $DN } Else{ $Object | add-member Noteproperty ContextInstanceDN "" } } Else { $Object | add-member Noteproperty ContextInstanceID "" $Object | add-member Noteproperty ContextInstanceDN "" } $Object | add-member Noteproperty Enforced $item.Enforced $Object | add-member Noteproperty OR_MPVersion $item.Identifier.Version.ToString() $arrObject.Add($Object) | Out-Null } If ($Object) { Remove-Variable -Name Object -ErrorAction SilentlyContinue } LogIt -msg "Done formatting ORs." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display # Just set variable here. Function should be dot-sourced when called. $ORs_F = $arrObject } ######################################################## Function Verify-OutDir { Param ( [string]$OutDir ) If (-NOT (Test-Path -Path $OutDir -PathType Container )) { Try { New-Item -Path $OutDir -ItemType Directory -Force -ErrorAction Stop Return $true }Catch{ Write-Error "Unable to verify output directory." Return $false } } Else { Return (Test-Path -Path $OutDir -PathType Container ) } } ######################################################## Function __LINE__ { $MyInvocation.ScriptLineNumber } ######################################################## #==================================================================================== #====================================== MAIN ====================================== [int]$info=0 [int]$critical=1 [int]$warn=2 # This will help control output sort order command Switch ($SortOrder ) { 'Ascending' { [BOOL]$SortOrder = $false } 'Descending' { [BOOL]$SortOrder = $true } Default { [BOOL]$SortOrder = $false } } # These are MPs that shouldn't be tampered with or consulted. $ExcludedMPs = @{ 1 = 'Microsoft.SystemCenter.SecureReferenceOverride' 2 = 'Microsoft.SystemCenter.GlobalServiceMonitor.SecureReferenceOverride' 3 = 'Microsoft.SystemCenter.Notifications.Internal' 4 = 'Microsoft.SystemCenter.NetworkDiscovery.Internal' 5 = 'Microsoft.SystemCenter.Advisor.SecureReferenceOverride' } If ($DateStampFile){ [string]$FileName = "overrides_$(Get-Date -F yyyyMMdd_HHmmss)" } Else { [string]$FileName = "overrides" } If (-NOT (Verify-OutDir -OutDir $OutDir)) { LogIt -msg "Unable to verify export directory [$($OutDir)]. Please run this task with permissions to create the directory or specify a valid path. Exiting." -Type $critical -EventID 9995 -Proceed $true -Line $(__LINE__) -Display Exit } # Functions are dot-sourced (which is perfectly fine) because output statements in the functions are meant to be seen in agent task. # Output statements in functions will contaminate function return values. . Load-Cache . Load-MPs -type 'All' [System.Collections.ArrayList]$FilteredMPs = @() ForEach ($Item in $FilterMPNames) { ForEach ($Name in @($Item.Split(',').Split(';').Split(':')) ) { $null = $FilteredMPs.Add($hashAllMPs[$Name]) } } #. Load-MPs -type 'All' . Select-Overrides -MPs $FilteredMPs # Result will populate '$ORs_F' . Format-OR -ORs $ORs If ($ORs_F.Count) { $ORs_F = $ORs_F | Sort-Object -Property $SortBy -Descending:$SortOrder } If (-NOT $ExportFormats){ LogIt -msg "Returning overrides to std out." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display Return $ORs_F } Else { Switch ($ExportFormats) { {$_ -match 'all|csv'} { Try{ $ORs_F | Export-Csv -Path (Join-Path $OutDir "$($FileName).csv") -NoTypeInformation -Force -Encoding UTF8 LogIt -msg "Exporting overrides to: [$((Join-Path $OutDir "$($FileName).csv"))] ..." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display } Catch { LogIt -msg "FAILED Exporting overrides to: [$((Join-Path $OutDir "$($FileName).csv"))] ..." -Type $warn -EventID 9995 -Proceed $WriteToEventLog -Line $(__LINE__) -Display } } {$_ -match 'all|html'} { Try{ $ORs_F | ConvertTo-Html -Head $css | Set-Content -Path (Join-Path $OutDir "$($FileName).html") -Encoding UTF8 -Force LogIt -msg "Exporting overrides to: [$((Join-Path $OutDir "$($FileName).html"))] ..." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display } Catch { LogIt -msg "FAILED Exporting overrides to: [$((Join-Path $OutDir "$($FileName).html"))] ..." -Type $warn -EventID 9995 -Proceed $WriteToEventLog -Line $(__LINE__) -Display } } {$_ -match 'all|json'} { Try{ $ORs_F | ConvertTo-Json | Set-Content -Path (Join-Path $OutDir "$($FileName).json") -Force -Encoding UTF8 LogIt -msg "Exporting overrides to: [$((Join-Path $OutDir "$($FileName).json"))] ..." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display } Catch { LogIt -msg "FAILED Exporting overrides to: [$((Join-Path $OutDir "$($FileName).json"))] ..." -Type $warn -EventID 9995 -Proceed $WriteToEventLog -Line $(__LINE__) -Display } } {$_ -match 'all|xml'} { Try{ $ORs_F | Export-Clixml -Path (Join-Path $OutDir "$($FileName).xml") -Force -Encoding UTF8 LogIt -msg "Exporting overrides to: [$((Join-Path $OutDir "$($FileName).xml"))] ..." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display } Catch { LogIt -msg "FAILED Exporting overrides to: [$((Join-Path $OutDir "$($FileName).xml"))] ..." -Type $warn -EventID 9995 -Proceed $WriteToEventLog -Line $(__LINE__) -Display } } default { LogIt -msg "FAILED Exporting overrides! Something is wrong with `$ExportFormats [$($ExportFormats)]" -Type $critical -EventID 9995 -Proceed $true -Line $(__LINE__) -Display } }#end switch } LogIt -msg "Script end ..." -Type $info -EventID 9990 -Proceed $WriteToEventLog -Line $(__LINE__) -Display } ####################################################################### <# .DESCRIPTION Super fast ping results for large quantities of hosts. .EXAMPLE Fast-Ping "myserver.domain.com","yourserver.domain.com" .EXAMPLE Get-Content C:\test\RandomHosts.txt | Select-Object -First 100 | Fast-Ping .NOTES Author: Tyson Paul Original Author: unknown Original script: https://community.idera.com/database-tools/powershell/powertips/b/tips/posts/final-super-fast-ping-command I modifed this a bit. I added the ability to determine the max number of hostnames to process without choking the Get-WmiObject cmdlet, then recursively probe the chunks of the input hosts array. #> Function Fast-Ping { param ( # make parameter pipeline-aware [Parameter(Mandatory,ValueFromPipeline)] # one or more computer names (array) [string[]]$ComputerName, $TimeoutMillisec = 4000 ) begin { [int]$maxParam = 16384 # use this to collect computer names that were sent via pipeline [Collections.ArrayList]$bucket = @() # hash table with error code to text translation $StatusCode_ReturnValue = @{ 0 = 'Success' 11001 = 'Buffer Too Small' 11002 = 'Destination Net Unreachable' 11003 = 'Destination Host Unreachable' 11004 = 'Destination Protocol Unreachable' 11005 = 'Destination Port Unreachable' 11006 = 'No Resources' 11007 = 'Bad Option' 11008 = 'Hardware Error' 11009 = 'Packet Too Big' 11010 = 'Request Timed Out' 11011 = 'Bad Request' 11012 = 'Bad Route' 11013 = 'TimeToLive Expired Transit' 11014 = 'TimeToLive Expired Reassembly' 11015 = 'Parameter Problem' 11016 = 'Source Quench' 11017 = 'Option Too Big' 11018 = 'Bad Destination' 11032 = 'Negotiating IPSEC' 11050 = 'General Failure' } # hash table with calculated property that translates # numeric return value into friendly text $statusFriendlyText = @{ Name = 'Status' Expression = { # take status code and use it as index into # the hash table with friendly names # make sure the key is of same data type (int) $StatusCode_ReturnValue[([int]$_.StatusCode)] } } # calculated property that returns $true when status -eq 0 $IsOnline = @{ Name = 'Online' Expression = { $_.StatusCode -eq 0 } } # do DNS resolution when system responds to ping $DNSName = @{ Name = 'DNSName' Expression = { if ($_.StatusCode -eq 0) { if ($_.Address -like '*.*.*.*') { [Net.DNS]::GetHostByAddress($_.Address).HostName } else { [Net.DNS]::GetHostByName($_.Address).HostName } } } } } process { # add each computer name to the bucket # we either receive a string array via parameter, or # the process block runs multiple times when computer # names are piped $ComputerName | ForEach-Object -Process { $name = $_ -replace ' ','' $null = $bucket.Add($name) } } end { # Recursively break down query strings that are too long. # Assemble the query with all addresses $query = $bucket -join "' or Address='" If ($query.length -gt $maxParam) { $tmpQuery = $query.Clone() $div = 1 # If the query is too large, figure out what size chunk of the addresses will be under threshold/max size While ($tmpQuery.Length -gt $maxParam) { $div++ $tmpQuery = $bucket[0..([math]::Floor(($bucket.Count / $div)))] -join "' or Address='" } # This size chunk of addresses should be small enough, acceptable as a parameter. $start = [math]::Floor(($bucket.Count / $div)) Write-Verbose -Message "Starting size: $start" $index = 0 For($i = $start; $i -le ($bucket.Count -1); $i = ([math]::Min(($i + $start),($bucket.Count -1))) ) { Write-Verbose -Message "Range:$($index)-$($i)" $Result += Fast-Ping -ComputerName $bucket[($index)..$i] -TimeoutMillisec $TimeoutMillisec If ($i -eq ($bucket.Count -1)) { break } $index = $i+1 } } Else { $Result += Get-WmiObject -Class Win32_PingStatus -Filter "(Address='$query') and timeout=$TimeoutMillisec" | Select-Object -Property Address, $DNSName, $IsOnline, $statusFriendlyText } Return $Result } } ####################################################################### <# .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[]. This function can be useful with an automation/ticketing solution. .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://monitoringguys.com/ #> 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-SCOMPowerShellModule 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 } } #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 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://monitoringguys.com/ 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 ) [System.Collections.ArrayList]$arrClasses = @() # 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 -eq $false) -AND ($_.Abstract -eq $false) } Class ClassStats{ [int]$Instances [string]$ClassName [string]$ClassDisplayName [string]$MPName } ForEach ($Class in $Classes) { $Instances = $Class | Get-SCOMClassInstance [ClassStats]$obj = @{ Instances = $Instances.Count ClassName = $Class.Name ClassDisplayName = $Class.DisplayName MPName = ($Class.Identifier.Domain[0]) } $Null = $arrClasses.Add($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 .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 Modifications by Tyson: 23020.08.11 - Added ManagementServer param. It's not fancy, but better than hardcoded localhost. #> Function Get-SCOMHealthCheckOpsConfig { Param ( [Parameter(Mandatory=$false, ValueFromPipeline=$false, Position=2, ParameterSetName='Parameter Set 1')] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [string]$ManagementServer='Localhost' ) ################################################################################################################### # # 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 . Import-SCOMPowerShellModule # Connect to localhost when running on the management server or define a server to connect to. $connect = New-SCManagementGroupConnection -ComputerName $ManagementServer # 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 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 | Select Name,DisplayName,Version,FullName | Format-Table .EXAMPLE Get-SCOMMPFileInfo -inDir 'C:\Program Files (x86)\System Center Management Packs' | Select-Object Name,DisplayName,Version,FullName,KeyToken, @{Name="References"; Expression={($_.References.GetEnumerator() | Out-String) } } | Out-GridView -PassThru The above example will display all of the MP properties in Gridview with the References column displayed in a more friendly format. .EXAMPLE Get-SCOMMPFileInfo -inDir 'C:\Program Files (x86)\System Center Management Packs' | Out-GridView -PassThru | % {& explorer.exe (Split-Path -Parent $_.FullName) } The above example will present a list of your management packs in Gridview. Once you select one or more items-OK, the folder(s) containing the management pack(s) will be opened with File Explorer. .EXAMPLE Get-SCOMMPFileInfo -inDir 'C:\MySealedMPs' | Format-Table | ConvertTo-Html | Set-Content C:\mpfiles.html; C:\mpfiles.html The above example will create an HTML report of your MP files then open the HTML file with the default application. .INPUTS System.String, Switch .OUTPUTS Custom object .NOTES Author: Tyson Paul Blog: https://monitoringguys.com/ History: 2020.02.27 - Improved efficiency. Added verbose output. 2020.02.21 - Added more examples. Removed Gridview and Passthru switches. 2019.07.29 - Added a few more MP properties to the returned object(s) 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 # deprecated # [switch]$GridView=$true, # deprecated #[switch]$passthru=$true ) [System.Collections.ArrayList]$arrMaster = @() $MPFiles = Get-ChildItem -Path $inDir -Recurse -Include *.mp,*.mpb,*.xml ForEach ($File in $MPFiles.FullName) { Write-Verbose $File Switch ($File) { {$_ -Match '\.mp$|\.xml$'} { $MP = Get-SCManagementPack -ManagementPackFile $File break; } # Get MPB (bundles) {$_ -Match '\.mpb$'} { #Sometimes bundles cannot be retrieved Try { $MP = Get-SCManagementPack -BundleFile $File }Catch { Write-Host "Unable to retrieve file: [ $($File) ]. FullName.Length:[$($File.Length)]" -F Red 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 FullName -Value $File $obj | Add-Member -Type NoteProperty -Name Description -Value $MP.Description $obj | Add-Member -Type NoteProperty -Name ID -Value $MP.ID.GUID $obj | Add-Member -Type NoteProperty -Name KeyToken -Value $MP.KeyToken $obj | Add-Member -Type NoteProperty -Name DefaultLanguageCode -Value $MP.DefaultLanguageCode $obj | Add-Member -Type NoteProperty -Name References -Value $MP.References Write-Verbose "$File" $Null = $arrMaster.Add($obj) }#end ForEach Return $arrMaster }#End Function ####################################################################### <# .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://monitoringguys.com/ 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 ####################################################################### <# .DESCRIPTION Will output all RunAs accounts and any associated Security Profiles. .EXAMPLE Get-SCOMRunAsProfilesAccounts The above example will connect to localhost and output RunAs account and Seucrity Profile information. .EXAMPLE Get-SCOMRunAsProfilesAccounts -ManagementServer 'ms01.contoso.com' The above example will connect to the designated server and output RunAs account and Seucrity Profile information. .EXAMPLE Get-SCOMRunAsProfilesAccounts | Out-Gridview -Passthru The above example will display results in Gridview. Any selected rows can be passed through to the PowerShell Console for further review or manipulation. .INPUTS System.String .OUTPUTS Custom object .NOTES Author: Tyson Paul, derived from the works of Kevin Holman, Dirk Brinkman, Mihai (Sarbulescu?) Blog: https://monitoringguys.com/ History: 2020.02.25 - Revision of previous version (v1.0, Kevin Holman); improved efficiency, modified formatting. Ref: Kevin Holman: https://kevinholman.com/2017/07/27/document-your-scom-runas-account-and-profiles-script/ .FUNCTIONALITY This function works best when piped to Out-Gridview or exported to CSV. .LINK Get-SCOMRunAsAccountName #> Function Get-SCOMRunAsProfilesAccounts { Param( [string]$ManagementServer = 'LOCALHOST' ) ############# FUNCTIONS ################### Function Get-ClassfromInstance{ [CmdletBinding(DefaultParameterSetName='Parameter Set 1', SupportsShouldProcess=$true, PositionalBinding=$false, ConfirmImpact='Medium')] [OutputType([tinyClass])] Param ( $Instance ) $class = (Get-SCOMClass -Id $instance.LeastDerivedNonAbstractMonitoringClassId) $tmpClass = [tinyClass]::new() $tmpClass.Id = $class.Id $tmpClass.Name = $class.Name If ($class.DisplayName.Length -ne 0) { $tmpClass.DisplayName = $class.DisplayName } Return $tmpClass } ########### END FUNCTIONS ################# ########### BEGIN MAIN ################# # My custom object Class Account { [string]$RunAsAccountName = '' [string]$Domain = '' [string]$Username = '' [string]$AccountType = '' [string]$ProfileName = '' [string]$ProfileDisplayName = '' [string]$TargetClassID = '' [string]$TargetClassName = '' [string]$TargetClassDisplayName = '' [string]$TargetID = '' [string]$TargetName = '' [string]$TargetDisplayName = '' } Class tinyClass { [string]$Id = '' [string]$Name = '' [string]$DisplayName = '' } # Main array for account collection [System.Collections.ArrayList]$AccountDataArray = @() # Load Modules and Connect to SCOM . Import-SCOMPowerShellModule # Remove any existing/default mgmt group connection Get-SCOMManagementGroupConnection | Remove-SCOMManagementGroupConnection # Connect to designated mgmt server (explicit) $MGConnection = New-SCManagementGroupConnection -ComputerName $ManagementServer $MG = Connect-OMManagementGroup -SDK $ManagementServer # Load Assembly and define ManagementGroup Object $CoreDLL = 'Microsoft.EnterpriseManagement.Core' $null = [reflection.assembly]::LoadWithPartialName($CoreDLL) $EMG = New-Object -TypeName Microsoft.EnterpriseManagement.EnterpriseManagementGroup -ArgumentList ($ManagementServer) #Process HealthService based Action Accounts Section #======================================================= ForEach ($RunAsProfile in (Get-SCOMRunAsProfile)) { # Get Health Service array associated with the profile $HSRef = $MG.GetMonitoringSecureDataHealthServiceReferenceBySecureReferenceId($RunAsProfile.ID) ForEach ($HS in $HSRef) { $TargetName = (Get-SCOMClassInstance -Id $HS.HealthServiceId).Displayname $MonitoringData = $HS.GetMonitoringSecureData() $tempAccount = [Account]::new() $tempAccount.RunAsAccountName = $MonitoringData.Name $tempAccount.Domain = $MonitoringData.Domain $tempAccount.Username = $MonitoringData.UserName $tempAccount.AccountType = $MonitoringData.SecureDataType $tempAccount.ProfileName = $RunAsProfile.Name $tempAccount.TargetID = $HS.HealthServiceId.Guid.ToString() $tempAccount.TargetName = $TargetName $tempAccount.TargetDisplayName = '' $tmpClass = Get-ClassfromInstance -Instance (Get-SCOMClassInstance -Id $HS.HealthServiceId) $tempAccount.TargetClassId = $tmpClass.Id $tempAccount.TargetClassName = $tmpClass.Name $tempAccount.TargetClassDisplayName = $tmpClass.DisplayName If ($null -ne $RunAsProfile.DisplayName ) { $tempAccount.ProfileDisplayName = $RunAsProfile.DisplayName } #$AccountDataArray += $tempAccount $null = $AccountDataArray.Add($tempAccount) } } #======================================================= # End ForEach RunAsProfile # Process all RunAsAccounts targeted at other targets #======================================================= #Get all RunAsAccounts $colAccounts = $EMG.Security.GetSecureData() #Loop through each RunAs account ForEach ($account in $colAccounts) { $secStorId = $account.SecureStorageId $stringBuilder = New-Object -TypeName System.Text.StringBuilder $secStorId | ForEach-Object {$null = $stringBuilder.Append($_.ToString('X2'))} $MPCriteria = "Value='{0}'" -f $stringBuilder.ToString() $moc = New-Object -TypeName Microsoft.EnterpriseManagement.Configuration.ManagementPackOverrideCriteria -ArgumentList ($MPCriteria) $overrides = $EMG.Overrides.GetOverrides($moc) If ($overrides.Count -eq 0) { $tempAccount = [Account]::new() $tempAccount.RunAsAccountName = $account.Name $tempAccount.Domain = $account.Domain $tempAccount.Username = $account.UserName $tempAccount.AccountType = $account.SecureDataType $tempAccount.ProfileName = 'No Profile Assigned' $null = $AccountDataArray.Add($tempAccount) } # Customizations/overrides exist for the account, process them all. Else { ForEach ($override in $overrides) { # Every account will typically have these values $tempAccount = [Account]::new() $tempAccount.RunAsAccountName = $account.Name $tempAccount.Domain = $account.Domain $tempAccount.Username = $account.UserName $tempAccount.AccountType = $account.SecureDataType $tempAccount.TargetClassId = $override.Context.Id.Guid.ToString() $tmpClass = Get-SCClass -Id $tempAccount.TargetClassId $tempAccount.TargetClassName = $tmpClass.Name If ($null -ne $tmpClass.DisplayName) { $tempAccount.TargetClassDisplayName = $tmpClass.DisplayName } # If a ContextInstance (specific object) exists, add instance-specific data. If ($null -ne $override.ContextInstance) { $tempAccount.TargetID = $override.ContextInstance.Guid.ToString() $TargetClassInstance = Get-SCOMClassInstance -Id $tempAccount.TargetID $tempAccount.TargetName = $TargetClassInstance.Name If ($null -ne $TargetClassInstance.DisplayName) { $tempAccount.TargetDisplayName = $TargetClassInstance.DisplayName } } $secRef = $EMG.Security.GetSecureReference($override.SecureReference.Id) $tempAccount.ProfileName = $secRef.Name If ($null -ne $secRef.DisplayName) { $tempAccount.ProfileDisplayName = $secRef.DisplayName } # Add item to array $null = $AccountDataArray.Add($tempAccount) } } #End ForEach Override } #======================================================= # End ForEach account # Sort by RunAsAccountName. This is a nice touch for the user. Return ($AccountDataArray | Sort-Object -Property RunAsAccountName) } #End Function ####################################################################### <# .NOTES Name: Get-SCOMAgentWorkflows Author: Tyson Paul Blog: https://monitoringguys.com/ Version History: 2022.06.09.1749 - v1 Previously Get-SCOMRunningWorkflows was a standalone function for "running" workflows only. .DESCRIPTION Will output an array of objects containing helpful information on instances and the workflows running/failed for those instances. .EXAMPLE # #Example 1 #All workflows (Running and Failed) are retrieved for the agent name provided. The results are saved in a variable, $r, which is piped to GridView. The function call is dot-sourced so that any subsequent calls to this function will execute faster. PS C:\> $r = . Get-SCOMAgentWorkflows -Status All -AgentName 'db01.contoso.com' -Verbose PS C:\> $r | Out-GridView #Example 2 # Agent names matching the mask provided are piped to the function. PS C:\> (Get-SCOMAgent -DNSHostName *db*).DisplayName | Get-SCOMAgentWorkflows #Example 3 # Results are sumarized by agents with failed workflows PS C:\> $result = Get-SCOMAgentWorkflows -AgentName 'ms02.contoso.com','db01.contoso.com','devdb01.contoso.com' PS C:\> $result | Where-Object Status -eq 'Failed' | Group-Object Agent | Select-Object @{Name="Failed";Expression={$_.Count}},Name Failed Name ------ ---- 14 devdb01.CONTOSO.COM 9 DB01.CONTOSO.COM #Advanced examples for analyzing the workflow data # Storing results to XML, then consuming results from XML. PS C:\> Get-SCOMAgentWorkflows -AgentName 'ad01.contoso.com' | Export-Clixml -Path 'C:\Test\ad01.contoso.com.XML' $WFs = Import-Clixml -Path 'C:\Test\ad01.contoso.com.XML' # Total Instances $TOI = $WFs.instance.count Write-Host "Total object instances: $TOI" # Worflows types: $WFAT = $WFs.workflows.workflow | Group-Object Name | Measure-Object | Select-Object count -ExpandProperty count Write-Host "Worflows types: $WFAT" # Total WF instances running, all types: $TWFAT = $WFs.wfcount | Measure-Object -Sum | Select-Object sum -ExpandProperty sum Write-Host "Total WF instances running, all types: $TWFAT" # Workflows of category type $Category = 'EventCollection' $WFoCT = $WFs.workflows.workflow | ? {($_.Category -match $Category) -AND ($_.WriteActionCollection -notmatch 'Alert')} $WFoCT_sum = $WFoCT | Group-Object Name | Measure-Object | Select-Object count -ExpandProperty Count Write-Host "Workflows of category type [$($Category), Not Alert]: $WFoCT_sum" # Total WF instances of category type $TWFIoCT = $WFoCT | Group-Object Name | Measure-Object -sum count | Select-Object sum -ExpandProperty sum Write-Host "Total WF instances of category type [$($Category)]: $TWFIoCT" #Lists ############################################### # Classes $classes = Get-SCOMClass -ID (($wfs.InstanceName).LeastDerivedNonAbstractManagementPackClassId | Group-Object).Name Write-Host "$($Classes.count) class target types" # Class Names $Classes = ($wfs.InstanceName).LeastDerivedNonAbstractManagementPackClassId` | Group-Object ` | Select-Object count ` , @{Name="Class"; Expression = {(Get-SCOMClass -id $_.Name).Name}} ` , @{Name="Id";Expression = {$_.Name}}, @{Name="Identifier"; Expression = {(Get-SCOMClass -id $_.Name).Identifier}} ` | Sort-Object count ` | Format-Table $Classes $allWorkflows = @{} # List of workflows, instance count, category $WFs.workflows.workflow | % {$allWorkflows[($_.Name)] = $_} $WFs.workflows.workflow | ? {($_.Category -match $Category) -AND ($_.WriteActionCollection -notmatch 'Alert')} | Group-Object Name | Select-Object count,name,@{Name='DisplayName';Expression = {$allWorkflows[($_.Name)].DisplayName}} ,@{Name='Category';Expression = {$Category}} | Sort-Object count | Format-Table # All workflow types with counts $WFs.workflows.workflow | Group-Object Name | Select-Object count,name,@{Name='DisplayName';Expression = {$allWorkflows[($_.Name)].DisplayName}} ,@{Name='Category';Expression = {$allWorkflows[($_.Name)].Category}} | Sort-Object count | Format-Table #> Function Get-SCOMAgentWorkflows { [CmdletBinding(DefaultParameterSetName='Parameter Set 1', SupportsShouldProcess=$false, PositionalBinding=$false, HelpUri = 'https://monitoringguys.com/', ConfirmImpact='Medium')] Param( # FQDN of one or more agents [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=0, ParameterSetName='AgentName')] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [string[]]$AgentName, # Param1 help description [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false)] [ValidateSet('TasktResult', 'Detailed')] $OutputType = 'Detailed', [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false)] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [ValidateSet('Failed','Running','All')] $Status = 'All', # Seconds before function will abandon waiting for task to complete [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false)] [int]$TimeoutSeconds=600 ) BEGIN { $HSClass = Get-SCOMClass -Name "Microsoft.SystemCenter.HealthService" $AllHS = (Get-SCOMClassInstance -Class $HSClass) [System.Collections.ArrayList]$AllResults = @() [System.Collections.ArrayList]$TaskNames = @() [System.Collections.ArrayList]$Task = @() $thisStatus = @{ "Microsoft.SystemCenter.GetAllRunningWorkflows" = "Running" "Microsoft.SystemCenter.GetAllFailedWorkflows" = "Failed" } $hashTasks = @{ "Microsoft.SystemCenter.GetAllRunningWorkflows" = [System.Collections.ArrayList]@() "Microsoft.SystemCenter.GetAllFailedWorkflows" = [System.Collections.ArrayList]@() } Switch ($Status) { 'All' { $NULL = $TaskNames.Add('Microsoft.SystemCenter.GetAllRunningWorkflows') $NULL = $TaskNames.Add('Microsoft.SystemCenter.GetAllFailedWorkflows') } 'Failed' { $NULL = $TaskNames.Add('Microsoft.SystemCenter.GetAllFailedWorkflows') } 'Running' { $NULL = $TaskNames.Add('Microsoft.SystemCenter.GetAllRunningWorkflows') } } $MinWait = 2 $WaitSeconds = [math]::Min($MinWait,$TimeoutSeconds) ############################ FUNCTIONS ################################ <# .NOTES Name: Get-SCOMAgentWorkflowsReport Author: Tyson Paul Blog: https://monitoringguys.com/ Version History: 2020.09.22.1433 - v1 .DESCRIPTION Will output an array of objects containing helpful information on instances and the workflows running for those instances. .EXAMPLE # #Example PS C:\> $TaskResult = Get-SCOMAgentWorkflows -AgentName 'ad01.contoso.com','db01.contoso.com' -OutputType 'TasktResult' PS C:\> $TaskResult | Get-SCOMAgentWorkflowsReport .EXAMPLE # #Example PS C:\> Get-SCOMAgentWorkflowsReport -TaskResult (Get-SCOMAgentWorkflows -AgentName 'ad01.contoso.com' -OutputType TasktResult) .EXAMPLE # #Example PS C:\> Get-SCOMAgentWorkflowsReport -TaskResult $TaskResult .INPUTS Task result from 'Microsoft.SystemCenter.GetAllRunningWorkflows' agent task. .OUTPUTS [System.Object[]] #> Function Get-SCOMAgentWorkflowsReport { Param ( [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, ParameterSetName='Parameter Set 1')] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [System.Object[]]$TaskResult, [Parameter(Mandatory=$true, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, ParameterSetName='Parameter Set 1')] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [ValidateSet('Failed','Running')] $Status ) BEGIN { ############################ FUNCTIONS ################################ Function Build-Obj { Param ( $WF, [string]$Type ='NotDefined' ) $tmp = New-Object PSCustomObject $tmp | Add-Member -MemberType NoteProperty -Name 'Type' -Value $type $tmp | Add-Member -MemberType NoteProperty -Name 'Workflow' -Value $wf Return $tmp } ######################## END FUNCTIONS ################################ Remove-Variable -Name 'Instances','rpt1' -ErrorAction Ignore [System.Collections.ArrayList]$arrReports = @() # Load all workflow types into one hash. dot-source this function to speed up multiple runs. If (-NOT $allWFTypes) { $allWFTypes=@{} $allClasses=@{} Write-Warning "Loading all workflows into cache object. This may require a few minutes. dot-source this function next time to speed up consecutive uses." ForEach ($item in @(Get-SCOMMonitor)) { $allWFTypes[($item.Name)] = ( Build-Obj -WF $item -Type Monitor) } ForEach ($item in @(Get-SCOMRule)) { $allWFTypes[($item.Name)] = ( Build-Obj -WF $item -Type Rule) } ForEach ($item in @(Get-SCOMDiscovery)) { $allWFTypes[($item.Name)] = ( Build-Obj -WF $item -Type Discovery) } # Build hash of all class types ForEach ($Class in (Get-SCOMClass) ){ $allClasses[$Class.Id.Guid] = $Class} } Else { Write-Verbose "'allWFTypes' object [$($allWFTypes.Count)] already cached. 'allClasses' object already cached [$($allClasses.Count)]. Will not rebuild; use existing. This will save time." } }#end BEGIN #---------------------------------------------------------------- PROCESS { ForEach ($Result in $TaskResult) { $ThisAgent = (Get-SCOMClassInstance -Id $Result.TargetObjectId) # Get the instance objects involved $Instances = (($Result.Output | ForEach-Object {[xml]$_}).DataItem) $hashInstances = @{} $Instances.Details.Instance | ForEach-Object {$hashInstances[($_.ID -Replace '{|}' ,'' )] = $_ } # Get basic workflow data: Instance ID (of target object), Workflow count, Workflow list/array $InstanceDetails = ([xml]($Result.Output)).DataItem.Details.Instance | Select-Object @{Name='Id';Expression={(($_.ID -Replace '{|}','').ToString())}}, @{Name='WFCount';Expression={$_.Workflow.Count} }, @{Name='Workflow';Expression={$_.Workflow | Sort-Object} } | Sort-Object WFCount,Workflow -Descending $InstanceInfo = [ordered]@{} # Build an object with lots of data about each Instance, and the workflows running on its behalf ForEach ($Obj in $InstanceDetails) { $thisInstance = (Get-SCOMClassInstance -Id $Obj.ID ) $objHash = [Ordered]@{ WFCount = $obj.WFCount Status = $Status Agent = $ThisAgent InstanceID = $Obj.ID InstanceFullName = ($thisInstance.FullName) InstanceName = $thisInstance Class = $allClasses[($ThisInstance.LeastDerivedNonAbstractManagementPackClassId.Guid)] Workflows = [array]($obj.Workflow | ForEach-Object {$allWFTypes[$_] }) } Try { $InstanceInfo[$objHash.InstanceFullName] = $objHash } Catch { Write-Warning "$_" } } # Report #1 $rpt1 = ForEach ($key in @($InstanceInfo.Keys)) { $Key | Select-Object ` -Property @{Name='WFCount';Expression={$InstanceInfo[$key].WFCount}} ` ,@{Name='Status';Expression={$InstanceInfo[$key].Status}} ` ,@{Name='Agent';Expression={$InstanceInfo[$key].Agent}} ` ,@{Name='InstanceFullName';Expression={$InstanceInfo[$key].InstanceFullName}} ` ,@{Name='InstanceName';Expression={$InstanceInfo[$key].InstanceName}} ` ,@{Name='InstanceID';Expression={$InstanceInfo[$key].InstanceID}} ` ,@{Name='Class';Expression={$InstanceInfo[$key].Class}} ` ,@{Name='Workflows';Expression={$InstanceInfo[$key].Workflows}} ` } $Null = $arrReports.Add($rpt1) }#end FOREACH }#end PROCESS #---------------------------------------------------------------- END { Return $arrReports }#end END } #End Function Get-SCOMAgentWorkflowsReport ######################## END FUNCTIONS ################################ } #End BEGIN PROCESS { $Instance = $NULL Try { Write-Verbose "Get instances of Health Service class with name(s): $AgentName" $NotAvailable = $AllHS | Where-Object {($_.Path -in $AgentName) -AND ($_.IsAvailable -eq $false )} ForEach ($UnavailableAgent in $NotAvailable) { Write-Warning "UNAVAILABLE: $($UnavailableAgent.DisplayName)" } $Instance = $AllHS | Where-Object {($_.Path -in $AgentName ) -AND ($_.IsAvailable -eq $true )} If (-NOT $Instance) { Throw "Agents not found or unavailable:`n$($AgentName)`n" } } Catch { Write-Warning $_ Continue; } ForEach ($TaskName in $TaskNames) { $TaskWF = $NULL $TaskResult = $NULL Write-Verbose "Get SCOM task: $TaskName" $TaskWF = Get-SCOMTask -Name $TaskName Try { Write-Verbose "Starting the task now $(Get-Date) for [$($Instance.Count)] agents:" #$Instance.DisplayName | ForEach-Object {Write-Verbose $_} $Error.Clear() $thisTask = Start-SCOMTask -Instance $Instance -Task $TaskWF -Verbose:$([bool]($VerbosePreference -eq 'Continue')) -ErrorAction Stop $ThisTask | Where-Object {$_} | ForEach-Object { $NULL = $hashTasks[$TaskName].Add($_) } } Catch { Write-Error "Failed to start task: $TaskName. $_`nExiting. " Continue; } } #End ForEach TaskName } #end PROCESS END { $ScriptTimer = [System.Diagnostics.Stopwatch]::StartNew() ForEach ($TaskName in @($hashTasks.Keys)) { # Keep looping while tasks are still running (Scheduled, Queued, etc. (other than Failed/Succeeded)) Do { $secondsRemain = "{0:N0}" -F $($TimeoutSeconds - $ScriptTimer.Elapsed.TotalSeconds) Write-Verbose "`n$(Get-Date): Waiting for task(s) to complete. [$($secondsRemain)] seconds remain until timeout. Sleeping for $($WaitSeconds) seconds." Start-Sleep -Seconds $WaitSeconds If (-NOT $hashTasks[$TaskName].Id.Count) { Write-Warning "No tasks exist for workflow: $($TaskName)" Continue; } $Results = (Get-SCOMTaskResult -Id $hashTasks[$TaskName].Id) } While ((([regex]::matches(($Results.Status),'Succeeded').Count + [regex]::matches(($Results.Status),'Failed').Count) -LT ($Instance.Count)) -AND ($ScriptTimer.Elapsed.TotalSeconds -le $TimeoutSeconds) ) $ScriptTimer.Stop() # If some task is not Status=Succeeded, show report ForEach ($TaskResult in ($Results | Where-Object {$_.Status -notmatch "Succeeded" }) ) { $ThisInstance = $Instance | Where-object {$_.Id.Guid -match $TaskResult.TargetObjectId.Guid} Write-Warning "$($TaskName):$($ThisInstance.Path):$($TaskResult.Status)" } #end ForEach TaskResult If ($ScriptTimer.Elapsed.TotalSeconds -gt $TimeoutSeconds){ Write-Warning "Timer expired. Check task status manually. TaskID: [$($Task.Id.Guid)]. Exiting" } Else { Switch ($OutputType) { 'TasktResult' { $Results | Where-Object {$_} | ForEach-Object {$NULL = $AllResults.Add($_)} } 'Detailed' { # dot-sourced to potentially reduce multiple loading of the workflow cache in this function (. Get-SCOMAgentWorkflowsReport -Status $thisStatus[$TaskName] -TaskResult $Results -Verbose:$VerbosePreference ) | Where-Object {$_} | ForEach-Object {$_ | ForEach-Object {$NULL = $AllResults.Add($_)} } } } } } Return $AllResults } }#End Get-SCOMAgentWorkflows ####################################################################### <# .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://monitoringguys.com/ 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://monitoringguys.com/', ConfirmImpact='Medium')] Param ( [Parameter(Mandatory=$true, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=0, ParameterSetName='Parameter Set 1')] [String] $String, [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=1, ParameterSetName='Parameter Set 1')] [ValidateSet("SHA", "SHA1", "MD5", "SHA256", "SHA384", "SHA512","MACTripleDES","RIPEMD160")] $Algorithm = "MACTripleDES" #SHA1 is default. https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.hashalgorithm.create?view=netframework-4.8 ) $StringBuilder = New-Object System.Text.StringBuilder [System.Security.Cryptography.HashAlgorithm]::Create($Algorithm).ComputeHash([System.Text.Encoding]::UTF8.GetBytes($String))|ForEach-Object{ [Void]$StringBuilder.Append($_.ToString("x2")) } $StringBuilder.ToString() } #End Function ####################################################################### <# .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 permission to install these: 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 PS C:\> (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 PS C:\> (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 PS C:\> New-SCOMClassGraph -ShowIndex The example above will display an html index page for all of the graph files. The html file will open with the default application. .EXAMPLE PS C:\> 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 .EXAMPLE #Example Script $int=0 $arr =@() ForEach ($class in (Get-SCOMClass)) { $obj = New-Object pscustomobject $obj | add-member -name 'Index' -Value $int -MemberType NoteProperty $obj | add-member -name 'Name' -Value ($Class.Name) -MemberType NoteProperty $obj | add-member -name 'DisplayName' -Value ($class.DisplayName) -MemberType NoteProperty $arr+=$obj $int++ } $arr | Out-GridView -PassThru | New-SCOMClassGraph -Caching:$false -Combine This example will present a gridview list of classes for you to select. Multi-select supported to combine numerous classes on a single graph. Be careful, don't overdo it. .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. .Parameter ShowIndex This will display an html index page for all of the graph files. The html file will open with the default application. .NOTES Author: Tyson Paul Blog: https://monitoringguys.com/ History 2020.12.30 - Improved GraphViz package detection.(Thanks, Mike N.) 2020.12.08 - Added ability to sanitize class "Description" text; remove unwanted characters which can cause graphviz to puke. 2020.06.19 - Added property types and additional info to classes and discovery nodes. Index will now get updated every run. 2020.03.12 - Removed prompts to install GraphViz and Chocolatey packages. Instead provided suggestions on how to install them. Defeated Coronavirus with cigars and whiskey. 2020.02.28 - Improved temp directory creation/verification 2019.10.02 - Fixed Discovery nodes bug. Discoveries appear correctly now. 2019.09.06 - Updated output dir to $env:Windir instead of $env:Temp 2019.06.25 - Fixed -Combine flaw 2019.06.24 - Added input field and search feature to index html 2019.06.06 - Improved Combine functionality. Added -ShowIndex switch 2019.04.29 - Added colored connected arrows to better identify relationships. 2018.06.14 - Initial release. .LINK Get-SCOMMPFileInfo Get-SCOMClassInfo #> Function New-SCOMClassGraph { [CmdletBinding(DefaultParameterSetName='Parameter Set 1', SupportsShouldProcess=$false, PositionalBinding=$false, HelpUri = 'https://monitoringguys.com/', 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, ParameterSetName='Parameter Set 1')] [Parameter(Mandatory=$false, ParameterSetName='Parameter Set 2')] [Parameter(Mandatory=$false, ParameterSetName='Parameter Set 3')] [string]$outDir, # This will enable/disable the use of a previous graph file. Disable: will create a new/fresh graph file. [Parameter(Mandatory=$false, ParameterSetName='Parameter Set 1')] [Parameter(Mandatory=$false, ParameterSetName='Parameter Set 2')] [Parameter(Mandatory=$false, ParameterSetName='Parameter Set 3')] [switch]$Caching=$false, # When combining multiple classes on the same graph the host node color might not appear correctly. However, any arrows will indicate the relationship type based on color. This will depend entirely on which classes get enumerated first. [Parameter(Mandatory=$false, ParameterSetName='Parameter Set 1')] [Parameter(Mandatory=$false, ParameterSetName='Parameter Set 2')] [Parameter(Mandatory=$false, ParameterSetName='Parameter Set 3')] [switch]$Combine, # This will open the graph file with the default application (.png) [Parameter(Mandatory=$false, ParameterSetName='Parameter Set 1')] [Parameter(Mandatory=$false, ParameterSetName='Parameter Set 2')] [Parameter(Mandatory=$false, ParameterSetName='Parameter Set 3')] [switch]$ShowGraph=$true, # This will include discovery nodes in the graph(s) [Parameter(Mandatory=$false, ParameterSetName='Parameter Set 1')] [Parameter(Mandatory=$false, ParameterSetName='Parameter Set 2')] [Parameter(Mandatory=$false, ParameterSetName='Parameter Set 3')] [switch]$ShowDiscoveries=$true, # This will create/update, then open the index file with the default application (.html) [Parameter(Mandatory=$false, Position=0, ParameterSetName='Parameter Set 4')] [switch]$ShowIndex, # If foreign/old graphs exist in the output directory (but class no longer exists in this mgmt group), delete graph files. [Parameter(Mandatory=$false, ParameterSetName='Parameter Set 1')] [Parameter(Mandatory=$false, ParameterSetName='Parameter Set 2')] [Parameter(Mandatory=$false, ParameterSetName='Parameter Set 3')] [Parameter(Mandatory=$false, ParameterSetName='Parameter Set 4')] [switch]$RemoveForeignGraphs=$false ) Begin{ ####################################################################### [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 . Import-SCOMPowerShellModule Import-Module 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 } } # Verify if GraphViz is present. If (-NOT ([bool](Get-Package -Name "GraphViz*" -ErrorAction SilentlyContinue))) { Write-Host "Required package 'GraphViz' does not exist." -F Red Try{ Write-Host "`nIs 'Chocolately' Package Manager installed? ( https://chocolatey.org )" -F Gray (choco)[0] Write-Host "Yes!" }Catch { Write-Host "`n'Chocolatey' (The Package Manager for Windows at https://chocolatey.org) does not exist OR is outdated." -F Red Write-Host "Please install/update 'Chocolatey' to v0.10.15 or later from https://chocolatey.org ." Write-host "`nOne way to install Chocolatey is with the following PowerShell commands:" Write-Host -F Cyan @' Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1')) '@ } #end Catch Write-Host "`nUse Chocolatey to install GraphViz with the following PowerShell command:" Write-Host 'choco install graphviz -y' -F Cyan Write-Error "`nExiting." -ErrorAction Stop } # 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 -Verbose | Install-Module -Verbose If ($?){ Write-Host "Module installed!" Import-Module PSGraph -Verbose } 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 } } } ############################################################### Function Load-hashBasicMPInfo { # Load all MPs into hash. Will need the MP names below $MPs = Get-SCOMManagementPack $hashMPBasicInfo = @{} ForEach ($MP in $MPs) { $obj = New-Object PSCUSTOMOBJECT $obj | Add-Member -MemberType NoteProperty -Name IsSealed -Value $($MP.Sealed.ToString()) If ($MP.DisplayName.Length -gt 0){ $obj | Add-Member -MemberType NoteProperty -Name DisplayName -Value $($MP.DisplayName) } Else { $obj | Add-Member -MemberType NoteProperty -Name DisplayName -Value $($MP.Name) } $obj | Add-Member -MemberType NoteProperty -Name Version -Value $($MP.Version.ToString() ) $hashMPBasicInfo.Add($MP.Name,$obj) } Return $hashMPBasicInfo } ############################################################### # 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, [System.Collections.Hashtable]$hashCollection=@{} ) Begin {} Process{ Write-Host $Class.Name -ForegroundColor Yellow $hashThisClass = @{} # If a base class exists, dig it If ([bool]$Class.Base.ID.Guid){ $BaseClass = ($hashAllClassesID[$Class.Base.ID.Guid] ) If (-NOT ($hashCollection[$BaseClass.Name])) { $hashCollection = (Dig-Class $BaseClass -hashCollection $hashCollection) } Try{ $hashCollection[$BaseClass.Name]["Edges"] += @{($Class.Name)=('child')} }Catch{ #Empty Catch is fine here } } # Make sure that if a class is hosting that its 'Hosting' flag gets set. This is important when multiple class targets are combined on a single graph. If (($IsHosting) -AND ($hashCollection[$Class.Name]) ) { $hashCollection[$Class.Name].Hosting = $True } # If a hosting class exists, dig it. If ($Class.Hosted){ $hashGraph2= @{} $hostClass = $Class.FindHostClass() If (-NOT ($hashCollection[$hostClass.Name])) { $hashHosting = (Dig-Class -Class $hostClass -IsHosting -hashCollection $hashCollection) $hashGraph2 = (Merge-HashTables -hmaster $hashCollection -htnew $hashHosting) $hashCollection = $hashGraph2 } Else { $hashCollection[$hostClass.Name].Hosting = $True } Try{ $hashCollection[$hostClass.Name]["Edges"] += @{($Class.Name)=('hosted')} }Catch{ #Empty Catch is fine here } } #endregion [System.Collections.ArrayList]$PropNames = @() If ([bool]$Class.DisplayName) { $NULL = $PropNames.Add((Stylize-Row -Color $c_classAttribute -Bold $s_ClasAttributeBold -thisString "DisplayName: $($Class.DisplayName.ToString())")) } $NULL = $PropNames.Add((Stylize-Row -Color $c_classAttribute -Bold $true -thisString "ID/GUID: $($Class.ID.Guid)")) $NULL = $PropNames.Add((Stylize-Row -Color $c_classAttribute -Bold $true -thisString "MP: $($Class.GetManagementPack().Name)")) $NULL = $PropNames.Add((Stylize-Row -Color $c_classAttribute -Bold $true -thisString "MPDisplayName: $(($Class.GetManagementPack()).DisplayName)")) $NULL = $PropNames.Add((Stylize-Row -Color $c_classAttribute -Bold $true -thisString "MPVersion: $(($Class.GetManagementPack()).Version)")) If ($Class.Hosted) { $NULL = $PropNames.Add((Stylize-Row -Color $c_classAttribute -Bold $true -thisString "Hosted: $($Class.Hosted.ToString())")) } Else { $NULL = $PropNames.Add((Stylize-Row -Color $c_classAttribute -Bold $s_ClasAttributeBold -thisString "Hosted: $($Class.Hosted.ToString())")) } If ([bool]$Class.Abstract) { $NULL = $PropNames.Add((Stylize-Row -Color $c_classAttribute -Bold $TRUE -thisString "Abstract: $($Class.Abstract.ToString())")) } Else{ $NULL = $PropNames.Add((Stylize-Row -Color $c_classAttribute -Bold $s_ClasAttributeBold -thisString "Abstract: $($Class.Abstract.ToString())")) } If ([bool]$Class.Extension) { $NULL = $PropNames.Add((Stylize-Row -Color $c_ExtensionFont -Bold $s_ClasAttributeBold -thisString "Extension: $($Class.Extension.ToString())")) } Else { $NULL = $PropNames.Add((Stylize-Row -Color $c_classAttribute -Bold $s_ClasAttributeBold -thisString "Extension: $($Class.Extension.ToString())")) } $NULL = $PropNames.Add((Stylize-Row -Color $c_classAttribute -Bold $s_ClasAttributeBold -thisString "Singleton: $($Class.Singleton.ToString())")) $NULL = $PropNames.Add((Stylize-Row -Color $c_classAttribute -Bold $s_ClasAttributeBold -thisString "Description: $(Customize-StringLength -String (Sanitize-String -String $Class.Description) -WordCount $LineBreak)")) # Get all properties of the class $classProps = $Class.GetProperties() | Sort-Object $tmpProps = @() ForEach ($Prop in $classProps) { $n = $Prop | Select-Object -Property Name -ExpandProperty Name If ($Prop.Key -eq $true){ $tmpProps += "<Font Color=`"Red`"><B>[$($Prop.Type)] $($n)</B></Font>" } Else { $tmpProps += "[$($Prop.Type)] $($n)" } } $PropNames += $tmpProps $hashStyling = Get-DefaultStyling $hashThisClass = [Ordered]@{ "PropNames" = $PropNames "Abstract" = $Class.Abstract "Singleton" = $Class.Singleton "Hosted" = $Class.Hosted "Hosting" = $IsHosting "Styling" = $hashStyling } # Keep any previous dig results, specifically Edges already identified. If ([bool]($hashCollection[$Class.Name].Edges)){ $hashThisClass.Edges = $hashCollection[$Class.Name].Edges } $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 { ForEach ($E in @($hmaster[$_].Edges.Keys)) { Try{ If (-NOT [System.Object]$htnew[$_].Edges.ContainsKey($E) ){ [System.Object]$htnew[$_].Edges.Add($E,$hmaster[$_].Edges.$E) } }Catch { # Empty catch is fine here } } } } #$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[$_].Singleton -eq $true){ # Table Header $hashtable[$_].Styling.FONTCOLOR = $c_SingletonHdrFont $hashtable[$_].Styling.BGCOLOR = $c_SingletonHdrBG # Table/Rows $hashtable[$_].Styling.FILLCOLOR = $c_SingletonFill $hashtable[$_].Styling.Style = $c_SingletonStyle $hashtable[$_].Styling.TABLECOLOR = $c_SingletonTable } 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){ # Remove any existing styling $newString = ($String -replace '(<Font)|(Color="[a-zA-Z]*">)|(<B>)|(<\/B>)|(<\/Font>)','' ) # Add styling $newString = $newString | Where-Object { (-not ([string]::IsNullOrEmpty($_))) } | ForEach-Object -Process {"<Font Color=`"$($Color)`"><B>$($_)</B></Font>"} If (-NOT $Bold){ $newString = ($newString -replace '(<B>)|(</B>)','' ) } Return $newString } } End{} } ############################################################### #Remove undesireable characters Function Sanitize-String { Param ( [string]$String ) $string = ($string -Replace '“|”', '"' ) Return $string } ############################################################### ############################################################### # A simple function to add a newline at every Nth word Function Customize-StringLength { Param ( [string]$String, [int]$WordCount = 5 ) $nthWord = 0 $finalString = "" $spaceRegex = [Regex] " " ForEach ($item in ($spaceRegex.split($String))) { $nthWord++ if (($nthWord % $WordCount) -eq 0){ $item = "<br/>$item" } $finalString += $item + " " } $finalString.TrimEnd().TrimStart() } ############################################################### 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, [hashtable]$hashDiscoveries = @{} ) #$discHash = @{} $discHash = $hashDiscoveries.Clone() # Get only discoveries which potentially create classes (not relationships) $discs = $hashAllDiscoveries.Values | 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)) { $PropNames = [System.Collections.ArrayList]@() $Null = $PropNames.Add( (Stylize-Row -thisString "DisplayName: $($Disc.DisplayName)" -Color $c_DiscDisplayName -Bold $true) ) $Null = $PropNames.Add( (Stylize-Row -thisString "TargetName: $([string]$disc.target.Identifier.Path)" -Color $c_DiscTarget -Bold $true) ) If ($hashAllClasses[$disc.target.Identifier.Path].DisplayName) { $Null = $PropNames.Add( (Stylize-Row -thisString "TargetDisplayName: $([string]$hashAllClasses[$disc.target.Identifier.Path].DisplayName)" -Color $c_DiscTarget -Bold $true) ) } Else { $Null = $PropNames.Add( (Stylize-Row -thisString "TargetDisplayName: " -Color $c_DiscTarget -Bold $true) ) } $Null = $PropNames.Add( (Stylize-Row -thisString "EnabledByDefault: $([string]$Disc.Enabled)" -Color $c_DiscTarget -Bold $true) ) $Null = $PropNames.Add( (Stylize-Row -thisString "ID/GUID: $([string]$Disc.ID.Guid)" -Color $c_DiscTarget -Bold $true) ) $Null = $PropNames.Add( (Stylize-Row -thisString "MP: $([string]$Disc.Identifier.Domain[0])" -Color $c_DiscTarget -Bold $true) ) If ($hashMPBasicInfo[$Disc.Identifier.Domain[0]].DisplayName) { $Null = $PropNames.Add( (Stylize-Row -thisString "MPDisplayName: $([string]$hashMPBasicInfo[$Disc.Identifier.Domain[0]].DisplayName)" -Color $c_DiscTarget -Bold $true) ) } $Null = $PropNames.Add( (Stylize-Row -thisString "MPVersion: $([string]$hashMPBasicInfo[$Disc.Identifier.Domain[0]].Version)" -Color $c_DiscTarget -Bold $true) ) $NULL = $PropNames.Add((Stylize-Row -thisString "Description: $(Customize-StringLength -String (Sanitize-String -String $Disc.Description) -WordCount $LineBreak)" -Color $c_DiscTarget -Bold $true )) ForEach ($Item in ($discCollection.propertycollection | Sort-Object PropertyID) ) { $tmpClassName = [string]$Item.TypeID.Identifier.Path[0] $PropertyName = [string]$Item.PropertyID $PropType = (($hashAllClasses[$tmpClassName]).PropertyCollection | Where-Object {$_.Name -eq $PropertyName}).Type.ToString() $Null = $PropNames.Add( (Stylize-Row -thisString "[$($PropType)] $PropertyName" -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++ Try{ $discHash.Add(([string]$disc.Name+$c),$thisHash) }Catch{ #empty Catch is fine here } } } } Return $discHash } ############################################################### Function New-Graph { # If all classes are meant to be combined into a single graph, then the filename will be calculated with a hash function. If ($Combine) { $hashstring = Get-StringHash -String ($hashGraph.GetEnumerator() | Out-String) $outPath = (Join-Path $OutDir ("MD5Hash_$($hashstring)" +".png" )) } Graph { #region Classes ForEach ($ClassKey in @($hashGraph.Keys)) { # These will determine if Hosted/Hosting nodes appear in the Legend at the top of the .png If ([bool]$hashGraph[$ClassKey].Hosted) {$HostedExists = $true} If ([bool]$hashGraph[$ClassKey].Hosting) {$HostingExists = $true} If ([bool]$hashGraph[$ClassKey].Singleton) {$SingletonExists = $true} $params = @{ Name = $ClassKey Row = $hashGraph[$ClassKey].PropNames } $params += $hashGraph[$ClassKey].Styling Record @params ForEach ($E in @($hashGraph[$ClassKey].Edges.Keys | Where-Object {$_.Length -gt 0}) ) { If ([bool]$hashGraph.$E.Hosted) { $EdgeStyle = "bold" } Else { $EdgeStyle = "bold" } [string]$edgeColor = ($hashEdgeColor[$hashGraph[$ClassKey].Edges.$E]) Edge -From $ClassKey -To $E @{color=$edgeColor;style=$EdgeStyle} } } #endregion Classes If ($ShowDiscoveries) { #region Discoveries $hashDiscoveries.Keys | ForEach-Object { $Params = @{ Name = $_ Label = $hashDiscoveries[$_].Label Row = $hashDiscoveries[$_].PropNames } $Params += $hashDiscoveries[$_].Styling Record @Params [string]$edgeColor = ($hashEdgeColor['Discovery']) Edge -From $_ -To ($hashDiscoveries[$_].Edges | Select-Object -Unique) @{color=$edgeColor} } #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 } If ($SingletonExists) { Record -Name "Group (Class)" ` -Row ` (Stylize-Row -thisString 'Class Attribute' -Color $c_classAttribute -Bold $s_ClasAttributeBold)` -BGCOLOR $c_SingletonHdrBG ` -FONTCOLOR $c_SingletonHdrFont ` -FILLCOLOR $c_SingletonFill ` -STYLE $c_SingletonStyle ` -TABLECOLOR $c_SingletonTable } 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 (ordinary)" ` -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 'Basic Discovery Info' -Color $c_DiscTarget -Bold $true ),` (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 #-------------------------------- } ####################################################################### <# .Synopsis Will create an html index file for all of the graphs present in the default directory. .DESCRIPTION This function will enumerate all of the .png files in the default ClassGraph directory and build an html file with links to all of the graphs. .EXAMPLE PS C:\> New-SCOMClassGraphIndex -indexFileName 'C:\SCOMgraphs\MySCOMClassGraphs.html' The above example will generate the index file at the location specified. .EXAMPLE PS C:\> New-SCOMClassGraphIndex The above example will simply generate a new index at the default location. .EXAMPLE PS C:\> New-SCOMClassGraphIndex -ShowIndex The above example will generate a new index at the default location and then open it with the default program. .Parameter Dir The output directory where the .png files should be located. This is also where the html index file will be stored. .Parameter filterExt The type of files to locate in the output directory. The index will be built from files of this type. .Parameter indexFileName The name of the index file to create. .Parameter ShowIndex This will open the index file with the default program. .NOTES Author: Tyson Paul Blog: https://monitoringguys.com/ History 2021.05.13.1034 - Updated New-SCOMClassGraphIndex; better handling of foreign graphs (graphs for which no class exists in the mgmt group) 2019.06.03 - Published to SCOMHelper module .LINK New-SCOMClassGraph #> Function New-SCOMClassGraphIndex { Param ( [string]$outDir, [string]$filterExt='png', [string]$indexFileName='ClassGraphIndex.html', [bool]$RemoveForeignGraphs, [switch]$ShowIndex ) If (-Not $outDir ) { Write-Verbose "No directory provided for index file. " Return } If (-NOT(Test-Path -Path $outDir -PathType Container)) { Write-Verbose "No directory found for index file. " Return } $SearchIconFileName = 'searchicon.png' [System.Byte[]]$SearchIcon = @(137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,0,21,0,0,0,21,8,6,0,0,0,169,23,165,150,0,0,0,1,115,82,71,66,0,174,206,28,233,0,0,0,4,103,65,77,65,0,0,177,143,11,252,97,5,0,0,0,9,112,72,89,115,0,0,18,116,0,0,18,116,1,222,102,31,120,0,0,0,2,98,75,71,68,0,255,135,143,204,191,0,0,0,9,118,112,65,103,0,0,1,42,0,0,1,41,0,80,22,101,49,0,0,0,37,116,69,88,116,100,97,116,101,58,99,114,101,97,116,101,0,50,48,49,51,45,48,52,45,49,48,84,48,54,58,53,57,58,48,55,45,48,55,58,48,48,142,65,137,81,0,0,0,37,116,69,88,116,100,97,116,101,58,109,111,100,105,102,121,0,50,48,49,51,45,48,52,45,49,48,84,48,54,58,53,57,58,48,55,45,48,55,58,48,48,255,28,49,237,0,0,0,25,116,69,88,116,83,111,102,116,119,97,114,101,0,119,119,119,46,105,110,107,115,99,97,112,101,46,111,114,103,155,238,60,26,0,0,0,17,116,69,88,116,84,105,116,108,101,0,115,101,97,114,99,104,45,105,99,111,110,194,131,236,125,0,0,2,42,73,68,65,84,56,79,165,148,73,171,234,64,16,133,43,237,172,32,142,224,74,17,87,46,149,44,4,17,197,127,237,74,112,4,5,193,165,162,162,91,39,156,16,231,251,238,41,210,33,209,168,240,238,7,33,73,119,245,169,234,170,234,86,126,126,33,3,247,251,157,166,211,41,173,86,43,58,30,143,60,230,118,187,41,24,12,82,34,145,224,239,111,152,68,7,131,1,141,199,99,114,58,157,36,132,32,69,81,120,28,38,143,199,131,174,215,43,197,98,49,202,100,50,60,254,14,93,180,211,233,208,102,179,33,135,195,193,19,136,24,66,0,226,118,187,157,191,49,14,155,98,177,200,255,86,176,40,34,156,205,102,28,161,20,75,165,82,20,14,135,57,98,56,27,141,70,60,7,241,219,237,70,129,64,128,84,85,213,100,204,40,191,6,63,149,74,133,60,30,15,139,33,103,249,124,94,155,54,211,235,245,104,189,94,179,240,233,116,162,92,46,199,226,207,8,20,5,17,2,136,190,19,4,217,108,150,109,97,135,20,32,255,86,8,84,25,91,196,214,176,229,111,164,211,105,182,197,154,229,114,169,141,154,17,104,27,20,2,222,35,145,136,54,252,158,80,40,196,182,88,35,187,226,25,161,21,159,145,45,244,9,68,104,196,82,84,54,51,4,183,219,45,127,127,2,54,50,74,32,91,205,136,192,73,129,55,155,205,198,109,243,141,225,112,200,66,16,181,170,60,16,241,120,156,46,151,11,111,11,253,215,239,247,181,169,87,224,20,61,43,11,155,76,38,181,25,51,194,235,245,82,52,26,101,65,68,48,159,207,169,217,108,114,63,74,118,187,29,117,187,93,154,76,38,220,82,136,18,15,214,89,161,31,211,106,181,170,159,24,164,3,78,100,17,16,25,210,131,7,230,50,167,216,97,185,92,214,143,182,196,116,161,180,219,109,46,4,162,121,238,4,152,193,41,222,16,193,60,156,226,146,41,149,74,228,114,185,52,203,39,81,176,88,44,248,164,32,119,82,24,139,253,126,63,31,14,116,11,156,227,45,35,62,159,207,124,193,224,168,131,23,81,35,48,6,198,40,0,118,211,106,181,94,132,11,133,2,249,124,190,207,162,159,216,239,247,212,104,52,76,194,184,100,16,241,127,139,130,195,225,64,245,122,93,23,70,154,80,232,63,137,2,220,29,181,90,77,191,233,208,41,127,22,5,200,39,250,24,157,160,170,42,253,3,11,167,101,180,126,138,179,206,0,0,0,0,73,69,78,68,174,66,96,130) # This is the cute, little 'search' icon that appears on the master class list search field $SearchIconPath = Join-Path $OutDir "CSS\$($SearchIconFileName)" If (-NOT(Test-Path -Path ($SearchIconPath) -PathType Leaf )) { New-Item -Path (Join-Path $OutDir "CSS") -ItemType Container -Force -ErrorAction SilentlyContinue | Out-Null $SearchIcon | Set-Content -Path $SearchIconPath -Encoding BYTE } # Load all classes into hash $SCOMClasses = Get-SCOMClass $hashClasses = @{} ForEach ($Class in $SCOMClasses) { $hashClasses.Add($Class.Name, $Class) } $head = @' <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>Class Catalog</title> </head> <body> <style type="text/css"> a { text-decoration: none; } a:link, a:visited { color: blue; } a:hover { color: red; } .tg {border-collapse:collapse;border-spacing:0;} .tg td{font-family:Arial, sans-serif;font-size:14px;padding:1px 6px;border-style:solid;border-width:1px;overflow:hidden;word-break:normal;border-color:black;} .tg th{font-family:Arial, sans-serif;font-size:14px;font-weight:normal;padding:1px 6px;border-style:solid;border-width:1px;overflow:hidden;word-break:normal;border-color:black;} .tg .defaultstyle{border-color:inherit;vertical-align:center;text-align:left} .tg .headerstyle{font-weight:bold;font-size:100%;border-color:inherit;vertical-align:center} .tg .columnNameStyle{border-color:inherit;vertical-align:center;text-align:center} .tg .classnamestyle{font-weight:bold;border-color:inherit;vertical-align:center} .tg .keystyle{font-weight:bold;color:#fe0000;border-color:inherit;text-align:right;vertical-align:center} .tg .keypropertystyle{font-weight:bold;color:#fe0000;border-color:inherit;vertical-align:center} .tg .propertystyle{color:#22771a;border-color:inherit;vertical-align:center;horizontal-align:center} propertystyle @media screen and (max-width: 767px) { .tg {width: auto !important;} .tg col {width: auto !important;} .tg-wrap {overflow-x: auto;-webkit-overflow-scrolling: touch;} } #myInput { background-image: url('./CSS/searchicon.png'); /* Add a search icon to input */ background-position: 10px 12px; /* Position the search icon */ background-repeat: no-repeat; /* Do not repeat the icon image */ width: 100%; /* Full-width */ font-size: 16px; /* Increase font-size */ padding: 12px 20px 12px 40px; /* Add some padding */ border: 1px solid #ddd; /* Add a grey border */ margin-bottom: 6px; /* Add some space below the input */ } #myTable tr { /* Add a bottom border to all table rows */ border-bottom: 1px solid #ddd; } #myTable tr.header, #myTable tr:hover { /* Add a grey background color to the table header and on hover */ background-color: #f1f1f1; } </style> <script> function myFunction() { // Declare variables var input, filter, table, tr, td1,td2,td4,td5,td6, i; input = document.getElementById("myInput"); filter = input.value.toUpperCase(); table = document.getElementById("myTable"); tr = table.getElementsByTagName("tr"); // Loop through all table rows, and hide those who don't match the search query for (i = 0; i < tr.length; i++) { td1 = tr[i].getElementsByTagName("td")[1]; td2 = tr[i].getElementsByTagName("td")[2]; td4 = tr[i].getElementsByTagName("td")[4]; td5 = tr[i].getElementsByTagName("td")[5]; td6 = tr[i].getElementsByTagName("td")[6]; if (td1) { if ( (td1.innerHTML.toUpperCase().indexOf(filter) > -1) || (td2.innerHTML.toUpperCase().indexOf(filter) > -1) || (td4.innerHTML.toUpperCase().indexOf(filter) > -1) || (td5.innerHTML.toUpperCase().indexOf(filter) > -1) || (td6.innerHTML.toUpperCase().indexOf(filter) > -1)) { tr[i].style.display = ""; } else { tr[i].style.display = "none"; } } } } </script> <input type="text" id="myInput" onkeyup="myFunction()" placeholder="Search for names.."> <div class="tg-wrap"> <table id="myTable" class="tg"> <tr> <th class="headerstyle">#</th> <th class="headerstyle">Class Name</th> <th class="headerstyle">DisplayName</th> <th class="headerstyle">IsAbstract</th> <th class="headerstyle">MP DisplayName</th> <th class="headerstyle">MP Name</th> <th class="headerstyle">MP Version</th> <th class="headerstyle">MP IsSealed</th> </tr> '@ $Rows = '' $thisRow = '' [int]$i = 0 $graphFiles = (Get-ChildItem -Path $outDir -Filter *.$($filterExt) -Exclude "*MD5Hash_*","searchicon.png" -Recurse ) ForEach ($file in $graphFiles) { $className = $file.Name.Replace(".$($filterExt)",'') # increase row count $i++ #region ForeignGraphDetected If ($SCOMClasses.Name -Notcontains $ClassName) { # If foreign/old graph exists (but class no longer exists in this mgmt group), delete graph file If ($RemoveForeignGraphs) { $file | Remove-Item -Force # Skip to next class Continue } # Keep foreign graph, add it to the list of recognized graphs. Else { $thisRow = @" <tr> <td class="defaultstyle">$($i)</td> <td class="defaultstyle"><a href="./$($file.Name)">$($className)</a></td> <td class="defaultstyle">N/A</td> <td class="defaultstyle">N/A</td> <td class="defaultstyle">N/A</td> <td class="defaultstyle">N/A</td> <td class="defaultstyle">N/A</td> <td class="defaultstyle">N/A</td> </tr> "@ } } #endregion ForeignGraphDetected # Class exists in mgmt group so add all related info to list. Else { Try{ $thisRow = @" <tr> <td class="defaultstyle">$($i)</td> <td class="defaultstyle"><a href="./$($file.Name)">$($className)</a></td> <td class="defaultstyle">$($hashClasses[$className].DisplayName)</td> <td class="defaultstyle">$($hashClasses[$className].Abstract)</td> <td class="defaultstyle">$($hashMPBasicInfo.($hashClasses[$className].Identifier.Domain[0]).DisplayName)</td> <td class="defaultstyle">$($hashClasses[$className].Identifier.Domain[0])</td> <td class="defaultstyle">$($hashMPBasicInfo.($hashClasses[$className].Identifier.Domain[0]).Version)</td> <td class="defaultstyle">$($hashMPBasicInfo.($hashClasses[$className].Identifier.Domain[0]).IsSealed)</td> </tr> "@ }Catch { Write-Error $Error[0].Exception } } $Rows += $thisRow } #ForEach $end += @' </table></div> </body></html> '@ $indexFilePath = (Join-Path $outDir $indexFileName) Write-Verbose "Index File: $($indexFilePath)" ($head + $Rows + $end) | Set-Content $indexFilePath If ($ShowIndex) { # Open the index file with default program & $indexFilePath } } ####################################################################### $LineBreak = 5 #region 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_DiscHdrFont = 'white' $c_DiscHdrBG = '#206dea' #Blue $c_DiscFill = '' $c_DiscStyle = '' $c_DiscTable = '#002560' #DarkBlue $c_DiscRow = 'blue' $c_DiscDisplayName = '#0646ad' $c_DiscTarget = '#0646ad' $c_hostedHdrFont = '#d87d22' #'orange' #'#e0bc60' $c_hostedHdrBG = 'white' #gray $c_hostedFill = '' #'#e0bc60' $c_hostedStyling = '' $c_hostedTable = 'green' #'orange' #'#c66d00' $c_hostingHdrFont = '#cc6602' $c_hostingHdrBG = 'white' $c_hostingFill = '#ffd089'#'#f7be6a' $c_hostingStyle = 'filled' $c_hostingTable = 'black' #$c_hostingFill #'#c96d12' $hashEdgeColor = [ordered]@{ Child = 'black' Discovery = $c_DiscDisplayName Hosted = $c_hostingHdrFont } $c_AbstHdrFont = '#706d72' #'gray' $c_AbstHdrBG = '#e9ccf9' $c_AbstFill = '' $c_AbstStyle = '' $c_AbstTable = '#e9ccf9' $c_SingletonHdrFont = 'black' #'gray' $c_SingletonHdrBG = '#706d72' $c_SingletonFill = '' $c_SingletonStyle = '' $c_SingletonTable = '#706d72' #endregion Styling/Colors # If multiple classes are meant to be combined on a single graph, the existence this variable will enable that functionality. If ($Combine){ $hashGraph = @{} } # Ensure an output directory has been specified If (-Not [Bool]$outDir ) { $outDir = (Join-Path (Join-Path $env:Windir "Temp") (Join-Path 'SCOMHelper' 'New-SCOMClassGraph' ) ) } # Attempt to create temp folder if needed If (-NOT (Test-Path -Path $OutDir -PathType Container)){ Write-Host "Attempting to create temp folder: $OutDir" -F Gray Try{ New-Item -ItemType Directory -Path $OutDir -Force -ErrorAction Stop }Catch{ Write-Host "Failed to create temp directory. " -F Yellow -B Red Write-Host "Run this tool as administrator." -F Yellow -B Red Write-Host "If that doesn't work, create this folder path manually: [$($OutDir)]" -F Yellow -B Red $null = Read-Host "Press any key to exit." Exit } } If ($ShowIndex) { # Create a new index file, then open it New-SCOMClassGraphIndex -outDir $outDir -ShowIndex -RemoveForeignGraphs $RemoveForeignGraphs Return } # Build basic MP info hash $hashMPBasicInfo = Load-hashBasicMPInfo If ($ShowDiscoveries){ . Load-SCOMCache -LoadDiscoveries } . Load-SCOMCache -LoadClasses }#end Begin Process{ #$hashGraph = @{} $hashDiscoveries = @{} If ([Bool]$ID){ #[System.Collections.ArrayList]$Class = @() $Class = New-Object -TypeName System.Collections.ArrayList #Get any matching keys(ID) from $hashAllClassesID ForEach ($thisID in $ID) { $thisID = $_ -Replace '{|}','' If ($hashAllClassesID[$thisID]) { $NULL = $Class.Add(($hashAllClassesID[$thisID])) } } } ElseIf ([Bool]$ClassName){ [System.Collections.ArrayList]$Class = @() ForEach ($thisClassName in $ClassName) { If (-NOT ($hashAllClasses.ContainsKey($thisClassName)) ){ Write-Host "WARNING: Class with name: [$($thisClassName)] does not exist!" -F Red -BackgroundColor Yellow continue } $NULL = $class.Add(($hashAllClasses[$thisClassName])) } } ForEach ($thisClass in $Class) { $HostedExists = $false $HostingExists = $false $SingletonExists = $false $fileNameBase = $thisClass.Name If (-NOT $Combine) { $outPath = (Join-Path $OutDir ("$fileNameBase" +".png" )) } # If the graph has already been created, no sense creating it again. Open exising file. If ($caching) { If ($Combine) { Write-Error "Cannot use -Combine:true -Caching:true together" Break } ElseIf (Test-Path -Path $outPath -PathType Leaf) { Invoke-Item $outPath Break } } If (-NOT $Combine){ # This will clear the variable for every iteration, start a fresh graph of class $hashGraph = @{} #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 clear the variable for every iteration, start a fresh graph of discoveries $hashDiscoveries = @{} #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 # Will output individual graphs New-Graph } Else { #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 -hashCollection $hashGraph) } } #end ForEach Class }#end Process End{ # Will output one graph to display all classes If ($Combine){ If ($ShowDiscoveries) { #This will get any discoveries related to any of the classes. $hashDiscoveries = Get-Discoveries -hashMaster $hashGraph -hashDiscoveries $hashDiscoveries # 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 New-Graph } If (-NOT $ShowIndex) { # Create a new index file, then open it New-SCOMClassGraphIndex -outDir $outDir -RemoveForeignGraphs $RemoveForeignGraphs } }#end End }#end New-SCOMClassGraph ####################################################################### <# .DESCRIPTION Will create an instance group containing Windows Computers [Microsoft.Windows.Computer] objects and optionally include related Health Service Watchers [Microsoft.SystemCenter.HealthServiceWatcher]. .EXAMPLE "ms01.contoso.com","db01.contoso.com" | New-SCOMComputerGroup -MPName "Accounting Team Computer/HSW Group 2020" -NameSpace "Accounting" -Verbose .EXAMPLE $FQDNs = (Get-SCOMAgent | Where-Object Name -like "*DB*").Name New-SCOMComputerGroup -MPName "My Database Computers-HSW Group" -ComputerName $FQDNs -NameSpace "DBSERVERS" -AddHealthServiceWatchers -Verbose -PassThru The above example will get all agent names which match the pattern and add the Computer objects as well as the corresponding Health Service Watcher objects. .INPUTS Accepts array of computer FQDN names (fully qualified domain name) .OUTPUTS SCOM Management Pack [Microsoft.EnterpriseManagement.Configuration.ManagementPack] .NOTES Script: New-SCOMComputerGroup Author: Tyson Paul (https://monitoringguys.com/2019/11/12/scomhelper/) Version History: 2020.08.18.1333 - Added MP and Group display name parameters 2020.08.11.1309 - Polished a bit. HSW objects are optional. 2020.06.17 - v1.0 #> Function New-SCOMComputerGroup { [CmdletBinding(DefaultParameterSetName='Parameter Set 1', SupportsShouldProcess=$false, PositionalBinding=$false, HelpUri = 'http://www.microsoft.com/', ConfirmImpact='Medium')] [Alias()] [OutputType([Microsoft.EnterpriseManagement.Configuration.ManagementPack])] Param ( # New MP custom name [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, ParameterSetName='Parameter Set 1')] [string]$MPName='', # New MP custom DISPLAY name [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, ParameterSetName='Parameter Set 1')] [string]$MPDisplayName='', # Name of computer to be added to the group (and associated Health Service Watcher instance) [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, ParameterSetName='Parameter Set 1')] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [string[]]$ComputerName, # Name of mgmt server to use for SDK connection [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, ParameterSetName='Parameter Set 1')] [string]$MgmtServerName, [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, ParameterSetName='Parameter Set 1')] [string]$GroupName, [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, ParameterSetName='Parameter Set 1')] [string]$GroupDisplayName, # Namespace for group name. Example: "Lab" or "Accounting". This will affect only the hidden/ugly element "ID" in the MP. [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, ParameterSetName='Parameter Set 1')] [string]$NameSpace = "GROUP", # This will include corresponding Health Service Watcher objects in the group. [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, ParameterSetName='Parameter Set 1')] [switch]$AddHealthServiceWatchers, # This will return the new [Microsoft.EnterpriseManagement.Configuration.ManagementPack] object. [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, ParameterSetName='Parameter Set 1')] [switch]$PassThru ) Begin { ###################################### FUNCTIONS ####################################################### ######################################################################################################## Function Build-RefString { Param ( $MP ) Return ("$($MP.Name)$($MP.version.tostring())".Replace('.','')) } ######################################################################################################## Function Clean-DisplayName { [OutputType([String])] Param( # Param1 help description [Parameter(Mandatory=$false, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=0, ParameterSetName='Parameter Set 1')] [string]$Name='' ) Begin{} Process{ Return ($Name -replace '[^a-zA-Z0-9. \-()]', '') } End {} } ######################################################################################################## Function Clean-ID { [OutputType([String])] Param( # Param1 help description [Parameter(Mandatory=$false, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, Position=0, ParameterSetName='Parameter Set 1')] [string]$Name='' ) Begin{} Process{ $tmp = $Name.Replace(' ','.').Replace('..','.').Replace('..','.') $tmp2 = ( ($tmp -replace '[^a-zA-Z0-9.]', '').Replace('..','.').Replace('..','.') ) Return $tmp2 } End{} } ######################################################################################################## Function _LINE_ { $MyInvocation.ScriptLineNumber } ######################################################################################################## ######################################################################################################## Function Connect-MgmtGroup { # Connect to SCOM mgmt group Try{ $MG = Get-SCOMManagementGroup -ErrorAction Stop -Verbose:$VerbosePreference If (-NOT $MG ) { Write-Verbose "No SCOMManagementGroupConnection" Throw } ElseIf($MgmtServerName) { If ($MG.ConnectionSettings.ServerName -ne $MgmtServerName){ Write-Verbose "Existing MG connection, wrong server: [$($MG.ConnectionSettings.ServerName)]. Establish connection to new server: [$($MgmtServerName)]..." Throw } } } Catch { Try { $MG = Connect-OMManagementGroup -SDK $MgmtServerName If (-NOT $MG) { Throw "No SCOMManagementGroupConnection" } }Catch { Write-Error "$(_LINE_): Unable to establish connection to designated mgmt server: [$($MgmtServerName)]. Exiting." Exit } } } ######################################################################################################## $DateStamp = (Get-Date -f "yyyyMMdd.hhmmss") # Clean names to remove invalid characters $CleanMPDisplayName = Clean-DisplayName -Name $MPDisplayName $CleanMPID = Clean-ID -Name $MPName If (-NOT $CleanMPID) { $CleanMPID = Clean-ID -Name $CleanMPDisplayName } If (-NOT $CleanMPID) { Write-Error "No valid MPName provided. Exiting." Return } # Will try to accomodate both group Name and DisplayName If ($GroupDisplayName) { Write-Verbose "Group DisplayName provided. Will attempt to clean it..." $GroupDisplayName = $GroupDisplayName | Clean-DisplayName } $CleanGroupID = $GroupName | Clean-ID If (-NOT $CleanGroupID) { Write-Verbose "No valid GroupName. Attempt to use cleaned group DisplayName (if provided)..." $CleanGroupID = $GroupDisplayName | Clean-ID } If (-NOT $CleanGroupID) { $CleanGroupID = "$($CleanMPID).$($DateStamp).Group" Write-Verbose "No valid GroupName. Will use default group naming: [$($CleanGroupID)]" } Write-Verbose @" New MP DisplayName provided by user:[$MPName] DisplayName after being sanitized:[$($CleanMPDisplayName)] MPID after being sanitized:[$($CleanMPID)]] "@ Write-Verbose "Attempt import of OperationsManager module" . Import-SCOMPowerShellModule Write-Verbose "Verify mgmt group connection..." . Connect-MgmtGroup Write-Verbose "Get necessary MP objects for building group definitions..." $Formula = '' # Get the necessary reference MPs $LibMp = Get-SCOMManagementPack -Name Microsoft.Windows.Library $InsMp = Get-SCOMManagementPack -Name Microsoft.SystemCenter.InstanceGroup.Library $SCmp = Get-SCOMManagementPack -Name Microsoft.SystemCenter.Library # Build the alias Strings $Libalias = Build-RefString -MP $LibMp $InsAlias = Build-RefString -MP $InsMp $SCAlias = Build-RefString -MP $SCmp # Check if the MP already exists if not we will create it $MPAlreadyExists = Get-SCOMManagementPack -Name $CleanMPID If($MPAlreadyExists) { Throw @" Error: Management pack with name [$($MPName)] already exists. New MP DisplayName provided by user:[$MPName] DisplayName after being sanitized:[$($CleanMPDisplayName)] MPID after being sanitized:[$($CleanMPID)]] "@ Try {$MG.Dispose()}Catch{<#Tidy up#>} Exit } [System.Collections.ArrayList]$arrWinComp = @() [System.Collections.ArrayList]$arrHSW = @() $WinCompClass = Get-SCOMClass -Name "Microsoft.Windows.Computer" $HSWClass = Get-SCOMClass -Name "Microsoft.SystemCenter.HealthServiceWatcher" }#end Begin Process { # Create collection of Computers and HSW objects ForEach ($CName in $ComputerName) { $WinComp = $NULL $Criteria = "DisplayName = '$CName'" $objCriteria = New-Object Microsoft.EnterpriseManagement.Monitoring.MonitoringObjectCriteria($Criteria ,$WinCompClass) $WinComp = $MG.GetMonitoringObjects($objCriteria) If (-NOT $WinComp) { Write-Warning "Computer: [$($CName)] not found! Verify that the fully qualified domain name exists." Continue } $CompID = $WinComp.Id.Guid Write-Verbose "Found Computer:`t[$($CName)], [$($CompID)]" $Null = $arrWinComp.Add(" <MonitoringObjectId>$CompID</MonitoringObjectId>") If ($AddHealthServiceWatchers) { $objCriteria = New-Object Microsoft.EnterpriseManagement.Monitoring.MonitoringObjectCriteria($Criteria ,$HSWClass) $HSW = $MG.GetMonitoringObjects($objCriteria) $WatchID = $HSW.Id.Guid Write-Verbose "Found HSW:`t`t[$($CName)], [$($WatchID)]" $Null = $arrHSW.Add(" <MonitoringObjectId>$WatchID</MonitoringObjectId>") } } }#end Process End { # Add Windows Computer GUIDs to explicit membership rule $Formula += @" <MembershipRule> <MonitoringClass>`$MPElement[Name="$($Libalias)!Microsoft.Windows.Server.Computer"]`$</MonitoringClass> <RelationshipClass>`$MPElement[Name="$($InsAlias)!Microsoft.SystemCenter.InstanceGroupContainsEntities"]`$</RelationshipClass> <IncludeList> $arrWinComp </IncludeList> </MembershipRule> "@ # Add HSW GUIDs to explicit membership rule, if appropriate If ($AddHealthServiceWatchers) { $Formula += @" <MembershipRule> <MonitoringClass>`$MPElement[Name="$($SCAlias)!Microsoft.SystemCenter.HealthServiceWatcher"]`$</MonitoringClass> <RelationshipClass>`$MPElement[Name="$($InsAlias)!Microsoft.SystemCenter.InstanceGroupContainsEntities"]`$</RelationshipClass> <IncludeList> $arrHSW </IncludeList> </MembershipRule> "@ } $store = New-Object Microsoft.EnterpriseManagement.Configuration.IO.ManagementPackFileStore # Create the MP Object Write-Verbose "Create new MP object (virtual)..." $MP = New-Object Microsoft.EnterpriseManagement.Configuration.ManagementPack($CleanMPID, $CleanMPID, (New-Object Version(1, 0, 0)), $store) # Set the MP Display Name $MP.DisplayName = $CleanMPDisplayName # Add MP references Write-Verbose "Add reverences to MP object..." $LibRef = New-object Microsoft.EnterpriseManagement.Configuration.ManagementPackReference($MP, $LibMp.Name, $LibMp.KeyToken, $LibMp.Version) $InsRef = New-object Microsoft.EnterpriseManagement.Configuration.ManagementPackReference($MP, $InsMp.Name, $InsMp.KeyToken, $InsMp.Version) $SCRef = New-object Microsoft.EnterpriseManagement.Configuration.ManagementPackReference($MP, $SCmp.Name, $SCmp.KeyToken, $SCmp.Version) $MP.References.Add($Libalias,$LibRef) $MP.References.Add($InsAlias,$InsRef) $MP.References.Add($SCAlias,$SCRef) Try { # Save Changes $MP.AcceptChanges() Write-Verbose "MP object was verified successfully." } Catch { Throw "Failed to verify new MP content.`nError:$($_)`nExiting." Try {$MG.Dispose()}Catch{<#Tidy up#>} Exit } Try { # Import MP Write-Verbose "Importing new empty MP into management group: [$($MG.Name)]..." $MG.ImportManagementPack($mp) } Catch { Throw "Failed to import new MP.`nError:$($_)`nExiting." Try {$MG.Dispose()}Catch{<#Tidy up#>} Exit } #TYSON $GroupID = "$($CleanMPID).Group" $GroupID = "$($CleanGroupID)" If ($GroupID -notmatch 'group$') { $GroupID = "$($GroupID).Group" } # Create Group Object and try to insert it into the MP. Write-Verbose "Create new group object: [$($GroupID)]..." $group = New-Object Microsoft.EnterpriseManagement.Monitoring.CustomMonitoringObjectGroup($NameSpace, $GroupID, $CleanMPID, $Formula) If ($GroupDisplayName) { $group.DisplayName = $GroupDisplayName } Try{ Write-Verbose "Inserting new Group formula into MP..." $MP.InsertCustomMonitoringObjectGroup($Group) Write-Verbose "Success!" } Catch{ Throw "Failed to add group formula to MP.`nError:$($_)`nExiting." Try {$MG.Dispose()}Catch{<#Try to tidy up#>} Exit } # Finally, get the new pack If ($PassThru) { Get-SCOMManagementPack -Name $CleanMPID } }#end End }#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://monitoringguys.com/ Date: 2018.04.30 History: v1.0 #> Function Ping-AllHosts { Param ( [long]$Count=1, [long]$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 ####################################################################### <# .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://monitoringguys.com/ 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 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://monitoringguys.com/ 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 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://monitoringguys.com/ Original Date: 2017.12.13 History: 2018.01.22: Refined. 2017/12/13: First version #> Function Show-SCOMModules { ############################################# Function Get-ModuleRunasProfileName { Param( $Module, $Profiles ) $ModuleRunAs = $Profiles | Where-Object { $_.Id.Guid -eq $module.RunAs.Id.GUID } Return ($ModuleRunAs.Name+";"+$ModuleRunAs.DisplayName) } ############################################# $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 This function is designed to launch a SCOM Property Bag PowerShell script or SCOM PowerShell discovery. It will output a formatted table to the screen. It can also output a hashtable or write the DataItem to an xml file.. .DESCRIPTION When authoring and troubleshooting a SCOM Property Bag or discovery PowerShell script it is helpful to be able to analyze the resulting DataItem. This script will make this process easier by outputing the DataItem(s) to the screen and optionally an XML file. The default output file will have the same path/name as the input file (+.xml). Script must propertly use 'MOM.ScriptAPI' and output the proerty bag. Why is this useful? It's easy to output the DataItem to the screen but then it's difficult to read/analyze. If you open the .xml file with a fancy text editor like Notepad++ you can leverage the XMLTools plugin to format the data nicely. This is especially helpful for large property bags. Example : $api = New-Object -ComObject 'MOM.ScriptAPI' $bag = $api.CreatePropertyBag() $bag.AddValue('Message',"Some message") $api.Return($bag) #This method will output the DataItem text for analysis and is only used for script testing/analysis. .EXAMPLE # # This will execute the example script and output the property bag data of each bag ($Bag assumed to be in the script) to <$env:windir\Temp\><script file name>_<uniquetimestamp>.xml. Individual bags will be returned in a formatted table. PS C:\> Show-SCOMPropertyBag 'C:\Program Files\WindowsPowerShell\Modules\SCOMHelper\SCOMPowerShellPropertyBagTypes.ps1' .EXAMPLE # # This will execute MyScript.ps1 and output the property bag data of each bag ($Bag assumed to be in the script) to <$env:windir\Temp\><script file name>_<uniquetimestamp>.xml and then open the .xml file with the default application. PS C:\> Show-SCOMPropertyBag -FilePath C:\test\MyScript.ps1 -ShowXML .EXAMPLE # # This will execute C:\test\MyScript.ps1 and output the property bag data of each bag ($Bag assumed to be in the script) to <$env:windir\Temp\><script file name>_<uniquetimestamp>.xml. # All property bag data will be displayed to the screen in one formatted table. Use -Verbose switch to display output file location. PS C:\> Show-SCOMPropertyBag -FilePath C:\test\MyScript.ps1 -Format Combined -Verbose .EXAMPLE # # This will store the discovery dataitem to a hash table variable. PS C:\> $hash = Show-SCOMPropertyBag -FilePath 'C:\Test\MyDiscoveryScript.ps1' -Format HashTable PS C:\> $hash.1 | Out-GridView .NOTES Name: Show-SCOMPropertyBag Author: Tyson Paul History: 2022.06.14 - Added ability to digest PB collections as well as discovery dataitems. 2019.04.19 - Initial release. #> Function Show-SCOMPropertyBag { [CmdletBinding(DefaultParameterSetName='Parameter Set 1', SupportsShouldProcess=$true, PositionalBinding=$false)] Param ( # The full path to the input script (PowerShell .ps1) file. [Parameter(Mandatory=$false, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, ValueFromRemainingArguments=$false, Position=0, ParameterSetName='Parameter Set 1')] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [string]$FilePath, # Powershell script file object. Typically this type of object would be obtained with Get-ChildItem. [Parameter(Mandatory=$false, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, ValueFromRemainingArguments=$false, Position=0, ParameterSetName='Parameter Set 2')] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [System.IO.FileInfo]$File, [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$true, Position=1)] [ValidateSet("Combined", "Separate","HashTable")] # Output formated objects (property bags) instead of default hash table. Not applicable to discovery bags. # Separate: Individual bags # Combined: One large table containing all data from all bags # HashTable: Hash table containing all bag data. Useful for spying on data values that don't otherwise display in the table format due to large size. [string]$Format='Separate', # This will open the .XML output file type with default application. [switch]$ShowXML ) #region BEGIN Begin{ #----------------- FUNCTIONS ----------------------- Function FormatEach { Param( $hash ) Return $hash.Value } #--------------- END FUNCTIONS --------------------- $hashVariantTypes = [ordered]@{ 0 = "Empty" 1 = "Null (unconfirmed)" 2 = "Short (unconfirmed)" 3 = "Integer" 4 = "Float" 5 = "Double" 6 = "Currency (unconfirmed)" 7 = "Date" 8 = "String" 9 = "Object (unconfirmed)" 10 = "Error (unconfirmed)" 11 = "Boolean" 12 = "Variant (unconfirmed)" 13 = "DataObject (unconfirmed)" 14 = "Decimal" 15 = "Byte (unconfirmed)" 16 = "Char (unconfirmed)" 17 = "Byte" 18 = "Char" 20 = "Long" } # Setup temp dir for output file. Using a redirector to an output file is the only way I could find to obtain the property bag data. $TempDir = (Join-Path (Join-Path $env:Windir "Temp") (Join-Path 'SCOMHelper' 'Show-SCOMPropertyBag') ) New-Item -Type Directory -Path $TempDir -ErrorAction SilentlyContinue | Out-Null If (-not(Test-Path -Path $TempDir -PathType Container)) { Write-Error "Unable to create/access directory: [$($TempDir)]. " Return $false } $hash = @{} $int=1 } #endregion BEGIN Process { $batchstamp = Get-Date -Format o | ForEach-Object {$_ -replace ":", "."} If ([bool]$File){ If (Test-Path -PathType Leaf -Path $File.FullName -ErrorAction SilentlyContinue){ $OutFile = (Join-Path $TempDir "$($File.Name)_$($batchstamp)_.xml" ) cmd /c PowerShell.exe -file $File.FullName > $OutFile } } Else { If (-NOT [bool]$FilePath) { #$FilePath = 'C:\Program Files\WindowsPowerShell\Modules\SCOMHelper\1.1\SCOMPowerShellPropertyBagTypes.ps1' $FilePath = Join-Path $psScriptRoot SCOMPowerShellPropertyBagTypes.ps1 } If (Test-Path -PathType Leaf -Path $FilePath -ErrorAction SilentlyContinue){ $OutFile = (Join-Path $TempDir "$(Split-Path $FilePath -Leaf)_$($batchstamp)_.xml") cmd /c PowerShell.exe -file $FilePath > $OutFile } Else { Throw "Problem with FilePath: [ $($FilePath) ]." } } # Remove generic typename that often appears as a result of standard '$bag' statement $content = ((Get-Content $OutFile -Raw ).Replace('System.__ComObject','').Replace('ÿ','')) -Replace '\<Collection\>|\<\/Collection\>' $content | Set-Content $OutFile $token = (Get-Date -f fffffff) # Add a delimiter after each DataItem, then split on it. $array = (($content -replace '\<\/DataItem\>',"</DataItem>$($token)" ) -split $token) ForEach ($DI in $array[0..($array.Count -2)]){ # If Discovery bag If (([xml]$DI).DataItem.DiscoveryType -match '0') { #ForEach () { } ForEach ($Item in ([xml]$DI).DataItem) { [System.Collections.ArrayList]$arrayItems = @() 'type','time','sourceHealthServiceId','DiscoveryType','DiscoverySourceType','DiscoverySourceObjectId','DiscoverySourceManagedEntity' | ForEach-Object { $obj = New-Object PSCUSTOMOBJECT $obj | Add-Member -MemberType NoteProperty -Name "ClassInstance" -Value 'N/A' $obj | Add-Member -MemberType NoteProperty -Name 'Name' -Value $_ $obj | Add-Member -MemberType NoteProperty -Name 'Value' -Value ($Item.$_) $NULL = $arrayItems.Add($obj) } ForEach ($ClassInstance in ($Item.ClassInstances).ClassInstance){ ForEach ($Settings in $ClassInstance.Settings){ ForEach ($Setting in $Settings.Setting){ $obj = New-Object PSCUSTOMOBJECT $obj | Add-Member -MemberType NoteProperty -Name "ClassInstance" -Value ([string]$ClassInstance.TypeId) $obj | Add-Member -MemberType NoteProperty -Name "Name" -Value ([string]$Setting.Name) $obj | Add-Member -MemberType NoteProperty -Name "Value" -Value ([string]$Setting.Value) $NULL = $arrayItems.Add($obj) } } } }#end ForEach Item } #end Discovery bag type # Assume property bag Else { #ForEach ($DI in $array[0..($array.Count -2)]){ [System.Collections.ArrayList]$arrayItems = @() 'type','time','sourceHealthServiceId' | ForEach-Object { $obj = New-Object PSCUSTOMOBJECT $obj | Add-Member -MemberType NoteProperty -Name 'Name' -Value $_ $obj | Add-Member -MemberType NoteProperty -Name 'VariantType' -Value '' $obj | Add-Member -MemberType NoteProperty -Name 'Value' -Value (([xml]$DI).DataItem.$_) $NULL = $arrayItems.Add($obj) } ForEach ($Property in ([xml]$DI).DataItem.Property){ $obj = New-Object PSCUSTOMOBJECT $obj | Add-Member -MemberType NoteProperty -Name "Name" -Value ([string]$Property.Name) $obj | Add-Member -MemberType NoteProperty -Name "VariantType" -Value ("{0,2},{1}" -F $([int]$Property.VariantType), $($hashVariantTypes.[int]$Property.VariantType)) $obj | Add-Member -MemberType NoteProperty -Name "Value" -Value ([string]$Property.'#text') $NULL = $arrayItems.Add($obj) } } $hash.Add($int,$arrayItems) $int++ } If ($ShowXML) { # Open with default application & $OutFile } Write-Verbose "Output file: $OutFile" } End { If ($Format -eq 'Separate'){ ForEach ($element in @($hash.GetEnumerator() )) { FormatEach -hash $element | Out-Host } } ElseIf ($Format -eq 'Combined'){ ForEach ($element in @($hash.GetEnumerator() )) { FormatEach -hash $element } } Else{ Return $hash } } } #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://monitoringguys.com/ 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://monitoringguys.com/', 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 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://monitoringguys.com/ History: 2019.05.01 - Fixed issue where output path would display extra backslash 2018.05.25 - Initial. .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 = (Join-Path $outDir ($file.Name + "($($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 = (Join-Path $outDir ($mp.Name + "($($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 ####################################################################### <# .DESCRIPTION Will overwrite an existing instance group or computer group containing Windows Computers [Microsoft.Windows.Computer] objects and optionally include related Health Service Watchers [Microsoft.SystemCenter.HealthServiceWatcher] if the original group type is Microsoft.SystemCenter.InstanceGroup. WARNING: This will replace existing group members with the objects for the provided computer names. This will technically not "add" to the exiting group. .EXAMPLE Update-SCOMComputerGroup -ComputerName 'ms01.contoso.com','ms02.contoso.com' -MgmtServerName ms01.contoso.com -GroupId '4f04c0c7-e3c3-8ef0-a710-1414dd320c5b' -PassThru -AddHealthServiceWatchers -Verbose The above example will update the designated instance group with the two Computers and associated Health Service Watchers. It will prompt for confirmation before updating the existing group membership. It will return the group object. .EXAMPLE # $FQDNs = (Get-SCOMAgent | Where-Object Name -like "*ProdDB*").Name Update-SCOMComputerGroup -ComputerName $FQDNs -MgmtServerName ms01.contoso.com -GroupName 'SQL.Production.SQLServerComputers.Group' -PassThru -Verbose -Force The above example will update the the designated group (instance group or Computer group) with all Computer objects that match the name criteria. . It will not prompt for confirmation before updating the existing group membership. It will return the group object. .INPUTS Accepts array of computer FQDN names (fully qualified domain name) .OUTPUTS SCOM Management Pack [Microsoft.EnterpriseManagement.Monitoring.MonitoringObjectGroup] .NOTES Script: Update-SCOMComputerGroup Author: Tyson Paul (https://monitoringguys.com/2019/11/12/scomhelper/) Version History: 2020.11.19.1613 - Added ability to add core references if they are missing. 2020.08.18.1703 - initial https://docs.microsoft.com/en-us/system-center/scom/manage-create-manage-groups?view=sc-om-2019 #> Function Update-SCOMComputerGroup { [CmdletBinding(SupportsShouldProcess=$false, PositionalBinding=$false, HelpUri = 'http://www.microsoft.com/', ConfirmImpact='Medium')] [Alias()] [OutputType([Microsoft.EnterpriseManagement.Monitoring.MonitoringObjectGroup])] Param ( # Name of computer to be added to the group (and associated Health Service Watcher instance if used with -AddHealthServiceWatchers parameter) [Parameter(Mandatory=$true, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false)] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [string[]]$ComputerName, # Name of mgmt server to use for SDK connection [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false)] [string]$MgmtServerName = 'LOCALHOST', # Name of the group (not DisplayName) [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, ParameterSetName='Parameter Set 1')] [string]$GroupName, # DisplayName of the group (not Name ) [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, ParameterSetName='Parameter Set 2')] [string]$GroupDisplayName, # ID/GUID of the group [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, ParameterSetName='Parameter Set 3')] [string]$GroupId, # This will include corresponding Health Service Watcher objects in the group if the group is an 'instance group'. [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false)] [switch]$AddHealthServiceWatchers, # This will return the new group [Microsoft.EnterpriseManagement.Monitoring.MonitoringObjectGroup] object. [Parameter(Mandatory=$false, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false)] [switch]$PassThru, # Will not prompt for confirmation [switch]$Force ) ######################################################################################################## ##################################### FUNCTIONS ####################################################### Function Get-GroupType { Param ( [string]$GroupName ) #Get the group class Write-Verbose "Getting the group '$GroupName'." $GroupClassCriteria = New-Object Microsoft.EnterpriseManagement.Configuration.MonitoringClassCriteria("Name='$GroupName'") $GroupClass = $MG.GetMonitoringClasses($GroupClassCriteria)[0] If ($GroupClass -eq $null) { Write-Error "$GroupName is not found." Return $false } Else { #Check if this monitoring class is actually an instance group or computer group Write-Verbose "Check if the group '$GroupName' is an instance group or a computer group." $GroupBaseTypes = $GroupClass.GetBaseTypes() Foreach ($item in $GroupBaseTypes) { If ($item.Id.Tostring() -eq '4ce499f1-0298-83fe-7740-7a0fbc8e2449') { Write-Verbose "'$GroupName' is an instance group." Return 'Microsoft.SystemCenter.InstanceGroup' } If ($item.Id.Tostring() -eq '0c363342-717b-5471-3aa5-9de3df073f2a') { Write-Verbose "'$GroupName' is a computer group." Return 'Microsoft.SystemCenter.ComputerGroup' } } # Should never reach this if group is valid Return $false } }#End Function Get-GroupType ######################################################################################################## <# Will return the MP which contains the Discovery for a group. Only if there exists a single discovery datasource for the group. #> Function Get-GroupDiscoveryMP { Param ( [string]$GroupName ) #Get the group class Write-Verbose "Getting the group '$GroupName'." $GroupClassCriteria = New-Object Microsoft.EnterpriseManagement.Configuration.MonitoringClassCriteria("Name='$GroupName'") $GroupClass = $MG.GetMonitoringClasses($GroupClassCriteria)[0] If ($GroupClass -eq $null) { Write-Error "$GroupName is not found." Return $false } #Check if this monitoring class is actually an instance group or computer group Write-Verbose "Check if the group '$GroupName' is an instance group or a computer group." $GroupBaseTypes = $GroupClass.GetBaseTypes() $bIsGroup = $false Foreach ($item in $GroupBaseTypes) { If ($item.Id.Tostring() -eq '4ce499f1-0298-83fe-7740-7a0fbc8e2449') { Write-Verbose "'$GroupName' is an instance group." $bIsGroup = $true } If ($item.Id.Tostring() -eq '0c363342-717b-5471-3aa5-9de3df073f2a') { Write-Verbose "'$GroupName' is a computer group." $bIsGroup = $true } } If ($bIsGroup -eq $false) { Write-Error "$GroupName is not an instance group or a computer group." Return $false } #Get Group object $GroupObject = $MG.GetMonitoringObjects($GroupClass)[0] $GroupDiscoveries = $GroupObject.GetMonitoringDiscoveries() $iGroupPopDiscoveryCount = 0 $GroupPopDiscovery = $null Foreach ($Discovery in $GroupDiscoveries) { $DiscoveryDS = $Discovery.DataSource #Microsft.SystemCenter.GroupPopulator ID is 488000ef-e20b-1ac4-d3b1-9d679435e1d7 If ($DiscoveryDS.TypeID.Id.ToString() -eq '488000ef-e20b-1ac4-d3b1-9d679435e1d7') { #This data source module is using Microsft.SystemCenter.GroupPopulator $iGroupPopDiscoveryCount = $iGroupPopDiscoveryCount + 1 $GroupPopDiscovery = $Discovery Write-Verbose "Group Populator discovery found: '$($GroupPopDiscovery.Name)'" } } If ($iGroupPopDiscoveryCount.count -eq 0) { Write-Error "No group populator discovery found for $GroupName." Return $false } If ($iGroupPopDiscoveryCount.count -gt 1) { Write-Error "$GroupName has multiple discoveries using Microsft.SystemCenter.GroupPopulator Module type. Unable to continue." Return $false } #Get the MP of where the group populator discovery is defined $GroupPopDiscoveryMP = $GroupPopDiscovery.GetManagementPack() $GroupPopDiscoveryMPName = $GroupPopDiscoveryMP.Name Write-Verbose "The group populator discovery '$($GroupPopDiscovery.Name)' is defined in management pack '$GroupPopDiscoveryMPName'." #Write Error and exit if the group discovery MP is sealed Write-Verbose "Checking if '$GroupPopDiscoveryMPName' MP is sealed." If ($GroupPopDiscoveryMP.sealed -eq $true) { Write-Error "Unable to update the group discovery because it is defined in a sealed MP: '$($GroupPopDiscoveryMP.DisplayName)'." Return $false } else { Write-Verbose "'$GroupPopDiscoveryMPName' MP is unsealed. OK to continue." } Return $GroupPopDiscoveryMP } ######################################################################################################## Function Create-ExplicitMembershipConfig { Param ( [Parameter(Mandatory=$true, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, ParameterSetName='Parameter Set 1')] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [array]$ComputerName, [Parameter(Mandatory=$true, ValueFromPipeline=$false, ValueFromPipelineByPropertyName=$false, ValueFromRemainingArguments=$false, ParameterSetName='Parameter Set 1')] [ValidateNotNull()] [ValidateNotNullOrEmpty()] [string]$GroupName, [string]$RelationshipClass, [string]$WindowsAlias='Windows', [string]$SCAlias='SystemCenter', [string]$InstanceGroupAlias='InstanceGroupLibrary' ) $WinCompClass = Get-SCOMClass -Name "Microsoft.Windows.Computer" $HSWClass = Get-SCOMClass -Name "Microsoft.SystemCenter.HealthServiceWatcher" [System.Collections.ArrayList]$arrWinComp = @() [System.Collections.ArrayList]$arrHSW = @() # Find appropriate Computer/HSW objects, build collections ForEach ($CName in $ComputerName) { $WinComp = $NULL $Criteria = "DisplayName = '$CName'" $objCriteria = New-Object Microsoft.EnterpriseManagement.Monitoring.MonitoringObjectCriteria($Criteria ,$WinCompClass) $WinComp = $MG.GetMonitoringObjects($objCriteria) If (-NOT $WinComp) { Write-Warning "Computer: [$($CName)] not found! Verify that the fully qualified domain name exists." Continue } $CompID = $WinComp.Id.Guid Write-Verbose "Found Computer:`t[$($CName)], [$($CompID)]" $Null = $arrWinComp.Add(" <MonitoringObjectId>$CompID</MonitoringObjectId>`n") If ($AddHealthServiceWatchers) { $objCriteria = New-Object Microsoft.EnterpriseManagement.Monitoring.MonitoringObjectCriteria($Criteria ,$HSWClass) $HSW = $MG.GetMonitoringObjects($objCriteria) $WatchID = $HSW.Id.Guid Write-Verbose "Found HSW:`t`t[$($CName)], [$($WatchID)]" $Null = $arrHSW.Add(" <MonitoringObjectId>$WatchID</MonitoringObjectId>`n") } } # Add Windows Computer GUIDs to explicit membership rule $Rules += @" <MembershipRule> <MonitoringClass>`$MPElement[Name="$($WindowsAlias)!Microsoft.Windows.Server.Computer"]`$</MonitoringClass> $($RelationshipClass) <IncludeList> $arrWinComp </IncludeList> </MembershipRule> "@ # If appropriate, add HSW rules to membership rules If ($arrHSW.Count) { $Rules += @" <MembershipRule> <MonitoringClass>`$MPElement[Name="$($SCAlias)!Microsoft.SystemCenter.HealthServiceWatcher"]`$</MonitoringClass> $($RelationshipClass) <IncludeList> $arrHSW </IncludeList> </MembershipRule> "@ } # Assemble the new configuration $NewConfiguration = @" <RuleId>`$MPElement`$</RuleId> <GroupInstanceId>`$MPElement[Name="$($GroupName)"]`$</GroupInstanceId> <MembershipRules> $($Rules) </MembershipRules> "@ Return $NewConfiguration }#End Funtion Create-ExplicitMembershipConfig ######################################################################################################## Write-Verbose "Attempt import of OperationsManager module" . Import-SCOMPowerShellModule Try { Write-Verbose "Verify mgmt group connection..." If ($MgmtServerName) { $MG = Connect-OMManagementGroup -SDK $MgmtServerName -Verbose:$VerbosePreference } ElseIf (-NOT $MG) { $MG = Connect-OMManagementGroup -SDK 'Localhost' -Verbose:$VerbosePreference } } Catch { Write-Error "Failed to connect to mgmt group. $_" Exit } [string]$WindowsAlias='Windows' [string]$SCAlias='SystemCenter' [string]$InstanceGroupAlias='InstanceGroupLibrary' $DateStamp = (Get-Date -f "yyyyMMdd.hhmmss") $BackupDir = (Join-Path (Join-Path $env:Windir "Temp") (Join-Path 'SCOMHelper' "Update-SCOMComputerGroup_$($DateStamp)" ) ) If ($GroupId){ $objGroup = (Get-SCOMGroup -Id $GroupId) } If ($GroupName) { $Class = Get-SCOMClass -Name $GroupName $objGroup = (Get-SCOMGroup -DisplayName ($Class.DisplayName)) } If ($GroupDisplayName) { $objGroup = (Get-SCOMGroup -DisplayName $GroupDisplayName) } $GroupName = $objGroup.FullName $GroupPopDiscoveryMP = Get-GroupDiscoveryMP -GroupName $GroupName If (-NOT $GroupPopDiscoveryMP) { Write-Error "Unable to retrieve management pack for group [$GroupName]. Exiting." Return } Try { New-Item -Path $BackupDir -ItemType Directory -Verbose -Force $GroupPopDiscoveryMP | Export-SCOMManagementPack -Path $BackupDir -Verbose -ErrorAction Stop Write-Verbose "Backup Directory: [$($BackupDir)]" } Catch { Throw "Unable to backup existing MP to path: [$($BackupDir)]. No action taken. Exiting. Error:`n$_" Return } <# When defining the new discovery configuration we will need to use a specific alias depending on the group type. Aliases tend to be unique in unsealed MPs. Get unique aliases from existing group MP so they can be used in the new/updated member Configuration rules. This approach is a little sloppy and I may update it someday if I expand the group population automation abilities. #> #ForEach ($Key in @($GroupPopDiscoveryMP.References.Keys)) { # Locate "Windows" alias If ($GroupPopDiscoveryMP.References.Value.Name -match '^Microsoft.Windows.Library$') { $thisAlias = ($GroupPopDiscoveryMP.References | Where-Object {$_.Value.Name -match '^Microsoft.Windows.Library$'} ).Key $WindowsAlias = $thisAlias #Continue; } #Add the reference if it does not exist Else { Add-MPReference -UnsealedMPName $GroupPopDiscoveryMP.Name -ReferenceMPName 'Microsoft.Windows.Library' -ReferenceAlias $WindowsAlias } # Locate "SystemCenter" alias If ( ($GroupPopDiscoveryMP.References.Value.Name -match '^Microsoft.SystemCenter.Library$' )) { $thisAlias = ($GroupPopDiscoveryMP.References | Where-Object {$_.Value.Name -match '^Microsoft.SystemCenter.Library$'} ).Key $SCAlias = $thisAlias #Continue; } #Add the reference if it does not exist Else { Add-MPReference -UnsealedMPName $GroupPopDiscoveryMP.Name -ReferenceMPName 'Microsoft.Windows.Library' -ReferenceAlias $SCAlias } # Locate "InstanceGroup" alias If ( ($GroupPopDiscoveryMP.References.Value.Name -match '^Microsoft.SystemCenter.InstanceGroup.Library$' )) { $thisAlias = ($GroupPopDiscoveryMP.References | Where-Object {$_.Value.Name -match '^Microsoft.SystemCenter.InstanceGroup.Library$'} ).Key $InstanceGroupAlias = $thisAlias #Continue; } #Add the reference if it does not exist Else { Add-MPReference -UnsealedMPName $GroupPopDiscoveryMP.Name -ReferenceMPName 'Microsoft.Windows.Library' -ReferenceAlias $InstanceGroupAlias } #} # Determine what type of group this is: InstanceGroup or ComputerGroup $GroupClassType = Get-GroupType -GroupName $GroupName # Construct the appropriate RelationshipClass string. This must agree with the exiting discovery/group type Switch ($GroupClassType){ 'Microsoft.SystemCenter.InstanceGroup' { $RelationshipClass = @" <RelationshipClass>`$MPElement[Name="$($InstanceGroupAlias)!Microsoft.SystemCenter.InstanceGroupContainsEntities"]`$</RelationshipClass> "@ } 'Microsoft.SystemCenter.ComputerGroup' { If ($AddHealthServiceWatchers) { Write-Error "Existing group type is [$($GroupClassType)]. Cannot add Health Service Watchers to exiting group. No Action taken. (Must be 'Microsoft.SstemCenter.InstanceGroup' to add other instance types. Exiting.)" Return } $RelationshipClass = @" <RelationshipClass>`$MPElement[Name="$($SCAlias)!Microsoft.SystemCenter.ComputerGroupContainsComputer"]`$</RelationshipClass> "@ } default { Write-Error "Unknown base class type [$($GroupClassType)] for group [$($GroupName)]. Exiting." Return } } Write-Verbose "Building new configuration..." $NewConfiguration = Create-ExplicitMembershipConfig -ComputerName $ComputerName -GroupName $GroupName -RelationshipClass $RelationshipClass -WindowsAlias $WindowsAlias -SCAlias $SCAlias -InstanceGroupAlias $InstanceGroupAlias Write-Verbose "New Configuration:`n$($NewConfiguration)" $Proceed = $false $r = '' If (-NOT $Force) { While ($r -notmatch 'y|n') { Write-Warning "Warning: This will overwrite existing group." $r = Read-Host 'Proceed? (Y/N)' } If ($r -match 'y') { $Proceed = $true } Else { Write-Warning "No action taken. Exiting." Return } } If ($Force -OR $Proceed) { $result = Update-OMGroupDiscovery2 -SDK $MgmtServerName -GroupName $GroupName -NewConfiguration $NewConfiguration -Verbose:$VerbosePreference Write-Verbose "Group Update succesful?: $result" } # Finally, get the new pack If ($PassThru) { Get-SCOMGroup -Id ($objGroup.Id) -Verbose:$VerbosePreference } }#End Function Update-SCOMComputerGroup ####################################################################### $TempFolder = (Join-Path (Join-Path $env:Windir "Temp") SCOMHelper ) If (-NOT (Test-Path -Path $TempFolder -PathType Container)){ Write-Host "Attempting to create temp folder: $TempFolder" -F DarkCyan Try{ New-Item -ItemType Directory -Path $TempFolder -Force -ErrorAction Stop }Catch{ Write-Host "Failed to create temp directory. Please run this tool as administrator." -F Red } } . $PSScriptRoot\Start-SCOMOverrideTool.ps1 New-Alias -Name 'Export-EffectiveMonitoringConfiguration' -Value 'Export-SCOMEffectiveMonitoringConfigurationReport' -ErrorAction Ignore New-Alias -Name 'Disable-SCOMAllEventRules' -Value 'Start-SCOMOverrideTool' -ErrorAction Ignore New-Alias -Name 'Get-SCOMRunningWorkflows' -Value 'Get-SCOMAgentWorkflows' -ErrorAction Ignore Export-ModuleMember -Alias * $Functions = @' Clear-SCOMCache Compare-String Convert-MAMLToHtml Deploy-SCOMAgent Disable-SCOMAllEventRules Export-SCOMEffectiveMonitoringConfigurationReport Export-SCOMEventsToCSV Export-SCOMKnowledge Export-SCOMOverrides Fast-Ping Get-SCOMAlertKnowledge Get-SCOMClassInfo Get-SCOMHealthCheckOpsConfig Get-SCOMMPFileInfo Get-SCOMRunAsAccountName Get-SCOMRunAsProfilesAccounts Get-SCOMAgentWorkflows Get-StringHash New-SCOMClassGraph New-SCOMComputerGroup Ping-AllHosts Remove-SCOMObsoleteReferenceFromMPFile Set-SCOMMPAliases Show-SCOMModules Show-SCOMPropertyBag Start-SCOMOverrideTool Test-Port Unseal-SCOMMP Update-SCOMComputerGroup '@ -split "\n" -replace "`r","" | Export-ModuleMember |