modules/SdnDiag.Health.psm1
# Copyright (c) Microsoft Corporation. # Licensed under the MIT License. Import-Module $PSScriptRoot\SdnDiag.Common.psm1 Import-Module $PSScriptRoot\SdnDiag.Server.psm1 Import-Module $PSScriptRoot\SdnDiag.NetworkController.psm1 Import-Module $PSScriptRoot\SdnDiag.NetworkController.FC.psm1 Import-Module $PSScriptRoot\SdnDiag.NetworkController.SF.psm1 Import-Module $PSScriptRoot\SdnDiag.Utilities.psm1 $configurationData = Import-PowerShellDataFile -Path "$PSScriptRoot\SdnDiag.Health.Config.psd1" New-Variable -Name 'SdnDiagnostics_Health' -Scope 'Script' -Force -Value @{ Cache = @{} Config = $configurationData } # confirm that the current system is supported to generate health faults $displayVersion = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name 'DisplayVersion' $productName = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -Name 'ProductName' if ($productName.ProductName -iin $script:SdnDiagnostics_Health.Config.HealthFaultSupportedProducts){ $productSupported = $true } if ($displayVersion.DisplayVersion -iin $script:SdnDiagnostics_Health.Config.HealthFaultSupportedBuilds){ $versionSupported = $true } if ($versionSupported -and $productSupported){ $script:SdnDiagnostics_Health.Config.HealthFaultEnabled = $true } ########################## #### CLASSES & ENUMS ##### ########################## class SdnFaultInfo { [datetime] $OccurrenceTime = [System.DateTime]::UtcNow [string] $KeyFaultingObjectDescription [string] $KeyFaultingObjectID [string] $KeyFaultingObjectType [string] $FaultingObjectLocation [string] $FaultDescription [string] $FaultActionRemediation } ########################## #### FAULT HELPERS ##### ########################## # pInvoke definition for fault APIs $signature = @' [DllImport("hcihealthutils.dll", CharSet = CharSet.Unicode, SetLastError = false)] public static extern int HciModifyFault( string entityType, string entityKey, string entityDescription, string entityLocation, string entityUniqueKey, uint action, string faultType, uint urgency, string title, string description, string actions, uint flag); [DllImport("hcihealthutils.dll", CharSet = CharSet.Unicode, SetLastError = false)] public static extern int HciModifyRelationship( string entityType, string entityKey, string entityDescription, string entityLocation, string entityUniqueKey, uint action, string parentEntityType, string parenetEntityKey, string parentEntityDescription, string parentEntityLocation, string parentEntityUniqueKey, string groupKey, uint urgency, uint relationshipType, uint flag); '@ function ValidateFault { param( [SdnFaultInfo] $Fault ) if ([string]::IsNullOrEmpty($Fault.KeyFaultingObjectDescription)) { throw "KeyFaultingObjectDescription is required" } if ([string]::IsNullOrEmpty($Fault.KeyFaultingObjectID)) { throw "KeyFaultingObjectID is required" } if ([string]::IsNullOrEmpty($Fault.KeyFaultingObjectType)) { throw "KeyFaultingObjectType is required" } } function LogWmiHealthFault { <# .SYNOPSIS Logs the WMI version of the health fault .PARAMETER fault The fault to log #> param( [object] $fault ) Write-Verbose " WmiFault:" Write-Verbose " (FaultId) $($fault.FaultId)" Write-Verbose " (FaultingObjectDescription) $($fault.FaultingObjectDescription)" Write-Verbose " (FaultingObjectLocation) $($fault.FaultingObjectLocation)" Write-Verbose " (FaultingObjectType) $($fault.FaultingObjectType)" Write-Verbose " (FaultingObjectUniqueId) $($fault.FaultingObjectUniqueId)" Write-Verbose " (FaultTime) $($fault.FaultTime)" Write-Verbose " (FaultType) $($fault.FaultType)" Write-Verbose " (Reason) $($fault.Reason)" } function ConvertFaultListToPsObjectList { <# .SYNOPSIS Converts a list of faults to a list of PSObjects (used by ASZ modules to emit telemetry events ) .PARAMETER faults The list of faults to convert #> param( [SdnFaultInfo[]] $faults, [ValidateSet("Create", "Delete")] [string] $faultType ) $faultList = @() foreach ($fault in $faults) { # convert properties of class SdnFaultInfo $faultList += [PSCustomObject]@{ OccurrenceTime = $fault.OccurrenceTime KeyFaultingObjectDescription = $fault.KeyFaultingObjectDescription KeyFaultingObjectID = $fault.KeyFaultingObjectID KeyFaultingObjectType = $fault.KeyFaultingObjectType FaultingObjectLocation = $fault.FaultingObjectLocation FaultDescription = $fault.FaultDescription FaultActionRemediation = $fault.FaultActionRemediation OperationType = $faultType } } return $faultList } function ConvertFaultToPsObject { <# .SYNOPSIS Converts a fault to a PSObject (used by ASZ modules to emit telemetry events ) .PARAMETER healthFault The fault to convert .PARAMETER faultOpType The operation type of the fault #> param( [SdnFaultInfo] $healthFault, [ValidateSet("Create", "Delete")] [string] $faultOpType ) # convert properties of class SdnFaultInfo $faultObject = [PSCustomObject]@{ OccurrenceTime = $healthFault.OccurrenceTime KeyFaultingObjectDescription = $healthFault.KeyFaultingObjectDescription KeyFaultingObjectID = $healthFault.KeyFaultingObjectID KeyFaultingObjectType = $healthFault.KeyFaultingObjectType FaultingObjectLocation = $healthFault.FaultingObjectLocation FaultDescription = $healthFault.FaultDescription FaultActionRemediation = $healthFault.FaultActionRemediation OperationType = $faultOpType } return $faultObject } function LogHealthFault { <# .SYNOPSIS Logs the health fault .PARAMETER fault The fault to log #> param( [object] $healthFault ) Write-Verbose " HealthFault:" Write-Verbose " (KeyFaultingObjectDescription) $($healthFault.KeyFaultingObjectDescription)" Write-Verbose " (KeyFaultingObjectID) $($healthFault.KeyFaultingObjectID)" Write-Verbose " (KeyFaultingObjectType) $($healthFault.KeyFaultingObjectType)" Write-Verbose " (FaultingObjectLocation) $($healthFault.FaultingObjectLocation)" Write-Verbose " (FaultDescription) $($healthFault.FaultDescription)" Write-Verbose " (FaultActionRemediation) $($healthFault.FaultActionRemediation)" } function LogHealthFaultToEventLog { <# .SYNOPSIS Logs the health fault to the event log .PARAMETER fault The fault to log #> [CmdletBinding()] param( [object] $fault, [ValidateSet("Create", "Delete")] [string] $operation ) if ([string]::IsNullOrEmpty($operation) ) { $operation = "" } $eventLogMessage = "SDN HealthServiceHealth Fault: $($fault.FaultDescription)" $eventLogMessage += "`r`n" $eventLogMessage += "Faulting Object Description: $($fault.KeyFaultingObjectDescription)" $eventLogMessage += "`r`n" $eventLogMessage += "Faulting Object ID: $($fault.KeyFaultingObjectID)" $eventLogMessage += "`r`n" $eventLogMessage += "Faulting Object Type: $($fault.KeyFaultingObjectType)" $eventLogMessage += "`r`n" $eventLogMessage += "Faulting Object Location: $($fault.FaultingObjectLocation)" $eventLogMessage += "`r`n" $eventLogMessage += "Fault Action Remediation: $($fault.FaultActionRemediation)" $eventLogMessage += "`r`n" $eventLogMessage += "Fault Operation: $($operation)" $eventLogJson = (ConvertTo-Json -InputObject $fault -Depth 5) $eventInstance = [System.Diagnostics.EventInstance]::new(1, 1) $evtObject = New-Object System.Diagnostics.EventLog; $evtObject.Log = $LOG_NAME $evtObject.Source = $LOG_SOURCE Write-Verbose "Source : $($LOG_SOURCE) Log : $($LOG_NAME) Message : $($eventLogMessage)" $evtObject.WriteEvent($eventInstance, @($eventLogMessage, $eventLogJson, $operation)) } function CreateorUpdateFault { param( [SdnFaultInfo] $Fault ) if (-NOT $script:SdnDiagnostics_Health.Config.HealthFaultEnabled) { return } ValidateFault -Fault $Fault InitFaults Write-Verbose "CreateorUpdateFault:" LogHealthFault -healthFault $Fault LogHealthFaultToEventLog -fault $Fault -operation Create if ([string]::IsNullOrEmpty($script:subsystemId)) { $script:subsystemId = (get-storagesubsystem Cluster*).UniqueId $script:entityTypeSubSystem = "Microsoft.Health.EntityType.Subsystem" } $retValue = [Microsoft.NetworkHud.FunctionalTests.Module.HciHealthUtils]::HciModifyFault( ` $Fault.KeyFaultingObjectDescription, # $entityType, ` $Fault.KeyFaultingObjectID, # $entityId, ` $Fault.KeyFaultingObjectDescription, # "E Desc", ` $Fault.FaultingObjectLocation, # $entityLocation, ` $Fault.KeyFaultingObjectID, # $entityId, ` $HCI_MODIFY_FAULT_ACTION_MODIFY, #action ` $Fault.KeyFaultingObjectType, # $faultType, ` $HEALTH_URGENCY_UNHEALTHY, # ` "Fault Title", ` $Fault.FaultDescription, # fault description $Fault.FaultActionRemediation, # fault remediation action $HCI_MODIFY_FAULT_FLAG_NONE) | Out-Null $retValue = [Microsoft.NetworkHud.FunctionalTests.Module.HciHealthUtils]::HciModifyRelationship( $Fault.KeyFaultingObjectDescription, # $entityType, ` $Fault.KeyFaultingObjectID, # $entityId, ` $Fault.KeyFaultingObjectDescription, # $entityDescription $Fault.FaultingObjectLocation, # $entityLocation, ` $Fault.KeyFaultingObjectID, # $entityId, ` $HCI_MODIFY_RELATIONSHIP_ACTION_MODIFY, ` $script:entityTypeSubSystem, ` $script:subsystemId, ` $null, ` $null, ` $script:subsystemId, ` "TestGroupKey", ` $HEALTH_URGENCY_UNHEALTHY, ` $HEALTH_RELATIONSHIP_COLLECTION, ` $HCI_MODIFY_RELATIONSHIP_FLAG_NONE) | Out-Null } function DeleteFaultBy { <# .SYNOPSIS Deletes a fault by its key properties, those with empty or a * will be ignored while comaprison for a broader clear operation .PARAMETER KeyFaultingObjectDescription The description of the faulting object .PARAMETER KeyFaultingObjectID The unique ID of the faulting object .PARAMETER KeyFaultingObjectType The type of the faulting object .PARAMETER FaultingObjectLocation The location of the faulting object #> param( [string] $KeyFaultingObjectDescription, [string] $KeyFaultingObjectID, [string] $KeyFaultingObjectType, [string] $FaultingObjectLocation, [switch] $Verbose ) if (-NOT $script:SdnDiagnostics_Health.Config.HealthFaultEnabled) { return } Write-Verbose "DeleteFault: " Write-Verbose "(KeyFaultingObjectDescription) $($KeyFaultingObjectDescription)" Write-Verbose "(KeyFaultingObjectID) $($KeyFaultingObjectID)" Write-Verbose "(KeyFaultingObjectType) $($KeyFaultingObjectType)" Write-Verbose "(FaultingObjectLocation) $($FaultingObjectLocation)" InitFaults # get all the system faults $faults = Get-HealthFault [bool] $match = $true [string[]] $matchFaultsId = @() foreach ($fault in $faults) { # delete the one(s) that match the filter # KeyFaultingObjectDescription, KeyFaultingObjectID, KeyFaultingObjectType may be empty , in which case # we will not consider them for comparison $match = [string]::IsNullOrEmpty($KeyFaultingObjectDescription) -or $KeyFaultingObjectDescription -eq "*" -or ` $KeyFaultingObjectDescription -eq $fault.FaultingObjectDescription; Write-Verbose "KeyFaultingObjectDescription $match" $match = $match -and ([string]::IsNullOrEmpty($KeyFaultingObjectID) -or $KeyFaultingObjectID -eq "*" -or ` $KeyFaultingObjectID -eq $fault.FaultingObjectUniqueId) Write-Verbose "KeyFaultingObjectID $match" $match = $match -and ([string]::IsNullOrEmpty($KeyFaultingObjectType) -or $KeyFaultingObjectType -eq "*" -or ` $KeyFaultingObjectType -eq $fault.FaultingObjectType) Write-Verbose "KeyFaultingObjectType $match" if ($match) { Write-Verbose "Deleting fault (ID) $($fault.FaultId)" $matchFaultsId += $fault.FaultId } } if ($matchFaultsId.Count -eq 0) { Write-Verbose "No faults found to delete" return } else { Write-Verbose "Found $($matchFaultsId.Count) faults to delete" } foreach ($faultId in $matchFaultsId) { DeleteFaultById -faultUniqueID $faultId } } function DeleteFaultById { <# .SYNOPSIS Deletes a fault by its unique ID .PARAMETER faultUniqueID The unique ID of the fault to delete #> param( [string] $faultUniqueID ) if (-NOT $script:SdnDiagnostics_Health.Config.HealthFaultEnabled) { return } if ([string]::IsNullOrEmpty($faultUniqueID)) { throw "Empty faultID" } InitFaults Write-Verbose "DeleteFaultById $faultId" $fault = Get-HealthFault | ? { $_.FaultId -eq $faultUniqueID } if ($null -eq $fault) { throw "Fault with ID $faultUniqueID not found" } else { LogWmiHealthFault -fault $fault } [Microsoft.NetworkHud.FunctionalTests.Module.HciHealthUtils]::HciModifyFault( ` $fault.FaultingObjectType, ` $fault.FaultingObjectUniqueId, ` "", ` $fault.FaultingObjectUniqueId, ` $fault.FaultingObjectUniqueId, ` $HCI_MODIFY_FAULT_ACTION_REMOVE, ` $fault.FaultType, ` $HEALTH_URGENCY_UNHEALTHY, ` "", ` "", ` "", ` $HCI_MODIFY_FAULT_FLAG_NONE) | Out-Null } function ShowFaultSet { <# .SYNOPSIS Shows the fault set .PARAMETER faultset The fault set to show #> param([object[]]$faultset) Write-Verbose "Success Faults (for rest res):" if ($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent) { if ($null -eq $faultset[0] -or $faultset[0].Count -eq 0) { Write-Verbose "(none)" return } foreach ($faultInst in $faultset[0]) { LogHealthFault -healthFault $faultInst } } Write-Verbose "Failure Faults (for rest res):" if ($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent) { if ($null -eq $faultset[1] -or $faultset[1].Count -eq 0) { Write-Verbose "(none)" return } foreach ($faultInst in $faultset[1]) { LogHealthFault -healthFault $faultInst } } } function UpdateFaultSet { <# .SYNOPSIS Updates the fault set and returns the health test object .PARAMETER successFaults The set of faults that were successful .PARAMETER failureFaults The set of faults that failed #> param( [object[]]$successFaults, [object[]]$failureFaults ) $healthTest = New-SdnHealthTest if ($null -ne $failureFaults -and $failureFaults.Count -gt 0) { $healthTest.Result = "FAIL" } foreach ($fault in $successFaults) { DeleteFaultBy -KeyFaultingObjectDescription $fault.KeyFaultingObjectDescription $convFault = ConvertFaultToPsObject -healthFault $fault -faultOpType "Delete" $healthTest.HealthFault += $convFault } foreach ($fault in $failureFaults) { CreateOrUpdateFault -Fault $fault $convFault = ConvertFaultToPsObject -healthFault $fault -faultOpType "Create" $healthTest.HealthFault += $convFault } $healthTest } function DeleteFault { <# .SYNOPSIS Deletes a fault .PARAMETER Fault The fault to delete #> [CmdletBinding()] param( [SdnFaultInfo] $Fault ) if (-NOT$script:SdnDiagnostics_Health.Config.HealthFaultEnabled) { return } ValidateFault -Fault $Fault InitFaults Write-Verbose "DeleteFault $($Fault.KeyFaultingObjectDescription) $($Fault.KeyFaultingObjectID) $($Fault.KeyFaultingObjectType)" if ([string]::IsNullOrEmpty($script:subsystemId)) { $script:subsystemId = (get-storagesubsystem Cluster*).UniqueId $script:entityTypeSubSystem = "Microsoft.Health.EntityType.Subsystem" } [Microsoft.NetworkHud.FunctionalTests.Module.HciHealthUtils]::HciModifyFault( ` $Fault.KeyFaultingObjectDescription, # $entityType, ` $Fault.KeyFaultingObjectID, # $entityId, ` $Fault.KeyFaultingObjectDescription, # "E Desc", ` $Fault.FaultingObjectLocation, # $entityLocation, ` $Fault.KeyFaultingObjectID, # $entityId, ` $HCI_MODIFY_FAULT_ACTION_REMOVE, #action ` $Fault.KeyFaultingObjectType, # $faultType, ` $HEALTH_URGENCY_UNHEALTHY, # ` "Fault Title", ` $Fault.FaultDescription, # fault description $Fault.FaultActionRemediation, # fault remediation action $HCI_MODIFY_FAULT_FLAG_NONE) | Out-Null } function Start-HealthFaultsTranscript { <# .SYNOPSIS Initializes the health runner transcript #> $logLocation = GetLogLocation if ($null -eq $logLocation) { return $false } else { $fullLogPath = Join-Path -Path $logLocation -ChildPath "SdnHealthTranscript.log" Start-Transcript -Path $fullLogPath -Append -ErrorAction SilentlyContinue $script:TranscriptStarted = $true return $true } } function StopHealthRunnerTranscript { <# .SYNOPSIS Stops the health runner transcript #> if ($script:TranscriptStarted) { Write-Host "Stopping transcript" Stop-Transcript -ErrorAction SilentlyContinue $script:TranscriptStarted = $false } } function InitFaults { <# .SYNOPSIS Initializes defaults and constants for fault handling #> [CmdletBinding()] param() Write-Verbose "InitFaults" if (-not ("Microsoft.NetworkHud.FunctionalTests.Module.HciHealthUtils" -as [type])) { Add-Type -MemberDefinition $signature -Name "HciHealthUtils" -Namespace "Microsoft.NetworkHud.FunctionalTests.Module" | Out-Null Write-Verbose "Registered HCI fault utilities" } New-Variable -Name 'HCI_MODIFY_FAULT_ACTION_MODIFY' -Scope 'Script' -Force -Value 0 New-Variable -Name 'HCI_MODIFY_FAULT_ACTION_REMOVE' -Scope 'Script' -Force -Value 1 New-Variable -Name 'HCI_MODIFY_RELATIONSHIP_ACTION_MODIFY' -Scope 'Script' -Force -Value 0 New-Variable -Name 'HCI_MODIFY_RELATIONSHIP_ACTION_REMOVE' -Scope 'Script' -Force -Value 1 New-Variable -Name 'HEALTH_RELATIONSHIP_UNKNOWN' -Scope 'Script' -Force -Value 0 New-Variable -Name 'HEALTH_RELATIONSHIP_COMPOSITION' -Scope 'Script' -Force -Value 1 New-Variable -Name 'HEALTH_RELATIONSHIP_CONTAINMENT' -Scope 'Script' -Force -Value 2 New-Variable -Name 'HEALTH_RELATIONSHIP_COLLECTION' -Scope 'Script' -Force -Value 3 New-Variable -Name 'HEALTH_URGENCY_UNKNOWN' -Scope 'Script' -Force -Value 255 New-Variable -Name 'HEALTH_URGENCY_HEALTHY' -Scope 'Script' -Force -Value 0 New-Variable -Name 'HEALTH_URGENCY_WARNING' -Scope 'Script' -Force -Value 1 New-Variable -Name 'HEALTH_URGENCY_UNHEALTHY' -Scope 'Script' -Force -Value 2 New-Variable -Name 'HCI_MODIFY_FAULT_FLAG_NONE' -Scope 'Script' -Force -Value 0 New-Variable -Name 'HCI_MODIFY_RELATIONSHIP_FLAG_NONE' -Scope 'Script' -Force -Value 0 New-Variable -Name 'LOG_NAME' -Scope 'Script' -Force -Value 'SdnHealthService' New-Variable -Name 'LOG_CHANNEL' -Scope 'Script' -Force -Value 'Admin' New-Variable -Name 'LOG_SOURCE' -Scope 'Script' -Force -Value 'HealthService' [bool] $eventLogFound = $false try { $evtLog = Get-EventLog -LogName $script:LOG_NAME -Source $script:LOG_SOURCE -ErrorAction SilentlyContinue if ($null -ne $evtLog) { $eventLogFound = $true } } catch { #get-eventlog throws even on erroraction silentlycontinue } try { if ($eventLogFound -eq $false) { New-EventLog -LogName $script:LOG_NAME -Source $script:LOG_SOURCE -ErrorAction SilentlyContinue } } catch { #failure to create event log is non-fatal } } function IsSdnFcClusterServiceRole { <# .SYNOPSIS Checks if the provided service role is an SDN cluster service role .PARAMETER ServiceName The name of the service role to check #> param([string] $ServiceName) # Define the list of valid service roles $validServiceRoles = @( "ApiService", "ControllerService", "FirewallService", "FnmService", "GatewayManager", "ServiceInsertion", "VSwitchService" ) # Check if the provided service role name is in the list return $validServiceRoles -contains $ServiceName } function IsSdnService { <# .SYNOPSIS Checks if the provided service name is an SDN agent service .PARAMETER serviceName The name of the service to check #> param([string] $serviceName) return $serviceName -in @( "NCHostAgent", "SlbHostAgent") } function IsCurrentNodeClusterOwner { <# .SYNOPSIS Checks if the current node is the owner of the cluster .NOTES This function is used to determine if the current node is the owner of the cluster. This is used to determine if the current node is the primary node in a cluster. #> $activeNode = Get-ClusterResource -ErrorAction Ignore | Where-Object { $_.OwnerGroup -eq "Cluster Group" -and $_.ResourceType -eq "IP Address" -and $_.Name -eq "Cluster IP Address" } if ( $null -eq $activeNode ) { Write-Verbose "Active $($activeNode.OwnerNode)" # todo : generate a fault on failing to generate a fault (or switch to different algorithm for picking the primary node) return $false } return ($activeNode.OwnerNode -eq $env:COMPUTERNAME) } function GetFaultFromConfigurationState { <# .SYNOPSIS Generates a fault from the configuration state .PARAMETER resources The resources to generate the fault from #> param( [object[]] $resources ) $healthFaults = @() # successful faults are just a stub holder for the resource # these are not created, but used for clearing out any older unhealthy states # these have KeyFaultingObjectType set to string.empty $successFaults = @() foreach ($resource in $resources) { ########################################################################################## ## ServiceState Fault Template (ServerResource) ########################################################################################## # $KeyFaultingObjectDescription (SDN ID) : [ResourceRef] # $KeyFaultingObjectID (ARC ID) : [ResourceMetadataID (if available) else ResourceRef] # $KeyFaultingObjectType (CODE) : "ConfgiStateCode" (if 2 more errors are found with same other properties will be concat) # $FaultingObjectLocation (SOURCE) : "Source (if keys of 2 errors collide they will be concatanated)" # $FaultDescription (MESSAGE) : "ConfigStateMessage (2 or more if errors collide)." # $FaultActionRemediation (ACTION) : "See <href> for more information on how to resolve this issue." # * Config state faults issued only from the primary Node ########################################################################################## if ($null -ne $resource.Properties.ConfigurationState -and $null -ne $resource.Properties.ConfigurationState.DetailedInfo -and ` $resource.Properties.ConfigurationState.DetailedInfo.Count -gt 0) { foreach ($detailedInfo in $resource.Properties.ConfigurationState.DetailedInfo) { # supression check for some of the known configuration states if (IsConfigurationStateSkipped -Source $detailedInfo.Source -Message $detailedInfo.Message -Code $detailedInfo.Code) { continue } # handle success cases if ($detailedInfo.Code -eq "Success") { $successFault = [SdnFaultInfo]::new() $successFault.KeyFaultingObjectDescription = $resource.ResourceRef $successFault.KeyFaultingObjectID = $resource.ResourceRef $successFault.KeyFaultingObjectType = [string]::Empty $successFault.FaultingObjectLocation = [string]::Empty $successFault.FaultDescription = [string]::Empty $successFaults += $successFault } else { # find any existing overlapping fault $existingFault = $healthFaults | Where-Object { $_.KeyFaultingObjectDescription -eq $resource.ResourceRef -and ` $_.KeyFaultingObjectType -eq $detailedInfo.Code } if ($null -ne $existingFault) { $existingFault.FaultDescription += ("; " + $detailedInfo.Message) $existingFault.FaultingObjectLocation += ("; " + $detailedInfo.Source) } else { $healthFault = [SdnFaultInfo]::new() $healthFault.KeyFaultingObjectDescription = $resource.ResourceRef $healthFault.KeyFaultingObjectType = $detailedInfo.Code $healthFault.FaultingObjectLocation = $detailedInfo.Source $healthFault.FaultDescription += $detailedInfo.Message # add resource metadata if available if ($null -ne $resource.Properties.ResourceMetadata) { $healthFault.KeyFaultingObjectID = $resource.Properties.ResourceMetadata } else { $healthFault.KeyFaultingObjectID = $resource.ResourceRef } } $healthFaults += $healthFault } } } else { # if configuration state is not available, we will clear out any existing faults if ($healthFaults.Count -eq 0) { $successFault = [SdnFaultInfo]::new() $successFault.KeyFaultingObjectDescription = $resource.ResourceRef $successFault.KeyFaultingObjectType = [string]::Empty $successFault.FaultingObjectLocation = [string]::Empty $successFault.FaultDescription = [string]::Empty $successFault.KeyFaultingObjectID = $resource.ResourceRef $successFaults += $successFault } } } foreach ($fault in $healthFaults) { LogWmiHealthFault -fault $fault } @($successFaults, $healthFaults) } function IsConfigurationStateSkipped { <# .SYNOPSIS Checks if the configuration state should be skipped .PARAMETER Source The source of the configuration state .PARAMETER Message The message of the configuration state .PARAMETER Code The code of the configuration state #> param( [string] $Source, [string] $Message, [string] $Code ) if ($Source -eq "SoftwareLoadbalancerManager") { if ($Code -eq "HostNotConnectedToController") { return $true } } $false } ########################## #### ARG COMPLETERS ###### ########################## $argScriptBlock = @{ Role = { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) $result = (Get-SdnFabricInfrastructureResult) if ([string]::IsNullOrEmpty($wordToComplete)) { return ($result.Role | Sort-Object -Unique) } return $result.Role | Where-Object { $_ -like "*$wordToComplete*" } | Sort-Object } Name = { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) $result = (Get-SdnFabricInfrastructureResult).RoleTest.HealthTest if ([string]::IsNullOrEmpty($wordToComplete)) { return ($result.Name | Sort-Object -Unique) } return $result | Where-Object { $_.Name -like "*$wordToComplete*" } | Sort-Object } } Register-ArgumentCompleter -CommandName 'Get-SdnFabricInfrastructureResult' -ParameterName 'Role' -ScriptBlock $argScriptBlock.Role Register-ArgumentCompleter -CommandName 'Get-SdnFabricInfrastructureResult' -ParameterName 'Name' -ScriptBlock $argScriptBlock.Name ########################## ####### FUNCTIONS ######## ########################## function New-SdnHealthTest { param ( [Parameter(Mandatory = $false)] [System.String]$Name = (Get-PSCallStack)[0].Command ) $object = [PSCustomObject]@{ Name = $Name Result = 'PASS' # default to PASS. Allowed values are PASS, WARN, FAIL OccurrenceTime = [System.DateTime]::UtcNow Properties = @() Remediation = @() HealthFault = [PSCustomObject]@() } return $object } function New-SdnRoleHealthReport { param ( [Parameter(Mandatory = $true)] [System.String]$Role ) $object = [PSCustomObject]@{ Role = $Role ComputerName = $env:COMPUTERNAME Result = 'PASS' # default to PASS. Allowed values are PASS, WARN, FAIL OccurrenceTime = [System.DateTime]::UtcNow HealthTest = @() # array of New-SdnHealthTest objects } return $object } function New-SdnFabricHealthReport { param ( [Parameter(Mandatory = $true)] [System.String]$Role ) $object = [PSCustomObject]@{ OccurrenceTime = [System.DateTime]::UtcNow Role = $Role Result = 'PASS' # default to PASS. Allowed values are PASS, WARN, FAIL RoleTest = @() # array of New-SdnRoleHealthReport objects } return $object } function Get-HealthData { param ( [Parameter(Mandatory = $true)] [System.String]$Property, [Parameter(Mandatory = $true)] [System.String]$Id ) $results = $script:SdnDiagnostics_Health.Config[$Property] return ($results[$Id]) } function Write-HealthValidationInfo { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [String]$ComputerName, [Parameter(Mandatory = $true)] [String]$Name, [Parameter(Mandatory = $false)] [String[]]$Remediation ) $details = Get-HealthData -Property 'HealthValidations' -Id $Name $outputString += "`r`n`r`n" $outputString += "--------------------------`r`n" $outputString += "[$ComputerName] $Name" $outputString += "`r`n`r`n" $outputString += "Description:`t$($details.Description)`r`n" $outputString += "Impact:`t`t`t$($details.Impact)`r`n" if (-NOT [string]::IsNullOrEmpty($Remediation)) { $outputString += "Remediation:`r`n`t - $($Remediation -join "`r`n`t - ")`r`n" } if (-NOT [string]::IsNullOrEmpty($details.PublicDocUrl)) { $outputString += "`r`n" $outputString += "Additional information can be found at $($details.PublicDocUrl).`r`n" } $outputString += "`r`n--------------------------`r`n" $outputString | Write-Host -ForegroundColor Yellow } function Debug-SdnFabricInfrastructure { <# .SYNOPSIS Executes a series of fabric validation tests to validate the state and health of the underlying components within the SDN fabric. .PARAMETER NetworkController Specifies the name or IP address of the network controller node on which this cmdlet operates. The parameter is optional if running on network controller node. .PARAMETER ComputerName Type the NetBIOS name, an IP address, or a fully qualified domain name of one or more remote computers. .PARAMETER Role The specific SDN role(s) to perform tests and validations for. If ommitted, defaults to all roles. .PARAMETER Credential Specifies a user account that has permission to perform this action. The default is the current user. .PARAMETER NcRestCertificate Specifies the client certificate that is used for a secure web request to Network Controller REST API. Enter a variable that contains a certificate or a command or expression that gets the certificate. .PARAMETER NcRestCredential Specifies a user account that has permission to perform this action against the Network Controller REST API. The default is the current user. .EXAMPLE PS> Debug-SdnFabricInfrastructure .EXAMPLE PS> Debug-SdnFabricInfrastructure -NetworkController 'NC01' -Credential (Get-Credential) -NcRestCredential (Get-Credential) #> [CmdletBinding(DefaultParameterSetName = 'Role')] param ( [Parameter(Mandatory = $false, ParameterSetName = 'Role')] [Parameter(Mandatory = $false, ParameterSetName = 'ComputerName')] [System.String]$NetworkController = $env:COMPUTERNAME, [Parameter(Mandatory = $false, ParameterSetName = 'Role')] [ValidateSet('Gateway', 'NetworkController', 'Server', 'LoadBalancerMux')] [String[]]$Role = ('Gateway', 'LoadBalancerMux', 'NetworkController', 'Server'), [Parameter(Mandatory = $true, ParameterSetName = 'ComputerName')] [System.String[]]$ComputerName, [Parameter(Mandatory = $false, ParameterSetName = 'Role')] [Parameter(Mandatory = $false, ParameterSetName = 'ComputerName')] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential = [System.Management.Automation.PSCredential]::Empty, [Parameter(Mandatory = $false, ParameterSetName = 'Role')] [Parameter(Mandatory = $false, ParameterSetName = 'ComputerName')] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $NcRestCredential = [System.Management.Automation.PSCredential]::Empty, [Parameter(Mandatory = $false, ParameterSetName = 'Role')] [Parameter(Mandatory = $false, ParameterSetName = 'ComputerName')] [X509Certificate]$NcRestCertificate ) $script:SdnDiagnostics_Health.Cache = $null $aggregateHealthReport = @() if (Test-ComputerNameIsLocal -ComputerName $NetworkController) { Confirm-IsNetworkController } if ($PSBoundParameters.ContainsKey('NcRestCertificate')) { $restCredParam = @{ NcRestCertificate = $NcRestCertificate } } else { $restCredParam = @{ NcRestCredential = $NcRestCredential } } $environmentInfo = Get-SdnInfrastructureInfo -NetworkController $NetworkController -Credential $Credential @restCredParam if ($null -eq $environmentInfo) { throw New-Object System.NullReferenceException("Unable to retrieve environment details") } try { # if we opted to specify the ComputerName rather than Role, we need to determine which role # the computer names are associated with if ($PSCmdlet.ParameterSetName -ieq 'ComputerName') { $Role = @() $ComputerName | ForEach-Object { $computerRole = $_ | Get-SdnRole -EnvironmentInfo $environmentInfo if ($computerRole) { $Role += $computerRole } } } $Role = $Role | Sort-Object -Unique foreach ($object in $Role) { "Processing tests for {0} role" -f $object.ToString() | Trace-Output -Level:Verbose $config = Get-SdnModuleConfiguration -Role $object.ToString() $roleHealthReport = New-SdnFabricHealthReport -Role $object.ToString() $sdnFabricDetails = [PSCustomObject]@{ ComputerName = $null NcUrl = $environmentInfo.NcUrl Role = $config EnvironmentInfo = $environmentInfo } # check to see if we were provided a specific computer(s) to test against # otherwise we will want to pick up the node name(s) from the environment info if ($ComputerName) { $sdnFabricDetails.ComputerName = $ComputerName } else { # in scenarios where there are not mux(es) or gateway(s) then we need to gracefully handle this # and move to the next role for processing if ($null -ieq $environmentInfo[$object.ToString()]) { "Unable to locate fabric nodes for {0}. Skipping health tests." -f $object.ToString() | Trace-Output -Level:Warning continue } $sdnFabricDetails.ComputerName = $environmentInfo[$object.ToString()] } $restApiParams = @{ NcUri = $sdnFabricDetails.NcUrl } $restApiParams += $restCredParam # before proceeding with tests, ensure that the computer objects we are testing against are running the latest version of SdnDiagnostics Install-SdnDiagnostics -ComputerName $sdnFabricDetails.ComputerName -Credential $Credential $params = @{ ComputerName = $sdnFabricDetails.ComputerName Credential = $Credential ScriptBlock = $null ArgumentList = @($restApiParams) } switch ($object) { 'Gateway' { $params.ScriptBlock = { param($boundParams) Debug-SdnGateway @boundParams } } 'LoadBalancerMux' { $params.ScriptBlock = { param($boundParams) Debug-SdnLoadBalancerMux @boundParams } } 'NetworkController' { $params.ScriptBlock = { param($boundParams) Debug-SdnNetworkController @boundParams } } 'Server' { $params.ScriptBlock = { param($boundParams) Debug-SdnServer @boundParams } } } $healthReport = Invoke-SdnCommand @params # evaluate the results of the tests and determine if any completed with Warning or FAIL # if so, we will want to set the Result of the report to reflect this foreach ($test in $healthReport) { if ($test.Result -ieq 'WARN') { $roleHealthReport.Result = 'WARN' } if ($test.Result -ieq 'FAIL') { $roleHealthReport.Result = 'FAIL' break } } $roleHealthReport.RoleTest += $healthReport $aggregateHealthReport += $roleHealthReport } } catch { $_ | Trace-Exception $_ | Write-Error } finally { if ($aggregateHealthReport) { # enumerate all the roles that were tested so we can determine if any completed with Warning or FAIL $aggregateHealthReport | ForEach-Object { if ($_.Result -ine 'PASS') { # enumerate all the individual role tests performed so we can determine if any completed that are not PASS $_.RoleTest | ForEach-Object { $c = $_.ComputerName $_.HealthTest | ForEach-Object { # enum only the health tests that failed if ($_.Result -ine 'PASS') { # add the remediation steps to an array list so we can pass it to the Write-HealthValidationInfo function # otherwise if we pass it directly, it will be treated as a single string $remediationList = [System.Collections.ArrayList]::new() $_.Remediation | ForEach-Object { [void]$remediationList.Add($_) } Write-HealthValidationInfo -ComputerName $c -Name $_.Name -Remediation $remediationList } } } } } # save the aggregate health report to cache so we can use it for further analysis $script:SdnDiagnostics_Health.Cache = $aggregateHealthReport } } if ($script:SdnDiagnostics_Health.Cache) { "Results for fabric health have been saved to cache for further analysis. Use 'Get-SdnFabricInfrastructureResult' to examine the results." | Trace-Output return $script:SdnDiagnostics_Health.Cache } } function GetLogLocation { <# .SYNOPSIS Gets the log location file path for SDN Health, returns null if none is set #> $RegistryPath = "HKLM:\SOFTWARE\Microsoft\SdnHealth" $logPath = Get-ItemProperty -Path $RegistryPath -Name LogPath -ErrorAction SilentlyContinue if ($null -ne $logPath) { return $logPath.LogPath } else { return $null } } function SetLogLocation { <# .SYNOPSIS Sets the location of the log path for the SDN diagnostics module .PARAMETER logPath The path to the log file #> param( [string] $logPath ) $RegistryPath = "HKLM:\SOFTWARE\Microsoft\SdnHealth" if (-not (Test-Path $RegistryPath)) { New-Item -Path $RegistryPath -Force | Out-Null } if ([string]::IsNullOrEmpty($logPath)) { Remove-ItemProperty -Path $RegistryPath -Name logPath -ErrorAction SilentlyContinue } else { New-ItemProperty -Path $RegistryPath -Name logPath -Value $logPath -Force | Out-Null } } function Start-SdnHealthFault { <# .SYNOPSIS Executes a series of fabric validation tests to validate the state and health of the underlying components within the SDN fabric. #> [CmdletBinding()] param ( [Parameter(Mandatory = $false)] [bool] $Poll = $false, [Parameter(Mandatory = $false)] [int] $PollIntervalSeconds = 30 ) Write-Verbose "Starting SDN Health Faults" [bool] $transcriptStarted = $false try { # todo : change logpath $transcriptFile = Join-Path -Path $Env:TEMP -ChildPath "SdnDiag.log" Start-Transcript -Path $transcriptFile -Append $transcriptStarted = $true do { # Test encapoverhead settings Test-SdnEncapOverhead # Test all SDN Services $validServiceRoles = @( "ApiService", "ControllerService", "FirewallService", "FnmService", "GatewayManager", "ServiceInsertion", "VSwitchService" ) Test-SdnClusterServiceState -ServiceName $validServiceRoles # Test all agent services $agentServices = @( 'NcHostAgent', 'SlbHostAgent' ) Test-SdnServiceState -ServiceName $agentServices # Test certificate related faults Test-SdnNonSelfSignedCertificateInTrustedRootStore # Test tenant configuration states Test-SdnConfigurationState if ($Poll) { Start-Sleep -Seconds $PollIntervalSeconds } } until($Poll -eq $false); } catch { $_ | Write-Error } finally { if ($transcriptStarted) { Stop-Transcript } } } function GetSdnResourceFromNc { <# .SYNOPSIS Wrapper around Get-SdnResource which attempts using different available certificates NOTE: this is specifically for ASZ env because the nc cmdlets do not work there .PARAMETER NcUri The base URI of the Network Controller. (https://<nc rest name>) .PARAMETER Resource The resource to retrieve from the Network Controller. .PARAMETER ApiVersion (optional) The version of the resource to retrieve from the Network Controller. note: if nothing is specified, v1 is queried #> [CmdletBinding()] param ( [Parameter(Mandatory = $false)] [string] $NcUri, [Parameter(Mandatory = $true)] [ValidateSet('Servers', 'NetworkInterfaces', 'VirtualNetworks', 'LogicalNetworks')] [String]$ResourceType, [Parameter(Mandatory = $false)] [String]$ApiVersion = 'v1' ) $certs = @() $certs += $null $resources = $null $NcUri = $NcUri.TrimEnd('/') $sdnRequestParams = @{ NcUri = $NcUri ResourceRef = $ResourceType ApiVersion = $ApiVersion NcRestCertificate = $null } try { $certs += Get-SdnServerCertificate [System.Array]::Reverse($certs) foreach ($cert in $certs) { if ($null -ieq $cert) { $sdnRequestParams = @{ NcUri = $NcUri ResourceRef = $ResourceType ApiVersion = $ApiVersion } } else { $sdnRequestParams = @{ NcUri = $NcUri ResourceRef = $ResourceType ApiVersion = $ApiVersion NcRestCertificate = $cert } Write-Verbose "Retrieving $NcUri with certificate $($cert.Subject) thumbprint $($cert.Thumbprint)" } try { $resources = Get-SdnResource @sdnRequestParams if ($resources) { Write-Verbose "Retrieved $($resources.Count) resources for $ResourceType" return $resources } } catch [System.Net.WebException] { if ( $_.Exception.Response.StatusCode -eq [System.Net.HttpStatusCode]::Unauthorized ) { continue } else { Write-Error $_ break } } catch { Write-Error $_ # dont try other certificates break } } return $null } catch { Write-Error $_ } } function Get-SdnFabricInfrastructureResult { <# .SYNOPSIS Returns the results that have been saved to cache as part of running Debug-SdnFabricInfrastructure. .PARAMETER Role The name of the SDN role that you want to return test results from within the cache. .PARAMETER Name The name of the test results you want to examine. .EXAMPLE PS> Get-SdnFabricInfrastructureResult .EXAMPLE PS> Get-SdnFabricInfrastructureResult -Role Server .EXAMPLE PS> Get-SdnFabricInfrastructureResult -Role Server -Name 'Test-SdnServiceState' #> [CmdletBinding()] param ( [Parameter(Mandatory = $false)] [String]$Role, [Parameter(Mandatory = $false)] [System.String]$Name ) $cacheResults = $script:SdnDiagnostics_Health.Cache if ($PSBoundParameters.ContainsKey('Role')) { if ($cacheResults) { $cacheResults = $cacheResults | Where-Object { $_.Role -eq $Role } } } if ($PSBoundParameters.ContainsKey('Name')) { if ($cacheResults) { $cacheResults = $cacheResults.HealthValidation | Where-Object { $_.Name -eq $Name } } } return $cacheResults } function Debug-SdnNetworkController { [CmdletBinding(DefaultParameterSetName = 'RestCredential')] param ( [Parameter(Mandatory = $true)] [ValidateScript({ if ($_.Scheme -ne "http" -and $_.Scheme -ne "https") { throw New-Object System.FormatException("Parameter is expected to be in http:// or https:// format.") } return $true })] [Uri]$NcUri, [Parameter(Mandatory = $false, ParameterSetName = 'RestCredential')] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $NcRestCredential = [System.Management.Automation.PSCredential]::Empty, [Parameter(Mandatory = $true, ParameterSetName = 'RestCertificate')] [X509Certificate]$NcRestCertificate ) Confirm-IsNetworkController $healthReport = New-SdnRoleHealthReport -Role 'NetworkController' try { # execute tests for network controller, regardless of the cluster type $healthReport.HealthTest += @( Test-SdnNonSelfSignedCertificateInTrustedRootStore ) # execute tests based on the cluster type switch ($Global:SdnDiagnostics.EnvironmentInfo.ClusterConfigType) { 'FailoverCluster' { $healthReport.HealthTest += @( Test-SdnDiagnosticsCleanupTaskEnabled -TaskName 'FcDiagnostics' ) } 'ServiceFabric' { $config_sf = Get-SdnModuleConfiguration -Role 'NetworkController_SF' [string[]]$services_sf = $config_sf.properties.services.Keys $healthReport.HealthTest += @( Test-SdnDiagnosticsCleanupTaskEnabled -TaskName 'SDN Diagnostics Task' Test-SdnServiceState -ServiceName $services_sf Test-SdnServiceFabricApplicationHealth Test-SdnServiceFabricClusterHealth Test-SdnServiceFabricNodeStatus ) } } # enumerate all the tests performed so we can determine if any completed with WARN or FAIL # if any of the tests completed with WARN, we will set the aggregate result to WARN # if any of the tests completed with FAIL, we will set the aggregate result to FAIL and then break out of the foreach loop # we will skip tests with PASS, as that is the default value foreach ($test in $healthReport.HealthTest) { if ($test.Result -eq 'WARN') { $healthReport.Result = $test.Result } elseif ($test.Result -eq 'FAIL') { $healthReport.Result = $test.Result break } } } catch { $_ | Trace-Exception $healthReport.Result = 'FAIL' } return $healthReport } function Debug-SdnServer { [CmdletBinding(DefaultParameterSetName = 'RestCredential')] param ( [Parameter(Mandatory = $true)] [ValidateScript({ if ($_.Scheme -ne "http" -and $_.Scheme -ne "https") { throw New-Object System.FormatException("Parameter is expected to be in http:// or https:// format.") } return $true })] [Uri]$NcUri, [Parameter(Mandatory = $false, ParameterSetName = 'RestCredential')] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $NcRestCredential = [System.Management.Automation.PSCredential]::Empty, [Parameter(Mandatory = $true, ParameterSetName = 'RestCertificate')] [X509Certificate]$NcRestCertificate ) Confirm-IsServer $config = Get-SdnModuleConfiguration -Role 'Server' [string[]]$services = $config.properties.services.Keys $healthReport = New-SdnRoleHealthReport -Role 'Server' $ncRestParams = $PSBoundParameters $serverResource = Get-SdnResource @ncRestParams -Resource:Servers try { # execute tests based on the cluster type switch ($Global:SdnDiagnostics.EnvironmentInfo.ClusterConfigType) { 'ServiceFabric' { $healthReport.HealthTest += @( Test-SdnDiagnosticsCleanupTaskEnabled -TaskName 'SDN Diagnostics Task' ) } 'FailoverCluster' { $healthReport.HealthTest += @( Test-SdnDiagnosticsCleanupTaskEnabled -TaskName 'FcDiagnostics' ) } } # these tests are executed locally and have no dependencies on network controller rest API being available $healthReport.HealthTest += @( Test-SdnNonSelfSignedCertificateInTrustedRootStore Test-SdnEncapOverhead Test-VfpDuplicateMacAddress Test-VMNetAdapterDuplicateMacAddress Test-SdnServiceState -ServiceName $services Test-SdnProviderNetwork Test-SdnHostAgentConnectionStateToApiService Test-SdnNetworkControllerApiNameResolution -NcUri $NcUri ) # these tests have dependencies on network controller rest API being available # and will only be executed if we have been able to get the data from the network controller if ($serverResource) { $healthReport.HealthTest += @( Test-ServerHostId -InstanceId $serverResource.InstanceId ) } # enumerate all the tests performed so we can determine if any completed with WARN or FAIL # if any of the tests completed with WARN, we will set the aggregate result to WARN # if any of the tests completed with FAIL, we will set the aggregate result to FAIL and then break out of the foreach loop # we will skip tests with PASS, as that is the default value foreach ($test in $healthReport.HealthTest) { if ($test.Result -eq 'WARN') { $healthReport.Result = $test.Result } elseif ($test.Result -eq 'FAIL') { $healthReport.Result = $test.Result break } } } catch { $_ | Trace-Exception $healthReport.Result = 'FAIL' } return $healthReport } function Debug-SdnLoadBalancerMux { [CmdletBinding(DefaultParameterSetName = 'RestCredential')] param ( [Parameter(Mandatory = $true)] [ValidateScript({ if ($_.Scheme -ne "http" -and $_.Scheme -ne "https") { throw New-Object System.FormatException("Parameter is expected to be in http:// or https:// format.") } return $true })] [Uri]$NcUri, [Parameter(Mandatory = $false, ParameterSetName = 'RestCredential')] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $NcRestCredential = [System.Management.Automation.PSCredential]::Empty, [Parameter(Mandatory = $true, ParameterSetName = 'RestCertificate')] [X509Certificate]$NcRestCertificate ) Confirm-IsLoadBalancerMux $config = Get-SdnModuleConfiguration -Role 'LoadBalancerMux' [string[]]$services = $config.properties.services.Keys $healthReport = New-SdnRoleHealthReport -Role 'LoadBalancerMux' $ncRestParams = $PSBoundParameters try { $muxCertRegKey = Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\SlbMux" -Name MuxCert $virtualServers = Get-SdnResource -Resource VirtualServers @ncRestParams $muxVirtualServer = $virtualServers | Where-Object { $_.properties.connections.managementaddresses -contains $muxCertRegKey.MuxCert } $loadBalancerMux = Get-SdnLoadBalancerMux @ncRestParams | Where-Object { $_.properties.virtualserver.resourceRef -ieq $muxVirtualServer.resourceRef } $peerRouters = $loadBalancerMux.properties.routerConfiguration.peerRouterConfigurations.routerIPAddress $healthReport.HealthTest += @( Test-SdnNonSelfSignedCertificateInTrustedRootStore Test-SdnServiceState -ServiceName $services Test-SdnDiagnosticsCleanupTaskEnabled -TaskName 'SDN Diagnostics Task' Test-SdnMuxConnectionStateToSlbManager Test-SdnNetworkControllerApiNameResolution -NcUri $NcUri ) # these tests have dependencies on network controller rest API being available # and will only be executed if we have been able to get the data from the network controller if ($muxVirtualServer) { $healthReport.HealthTest += @( Test-SdnMuxConnectionStateToRouter -RouterIPAddress $peerRouters ) } # enumerate all the tests performed so we can determine if any completed with WARN or FAIL # if any of the tests completed with WARN, we will set the aggregate result to WARN # if any of the tests completed with FAIL, we will set the aggregate result to FAIL and then break out of the foreach loop # we will skip tests with PASS, as that is the default value foreach ($test in $healthReport.HealthTest) { if ($test.Result -eq 'WARN') { $healthReport.Result = $test.Result } elseif ($test.Result -eq 'FAIL') { $healthReport.Result = $test.Result break } } } catch { $_ | Trace-Exception $healthReport.Result = 'FAIL' } return $healthReport } function Debug-SdnGateway { [CmdletBinding(DefaultParameterSetName = 'RestCredential')] param ( [Parameter(Mandatory = $true, ParameterSetName = 'RestCredential')] [Parameter(Mandatory = $true, ParameterSetName = 'RestCertificate')] [ValidateScript({ if ($_.Scheme -ne "http" -and $_.Scheme -ne "https") { throw New-Object System.FormatException("Parameter is expected to be in http:// or https:// format.") } return $true })] [Uri]$NcUri, [Parameter(Mandatory = $false, ParameterSetName = 'RestCredential')] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $NcRestCredential = [System.Management.Automation.PSCredential]::Empty, [Parameter(Mandatory = $true, ParameterSetName = 'RestCertificate')] [X509Certificate]$NcRestCertificate ) Confirm-IsRasGateway $config = Get-SdnModuleConfiguration -Role 'Gateway' [string[]]$services = $config.properties.services.Keys $healthReport = New-SdnRoleHealthReport -Role 'Gateway' $ncRestParams = @{ NcUri = $NcUri } switch ($PSCmdlet.ParameterSetName) { 'RestCredential' { $ncRestParams += @{ NcRestCredential = $NcRestCredential } } 'RestCertificate' { $ncRestParams += @{ NcRestCertificate = $NcRestCertificate } } } try { $healthReport.HealthTest += @( Test-SdnNonSelfSignedCertificateInTrustedRootStore Test-SdnDiagnosticsCleanupTaskEnabled -TaskName 'SDN Diagnostics Task' Test-SdnServiceState -ServiceName $services ) # enumerate all the tests performed so we can determine if any completed with Warning or FAIL # if any of the tests completed with Warning, we will set the aggregate result to Warning # if any of the tests completed with FAIL, we will set the aggregate result to FAIL and then break out of the foreach loop # we will skip tests with PASS, as that is the default value foreach ($test in $healthReport.HealthTest) { if ($test.Result -eq 'Warning') { $healthReport.Result = $test.Result } elseif ($test.Result -eq 'FAIL') { $healthReport.Result = $test.Result break } } } catch { $_ | Trace-Exception $healthReport.Result = 'FAIL' } return ( $healthReport ) } ################################### #### COMMON HEALTH VALIDATIONS #### ################################### function Test-SdnNonSelfSignedCertificateInTrustedRootStore { <# .SYNOPSIS Validate the Cert in Host's Root CA Store to detect if any Non Root Cert exist #> [CmdletBinding()] param () Write-Verbose "$($PSCmdlet.MyInvocation.MyCommand.Name) invoked" $sdnHealthTest = New-SdnHealthTest $array = @() try { $rootCerts = Get-ChildItem -Path 'Cert:LocalMachine\Root' | Where-Object { $_.Issuer -ne $_.Subject } if ($rootCerts -or $rootCerts.Count -gt 0) { $sdnHealthTest.Result = 'FAIL' $rootCerts | ForEach-Object { $sdnHealthTest.Remediation += "Remove Certificate Thumbprint: $($_.Thumbprint) Subject: $($_.Subject)" $array += [PSCustomObject]@{ Thumbprint = $_.Thumbprint Subject = $_.Subject Issuer = $_.Issuer } } } $sdnHealthTest.Properties = $array ########################################################################################## ## ServiceState Fault Template ########################################################################################## # $KeyFaultingObjectDescription (SDN ID) : [HostName] # $KeyFaultingObjectID (ARC ID) : [HostName] # $KeyFaultingObjectType (CODE) : "NonSelfSignedCertificateInTrustedRootStore" # $FaultingObjectLocation (SOURCE) : "CertificateConfiguration" # $FaultDescription (MESSAGE) : "A non self signed ceritificate was found in trusted root store. This may lead to authentication problems." # $FaultActionRemediation (ACTION) : "Investigate and remove certificate with subject [SubjectNamesCsv]" # * Fault may be issued from each node ########################################################################################## if ($null -ne $array.Subject -and $array.Subject.Count -gt 0) { $subjectNames = [string]::Join(",", $array.Subject) } else { $subjectNames = "" } $healthFault = [SdnFaultInfo]::new() $healthFault.KeyFaultingObjectDescription = $Env:COMPUTERNAME $healthFault.KeyFaultingObjectID = $Env:COMPUTERNAME $healthFault.KeyFaultingObjectType = "NonSelfSignedCertificateInTrustedRootStore" $healthFault.FaultingObjectLocation = "CertificateConfiguration" $healthFault.FaultDescription = "A non self signed ceritificate was found in trusted root store. This may lead to authentication problems." $healthFault.FaultActionRemediation = "Investigate and remove certificate with subject(s) $($subjectNames)." if ( $rootCerts -or $rootCerts.Count -gt 0) { CreateorUpdateFault -Fault $healthFault $convFault = ConvertFaultToPsObject -healthFault $healthFault -faultOpType "Create" $sdnHealthTest.HealthFault += $convFault } else { DeleteFault -Fault $healthFault $convFault = ConvertFaultToPsObject -healthFault $healthFault -faultOpType "Delete" $sdnHealthTest.HealthFault += $convFault } } catch { $_ | Trace-Exception $sdnHealthTest.Result = 'FAIL' } finally { Write-Verbose "$($PSCmdlet.MyInvocation.MyCommand.Name) exiting" } return $sdnHealthTest } function Test-SdnServiceState { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [String[]]$ServiceName ) Write-Verbose "$($PSCmdlet.MyInvocation.MyCommand.Name) invoked for $($ServiceName)" $sdnHealthTest = New-SdnHealthTest $failureDetected = $false $array = @() try { foreach ($service in $ServiceName) { $result = Get-Service -Name $service -ErrorAction Ignore if ($result) { $array += [PSCustomObject]@{ ServiceName = $result.Name Status = $result.Status } if ($result.Status -ine 'Running') { $failureDetected = $true $sdnHealthTest.Remediation += "[$service] Start the service" } } else { $failureDetected = $true } ########################################################################################## ## ServiceState Fault Template ########################################################################################## # $KeyFaultingObjectDescription (SDN ID) : [HostName] # $KeyFaultingObjectID (ARC ID) : [ServiceName] # $KeyFaultingObjectType (CODE) : [ServiceDown] # $FaultingObjectLocation (SOURCE) : [ServiceName] # $FaultDescription (MESSAGE) : Service [ServiceName] is not up. # $FaultActionRemediation (ACTION) : [ServiceName] Start the service # *ServiceState faults will be reported from each node ########################################################################################## $healthFault = [SdnFaultInfo]::new() $healthFault.KeyFaultingObjectDescription = $Env:COMPUTERNAME $healthFault.KeyFaultingObjectID = $service $healthFault.KeyFaultingObjectType = "ServiceDown" $healthFault.FaultingObjectLocation = $service $healthFault.FaultDescription = "Service $($service) is not up." $healthFault.FaultActionRemediation = "Start the cluster service role $($service) from failover cluster manager" if ($result.Status -ine 'Running') { Write-Verbose "Creating fault for $($service) status $($result.Status)" CreateorUpdateFault -Fault $healthFault $convFault = ConvertFaultToPsObject -healthFault $healthFault -faultOpType "Create" $sdnHealthTest.HealthFault += $convFault } else { Write-Verbose "No fault(s) on $($service) clearing any existing ones" DeleteFault -Fault $healthFault $convFault = ConvertFaultToPsObject -healthFault $healthFault -faultOpType "Delete" $sdnHealthTest.HealthFault += $convFault } } if ($failureDetected) { $sdnHealthTest.Result = 'FAIL' } if ($array) { $sdnHealthTest.Properties = $array } } catch { $_ | Trace-Exception $sdnHealthTest.Result = 'FAIL' } finally { Write-Verbose "$($PSCmdlet.MyInvocation.MyCommand.Name) exiting" } return $sdnHealthTest } function Test-SdnClusterServiceState { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [String[]]$ServiceName ) $isCurrentNodeClusterOwner = IsCurrentNodeClusterOwner if ($isCurrentNodeClusterOwner -eq $false) { Write-Verbose "This node is not the cluster owner. Skipping health tests." return } Write-Verbose "$($PSCmdlet.MyInvocation.MyCommand.Name) invoked" $sdnHealthTest = New-SdnHealthTest $failureDetected = $false $array = @() try { foreach ($service in $ServiceName) { $result = Get-ClusterGroup -Name $service -ErrorAction Ignore if ($result) { $array += [PSCustomObject]@{ ServiceName = $result.Name Status = $result.State } Write-Verbose "$service state $($result.State)" if ($result.State -ine 'Online') { $failureDetected = $true $sdnHealthTest.Remediation += "[$service] Start the service" } ########################################################################################## ## FailoverClusterServiceState Fault Template ########################################################################################## # $KeyFaultingObjectDescription (SDN ID) : [ServiceName] # $KeyFaultingObjectID (ARC ID) : [ServiceName] # $KeyFaultingObjectType (CODE) : ServiceUnavailable # $FaultingObjectLocation (SOURCE) : [ServiceName] # $FaultDescription (MESSAGE) : Service [ServiceName] is not up. # $FaultActionRemediation (ACTION) : [ServiceName] Start the service # *ServiceState faults will be reported only on one (primary) cluster node ########################################################################################## $healthFault = [SdnFaultInfo]::new() $healthFault.KeyFaultingObjectDescription = $service $healthFault.KeyFaultingObjectID = $service $healthFault.KeyFaultingObjectType = "ServiceUnavailable" $healthFault.FaultingObjectLocation = $service $healthFault.FaultDescription = "Service $($service) is $($result.State) on Failover Cluster" $healthFault.FaultActionRemediation = "Start the cluster service role $($service)" if ($result.State -ine 'Online') { Write-Verbose "Creating fault for $($service)" CreateorUpdateFault -Fault $healthFault $convFault = ConvertFaultToPsObject -healthFault $healthFault -faultOpType "Create" $sdnHealthTest.HealthFault += $convFault } else { Write-Verbose "No fault(s) on $($service)" DeleteFault -Fault $healthFault $convFault = ConvertFaultToPsObject -healthFault $healthFault -faultOpType "Delete" $sdnHealthTest.HealthFault += $convFault } } else { $sdnHealthTest.Result = 'FAIL' } } if ($failureDetected) { $sdnHealthTest.Result = 'FAIL' } $sdnHealthTest.Properties = $array } catch { $_ | Trace-Exception $_ | Write-Error } finally { Write-Verbose "$($PSCmdlet.MyInvocation.MyCommand.Name) exiting" } return $sdnHealthTest } function Test-SdnDiagnosticsCleanupTaskEnabled { <# .SYNOPSIS Ensures the scheduled task responsible for etl compression is enabled and running #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateSet('FcDiagnostics', 'SDN Diagnostics Task')] [String]$TaskName ) $sdnHealthTest = New-SdnHealthTest try { # check to see if logging is enabled on the registry key $isLoggingEnabled = Get-ItemPropertyValue -Path "HKLM:\Software\Microsoft\NetworkController\Sdn\Diagnostics\Parameters" -Name 'IsLoggingEnabled' -ErrorAction Ignore # in this scenario, logging is currently disabled so scheduled task will not be available if ($isLoggingEnabled ) { try { $result = Get-ScheduledTask -TaskName $TaskName -ErrorAction Stop if ($result.State -ieq 'Disabled') { $sdnHealthTest.Result = 'FAIL' $sdnHealthTest.Remediation += "Use 'Repair-SdnDiagnosticsScheduledTask -TaskName $TaskName'." } } catch { $_ | Trace-Exception $sdnHealthTest.Result = 'FAIL' } } } catch { $_ | Trace-Exception $sdnHealthTest.Result = 'FAIL' } return $sdnHealthTest } function Test-SdnNetworkControllerApiNameResolution { <# .SYNOPSIS Validates that the Network Controller API is resolvable via DNS #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateScript({ if ($_.Scheme -ne "http" -and $_.Scheme -ne "https") { throw New-Object System.FormatException("Parameter is expected to be in http:// or https:// format.") } return $true })] [Uri]$NcUri ) $sdnHealthTest = New-SdnHealthTest try { # check to see if the Uri is an IP address or a DNS name # if it is a DNS name, we need to ensure that it is resolvable # if it is an IP address, we can skip the DNS resolution check $isIpAddress = [System.Net.IPAddress]::TryParse($NcUri.Host, [ref]$null) if (-NOT $isIpAddress) { $dnsResult = Resolve-DnsName -Name $NcUri.Host -ErrorAction Ignore if ($null -eq $dnsResult) { $sdnHealthTest.Result = 'FAIL' $sdnHealthTest.Remediation += "Ensure that the DNS server(s) are reachable and DNS record exists." } } } catch { $_ | Trace-Exception $sdnHealthTest.Result = 'FAIL' } return $sdnHealthTest } ################################### #### SERVER HEALTH VALIDATIONS #### ################################### function Test-SdnEncapOverhead { <# .SYNOPSIS Validate EncapOverhead configuration on the network adapter #> [CmdletBinding()] param () Confirm-IsServer Write-Verbose "$($PSCmdlet.MyInvocation.MyCommand.Name) invoked" [int]$encapOverheadExpectedValue = 160 [int]$jumboPacketExpectedValue = 1674 # this is default 1514 MTU + 160 encap overhead $sdnHealthTest = New-SdnHealthTest [bool] $misconfigurationFound = $false [string[]] $misconfiguredNics = @() try { # check to see if provider addresses are configured # if not, we know that workloads have not been deployed and we can skip this test # as none of the settings will be configured $providerAddreses = Get-SdnProviderAddress if ($null -ieq $providerAddreses -or $providerAddreses.Count -eq 0) { return $sdnHealthTest } $encapOverheadResults = Get-SdnNetAdapterEncapOverheadConfig if ($null -eq $encapOverheadResults) { # skip generation of fault if we cannot determine status confidently $sdnHealthTest.Result = 'FAIL' } else { $encapOverheadResults | ForEach-Object { # if encapoverhead is not enabled, this is most commonly due to network adapter firmware or driver # recommendations are to update the firmware and driver to the latest version and make sure not using default inbox drivers if ($_.EncapOverheadEnabled -eq $false) { # in this scenario, encapoverhead is disabled and we have the expected jumbo packet value # packets will be allowed to traverse the network without being dropped after adding VXLAN/GRE headers if ($_.JumboPacketValue -ge $jumboPacketExpectedValue) { # will not do anything as configuring the jumbo packet is viable workaround if encapoverhead is not supported on the network adapter # this is a PASS scenario } # in this scenario, encapoverhead is disabled and we do not have the expected jumbo packet value # this will result in a failure on the test as it will result in packets being dropped if we exceed default MTU if ($_.JumboPacketValue -lt $jumboPacketExpectedValue) { $sdnHealthTest.Result = 'FAIL' $sdnHealthTest.Remediation += "[$($_.NetAdapterInterfaceDescription)] Ensure the latest firmware and drivers are installed to support EncapOverhead. Configure JumboPacket to $jumboPacketExpectedValue if EncapOverhead is not supported." $misconfigurationFound = $true $misconfiguredNics += $_.NetAdapterInterfaceDescription } } # in this case, the encapoverhead is enabled but the value is less than the expected value if ($_.EncapOverheadEnabled -and $_.EncapOverheadValue -lt $encapOverheadExpectedValue) { $sdnHealthTest.Result = 'FAIL' $sdnHealthTest.Remediation += "[$($_.NetAdapterInterfaceDescription)] Ensure the latest firmware and drivers are installed to support EncapOverhead. Configure JumboPacket to $jumboPacketExpectedValue if EncapOverhead is not supported." $misconfigurationFound = $true $misconfiguredNics += $_.NetAdapterInterfaceDescription } $FAULTNAME = "InvalidEncapOverheadConfiguration" ########################################################################################## ## EncapOverhead Fault Template ########################################################################################## # $KeyFaultingObjectDescription (SDN ID) : [HostName] # $KeyFaultingObjectID (ARC ID) : [NetworkAdapterIfDesc] # $KeyFaultingObjectType (CODE) : InvalidEncapOverheadConfiguration # $FaultingObjectLocation (SOURCE) : [HostName] # $FaultDescription (MESSAGE) : EncapOverhead is not enabled or configured correctly for <AdapterNames> on host <HostName>. # $FaultActionRemediation (ACTION) : JumboPacket should be enabled & EncapOverhead must be configured to support SDN. Please check NetworkATC configuration for configuring optimal networking configuration. # *EncapOverhead Faults will be reported from each node ########################################################################################## $sdnHealthFault = [SdnFaultInfo]::new() $sdnHealthFault.KeyFaultingObjectDescription = $env:COMPUTERNAME $sdnHealthFault.KeyFaultingObjectID = $_.NetAdapterInterfaceDescription $sdnHealthFault.KeyFaultingObjectType = $FAULTNAME $sdnHealthFault.FaultingObjectLocation = $env:COMPUTERNAME $sdnHealthFault.FaultDescription = "EncapOverhead is not enabled or configured correctly for $($_.NetAdapterInterfaceDescription) on host $env:COMPUTERNAME." $sdnHealthFault.FaultActionRemediation = "JumboPacket should be enabled & EncapOverhead must be configured to support SDN. Please check NetworkATC configuration for configuring optimal networking configuration." if ($misconfigurationFound -eq $true) { CreateorUpdateFault -Fault $sdnHealthFault $sdnHealthTest.HealthFault += ConvertFaultToPsObject -healthFault $sdnHealthFault -faultOpType "Create" } else { Write-Verbose "No fault(s) on EncapOverhead, clearing any existing ones" # clear all existing faults for host($FAULTNAME) # todo: validate multiple hosts reporting the same fault DeleteFaultBy -KeyFaultingObjectDescription $env:COMPUTERNAME -KeyFaultingObjectType $FAULTNAME $sdnHealthTest.HealthFault += ConvertFaultToPsObject -healthFault $sdnHealthFault -faultOpType "Delete" } } } if ($misconfiguredNics) { $sdnHealthTest.Properties = $misconfiguredNics } } catch { $_ | Trace-Exception $sdnHealthTest.Result = 'FAIL' } finally { Write-Verbose "$($PSCmdlet.MyInvocation.MyCommand.Name) exiting" } return $sdnHealthTest } function Test-ServerHostId { <# .SYNOPSIS Queries the NCHostAgent HostID registry key value ensure the HostID matches known InstanceID #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string[]]$InstanceId ) Confirm-IsServer $sdnHealthTest = New-SdnHealthTest $regkeyPath = 'HKLM:\SYSTEM\CurrentControlSet\Services\NcHostAgent\Parameters' try { $regHostId = Get-ItemProperty -Path $regkeyPath -Name 'HostId' -ErrorAction Ignore if ($null -ieq $regHostId) { $sdnHealthTest.Result = 'FAIL' } else { if ($regHostId.HostId -inotin $InstanceId) { $sdnHealthTest.Result = 'FAIL' $sdnHealthTest.Remediation += "Update the HostId registry under $regkeyPath to match the correct InstanceId from the NC Servers API." $sdnHealthTest.Properties = [PSCustomObject]@{ HostID = $regHostId } } } } catch { $_ | Trace-Exception $sdnHealthTest.Result = 'FAIL' } return $sdnHealthTest } function Test-VfpDuplicateMacAddress { [CmdletBinding()] param () Confirm-IsServer $sdnHealthTest = New-SdnHealthTest try { $vfpPorts = Get-SdnVfpVmSwitchPort $duplicateObjects = $vfpPorts | Where-Object { $_.MACaddress -ne '00-00-00-00-00-00' -and $null -ne $_.MacAddress } | Group-Object -Property MacAddress | Where-Object { $_.Count -ge 2 } if ($duplicateObjects) { $sdnHealthTest.Result = 'FAIL' $duplicateObjects | ForEach-Object { $sdnHealthTest.Remediation += "[$($_.Name)] Resolve the duplicate MAC address issue with VFP." } } $sdnHealthTest.Properties = [PSCustomObject]@{ DuplicateVfpPorts = $duplicateObjects.Group VfpPorts = $vfpPorts } } catch { $_ | Trace-Exception $sdnHealthTest.Result = 'FAIL' } return $sdnHealthTest } function Test-VMNetAdapterDuplicateMacAddress { [CmdletBinding()] param () Confirm-IsServer $sdnHealthTest = New-SdnHealthTest try { $vmNetAdapters = Get-SdnVMNetworkAdapter $duplicateObjects = $vmNetAdapters | Group-Object -Property MacAddress | Where-Object { $_.Count -ge 2 } if ($duplicateObjects) { $sdnHealthTest.Result = 'FAIL' $duplicateObjects | ForEach-Object { $sdnHealthTest.Remediation += "[$($_.Name)] Resolve the duplicate MAC address issue with VMNetworkAdapters." } } $sdnHealthTest.Properties = [PSCustomObject]@{ DuplicateVMNetworkAdapters = $duplicateObjects.Group VMNetworkAdapters = $vmNetAdapters } } catch { $_ | Trace-Exception $sdnHealthTest.Result = 'FAIL' } return $sdnHealthTest } function Test-SdnProviderNetwork { <# .SYNOPSIS Validate the health of the provider network by pinging the provider addresses. #> [CmdletBinding()] param () Confirm-IsServer $sdnHealthTest = New-SdnHealthTest try { $addressMapping = Get-SdnOvsdbAddressMapping if (-NOT ($null -eq $addressMapping -or $addressMapping.Count -eq 0)) { $providerAddreses = $addressMapping.ProviderAddress | Sort-Object -Unique $connectivityResults = Test-SdnProviderAddressConnectivity -ProviderAddress $providerAddreses foreach ($destination in $connectivityResults) { $failureDetected = $false $sourceIPAddress = $destination.SourceAddress[0] $destinationIPAddress = $destination.DestinationAddress[0] $jumboPacketResult = $destination | Where-Object { $_.BufferSize -gt 1472 } $standardPacketResult = $destination | Where-Object { $_.BufferSize -le 1472 } if ($destination.Status -ine 'Success') { $remediationMsg = $null $failureDetected = $true # if both jumbo and standard icmp tests fails, indicates a failure in the physical network if ($jumboPacketResult.Status -ieq 'Failure' -and $standardPacketResult.Status -ieq 'Failure') { $remediationMsg = "Unable to ping Provider Addresses. Ensure ICMP enabled on $sourceIPAddress and $destinationIPAddress. If issue persists, investigate physical network." $sdnHealthTest.Remediation += $remediationMsg } # if standard MTU was success but jumbo MTU was failure, indication that jumbo packets or encap overhead has not been setup and configured # either on the physical nic or within the physical switches between the provider addresses if ($jumboPacketResult.Status -ieq 'Failure' -and $standardPacketResult.Status -ieq 'Success') { $remediationMsg = "Ensure the physical network between $sourceIPAddress and $destinationIPAddress are configured to support VXLAN or NVGRE encapsulated packets with minimum MTU of 1660." $sdnHealthTest.Remediation += $remediationMsg } } } } if ($failureDetected) { $sdnHealthTest.Result = 'FAIL' } if ($connectivityResults) { $sdnHealthTest.Properties = [PSCustomObject]@{ PingResult = $connectivityResults } } } catch { $_ | Trace-Exception $sdnHealthTest.Result = 'FAIL' } return $sdnHealthTest } function Test-SdnHostAgentConnectionStateToApiService { <# SYNOPSIS Validate the health of the Network Controller Host Agent connection to the Network Controller API Service. #> [CmdletBinding()] param() Confirm-IsServer $sdnHealthTest = New-SdnHealthTest try { $tcpConnection = Get-NetTCPConnection -RemotePort 6640 -ErrorAction Ignore if ($null -eq $tcpConnection -or $tcpConnection.State -ine 'Established') { $sdnHealthTest.Result = 'FAIL' } if ($tcpConnection) { if ($tcpConnection.ConnectionState -ine 'Connected') { $serviceState = Get-Service -Name NCHostAgent -ErrorAction Stop if ($serviceState.Status -ine 'Running') { $sdnHealthTest.Result = 'WARN' $sdnHealthTest.Remediation += "Ensure the NCHostAgent service is running." } else { $sdnHealthTest.Result = 'FAIL' $sdnHealthTest.Remediation += "Ensure that Network Controller ApiService is healthy and operational. Investigate and fix TCP / TLS connectivity issues." } } } } catch { $_ | Trace-Exception $sdnHealthTest.Result = 'FAIL' } return $sdnHealthTest } ################################### ###### NC HEALTH VALIDATIONS ###### ################################### function Test-SdnServiceFabricApplicationHealth { <# .SYNOPSIS Validate the health of the Network Controller application within Service Fabric. #> [CmdletBinding()] param () $sdnHealthTest = New-SdnHealthTest try { $applicationHealth = Get-SdnServiceFabricApplicationHealth -ErrorAction Stop if ($applicationHealth.AggregatedHealthState -ine 'Ok') { $sdnHealthTest.Result = 'FAIL' $sdnHealthTest.Remediation += "Examine the Service Fabric Application Health for Network Controller to determine why the health is not OK." } } catch { $_ | Trace-Exception $sdnHealthTest.Result = 'FAIL' } return $sdnHealthTest } function Test-SdnServiceFabricClusterHealth { <# .SYNOPSIS Validate the health of the Network Controller cluster within Service Fabric. #> [CmdletBinding()] param () $sdnHealthTest = New-SdnHealthTest try { $clusterHealth = Get-SdnServiceFabricClusterHealth -ErrorAction Stop if ($clusterHealth.AggregatedHealthState -ine 'Ok') { $sdnHealthTest.Result = 'FAIL' $sdnHealthTest.Remediation += "Examine the Service Fabric Cluster Health for Network Controller to determine why the health is not OK." } } catch { $_ | Trace-Exception $sdnHealthTest.Result = 'FAIL' } return $sdnHealthTest } function Test-SdnServiceFabricNodeStatus { <# .SYNOPSIS Validate the health of the Network Controller nodes within Service Fabric. #> [CmdletBinding()] param () $sdnHealthTest = New-SdnHealthTest try { $ncNodes = Get-SdnServiceFabricNode -NodeName $env:COMPUTERNAME -ErrorAction Stop if ($null -eq $ncNodes) { $sdnHealthTest.Result = 'FAIL' } else { if ($ncNodes.NodeStatus -ine 'Up') { $sdnHealthTest.Result = 'FAIL' $sdnHealthTest.Remediation = 'Examine the Service Fabric Nodes for Network Controller to determine why the node is not Up.' } } } catch { $_ | Trace-Exception $sdnHealthTest.Result = 'FAIL' } return $sdnHealthTest } function Test-SdnResourceConfigurationState { <# .SYNOPSIS Validate that the configurationState of the resources. #> [CmdletBinding(DefaultParameterSetName = 'RestCredential')] param ( [Parameter(Mandatory = $true)] [string]$Resource, [Parameter(Mandatory = $true)] [ValidateScript({ if ($_.Scheme -ne "http" -and $_.Scheme -ne "https") { throw New-Object System.FormatException("Parameter is expected to be in http:// or https:// format.") } return $true })] [Uri]$NcUri, [Parameter(Mandatory = $false, ParameterSetName = 'RestCredential')] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $NcRestCredential = [System.Management.Automation.PSCredential]::Empty, [Parameter(Mandatory = $true, ParameterSetName = 'RestCertificate')] [X509Certificate]$NcRestCertificate ) $sdnHealthTest = New-SdnHealthTest $array = @() try { "Validating configuration state of {0}" -f $SdnEnvironmentObject.Role.ResourceName | Trace-Output $sdnResources = Get-SdnResource @PSBoundParameters foreach ($object in $sdnResources) { # if we have a resource that is not in a success state, we will skip validation # as we do not expect configurationState to be accurate if provisioningState is not Success if ($object.properties.provisioningState -ine 'Succeeded') { continue } # examine the configuration state of the resources and display errors to the screen $errorMessages = @() switch ($object.properties.configurationState.Status) { 'Warning' { # if we already have a failure, we will not change the result to warning if ($sdnHealthTest.Result -ne 'FAIL') { $sdnHealthTest.Result = 'WARNING' } $traceLevel = 'Warning' } 'Failure' { $sdnHealthTest.Result = 'FAIL' $traceLevel = 'Error' } 'InProgress' { # if we already have a failure, we will not change the result to warning if ($sdnHealthTest.Result -ne 'FAIL') { $sdnHealthTest.Result = 'WARNING' } $traceLevel = 'Warning' } 'Uninitialized' { # in scenarios where state is redundant, we will not fail the test if ($object.properties.state -ieq 'Redundant') { # do nothing } else { # if we already have a failure, we will not change the result to warning if ($sdnHealthTest.Result -ne 'FAIL') { $sdnHealthTest.Result = 'WARNING' } $traceLevel = 'Warning' } } default { $traceLevel = 'Verbose' } } if ($object.properties.configurationState.detailedInfo) { foreach ($detail in $object.properties.configurationState.detailedInfo) { switch ($detail.code) { 'Success' { # do nothing } default { $errorMessages += $detail.message try { $errorDetails = Get-HealthData -Property 'ConfigurationStateErrorCodes' -Id $detail.code $sdnHealthTest.Remediation += "[{0}] {1}" -f $object.resourceRef, $errorDetails.Action } catch { "Unable to locate remediation actions for {0}" -f $detail.code | Trace-Output -Level:Warning $remediationString = "[{0}] Examine the configurationState property to determine why configuration failed." -f $object.resourceRef $sdnHealthTest.Remediation += $remediationString } } } } # print the overall configuration state to screen, with each of the messages that were captured # as part of the detailedinfo property if ($errorMessages) { $msg = "{0} is reporting configurationState status {1}:`n`t- {2}" -f $object.resourceRef, $object.properties.configurationState.Status, ($errorMessages -join "`n`t- ") } else { $msg = "{0} is reporting configurationState status {1}" -f $object.resourceRef, $object.properties.configurationState.Status } $msg | Trace-Output -Level $traceLevel.ToString() } $details = [PSCustomObject]@{ resourceRef = $object.resourceRef configurationState = $object.properties.configurationState } $array += $details } $sdnHealthTest.Properties = $array } catch { $_ | Trace-Exception $sdnHealthTest.Result = 'FAIL' } return $sdnHealthTest } function Test-SdnResourceProvisioningState { <# .SYNOPSIS Validate that the provisioningState of the resources. #> [CmdletBinding(DefaultParameterSetName = 'RestCredential')] param ( [Parameter(Mandatory = $true)] [string]$Resource, [Parameter(Mandatory = $true)] [ValidateScript({ if ($_.Scheme -ne "http" -and $_.Scheme -ne "https") { throw New-Object System.FormatException("Parameter is expected to be in http:// or https:// format.") } return $true })] [Uri]$NcUri, [Parameter(Mandatory = $false, ParameterSetName = 'RestCredential')] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $NcRestCredential = [System.Management.Automation.PSCredential]::Empty, [Parameter(Mandatory = $true, ParameterSetName = 'RestCertificate')] [X509Certificate]$NcRestCertificate ) $sdnHealthTest = New-SdnHealthTest $array = @() try { "Validating provisioning state of {0}" -f $Resource | Trace-Output $sdnResources = Get-SdnResource @PSBoundParameters foreach ($object in $sdnResources) { # examine the provisioning state of the resources and display errors to the screen $msg = "{0} is reporting provisioning state: {1}" -f $object.resourceRef, $object.properties.provisioningState switch ($object.properties.provisioningState) { 'Failed' { $sdnHealthTest.Result = 'FAIL' $msg | Trace-Output -Level:Error $sdnHealthTest.Remediation += "[$($object.resourceRef)] Examine the Network Controller logs to determine why provisioning is $($object.properties.provisioningState)." } 'Updating' { # if we already have a failure, we will not change the result to warning if ($sdnHealthTest.Result -ne 'FAIL') { $sdnHealthTest.Result = 'WARNING' } # since we do not know what operations happened prior to this, we will log a warning # and ask the user to monitor the provisioningState $msg | Trace-Output -Level:Warning $sdnHealthTest.Remediation += "[$($object.resourceRef)] Is reporting $($object.properties.provisioningState). Monitor to ensure that provisioningState moves to Succeeded." } default { # this should cover scenario where provisioningState is 'Deleting' or Succeeded $msg | Trace-Output -Level:Verbose } } $details = [PSCustomObject]@{ resourceRef = $object.resourceRef provisioningState = $object.properties.provisioningState } $array += $details } $sdnHealthTest.Properties = $array return $sdnHealthTest } catch { $_ | Trace-Exception $sdnHealthTest.Result = 'FAIL' } return $sdnHealthTest } function Test-SdnConfigurationState { [CmdletBinding()] param ( [Parameter(Mandatory = $false, ParameterSetName = 'RestCredential')] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $NcRestCredential = [System.Management.Automation.PSCredential]::Empty, [Parameter(Mandatory = $true, ParameterSetName = 'RestCertificate')] [X509Certificate]$NcRestCertificate ) Write-Verbose "$($PSCmdlet.MyInvocation.MyCommand.Name) invoked" try { $isCurrentNodeClusterOwner = IsCurrentNodeClusterOwner if ($false -eq $isCurrentNodeClusterOwner) { Write-Verbose "This node is not the cluster owner. Skipping health tests." return } # servers $items = Get-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\NcHostAgent\Parameters\ $NcUri = "https://$($items.PeerCertificateCName)" $configStateHealths = @() # generate faults for servers $servers = GetSdnResourceFromNc -ResourceType 'Servers' -NcUri $NcUri $faultSet = GetFaultFromConfigurationState -resources $servers ShowFaultSet -faultset $faultSet $serverHealthTest = UpdateFaultSet -successFaults $faultSet[0] -FailureFaults $faultSet[1] $serverHealthTest.Name = "servers" $configStateHealths += $serverHealthTest # generate faults for vnics $vnics = GetSdnResourceFromNc -Resource 'NetworkInterfaces' -NcUri $NcUri $faultSet = GetFaultFromConfigurationState -resources $vnics ShowFaultSet -faultset $faultSet $vnicHealthTest = UpdateFaultSet -successFaults $faultSet[0] -FailureFaults $faultSet[1] $vnicHealthTest.Name = "networkinterfaces" $configStateHealths += $vnicHealthTest # generate faults for lnets $vnics = GetSdnResourceFromNc -Resource 'LogicalNetworks' -NcUri $NcUri $faultSet = GetFaultFromConfigurationState -resources $vnics ShowFaultSet -faultset $faultSet $vnicHealthTest = UpdateFaultSet -successFaults $faultSet[0] -FailureFaults $faultSet[1] $vnicHealthTest.Name = "logicalnetworks" $configStateHealths += $vnicHealthTest } catch { $_ | Write-Error } finally { Write-Verbose "$($PSCmdlet.MyInvocation.MyCommand.Name) exiting" } } ################################### ##### MUX HEALTH VALIDATIONS ###### ################################### function Test-SdnMuxConnectionStateToRouter { <# SYNOPSIS Validates the TCP connectivity for BGP endpoint to the routers. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string[]]$RouterIPAddress ) Confirm-IsLoadBalancerMux $sdnHealthTest = New-SdnHealthTest try { foreach ($router in $RouterIPAddress) { $tcpConnection = Get-NetTCPConnection -RemotePort 179 -RemoteAddress $router -ErrorAction Ignore if ($null -eq $tcpConnection -or $tcpConnection.State -ine 'Established') { $sdnHealthTest.Result = 'FAIL' $sdnHealthTest.Remediation += "Examine the TCP connectivity for router $router to determine why TCP connection is not established." } } } catch { $_ | Trace-Exception $sdnHealthTest.Result = 'FAIL' } return $sdnHealthTest } function Test-SdnMuxConnectionStateToSlbManager { <# SYNOPSIS Validates the TCP / TLS connectivity to the SlbManager service. #> [CmdletBinding()] param() Confirm-IsLoadBalancerMux $sdnHealthTest = New-SdnHealthTest try { $tcpConnection = Get-NetTCPConnection -LocalPort 8560 -ErrorAction Ignore if ($null -eq $tcpConnection -or $tcpConnection.State -ine 'Established') { $sdnHealthTest.Result = 'FAIL' $sdnHealthTest.Remediation += "Move SlbManager service primary role to another node. Examine the TCP / TLS connectivity for the SlbManager service." } } catch { $_ | Trace-Exception $sdnHealthTest.Result = 'FAIL' } return $sdnHealthTest } # SIG # Begin signature block # MIIoQgYJKoZIhvcNAQcCoIIoMzCCKC8CAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBefnpVJPym9ZFD # q7+1RfXOkf1sL2ayI/vWKCnjfTiZJKCCDXYwggX0MIID3KADAgECAhMzAAAEBGx0 # Bv9XKydyAAAAAAQEMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p # bmcgUENBIDIwMTEwHhcNMjQwOTEyMjAxMTE0WhcNMjUwOTExMjAxMTE0WjB0MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB # AQC0KDfaY50MDqsEGdlIzDHBd6CqIMRQWW9Af1LHDDTuFjfDsvna0nEuDSYJmNyz # NB10jpbg0lhvkT1AzfX2TLITSXwS8D+mBzGCWMM/wTpciWBV/pbjSazbzoKvRrNo # DV/u9omOM2Eawyo5JJJdNkM2d8qzkQ0bRuRd4HarmGunSouyb9NY7egWN5E5lUc3 # a2AROzAdHdYpObpCOdeAY2P5XqtJkk79aROpzw16wCjdSn8qMzCBzR7rvH2WVkvF # HLIxZQET1yhPb6lRmpgBQNnzidHV2Ocxjc8wNiIDzgbDkmlx54QPfw7RwQi8p1fy # 4byhBrTjv568x8NGv3gwb0RbAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE # AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQU8huhNbETDU+ZWllL4DNMPCijEU4w # RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW # MBQGA1UEBRMNMjMwMDEyKzUwMjkyMzAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci # tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j # b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG # CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu # Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0 # MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAIjmD9IpQVvfB1QehvpC # Ge7QeTQkKQ7j3bmDMjwSqFL4ri6ae9IFTdpywn5smmtSIyKYDn3/nHtaEn0X1NBj # L5oP0BjAy1sqxD+uy35B+V8wv5GrxhMDJP8l2QjLtH/UglSTIhLqyt8bUAqVfyfp # h4COMRvwwjTvChtCnUXXACuCXYHWalOoc0OU2oGN+mPJIJJxaNQc1sjBsMbGIWv3 # cmgSHkCEmrMv7yaidpePt6V+yPMik+eXw3IfZ5eNOiNgL1rZzgSJfTnvUqiaEQ0X # dG1HbkDv9fv6CTq6m4Ty3IzLiwGSXYxRIXTxT4TYs5VxHy2uFjFXWVSL0J2ARTYL # E4Oyl1wXDF1PX4bxg1yDMfKPHcE1Ijic5lx1KdK1SkaEJdto4hd++05J9Bf9TAmi # u6EK6C9Oe5vRadroJCK26uCUI4zIjL/qG7mswW+qT0CW0gnR9JHkXCWNbo8ccMk1 # sJatmRoSAifbgzaYbUz8+lv+IXy5GFuAmLnNbGjacB3IMGpa+lbFgih57/fIhamq # 5VhxgaEmn/UjWyr+cPiAFWuTVIpfsOjbEAww75wURNM1Imp9NJKye1O24EspEHmb # DmqCUcq7NqkOKIG4PVm3hDDED/WQpzJDkvu4FrIbvyTGVU01vKsg4UfcdiZ0fQ+/ # V0hf8yrtq9CkB8iIuk5bBxuPMIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq # hkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x # EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv # bjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 # IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEwOTA5WjB+MQswCQYDVQQG # EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG # A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQg # Q29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC # CgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+laUKq4BjgaBEm6f8MMHt03 # a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc6Whe0t+bU7IKLMOv2akr # rnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4Ddato88tt8zpcoRb0Rrrg # OGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+lD3v++MrWhAfTVYoonpy # 4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nkkDstrjNYxbc+/jLTswM9 # sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6A4aN91/w0FK/jJSHvMAh # dCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmdX4jiJV3TIUs+UsS1Vz8k # A/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL5zmhD+kjSbwYuER8ReTB # w3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zdsGbiwZeBe+3W7UvnSSmn # Eyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3T8HhhUSJxAlMxdSlQy90 # lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS4NaIjAsCAwEAAaOCAe0w # ggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRIbmTlUAXTgqoXNzcitW2o # ynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYD # VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBDuRQFTuHqp8cx0SOJNDBa # BgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2Ny # bC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3JsMF4GCCsG # AQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3dy5taWNyb3NvZnQuY29t # L3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3J0MIGfBgNV # HSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEFBQcCARYzaHR0cDovL3d3 # dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1hcnljcHMuaHRtMEAGCCsG # AQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkAYwB5AF8AcwB0AGEAdABl # AG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn8oalmOBUeRou09h0ZyKb # C5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7v0epo/Np22O/IjWll11l # hJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0bpdS1HXeUOeLpZMlEPXh6 # I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/KmtYSWMfCWluWpiW5IP0 # wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvyCInWH8MyGOLwxS3OW560 # STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBpmLJZiWhub6e3dMNABQam # ASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJihsMdYzaXht/a8/jyFqGa # J+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYbBL7fQccOKO7eZS/sl/ah # XJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbSoqKfenoi+kiVH6v7RyOA # 9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sLgOppO6/8MO0ETI7f33Vt # Y5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtXcVZOSEXAQsmbdlsKgEhr # /Xmfwb1tbWrJUnMTDXpQzTGCGiIwghoeAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw # EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN # aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp # Z25pbmcgUENBIDIwMTECEzMAAAQEbHQG/1crJ3IAAAAABAQwDQYJYIZIAWUDBAIB # BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO # MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIIPDzdas7vxERLfesplV4ZIX # 5vs9M2d7rKj1dPmfIPTPMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A # cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB # BQAEggEAWVMwJshfvKPVQBmKKZxTyaQatsZtDKmv6G1g7vtxDLeF37ErUpC1nOnH # /C3/KUFNE3NCin2YgJiDTRpWCbGJhWkjDHw67cukBinSNM4ZJMmJRoMGYxEuiFBA # +IWUmTXv6qc7uu42Ru/X4DcW5eAcmJ4ErmXO2v/+Sz19rnAiZgllisyCttbc4EA4 # p5LB2Z4CxVrTKiiErLZpyxPnCC8UOZaewfIO9AYOSvA60t/dVG9Pk4eETI6JA64r # BPvPGufHzCxH51S7dtoERRf9zfVHFYjiLwDf/ElGkPNAs4bcaMVoRiIFXQ78fzzv # juXbh/pCykB2UR3yWrIBft23XBNAU6GCF6wwgheoBgorBgEEAYI3AwMBMYIXmDCC # F5QGCSqGSIb3DQEHAqCCF4UwgheBAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFaBgsq # hkiG9w0BCRABBKCCAUkEggFFMIIBQQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl # AwQCAQUABCCapt1FBldY0eDYUsd0jfaGQ83pgkxq5xzdCXSRzwZRIgIGZ5qn08z2 # GBMyMDI1MDEzMDAxNTczNC43NjdaMASAAgH0oIHZpIHWMIHTMQswCQYDVQQGEwJV # UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE # ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJl # bGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVT # Tjo0MDFBLTA1RTAtRDk0NzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAg # U2VydmljZaCCEfowggcoMIIFEKADAgECAhMzAAAB/tCowns0IQsBAAEAAAH+MA0G # CSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9u # MRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRp # b24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMB4XDTI0 # MDcyNTE4MzExOFoXDTI1MTAyMjE4MzExOFowgdMxCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9w # ZXJhdGlvbnMgTGltaXRlZDEnMCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjQwMUEt # MDVFMC1EOTQ3MSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNl # MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvLwhFxWlqA43olsE4PCe # gZ4mSfsH2YTSKEYv8Gn3362Bmaycdf5T3tQxpP3NWm62YHUieIQXw+0u4qlay4AN # 3IonI+47Npi9fo52xdAXMX0pGrc0eqW8RWN3bfzXPKv07O18i2HjDyLuywYyKA9F # mWbePjahf9Mwd8QgygkPtwDrVQGLyOkyM3VTiHKqhGu9BCGVRdHW9lmPMrrUlPWi # YV9LVCB5VYd+AEUtdfqAdqlzVxA53EgxSqhp6JbfEKnTdcfP6T8Mir0HrwTTtV2h # 2yDBtjXbQIaqycKOb633GfRkn216LODBg37P/xwhodXT81ZC2aHN7exEDmmbiWss # jGvFJkli2g6dt01eShOiGmhbonr0qXXcBeqNb6QoF8jX/uDVtY9pvL4j8aEWS49h # KUH0mzsCucIrwUS+x8MuT0uf7VXCFNFbiCUNRTofxJ3B454eGJhL0fwUTRbgyCbp # LgKMKDiCRub65DhaeDvUAAJT93KSCoeFCoklPavbgQyahGZDL/vWAVjX5b8Jzhly # 9gGCdK/qi6i+cxZ0S8x6B2yjPbZfdBVfH/NBp/1Ln7xbeOETAOn7OT9D3UGt0q+K # iWgY42HnLjyhl1bAu5HfgryAO3DCaIdV2tjvkJay2qOnF7Dgj8a60KQT9QgfJfwX # nr3ZKibYMjaUbCNIDnxz2ykCAwEAAaOCAUkwggFFMB0GA1UdDgQWBBRvznuJ9SU2 # g5l/5/b+5CBibbHF3TAfBgNVHSMEGDAWgBSfpxVdAF5iXYP05dJlpxtTNRnpcjBf # BgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3Bz # L2NybC9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcmww # bAYIKwYBBQUHAQEEYDBeMFwGCCsGAQUFBzAChlBodHRwOi8vd3d3Lm1pY3Jvc29m # dC5jb20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0El # MjAyMDEwKDEpLmNydDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUF # BwMIMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQsFAAOCAgEAiT4NUvO2lw+0 # dDMtsBuxmX2o3lVQqnQkuITAGIGCgI+sl7ZqZOTDd8LqxsH4GWCPTztc3tr8AgBv # sYIzWjFwioCjCQODq1oBMWNzEsKzckHxAzYo5Sze7OPkMA3DAxVq4SSR8y+TRC2G # cOd0JReZ1lPlhlPl9XI+z8OgtOPmQnLLiP9qzpTHwFze+sbqSn8cekduMZdLyHJk # 3Niw3AnglU/WTzGsQAdch9SVV4LHifUnmwTf0i07iKtTlNkq3bx1iyWg7N7jGZAB # RWT2mX+YAVHlK27t9n+WtYbn6cOJNX6LsH8xPVBRYAIRVkWsMyEAdoP9dqfaZzwX # GmjuVQ931NhzHjjG+Efw118DXjk3Vq3qUI1re34zMMTRzZZEw82FupF3viXNR3DV # OlS9JH4x5emfINa1uuSac6F4CeJCD1GakfS7D5ayNsaZ2e+sBUh62KVTlhEsQRHZ # RwCTxbix1Y4iJw+PDNLc0Hf19qX2XiX0u2SM9CWTTjsz9SvCjIKSxCZFCNv/zpKI # lsHx7hQNQHSMbKh0/wwn86uiIALEjazUszE0+X6rcObDfU4h/O/0vmbF3BMR+45r # AZMAETJsRDPxHJCo/5XGhWdg/LoJ5XWBrODL44YNrN7FRnHEAAr06sflqZ8eeV3F # uDKdP5h19WUnGWwO1H/ZjUzOoVGiV3gwggdxMIIFWaADAgECAhMzAAAAFcXna54C # m0mZAAAAAAAVMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UE # CBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9z # b2Z0IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZp # Y2F0ZSBBdXRob3JpdHkgMjAxMDAeFw0yMTA5MzAxODIyMjVaFw0zMDA5MzAxODMy # MjVaMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQH # EwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNV # BAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwMIICIjANBgkqhkiG9w0B # AQEFAAOCAg8AMIICCgKCAgEA5OGmTOe0ciELeaLL1yR5vQ7VgtP97pwHB9KpbE51 # yMo1V/YBf2xK4OK9uT4XYDP/XE/HZveVU3Fa4n5KWv64NmeFRiMMtY0Tz3cywBAY # 6GB9alKDRLemjkZrBxTzxXb1hlDcwUTIcVxRMTegCjhuje3XD9gmU3w5YQJ6xKr9 # cmmvHaus9ja+NSZk2pg7uhp7M62AW36MEBydUv626GIl3GoPz130/o5Tz9bshVZN # 7928jaTjkY+yOSxRnOlwaQ3KNi1wjjHINSi947SHJMPgyY9+tVSP3PoFVZhtaDua # Rr3tpK56KTesy+uDRedGbsoy1cCGMFxPLOJiss254o2I5JasAUq7vnGpF1tnYN74 # kpEeHT39IM9zfUGaRnXNxF803RKJ1v2lIH1+/NmeRd+2ci/bfV+AutuqfjbsNkz2 # K26oElHovwUDo9Fzpk03dJQcNIIP8BDyt0cY7afomXw/TNuvXsLz1dhzPUNOwTM5 # TI4CvEJoLhDqhFFG4tG9ahhaYQFzymeiXtcodgLiMxhy16cg8ML6EgrXY28MyTZk # i1ugpoMhXV8wdJGUlNi5UPkLiWHzNgY1GIRH29wb0f2y1BzFa/ZcUlFdEtsluq9Q # BXpsxREdcu+N+VLEhReTwDwV2xo3xwgVGD94q0W29R6HXtqPnhZyacaue7e3Pmri # Lq0CAwEAAaOCAd0wggHZMBIGCSsGAQQBgjcVAQQFAgMBAAEwIwYJKwYBBAGCNxUC # BBYEFCqnUv5kxJq+gpE8RjUpzxD/LwTuMB0GA1UdDgQWBBSfpxVdAF5iXYP05dJl # pxtTNRnpcjBcBgNVHSAEVTBTMFEGDCsGAQQBgjdMg30BATBBMD8GCCsGAQUFBwIB # FjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9y # eS5odG0wEwYDVR0lBAwwCgYIKwYBBQUHAwgwGQYJKwYBBAGCNxQCBAweCgBTAHUA # YgBDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU # 1fZWy4/oolxiaNE9lJBb186aGMQwVgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDovL2Ny # bC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0XzIw # MTAtMDYtMjMuY3JsMFoGCCsGAQUFBwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0cDov # L3d3dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXRfMjAxMC0w # Ni0yMy5jcnQwDQYJKoZIhvcNAQELBQADggIBAJ1VffwqreEsH2cBMSRb4Z5yS/yp # b+pcFLY+TkdkeLEGk5c9MTO1OdfCcTY/2mRsfNB1OW27DzHkwo/7bNGhlBgi7ulm # ZzpTTd2YurYeeNg2LpypglYAA7AFvonoaeC6Ce5732pvvinLbtg/SHUB2RjebYIM # 9W0jVOR4U3UkV7ndn/OOPcbzaN9l9qRWqveVtihVJ9AkvUCgvxm2EhIRXT0n4ECW # OKz3+SmJw7wXsFSFQrP8DJ6LGYnn8AtqgcKBGUIZUnWKNsIdw2FzLixre24/LAl4 # FOmRsqlb30mjdAy87JGA0j3mSj5mO0+7hvoyGtmW9I/2kQH2zsZ0/fZMcm8Qq3Uw # xTSwethQ/gpY3UA8x1RtnWN0SCyxTkctwRQEcb9k+SS+c23Kjgm9swFXSVRk2XPX # fx5bRAGOWhmRaw2fpCjcZxkoJLo4S5pu+yFUa2pFEUep8beuyOiJXk+d0tBMdrVX # VAmxaQFEfnyhYWxz/gq77EFmPWn9y8FBSX5+k77L+DvktxW/tM4+pTFRhLy/AsGC # onsXHRWJjXD+57XQKBqJC4822rpM+Zv/Cuk0+CQ1ZyvgDbjmjJnW4SLq8CdCPSWU # 5nR0W2rRnj7tfqAxM328y+l7vzhwRNGQ8cirOoo6CGJ/2XBjU02N7oJtpQUQwXEG # ahC0HVUzWLOhcGbyoYIDVTCCAj0CAQEwggEBoYHZpIHWMIHTMQswCQYDVQQGEwJV # UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE # ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJl # bGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVT # Tjo0MDFBLTA1RTAtRDk0NzElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAg # U2VydmljZaIjCgEBMAcGBSsOAwIaAxUAhGNHD/a7Q0bQLWVG9JuGxgLRXseggYMw # gYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE # BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD # VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQsF # AAIFAOtFJlMwIhgPMjAyNTAxMjkyMjEyMzVaGA8yMDI1MDEzMDIyMTIzNVowczA5 # BgorBgEEAYRZCgQBMSswKTAKAgUA60UmUwIBADAGAgEAAgEHMAcCAQACAhJaMAoC # BQDrRnfTAgEAMDYGCisGAQQBhFkKBAIxKDAmMAwGCisGAQQBhFkKAwKgCjAIAgEA # AgMHoSChCjAIAgEAAgMBhqAwDQYJKoZIhvcNAQELBQADggEBAEVUfnwLBHflxbMB # K2iYwJ9ALJO/wXQlqJW7fBajtBKvH5A7TjeT2yjkTTx3iUAJ7cBhvPrDjZb6jfvM # XE8zS0BJs1sXhZd50riJEr28G0BpKF98+MCmpDkYsW4rs9UG3NQwCp2idDao/r9S # qgx7qwdSffRVUuhpF7g6HZcEQET1ZsZIaUEc2mJt89ZAb4IJpnlvyMjaUyYbhvmY # vFUp5Twm1QJoYq+0ba7D1yIvxocrdYBnSNxFvn8ErTJczV32TB4jVs03o6z+A2Hr # cs2tnGLyYHgdHgS6GcaiXWLn6O1WU3KOdOznbndd7pnJ8LrbCmf52D32Q/3ti+Bk # +kss/qUxggQNMIIECQIBATCBkzB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2Fz # aGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENv # cnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAx # MAITMwAAAf7QqMJ7NCELAQABAAAB/jANBglghkgBZQMEAgEFAKCCAUowGgYJKoZI # hvcNAQkDMQ0GCyqGSIb3DQEJEAEEMC8GCSqGSIb3DQEJBDEiBCBlO6Kk4OQgutKq # 2yYda02N30JfrMM+RKj08vdXfCH6LDCB+gYLKoZIhvcNAQkQAi8xgeowgecwgeQw # gb0EIBGFzN38U8ifGNH3abaE9apz68Y4bX78jRa2QKy3KHR5MIGYMIGApH4wfDEL # MAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1v # bmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWlj # cm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAH+0KjCezQhCwEAAQAAAf4w # IgQgeWPZgga2qcYWbSJvB40nHo7XV9rP/XeGDP7Fx2wvFFEwDQYJKoZIhvcNAQEL # BQAEggIAOAZu2wwDuPKoAA7CjN9NkcNhKemfs3v+WROUOJqBUk0kjCoqbywXXrgO # P2BluHvkYwOzXEHqm1VGgRRzbQAamRUWbsDUd8eE/kg0Xebo8AVPs1OMjneeR+Wk # +iFqS9ldgs+oKujsOZCdhk3mEUQntcPUDoCX2SLfN+KeJxbXNXtDgXmEEe96kDoM # hVpiQ2xuBCnkwYGTU6lkO/E5dwYAVSlHh0E5QiqlxAD7sPvRsmKeGaEpL7HKrPKX # f0pNjzKky5AUw3UbxQF65wLeA1LfWAymsKJVmv6OL2JH1dlgsWq/r8wIa8uyheSj # EbDdNoBcAKx7DuGodTtKShXBMnQfcqC5yScLxfCpn4yA8YyHmKdouAj5mjwvNnfR # LPCN5M9zANq7ZKr+SAl8NDSdbuYYCOnY1aukWY8/Rs8SOI7fqFynv6NHaNCtiANJ # zF1vC1lkFlZbPG32dLZtfXsHThYVZJdJ8PVaU3HOpj1ncmP1eWQGE498y6pVLCuc # 8mYVt+KUsdY9tbGly8cauy2L88Yl38IZ9xCXJ4rxOeZJ3GrZ8ecHkQr1/FR+Ndlx # /NYgYgn8ML1Z4alDXHEkCgpEN/7TXVsCpLhuF94n04SgXhi83FdIo6oVj0fYk3YH # WvH6Ba0rrzb5KnqcV5iHd6zZcvOhYY3L5kwWarJJE8uz8Aj78zc= # SIG # End signature block |