#############################################################################
# 1. Discover problematic QoE instances. ( Healthy instances are not discovered at all )
# 2. For each QoE instance, set values of Attributes with alerting context.
# ( These will be used at Analysis step )
#
#############################################################################
# Parameters
#
# [ ComputerName ] - Computer (FQDN) that Lync Monitoring Server is hosted on
# [ SourceId ] - Source ID for the discovery
# [ ManagedEntityId ] - Management Entity ID for the discovery
#
#############################################################################
#############################################################################
#LOGS & EXCEPTIONS###########################################################
$exceptionOccured = $false;
# Mapping from (CategoryId from SCOM sproc) to (Category Name)
$CATEGORY_NAME_MAP =
@{
"1"=$CAT_MediationServer;
"2"=$CAT_AVConf;
"3"=$CAT_GatewayMS;
"4"=$CAT_GatewayClient;
"5"=$CAT_Subnet;
"6"=$CAT_IntraUserSite;
"7"=$CAT_InterUserSite;
"8"=$CAT_InterRegion;
}
# Mapping from Category Name to string to show as category in alert description
$CATEGORY_DISPLAY_NAME_MAP =
@{
$CAT_MediationServer="Mediation Server and client endpoint leg";
$CAT_AVConf ="A/V Conferencing Server";
$CAT_GatewayMS ="Gateway and Mediation Server leg";
$CAT_GatewayClient ="Gateway (Mediation Server bypass)";
$CAT_Subnet ="Calls from or to a subnet";
$CAT_IntraUserSite ="Calls within a site";
$CAT_InterUserSite ="Calls between sites";
$CAT_InterRegion ="Calls between regions";
}
# Mapping from Category Name to Warning Threshold
$CATEGORY_WARNING_THRESHOLD_MAP =
@{
$CAT_MediationServer=$MediationServer_WarningAlertThresholdPercentage;
$CAT_AVConf =$AVConf_WarningAlertThresholdPercentage;
$CAT_GatewayMS =$GatewayMS_WarningAlertThresholdPercentage;
$CAT_GatewayClient =$GatewayClient_WarningAlertThresholdPercentage;
$CAT_Subnet =$Subnet_WarningAlertThresholdPercentage;
$CAT_IntraUserSite =$IntraUserSite_WarningAlertThresholdPercentage;
$CAT_InterUserSite =$InterUserSite_WarningAlertThresholdPercentage;
$CAT_InterRegion =$InterRegion_WarningAlertThresholdPercentage;
}
# Mapping from Category Name to Critical Threshold
$CATEGORY_CRITICAL_THRESHOLD_MAP =
@{
$CAT_MediationServer=$MediationServer_CriticalAlertThresholdPercentage;
$CAT_AVConf =$AVConf_CriticalAlertThresholdPercentage;
$CAT_GatewayMS =$GatewayMS_CriticalAlertThresholdPercentage;
$CAT_GatewayClient =$GatewayClient_CriticalAlertThresholdPercentage;
$CAT_Subnet =$Subnet_CriticalAlertThresholdPercentage;
$CAT_IntraUserSite =$IntraUserSite_CriticalAlertThresholdPercentage;
$CAT_InterUserSite =$InterUserSite_CriticalAlertThresholdPercentage;
$CAT_InterRegion =$InterRegion_CriticalAlertThresholdPercentage;
}
# Mapping from Category Name to Critical Threshold
$CATEGORY_EXCLUDE_INSTANCES_MAP =
@{
$CAT_MediationServer=$MediationServer_ExcludeInstances;
$CAT_AVConf =$AVConf_ExcludeInstances;
$CAT_GatewayMS =$GatewayMS_ExcludeInstances;
$CAT_GatewayClient =$GatewayClient_ExcludeInstances;
$CAT_Subnet =$Subnet_ExcludeInstances;
$CAT_IntraUserSite =$IntraUserSite_ExcludeInstances;
$CAT_InterUserSite =$InterUserSite_ExcludeInstances;
$CAT_InterRegion =$InterRegion_ExcludeInstances;
}
# Column ID for each column of the result set of QoeGetScomInstance sproc
# See dev\server\qoe\aggregator\sql\perfcountergeneration.sql for the resultset of QoeGetScomInstance sproc
$SPROC_COLUMN_ID_MAP =
@{
"CategoryId"=0;
"Name"=1;
"ReportParam1"=2;
"ReportParam2"=3;
"ReportParam3"=4;
"ReportParam4"=5;
"TotalAudioCalls"=6;
"TotalPoorAudioCalls"=7;
"Degradation"=8;
"Jitter"=9;
"PacketLoss"=10;
"RoundTripTime"=11;
"Concealed"=12;
"Stretched"=13;
"Compressed"=14;
"HealthyInstanceList"=15
}
# Registry for keeping discovery signature of each problematic instance
$MICROSOFT_REG_KEY_PATH="Software\Microsoft"
$RTC_REG_KEY_PATH="Real-Time Communications"
$MONSRV_REG_KEY_PATH=$MICROSOFT_REG_KEY_PATH + "\" + $RTC_REG_KEY_PATH
$SCOM_INST_REG_KEY="SCOM"
#############################################################################
#QoE Monitoring type-specific configuration
#All fields should be initialized based on the server role type and used in readonly mode
#############################################################################
$Global:gConfig_NodeType = ""
# Get CategoryId from Category Name
function GetCategoryId($CategoryName)
{
$CategoryId = $null
# Lookup mapping from category id to category name
foreach( $category in $CATEGORY_NAME_MAP.GetEnumerator() )
{
if ( $CategoryName -eq $category.Value )
{
# Return category id
$CategoryId = $category.Name
}
}
if (!$CategoryId)
{
throw ("Unsupported category name : {0}" -f $CategoryName)
}
$CategoryId
}
function EncodeUrl([string] $url)
{
[System.Web.HttpUtility]::UrlEncode($url)
}
# Read registry entry. return $null it does not exist
function ReadRegValue( $sRegKey, $sRegEntryName )
{
$subKey = $gReg.OpenSubKey( $sRegKey )
if ($subKey -eq $null)
{
$null
}
else
{
$subKey.GetValue($sRegEntryName)
}
}
# Read DWORD registry entry and return the value as long type
# Return nDefaultValue if the key or entry does not exist
function ReadRegDWORD( $sRegKey, $sRegEntryName, $nDefaultValue )
{
$regValue = ReadRegValue $sRegKey `
$sRegEntryName
# Read String registry entry and return the value as string type
# Return sDefaultValue if the key or entry does not exist
function ReadRegString( $sRegKey, $sRegEntryName, $sDefaultValue )
{
$regValue = ReadRegValue $sRegKey `
$sRegEntryName
if ($regValue -eq $null )
{
$sDefaultValue
}
else
{
[string] $regValue
}
}
# return a string with two digits.
# make it start with "0" if the input string contains only one digit
function TwoDigits( $sDigits )
{
$s = "0" + $sDigits
if ( $s.length -lt 2 )
{
"00"
}
else
{
$s.substring($s.length-2, 2)
}
}
# return a string that formats the input number with a specific number of digits beyond the decimal point
# Ex> FormatNumber(10.1, 3) => 10.100
# FormatNumber(10.1, 5) => 10.10000
function FormatNumber($Number, $DigitsBeyondDecimalPoint)
{
$formatString = "{0:N"+$DigitsBeyondDecimalPoint+"}"
$formatString -f $Number
}
# Encapsulate connection and reader to query context.
$QueryContext = @( [ref]$SqlConnection, [ref]$SqlDataReader )
$QueryContext
}
# Close connection and reader in the query context.
function DB_CloseQueryContext($QueryContext ) {
$QueryContext[1].Value.Close() # SqlDataReader
$QueryContext[0].Value.Close() # SqlConnection
}
# Get sql data reader from the query context.
function DB_GetDataReaderReference($QueryContext ) {
$RefSqlDataReader = $QueryContext[1]
$RefSqlDataReader
}
# Get column value by column name from a data reader
function Reader_GetColumn( [ref]$dataReader, $columnName)
{
$dataReader.Value[ $SPROC_COLUMN_ID_MAP[ $columnName ] ]
}
# Get column value of string type
# - Sprocs run full outer join on audio metrics
# - with read uncommitted isolation level.
# - Return empty string in case the value from database is null
function Reader_GetStringColumn( [ref]$dataReader, $columnName)
{
$value = Reader_GetColumn $dataReader `
$columnName
if ( (!$value) -or ($value -is [DbNull]) ) {
[string] ""
}
else
{
[string] $value
}
}
# Get column value of 64 bit int type
# - Sprocs run full outer join on audio metrics
# - with read uncommitted isolation level.
# - Return 0 in case the value from database is null
function Reader_GetInt64Column( [ref]$dataReader, $columnName)
{
$value = Reader_GetColumn $dataReader `
$columnName
if ( (!$value) -or ($value -is [DbNull]) ) {
[long] 0
}
else
{
[long] $value
}
}
# Get column value of 64 bit int type
# - Sprocs run full outer join on audio metrics
# - with read uncommitted isolation level.
# - Return 0 in case the value from database is null
function Reader_GetDoubleColumn( [ref]$dataReader, $columnName)
{
$value = Reader_GetColumn $dataReader `
$columnName
if ( (!$value) -or ($value -is [DbNull]) ) {
[double] 0
}
else
{
[double] $value
}
}
function Reader_GetCategoryId( [ref]$dataReader)
{
Reader_GetStringColumn $dataReader `
"CategoryId"
}
function Reader_GetName( [ref]$dataReader)
{
Reader_GetStringColumn $dataReader `
"Name"
}
function Reader_GetReportParam1( [ref]$dataReader)
{
Reader_GetStringColumn $dataReader `
"ReportParam1"
}
function Reader_GetReportParam2( [ref]$dataReader)
{
Reader_GetStringColumn $dataReader `
"ReportParam2"
}
function Reader_GetReportParam3( [ref]$dataReader)
{
Reader_GetStringColumn $dataReader `
"ReportParam3"
}
function Reader_GetReportParam4( [ref]$dataReader)
{
Reader_GetStringColumn $dataReader `
"ReportParam4"
}
function Reader_GetTotalAudioCalls([ref] $dataReader)
{
Reader_GetInt64Column $dataReader `
"TotalAudioCalls"
}
function Reader_GetTotalPoorAudioCalls([ref] $dataReader)
{
Reader_GetInt64Column $dataReader `
"TotalPoorAudioCalls"
}
function Reader_GetDegradation([ref] $dataReader)
{
Reader_GetDoubleColumn $dataReader `
"Degradation"
}
function Reader_GetJitter([ref] $dataReader)
{
Reader_GetDoubleColumn $dataReader `
"Jitter"
}
function Reader_GetPacketLoss([ref] $dataReader)
{
Reader_GetDoubleColumn $dataReader `
"PacketLoss"
}
function Reader_GetRoundTripTime([ref] $dataReader)
{
Reader_GetDoubleColumn $dataReader `
"RoundTripTime"
}
function Reader_GetConcealed([ref] $dataReader)
{
Reader_GetDoubleColumn $dataReader `
"Concealed"
}
function Reader_GetStretched([ref] $dataReader)
{
Reader_GetDoubleColumn $dataReader `
"Stretched"
}
function Reader_GetCompressed([ref] $dataReader)
{
Reader_GetDoubleColumn $dataReader `
"Compressed"
}
function Reader_GetHealthyInstanceList([ref] $dataReader)
{
Reader_GetStringColumn $dataReader `
"HealthyInstanceList"
}
#############################################################################
# Functions for getting valid alerting parameters
#############################################################################
function GetMinutesToQuery() {
if ( $Default_MinutesToQuery -lt 1 ) {
$E_MinutesToQuery
} else {
$Default_MinutesToQuery
}
}
function GetMinCallsAffected() {
if ( $Default_MinCallsAffected -lt 1 ) {
$E_MinCallsAffected
} else {
$Default_MinCallsAffected
}
}
function GetMinUsersAffected() {
if ( $Default_MinUsersAffected -lt 1 ) {
$E_MinUsersAffected
} else {
$Default_MinUsersAffected
}
}
function GetIncludeWIFICalls() {
if ( $Default_IncludeWIFICalls -eq "true") {
1
} else {
0
}
}
function GetIncludeVPNCalls() {
if ( $Default_IncludeVPNCalls -eq "true") {
1
} else {
0
}
}
function GetIncludeExternalCalls() {
if ( $Default_IncludeExternalCalls -eq "true") {
1
} else {
0
}
}
#############################################################################
# Functions for parsing list of excluded instances separated by comma
#############################################################################
# Input :
# $InstanceList - list of instance names separated by comma
# Output :
# an hashtable containing each instance name as a key
#
# The format of InstanceList :
# It is standard CSV format used by excel
# (1) Instance names are separated by comma
# a,b
# => a
# b
# (2) In case comma(,) is included in the instance name, enclose it with quote(")
# "a,b", c
# => a,b
# c
# (3) In case quote(") is included in the instance name, write quote once more
# """a,b""",c
# => "a,b"
# c
#
# Implementation Details :
# Let's use ConvertFrom-CSV PS Cmdlet for parsing the input string.
# There is no need to reinvent the wheel. :)
# ConvertFrom-CSV requires at lease 1 header and 1 row.
# So we simply use the comma separated instance names both for header and row
function GetInstanceNameHash( $InstanceList ) {
$InstanceNameHash = @{}
if ( $InstanceList ) {
# Add as a row in CSV content
$csvContent = $InstanceList
$csvObject = $null
# crate header with 1000 columns. ( c1, c2, c3, ... , c1000 )
# 1000 columns are enough, because the maximum length of string SCOM Console can handle is 4000.
# (The length of an instance name should be greater than 4, so 1000 instances are enough)
$i = 1
$header = @()
while($i -le 1000) {
$header += ("c" + $i)
$i ++
}
# Create CSV object based on the content
$csvObject = $csvContent | ConvertFrom-Csv -Header $header
if ($csvObject -eq $null) {
# In case the format of the string is wrong :
# BUGBUG - raise an internal alert in SCOM
} else {
foreach ($csvColumn in (get-member -inputobject $csvObject | where {$_.MemberType -eq "NoteProperty"}) ) {
$InstanceName = $csvObject.($csvColumn.Name)
if ($InstanceName -ne $null) {
# escape single quote(') to avoide SQL injection
$InstanceName = $InstanceName.replace("'","''")
# Add to hash table only if the instance name does not exist.
if ( ! $InstanceNameHash.ContainsKey( $InstanceName ) )
{
# Add to Hash (Key : Instance Name, Value : null (not used))
$InstanceNameHash.Add( $InstanceName, $null )
}
}
}
}
}
$InstanceNameHash
}
#############################################################################
# Functions for getting alert setting XML / Parsing report home page URL
#############################################################################
# Get a list of CategoryThresholds
function GetCategoryThresholdXML()
{
foreach( $Category in $CATEGORY_NAME_MAP.GetEnumerator() )
{
# Category Id in QoE database
$CategoryId = $Category.Name
# Category name
$CategoryName = $Category.Value
$CategoryWarning = [double] -1.0
$CategoryCritical = [double] -1.0
# Get a list of CategoryThresholds
function GetExcludeInstancesXML()
{
foreach( $Category in $CATEGORY_NAME_MAP.GetEnumerator() )
{
$CategoryId = $Category.Name
# Category name
$CategoryName = $Category.Value
$InstanceListSeparatedByComma = $CATEGORY_EXCLUDE_INSTANCES_MAP[$CategoryName]
foreach ( $InstanceName in (GetInstanceNameHash $InstanceListSeparatedByComma).Keys ) {
TRACE("Read suppressed instance(Category="+$CategoryName+";InstanceName="+$InstanceName+")")
"
<ExcludeInstance
Category=`""+$CategoryName+"`"
CategoryId=`""+$CategoryId+"`"
InstanceName=`""+$InstanceName+"`"
/>
"
}
}
}
# Get the reporting service base URL which will become the prefix of troubleshooting URLs
function GetReportingServiceBaseUrl()
{
$BaseUrl = $ReportingServiceHomePageUrl
if($BaseUrl)
{
$url = $null
if([System.Uri]::TryCreate($BaseUrl, [System.UriKind]::RelativeOrAbsolute, [ref]$url))
{
if($url -ne $null -and $url.Segments.Length -gt 2)
{
$BaseUrl = $url.Scheme + "://" + $url.Authority + $url.Segments[0] + $url.Segments[1]
############################
# Get QoE alert settings.
# Parameters
# - [Out] AlertSettingXML - XML string that holds QoE alert settings,
# which will be passed to the sproc that queries QoE database
function GetAlertSettings([ref] $AlertSettingXML ) {
# The XML string that will be passed to sproc that queries QoE database
$AlertSettingXML.Value =
"
<AlertSetting>
<Settings
MinCallsAffected=`""+(GetMinCallsAffected)+"`"
MinUsersAffected=`""+(GetMinUsersAffected)+"`"
IncludeWIFICalls=`""+(GetIncludeWIFICalls)+"`"
IncludeVPNCalls=`""+(GetIncludeVPNCalls)+"`"
IncludeExternalCalls=`""+(GetIncludeExternalCalls)+"`"
/>
<CategoryThresholds>
"+(GetCategoryThresholdXML)+"
</CategoryThresholds>
<ExcludeInstances>
"+(GetExcludeInstancesXML)+"
</ExcludeInstances>
</AlertSetting>
"
}
#############################################################################
# Methods for MonitoringNodeConfig
#############################################################################
function MNC_Initialize($MonitoringNodeType)
{
$Global:gConfig_NodeType = $MonitoringNodeType
switch( $MonitoringNodeType.ToUpper() )
{
"AVConf"
{
# Variables used for the discovery
$Global:gConfig_QoENodeTypeId = "$MPElement[Name='Microsoft.LS.2013.QoE.MonitoringNode.AVConf']$"
$Global:gConfig_AlertingContextTypeId = "$MPElement[Name='Microsoft.LS.2013.QoE.AlertingContext.AVConf']$"
$Global:gConfig_QoEMetricTypeIds[0] = "$MPElement[Name='Microsoft.LS.2013.QoE.Metric.AudioQuality.AVConf']$"
$Global:gConfig_QoEMetricTypeCount = 1
$TRUE
}
"MediationServer"
{
# Variables used for the discovery
$Global:gConfig_QoENodeTypeId = "$MPElement[Name='Microsoft.LS.2013.QoE.MonitoringNode.MediationServer']$"
$Global:gConfig_AlertingContextTypeId = "$MPElement[Name='Microsoft.LS.2013.QoE.AlertingContext.MediationServer']$"
"GatewayMS"
{
# Variables used for the discovery
$Global:gConfig_QoENodeTypeId = "$MPElement[Name='Microsoft.LS.2013.QoE.MonitoringNode.GatewayMS']$"
$Global:gConfig_AlertingContextTypeId = "$MPElement[Name='Microsoft.LS.2013.QoE.AlertingContext.GatewayMS']$"
"GatewayClient"
{
# Variables used for the discovery
$Global:gConfig_QoENodeTypeId = "$MPElement[Name='Microsoft.LS.2013.QoE.MonitoringNode.GatewayClient']$"
$Global:gConfig_AlertingContextTypeId = "$MPElement[Name='Microsoft.LS.2013.QoE.AlertingContext.GatewayClient']$"
"Subnet"
{
# Variables used for the discovery
$Global:gConfig_QoENodeTypeId = "$MPElement[Name='Microsoft.LS.2013.QoE.MonitoringNode.Subnet']$"
$Global:gConfig_AlertingContextTypeId = "$MPElement[Name='Microsoft.LS.2013.QoE.AlertingContext.Subnet']$"
"IntraUserSite"
{
# Variables used for the discovery
$Global:gConfig_QoENodeTypeId = "$MPElement[Name='Microsoft.LS.2013.QoE.MonitoringNode.IntraUserSite']$"
$Global:gConfig_AlertingContextTypeId = "$MPElement[Name='Microsoft.LS.2013.QoE.AlertingContext.IntraUserSite']$"
$Global:gConfig_QoEMetricTypeIds[0] = "$MPElement[Name='Microsoft.LS.2013.QoE.Metric.AudioQuality.IntraUserSite']$"
$Global:gConfig_QoEMetricTypeCount = 1
$TRUE
}
"InterUserSite"
{
# Variables used for the discovery
$Global:gConfig_QoENodeTypeId = "$MPElement[Name='Microsoft.LS.2013.QoE.MonitoringNode.InterUserSite']$"
$Global:gConfig_AlertingContextTypeId = "$MPElement[Name='Microsoft.LS.2013.QoE.AlertingContext.InterUserSite']$"
"InterRegion"
{
# Variables used for the discovery
$Global:gConfig_QoENodeTypeId = "$MPElement[Name='Microsoft.LS.2013.QoE.MonitoringNode.InterRegion']$"
$Global:gConfig_AlertingContextTypeId = "$MPElement[Name='Microsoft.LS.2013.QoE.AlertingContext.InterRegion']$"
# Add properties related to reporting URL to the hash table
function AddReportUrlProps( [ref] $RefDataReader, $QueryStartUTCTime, $QueryEndUTCTime, $outHash )
{
$outHash.Add("ReportUrlPeriodStart", (EncodeUrl (FormatUTCTime $QueryStartUTCTime) ) )
$outHash.Add("ReportUrlPeriodEnd", (EncodeUrl (FormatUTCTime $QueryEndUTCTime) ) )
# 1 means external calls are not included
$ReportUrlParamIncludeExternalCalls="1"
# 0 means calls via VPN are not included
$ReportUrlParamIncludeVPNCalls="0"
# 0 means calls via WIFI are not included
$ReportUrlParamIncludeWIFICalls="0"
if ( $Default_IncludeExternalCalls -eq "true" ) {
# 2 means external calls are included
$ReportUrlParamIncludeExternalCalls = "2"
}
if ( $Default_IncludeVPNCalls -eq "true" ) {
# 2 means calls via VPN are not included
$ReportUrlParamIncludeVPNCalls="2"
}
if ( $Default_IncludeWIFICalls -eq "true" ) {
# 2 means calls via WIFI are not included
$ReportUrlParamIncludeWIFICalls="2"
}
# Add AlertContent property to the hash table
# These will be printed in the alert description
function AddAlertContentProp( $QoEInstanceName, $hash )
{
$CategoryDisplayName = $CATEGORY_DISPLAY_NAME_MAP[ $Global:gConfig_NodeType ]
$ReportUrlMessage = ""
if ( $REPORT_URL_PREFIX )
{
$ReportUrlMessage = "For details about this alert, open the following Monitoring Server report in a browser:"
}
else
{
$ReportUrlMessage =
"Reporting URL is not available because Monitoring Server Reports are not deployed." +
"`nTo see reports that can assist with troubleshooting, deploy Monitoring Server Reports."
}
$AlertContentFormat =
"
Following are the details for this media quality alert:
Following are the average values for call classification metrics:
Average network degradation = {5}
Average jitter (ms) = {6}
Average packet loss (%) = {7}
Average round trip time (ms) = {8}
Average concealed metric (%) = {9}
Average stretched metric (%) = {10}
Average compressed metric (%) = {11}
Following are the alert parameters:
Calls over VPN included = {12}
Calls over WIFI included = {13}
Calls from external users included = {14}
Media quality is checked for the past (min) = {15}
A critical alert is raised when poor quality calls percentage is greater than (%) = {16}
A warning alert is raised when poor quality calls percentage is greater than (%) = {17}
Minimum number of calls affected to raise an alert = {18}
Minimum number of users affected to raise an alert = {19}
###############################################################################
# Methods for discovering problematic QoE instances and analyzing health states
###############################################################################
# Calculate percentage of bad calls
function CalculatePercent($BadCalls, $TotalCalls)
{
if ($TotalCalls -le 0) {
0
}
else
{
$BadCalls * 100 / $TotalCalls
}
}
# Get alerting context for a healthy instance
function GetAlertingContextForHealthyInstance($outAttrHash)
{
# Alert summary information such as N0C0D0P0
$AlertSummary = ""
foreach ($s in $AUDIO_COUNTER_SYMBOL) {
$AlertSummary = $AlertSummary + $s + $ALERT_GOOD #Healthy
}
$outAttrHash.Add("Alert", $AlertSummary)
}
# Get alerting context for a healthy instance group
function GetAlertingContextForHealthyInstanceGroup([ref] $dataReader, $outAttrHash)
{
# Alert summary information such as N0C0D0P0
GetAlertingContextForHealthyInstance $outAttrHash
# List of names of healthy instances
# Ex> inst1, inst2, ..., inst100, and 237 more.
$outAttrHash.Add( "HealthyInstanceList",
( Reader_GetHealthyInstanceList $dataReader ) )
}
# Get alerting context for a problematic instance
function GetAlertingContextForProblematicInstance(
[ref] $dataReader,
$MinimumCallVolume, $ErrorLevel, $WarningLevel,
$QueryStartUTCTime, $QueryEndUTCTime,
$outAttrHash)
{
$bHealthy = $TRUE
if ( $sPercent -lt $WarningLevel ) {
$sResult = $sResult + $AUDIO_COUNTER_SYMBOL[$i] + $ALERT_GOOD #Healthy
} elseif ( $sPercent -lt $ErrorLevel ) {
$bAlert = $TRUE
$sResult = $sResult + $AUDIO_COUNTER_SYMBOL[$i] + $ALERT_WARN #Warn
} else {
$bAlert = $TRUE
$sResult = $sResult + $AUDIO_COUNTER_SYMBOL[$i] + $ALERT_ERROR #Error
}
} else {
# Show instances as Healthy when the number of calls are below the min threshold.
#
# We cannot measure the Health State when the number of calls is below the threshold,
# because it could be incorrect dew to small number of samples.
# We can say this state as Unknown.
#
# We show the Unknown state as Healthy for passive monitoring like QoE Monitoring.
$sResult = $sResult + $AUDIO_COUNTER_SYMBOL[$i] + $ALERT_GOOD #Healthy
}
}
if ( $bAlert )
{
$bHealthy = $FALSE
$AudioCallVolumeValue = $AudioCallVolume
$PoorAudioCallVolumeValue = $PoorAudioCallVolume
}
else
{
# Need to set as empty string in case the instance is healthy.
# Otherwise the instance will have previous value for this attribute
$AudioCallVolumeValue = ""
$PoorAudioCallVolumeValue = ""
}
#Collect number of calls for Healthy/Warning/Critical
$outAttrHash.Add( "AudioCalls", $AudioCallVolumeValue )
$outAttrHash.Add( "PoorAudioCalls", $PoorAudioCallVolumeValue )
#Collect values of related audio perf counters which will be used to generate elaborate alert description
for ($i = 0; $i -lt $AudioCounters.Count; $i ++ )
{
if ( $bAlert )
{
$sPercent = FormatNumber $AudioCounters[$i] `
$PERCENT_DIGITS
}
else
{
# Need to set as empty string in case the instance is healthy.
# Otherwise the instance will have previous value for this attribute
$sPercent = ""
}
$outAttrHash.Add( $AUDIO_COUNTER_ALIAS[$i], $sPercent )
}
if ( ! $bHealthy )
{
#Generate property values related to the troubleshooting report URL for problematic instances ( = at least one Error/Warning QoE Metric)
AddReportUrlProps $RefDataReader `
$QueryStartUTCTime `
$QueryEndUTCTime `
$outAttrHash
AddAlertContentProp $InstanceName `
$outAttrHash
}
$outAttrHash.Add("Alert", $sResult)
}
# For a QoE instance, Add properties with alerting contexts
# $attrHash hold (Name, Value) pair for attributes which has alerting contexts
function AddAlertingContextProps( $QoEInstance, $sInstanceName, $attrHash )
{
$NotFoundAttrs = ""
foreach ($attr in $attrHash.GetEnumerator()) {
$attrName = $attr.Name
$attrValue = $attr.Value
# Common to Healthy and Problematic Instances
$commonAttrIdentityMap = @{
"HealthyInstanceList" = "$MPElement[Name='Microsoft.LS.2013.QoE.MonitoringNode']/HealthyInstanceList$"
}
# For a QoE Metric instance, Add properties related to alerting parameters in the alert description
# $attrHash hold (Name, Value) pair for attributes which has alerting contexts
function AddAlertingParamProps( $MetricInstance, $attrHash )
{
$NotFoundAttrs = ""
# Add a QoE instance
# $attrHash hold (Name, Value) pair for attributes which has alerting contexts
function AddInstance($sInstanceName, $attrHash)
{
#Add the instance of the QoE alerting context
$AlertingContext = $DiscoveryData.CreateClassInstance($Global:gConfig_AlertingContextTypeId)
#TRACE ("GUID:AlertingContext="+$Global:gConfig_AlertingContextTypeId+"`n")
#Add the instance of the QoE monitoring node
$QoEInstance = $DiscoveryData.CreateClassInstance($Global:gConfig_QoENodeTypeId)
#TRACE ("GUID:QoE Node="+$Global:gConfig_QoENodeTypeId+"`n")
#Add instances of the QoE metrics hosted by this node
for ($i = 0; $i -lt $Global:gConfig_QoEMetricTypeCount; $i++)
{
$oQoEMetric = $DiscoveryData.CreateClassInstance($Global:gConfig_QoEMetricTypeIds[$i])
#TRACE ("GUID:QoE Metric="+$Global:gConfig_QoEMetricTypeIds[$i]+"`n")
# Open registry key in R/W mode. Quit the script if it does not exist.
function OpenRegKey($RegKeyPath)
{
$subKey = $gReg.OpenSubKey( $RegKeyPath, $true )
if ($subKey -eq $null)
{
throw ("Registry key path is not created : {0}" -f $RegKeyPath)
}
$subKey
}
# Create the registry key if it does not exist
function EnsureRegKey($RegKeyPath, $RegKeyName)
{
$key = OpenRegKey $RegKeyPath
try {
$key.CreateSubKey( $RegKeyName ).Close()
} finally {
$key.Close()
}
}
# Update the registry entry w/ the specified value
# Create the registry entry if it does not exist
function EnsureRegStringEntry($RegKeyPath, $RegEntryName, $RegEntryValue)
{
$key = OpenRegKey $RegKeyPath
try {
$key.SetValue( $RegEntryName, $RegEntryValue )
} finally {
$key.Close()
}
}
# Get Registry key path for the discovery signature of a QoE category
function GetRegKeyPath($CategoryId)
{
$MONSRV_REG_KEY_PATH + "\" + $SCOM_INST_REG_KEY + "\" + $CategoryId
}
# Discover an instance as Healthy
function DiscoverHealthyInstance($CategoryId, $InstanceName)
{
$attrHash = $null
$attrHash = @{}
# Alert summary information such as N0C0D0P0
GetAlertingContextForHealthyInstance $attrHash
AddInstance $InstanceName `
$attrHash
}
# Update discovery signature for an instance in Reg
#
# Key : HKCU:\Software\Microsoft\Real-Time Communications\Scom\$CategoryId
# Entity
# Name : $InstanceName
# Type : String
# Value : $DiscoverySignature
#
# For the details, see the comment inside of DiscoverInstances about BUG-209236
function UpdateDiscoverySignature( $CategoryId, $InstanceName, $DiscoverySignature)
{
EnsureRegKey $MONSRV_REG_KEY_PATH `
$SCOM_INST_REG_KEY
# Iterate each discovered instance in registry
# Discover as the instance healthy and remove it from the registry if the
# discovery signature does not match with the one of the current discovery.
#
# For the details, see the comment inside of DiscoverInstances about BUG-209236
function DiscoverHealthyInstanceIfNotMatchDiscoverySignature($DiscoverySignature)
{
foreach( $category in $CATEGORY_NAME_MAP.GetEnumerator() )
{
# get category id
$CategoryId = $category.Name
if ($key -ne $null)
{
TRACE("Opened registry key path for discovery signature check : "+$RegKeyPath)
try {
foreach( $InstanceName in $key.GetValueNames() )
{
# The value of the registry entry is the discovery signature for this instance
$InstDiscoverySignature = $key.GetValue($InstanceName)
if ($InstDiscoverySignature -ne $DiscoverySignature) {
# Query QoE DB to get all problematic instances and healthy instance groups
# Discover Instances (for each category)
# 1 healthy instance group for all healthy QoE instances
# 1 SCOM instance for each problematic(=warning or critical) QoE instance
function DiscoverInstances()
{
# Fix BUG-209236 [QoE SCOM] Active Alerts never get resolved
#
# SCOM does not auto resolve alerts if a problematic instance is not
# discovered in the next discovery.
#
# We should a discover healthy instance right before a problematic
# instance is undiscovered to auto-resolve alerts for it.
#
# For each run of this discovery script, we will get a GUID and call it as discovery
# signature. All instances discovered in an execution of discovery will have the
# same discovery signature and we will update the discovery signature for each
# discovered instance in Registry.
#
# After the discovery is finished, the script will iterate each
# instance in the registry. If the discovery signature is
# different from the one of the current discovery, we can assume
# that the instance was discovered in the previous discovery.
# In this case we can simply discover the instance as healthy so that all
# alerts are auto-resolved.
#
# Of course, we should remove the instance from the registry so that it is not discovered as
# healthy again in the next discovery.
#
$DiscoverySignature = [guid]::NewGuid().ToString()
$AlertSettingsXML = ""
# Aet threshold Settings from input parameters
GetAlertSettings ([ref]$AlertSettingsXML)
TRACE("AlertSettingsXML="+$AlertSettingsXML)
$QueryEndUTCTime = [System.DateTime]::UtcNow
# Subtract 15 sec from the endtime to avoid collisions with calls that are being inserted currently.
$QueryEndUTCTime = $QueryEndUTCTime.AddSeconds(-15)
$QueryStartUTCTime = $QueryEndUTCTime.AddMinutes( -1 * $Default_MinutesToQuery )
# Cuation : Should use [ref] type for SqlDataReader.
# Otherwise SqlDataReader reads all records from resultset once
# (1)it is assigned to a varible or
# (2)passed as an argument to a function
$RefDataReader = DB_GetDataReaderReference $QueryContext
# If no registry is found for the report URL, set $REPORT_URL_PREFIX to $null
$REPORT_URL_PREFIX = GetReportingServiceBaseUrl
if ( $REPORT_URL_PREFIX ) {
TRACE("Detected Reporting URL => " + $REPORT_URL_PREFIX )
}